From 603090205a45830035bb10d4c289a840fcfc9c18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Helle=C5=9Ben?= Date: Fri, 27 Mar 2026 04:26:09 +0100 Subject: [PATCH 1/2] Use templates for rendering --- feeds/urls.py | 22 ++++- feeds/views.py | 63 ++++--------- templates/base.html | 148 ++++++++++++++++++++++++++---- templates/feeds/entry_detail.html | 34 +++++++ templates/feeds/feed_detail.html | 49 ++++++++++ templates/feeds/feed_list.html | 2 +- 6 files changed, 247 insertions(+), 71 deletions(-) create mode 100644 templates/feeds/entry_detail.html create mode 100644 templates/feeds/feed_detail.html diff --git a/feeds/urls.py b/feeds/urls.py index 163c7f0..be9e7e5 100644 --- a/feeds/urls.py +++ b/feeds/urls.py @@ -10,9 +10,25 @@ if TYPE_CHECKING: urlpatterns: list[URLPattern | URLResolver] = [ - path("", views.home, name="home"), - path("feeds/", views.feed_list, name="feed-list"), - path("feeds//", views.feed_detail, name="feed-detail"), + # / + path( + "", + views.home, + name="home", + ), + # /feeds/ + path( + "feeds/", + views.feed_list, + name="feeds", + ), + # /feeds// + path( + "feeds//", + views.feed_detail, + name="details", + ), + # /feeds//entries// path( "feeds//entries//", views.entry_detail, diff --git a/feeds/views.py b/feeds/views.py index 5780b22..e2331a9 100644 --- a/feeds/views.py +++ b/feeds/views.py @@ -4,7 +4,7 @@ import html import json from typing import TYPE_CHECKING -from django.http import HttpResponse +from django.db.models.query import QuerySet from django.shortcuts import get_object_or_404 from django.shortcuts import redirect from django.shortcuts import render @@ -14,6 +14,7 @@ from feeds.models import Feed if TYPE_CHECKING: from django.http import HttpRequest + from django.http import HttpResponse from pytest_django.asserts import QuerySet @@ -38,35 +39,16 @@ def feed_detail(request: HttpRequest, feed_id: int) -> HttpResponse: HttpResponse: An HTML response containing the feed details and its entries. """ feed: Feed = get_object_or_404(Feed, id=feed_id) - entries: QuerySet[Entry, Entry] = Entry.objects.filter(feed=feed).order_by( "-published_at", "-fetched_at", )[:50] - html: list[str] = [ - "", - f"FeedVault - {feed.url}", - "

Feed Detail

", - f"

URL: {feed.url}

", - f"

Domain: {feed.domain}

", - f"

Active: {'yes' if feed.is_active else 'no'}

", - f"

Created: {feed.created_at}

", - f"

Last fetched: {feed.last_fetched_at}

", - "

Entries (latest 50)

", - "
    ", - ] - for entry in entries: - title: str | None = entry.data.get("title") if entry.data else None - summary: str | None = entry.data.get("summary") if entry.data else None - snippet: str = title or summary or "[no title]" - html.append( - f"
  • {entry.published_at or entry.fetched_at}: " - f'{snippet} ' - f"(id: {entry.entry_id})
  • ", - ) - html.extend(("
", '

Back to list

', "")) - return HttpResponse("\n".join(html)) + context: dict[str, Feed | QuerySet[Entry, Entry]] = { + "feed": feed, + "entries": entries, + } + return render(request, "feeds/feed_detail.html", context) def entry_detail(request: HttpRequest, feed_id: int, entry_id: int) -> HttpResponse: @@ -83,32 +65,19 @@ def entry_detail(request: HttpRequest, feed_id: int, entry_id: int) -> HttpRespo feed: Feed = get_object_or_404(Feed, id=feed_id) entry: Entry = get_object_or_404(Entry, id=entry_id, feed=feed) - # Render images if present in entry.data - entry_data_html: str = "" + # Prepare entry data for display if entry.data: formatted_json: str = json.dumps(entry.data, indent=2, ensure_ascii=False) - escaped_json: str = html.escape(formatted_json) - note: str = '
Note: HTML in the JSON is escaped for display and will not be rendered as HTML.
' - entry_data_html: str = f"{note}
{escaped_json}
" + escaped_json: str | None = html.escape(formatted_json) else: - entry_data_html: str = "

[No data]

" + escaped_json: str | None = None - html_lines: list[str] = [ - "", - f"FeedVault - Entry {entry.entry_id}", - "

Entry Detail

", - f"

Feed: {feed.url}

", - f"

Entry ID: {entry.entry_id}

", - f"

Published: {entry.published_at}

", - f"

Fetched: {entry.fetched_at}

", - f"

Content Hash: {entry.content_hash}

", - f"

Error Message: {entry.error_message or '[none]'}

", - "

Entry Data

", - entry_data_html, - f'

Back to feed

', - "", - ] - return HttpResponse("\n".join(html_lines)) + context = { + "feed": feed, + "entry": entry, + "escaped_json": escaped_json, + } + return render(request, "feeds/entry_detail.html", context) def home(request: HttpRequest) -> HttpResponse: diff --git a/templates/base.html b/templates/base.html index ae5836d..690546f 100644 --- a/templates/base.html +++ b/templates/base.html @@ -3,33 +3,141 @@ - - - {% block title %} - FeedVault - {% endblock title %} - + {% if description %}{% endif %} + {% if keywords %}{% endif %} + {% if author %}{% endif %} + {% if canonical %}{% endif %} + {{ title|default:"FeedVault" }} + + {% if messages %} + + {% endif %}
-

FeedVault

- +

+ FeedVault +

+
+
+ Archive of + web feeds. + {{ stats }} + +
+
+ +
+
+ +
- {% block content %} - {% endblock content %} + {% block content %}{% endblock %}
+
-

Web scraping is not a crime. - No rights reserved. - A birthday present for Plipp ❤️

+ +
+
Web scraping is not a crime.
+
No rights reserved.
+
+
+
TheLovinator#9276 on Discord
+
A birthday present for Plipp ❤️
+
+
diff --git a/templates/feeds/entry_detail.html b/templates/feeds/entry_detail.html new file mode 100644 index 0000000..2adb2c7 --- /dev/null +++ b/templates/feeds/entry_detail.html @@ -0,0 +1,34 @@ +{% extends "base.html" %} +{% block content %} +

Entry Detail

+

+ Feed: {{ feed.url }} +

+

+ Entry ID: {{ entry.entry_id }} +

+

+ Published: {{ entry.published_at }} +

+

+ Fetched: {{ entry.fetched_at }} +

+

+ Content Hash: {{ entry.content_hash }} +

+

+ Error Message: {{ entry.error_message|default:"[none]" }} +

+

Entry Data

+ {% if escaped_json %} +
+ Note: HTML in the JSON is escaped for display and will not be rendered as HTML. +
+
{{ escaped_json|safe }}
+ {% else %} +

[No data]

+ {% endif %} +

+ Back to feed +

+{% endblock content %} diff --git a/templates/feeds/feed_detail.html b/templates/feeds/feed_detail.html new file mode 100644 index 0000000..9c4571c --- /dev/null +++ b/templates/feeds/feed_detail.html @@ -0,0 +1,49 @@ +{% extends "base.html" %} +{% block content %} +

Feed Detail

+

+ URL: {{ feed.url }} +

+

+ Domain: {{ feed.domain }} +

+

+ Active: {{ feed.is_active|yesno:"yes,no" }} +

+

+ Created: {{ feed.created_at }} +

+

+ Last fetched: {{ feed.last_fetched_at }} +

+

Entries (latest 50)

+ + + + + + + + + {% for entry in entries %} + + + + + {% endfor %} + +
Title/DescriptionEntry ID
+ + {{ entry.data.title|default:entry.data.description|default:"[no title]" }} + +
+ {{ entry.published_at|default:entry.fetched_at }} +
+ + {{ entry.entry_id|cut:'https://'|truncatechars:50 }} + +
+

+ Back to list +

+{% endblock content %} diff --git a/templates/feeds/feed_list.html b/templates/feeds/feed_list.html index d6c9e55..d40f8c3 100644 --- a/templates/feeds/feed_list.html +++ b/templates/feeds/feed_list.html @@ -4,7 +4,7 @@ From f9cac0974dfb67963aa03bd185539e04657ef26e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Helle=C5=9Ben?= Date: Fri, 27 Mar 2026 04:29:15 +0100 Subject: [PATCH 2/2] Refactor archive_feed command to confirm deletion of entries before proceeding --- feeds/management/commands/archive_feed.py | 33 ++++++++++++++++------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/feeds/management/commands/archive_feed.py b/feeds/management/commands/archive_feed.py index 3f5a500..85f3b18 100644 --- a/feeds/management/commands/archive_feed.py +++ b/feeds/management/commands/archive_feed.py @@ -56,14 +56,18 @@ class Command(BaseCommand): if count == 0: msg = f"No entries found for feed: {url}" self.stdout.write(self.style.WARNING(msg)) - else: + proceed: bool = True if not force: - return self.confirm_and_list_entries(url, entries_qs, count) - - entries_qs.delete() - msg = f"Deleted {count} entries for feed: {url}" - self.stdout.write(self.style.SUCCESS(msg)) + proceed: bool = self.confirm_and_list_entries( + url=url, + entries_qs=entries_qs, + count=count, + ) + if proceed: + entries_qs.delete() + msg = f"Deleted {count} entries for feed: {url}" + self.stdout.write(self.style.SUCCESS(msg)) new_entries: int = fetch_and_archive_feed(feed) if new_entries: @@ -73,15 +77,23 @@ class Command(BaseCommand): else: msg: str = "\tFeed is up to date, but no new entries were archived." self.stdout.write(self.style.WARNING(msg)) - return None def confirm_and_list_entries( self, url: str, entries_qs: QuerySet[Entry, Entry], count: int, - ) -> None: - """Confirm with the user before deleting entries and list some of them.""" + ) -> bool: + """Confirm with the user before deleting entries and list some of them. + + Args: + url (str): The URL of the feed. + entries_qs (QuerySet[Entry, Entry]): The queryset of entries to be deleted. + count (int): The total number of entries to be deleted. + + Returns: + True if user confirms, False otherwise. + """ msg: str = f"The following {count} entries will be removed for feed: {url}" self.stdout.write(self.style.WARNING(msg)) @@ -94,7 +106,8 @@ class Command(BaseCommand): confirm: str = input("Are you sure you want to proceed? (yes/no): ") if confirm.lower() != "yes": self.stdout.write(self.style.ERROR("Operation cancelled.")) - return + return False + return True def get_entry_title(entry: Entry) -> str | None: