Compare commits
No commits in common. "f9cac0974dfb67963aa03bd185539e04657ef26e" and "ac01862a1779f3d31e62c6ade9fb541c42b25282" have entirely different histories.
f9cac0974d
...
ac01862a17
7 changed files with 82 additions and 271 deletions
|
|
@ -56,18 +56,14 @@ 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:
|
||||
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))
|
||||
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))
|
||||
|
||||
new_entries: int = fetch_and_archive_feed(feed)
|
||||
if new_entries:
|
||||
|
|
@ -77,23 +73,15 @@ 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,
|
||||
) -> 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.
|
||||
"""
|
||||
) -> None:
|
||||
"""Confirm with the user before deleting entries and list some of them."""
|
||||
msg: str = f"The following {count} entries will be removed for feed: {url}"
|
||||
self.stdout.write(self.style.WARNING(msg))
|
||||
|
||||
|
|
@ -106,8 +94,7 @@ 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 False
|
||||
return True
|
||||
return
|
||||
|
||||
|
||||
def get_entry_title(entry: Entry) -> str | None:
|
||||
|
|
|
|||
|
|
@ -10,25 +10,9 @@ if TYPE_CHECKING:
|
|||
|
||||
|
||||
urlpatterns: list[URLPattern | URLResolver] = [
|
||||
# /
|
||||
path(
|
||||
"",
|
||||
views.home,
|
||||
name="home",
|
||||
),
|
||||
# /feeds/
|
||||
path(
|
||||
"feeds/",
|
||||
views.feed_list,
|
||||
name="feeds",
|
||||
),
|
||||
# /feeds/<feed_id>/
|
||||
path(
|
||||
"feeds/<int:feed_id>/",
|
||||
views.feed_detail,
|
||||
name="details",
|
||||
),
|
||||
# /feeds/<feed_id>/entries/<entry_id>/
|
||||
path("", views.home, name="home"),
|
||||
path("feeds/", views.feed_list, name="feed-list"),
|
||||
path("feeds/<int:feed_id>/", views.feed_detail, name="feed-detail"),
|
||||
path(
|
||||
"feeds/<int:feed_id>/entries/<int:entry_id>/",
|
||||
views.entry_detail,
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import html
|
|||
import json
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from django.db.models.query import QuerySet
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.shortcuts import redirect
|
||||
from django.shortcuts import render
|
||||
|
|
@ -14,7 +14,6 @@ from feeds.models import Feed
|
|||
|
||||
if TYPE_CHECKING:
|
||||
from django.http import HttpRequest
|
||||
from django.http import HttpResponse
|
||||
from pytest_django.asserts import QuerySet
|
||||
|
||||
|
||||
|
|
@ -39,16 +38,35 @@ 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]
|
||||
|
||||
context: dict[str, Feed | QuerySet[Entry, Entry]] = {
|
||||
"feed": feed,
|
||||
"entries": entries,
|
||||
}
|
||||
return render(request, "feeds/feed_detail.html", context)
|
||||
html: list[str] = [
|
||||
"<!DOCTYPE html>",
|
||||
f"<html><head><title>FeedVault - {feed.url}</title></head><body>",
|
||||
"<h1>Feed Detail</h1>",
|
||||
f"<p><b>URL:</b> {feed.url}</p>",
|
||||
f"<p><b>Domain:</b> {feed.domain}</p>",
|
||||
f"<p><b>Active:</b> {'yes' if feed.is_active else 'no'}</p>",
|
||||
f"<p><b>Created:</b> {feed.created_at}</p>",
|
||||
f"<p><b>Last fetched:</b> {feed.last_fetched_at}</p>",
|
||||
"<h2>Entries (latest 50)</h2>",
|
||||
"<ul>",
|
||||
]
|
||||
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"<li><b>{entry.published_at or entry.fetched_at}:</b> "
|
||||
f'<a href="/feeds/{feed.pk}/entries/{entry.pk}/">{snippet}</a> '
|
||||
f"<small>(id: {entry.entry_id})</small></li>",
|
||||
)
|
||||
html.extend(("</ul>", '<p><a href="/">Back to list</a></p>', "</body></html>"))
|
||||
return HttpResponse("\n".join(html))
|
||||
|
||||
|
||||
def entry_detail(request: HttpRequest, feed_id: int, entry_id: int) -> HttpResponse:
|
||||
|
|
@ -65,19 +83,32 @@ 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)
|
||||
|
||||
# Prepare entry data for display
|
||||
# Render images if present in entry.data
|
||||
entry_data_html: str = ""
|
||||
if entry.data:
|
||||
formatted_json: str = json.dumps(entry.data, indent=2, ensure_ascii=False)
|
||||
escaped_json: str | None = html.escape(formatted_json)
|
||||
escaped_json: str = html.escape(formatted_json)
|
||||
note: str = '<div style="font-size:small;color:#666;margin-bottom:4px;">Note: HTML in the JSON is escaped for display and will not be rendered as HTML.</div>'
|
||||
entry_data_html: str = f"{note}<pre>{escaped_json}</pre>"
|
||||
else:
|
||||
escaped_json: str | None = None
|
||||
entry_data_html: str = "<p>[No data]</p>"
|
||||
|
||||
context = {
|
||||
"feed": feed,
|
||||
"entry": entry,
|
||||
"escaped_json": escaped_json,
|
||||
}
|
||||
return render(request, "feeds/entry_detail.html", context)
|
||||
html_lines: list[str] = [
|
||||
"<!DOCTYPE html>",
|
||||
f"<html><head><title>FeedVault - Entry {entry.entry_id}</title></head><body>",
|
||||
"<h1>Entry Detail</h1>",
|
||||
f"<p><b>Feed:</b> <a href='/feeds/{feed.pk}/'>{feed.url}</a></p>",
|
||||
f"<p><b>Entry ID:</b> {entry.entry_id}</p>",
|
||||
f"<p><b>Published:</b> {entry.published_at}</p>",
|
||||
f"<p><b>Fetched:</b> {entry.fetched_at}</p>",
|
||||
f"<p><b>Content Hash:</b> {entry.content_hash}</p>",
|
||||
f"<p><b>Error Message:</b> {entry.error_message or '[none]'} </p>",
|
||||
"<h2>Entry Data</h2>",
|
||||
entry_data_html,
|
||||
f'<p><a href="/feeds/{feed.pk}/">Back to feed</a></p>',
|
||||
"</body></html>",
|
||||
]
|
||||
return HttpResponse("\n".join(html_lines))
|
||||
|
||||
|
||||
def home(request: HttpRequest) -> HttpResponse:
|
||||
|
|
|
|||
|
|
@ -3,141 +3,33 @@
|
|||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
{% if description %}<meta name="description" content="{{ description }}" />{% endif %}
|
||||
{% if keywords %}<meta name="keywords" content="{{ keywords }}" />{% endif %}
|
||||
{% if author %}<meta name="author" content="{{ author }}" />{% endif %}
|
||||
{% if canonical %}<link rel="canonical" href="{{ canonical }}" />{% endif %}
|
||||
<title>{{ title|default:"FeedVault" }}</title>
|
||||
<style>
|
||||
html {
|
||||
max-width: 88ch;
|
||||
padding: calc(1vmin + 0.5rem);
|
||||
margin-inline: auto;
|
||||
font-size: clamp(1em, 0.909em + 0.45vmin, 1.25em);
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||
Helvetica, Arial, sans-serif;
|
||||
color-scheme: light dark;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
header {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.search {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 1rem;
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.leftright {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.left {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.right {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
height: 10rem;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.messages {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.success {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.warning {
|
||||
color: orange;
|
||||
}
|
||||
</style>
|
||||
<meta name="keywords" content="feeds, archive, FeedVault" />
|
||||
<title>
|
||||
{% block title %}
|
||||
FeedVault
|
||||
{% endblock title %}
|
||||
</title>
|
||||
</head>
|
||||
<body>
|
||||
{% if messages %}
|
||||
<ul class="messages" role="alert" aria-live="polite">
|
||||
{% for message in messages %}
|
||||
<li {% if message.tags %}class="{{ message.tags }}"{% endif %}
|
||||
role="alert">{{ message }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
<header>
|
||||
<h1>
|
||||
<a href="{% url 'home' %}" aria-label="FeedVault Home">FeedVault</a>
|
||||
</h1>
|
||||
<h1>FeedVault</h1>
|
||||
<nav>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="{% url 'home' %}">Home</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'feed-list' %}">Feeds</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
<div class="leftright">
|
||||
<div class="left">
|
||||
<small>Archive of
|
||||
<a href="https://en.wikipedia.org/wiki/Web_feed"
|
||||
aria-label="Wikipedia article on web feeds">web feeds</a>.
|
||||
{{ stats }}
|
||||
</small>
|
||||
</div>
|
||||
<div class="right">
|
||||
<form role="search"
|
||||
action="#"
|
||||
method="get"
|
||||
class="search"
|
||||
aria-label="Search form">
|
||||
<input type="search" name="q" placeholder="Search" aria-label="Search input" />
|
||||
<button type="submit" aria-label="Search button">Search</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<nav aria-label="Main navigation">
|
||||
<small>
|
||||
<div class="leftright">
|
||||
<div class="left">
|
||||
<a href="{% url 'home' %}" aria-label="Home page">Home</a> |
|
||||
<a href="{% url 'feeds' %}" aria-label="Feeds page">Feeds</a> |
|
||||
<a href="#" aria-label="Contact page">Contact</a>
|
||||
</div>
|
||||
<div class="right">
|
||||
<a href="https://github.com/TheLovinator1/FeedVault"
|
||||
aria-label="GitHub page">GitHub</a> |
|
||||
<a href="https://github.com/sponsors/TheLovinator1"
|
||||
aria-label="Donate page">Donate</a>
|
||||
</div>
|
||||
</div>
|
||||
</small>
|
||||
</nav>
|
||||
<hr />
|
||||
<main>
|
||||
{% block content %}<!-- default content -->{% endblock %}
|
||||
{% block content %}
|
||||
{% endblock content %}
|
||||
</main>
|
||||
<hr />
|
||||
<footer>
|
||||
<small>
|
||||
<div class="leftright">
|
||||
<div class="left">Web scraping is not a crime.</div>
|
||||
<div class="right">No rights reserved.</div>
|
||||
</div>
|
||||
<div class="leftright">
|
||||
<div class="left">TheLovinator#9276 on Discord</div>
|
||||
<div class="right">A birthday present for Plipp ❤️</div>
|
||||
</div>
|
||||
</small>
|
||||
<p>Web scraping is not a crime. - No rights reserved. - A birthday present for Plipp ❤️</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1,34 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<h1>Entry Detail</h1>
|
||||
<p>
|
||||
<b>Feed:</b> <a href="{% url 'details' feed.pk %}">{{ feed.url }}</a>
|
||||
</p>
|
||||
<p>
|
||||
<b>Entry ID:</b> {{ entry.entry_id }}
|
||||
</p>
|
||||
<p>
|
||||
<b>Published:</b> {{ entry.published_at }}
|
||||
</p>
|
||||
<p>
|
||||
<b>Fetched:</b> {{ entry.fetched_at }}
|
||||
</p>
|
||||
<p>
|
||||
<b>Content Hash:</b> {{ entry.content_hash }}
|
||||
</p>
|
||||
<p>
|
||||
<b>Error Message:</b> {{ entry.error_message|default:"[none]" }}
|
||||
</p>
|
||||
<h2>Entry Data</h2>
|
||||
{% if escaped_json %}
|
||||
<div style="font-size:small;color:#666;margin-bottom:4px;">
|
||||
Note: HTML in the JSON is escaped for display and will not be rendered as HTML.
|
||||
</div>
|
||||
<pre>{{ escaped_json|safe }}</pre>
|
||||
{% else %}
|
||||
<p>[No data]</p>
|
||||
{% endif %}
|
||||
<p>
|
||||
<a href="{% url 'details' feed.pk %}">Back to feed</a>
|
||||
</p>
|
||||
{% endblock content %}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<h1>Feed Detail</h1>
|
||||
<p>
|
||||
<b>URL:</b> {{ feed.url }}
|
||||
</p>
|
||||
<p>
|
||||
<b>Domain:</b> {{ feed.domain }}
|
||||
</p>
|
||||
<p>
|
||||
<b>Active:</b> {{ feed.is_active|yesno:"yes,no" }}
|
||||
</p>
|
||||
<p>
|
||||
<b>Created:</b> {{ feed.created_at }}
|
||||
</p>
|
||||
<p>
|
||||
<b>Last fetched:</b> {{ feed.last_fetched_at }}
|
||||
</p>
|
||||
<h2>Entries (latest 50)</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Title/Description</th>
|
||||
<th>Entry ID</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for entry in entries %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{% url 'entry-detail' feed.pk entry.pk %}">
|
||||
{{ entry.data.title|default:entry.data.description|default:"[no title]" }}
|
||||
</a>
|
||||
<br />
|
||||
<small>{{ entry.published_at|default:entry.fetched_at }}</small>
|
||||
</td>
|
||||
<td>
|
||||
<small>
|
||||
<a href="{{ entry.entry_id }}">{{ entry.entry_id|cut:'https://'|truncatechars:50 }}</a>
|
||||
</small>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<p>
|
||||
<a href="{% url 'feeds' %}">Back to list</a>
|
||||
</p>
|
||||
{% endblock content %}
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
<ul>
|
||||
{% for feed in feeds %}
|
||||
<li>
|
||||
<a href="{% url 'details' feed.pk %}">{{ feed.url }}</a>
|
||||
<a href="{% url 'feed-detail' feed.pk %}">{{ feed.url }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue