You will need a user to add feeds now
This commit is contained in:
parent
61b9db1333
commit
2058054c99
5 changed files with 94 additions and 29 deletions
|
|
@ -3,6 +3,7 @@ from __future__ import annotations
|
||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
from time import mktime, struct_time
|
from time import mktime, struct_time
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
from urllib.parse import ParseResult, urlparse
|
from urllib.parse import ParseResult, urlparse
|
||||||
|
|
||||||
import feedparser
|
import feedparser
|
||||||
|
|
@ -11,6 +12,9 @@ from feedparser import FeedParserDict
|
||||||
|
|
||||||
from feeds.models import Author, Domain, Entry, Feed, Generator, Publisher
|
from feeds.models import Author, Domain, Entry, Feed, Generator, Publisher
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from django.contrib.auth.models import AbstractBaseUser, AnonymousUser
|
||||||
|
|
||||||
logger: logging.Logger = logging.getLogger(__name__)
|
logger: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -205,8 +209,16 @@ def add_entry(feed: Feed, entry: FeedParserDict) -> Entry | None:
|
||||||
return _entry
|
return _entry
|
||||||
|
|
||||||
|
|
||||||
def add_feed(url: str | None) -> None | Feed:
|
def add_feed(url: str | None, user: AbstractBaseUser | AnonymousUser) -> Feed | None:
|
||||||
"""Add a feed to the database."""
|
"""Add a feed to the database.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
url: The URL of the feed.
|
||||||
|
user: The user adding the feed.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The feed that was added.
|
||||||
|
"""
|
||||||
# Parse the feed.
|
# Parse the feed.
|
||||||
parsed_feed: dict | None = parse_feed(url=url)
|
parsed_feed: dict | None = parse_feed(url=url)
|
||||||
if not parsed_feed:
|
if not parsed_feed:
|
||||||
|
|
@ -233,6 +245,7 @@ def add_feed(url: str | None) -> None | Feed:
|
||||||
# Create the feed
|
# Create the feed
|
||||||
feed = Feed(
|
feed = Feed(
|
||||||
feed_url=url,
|
feed_url=url,
|
||||||
|
user=user,
|
||||||
domain=domain,
|
domain=domain,
|
||||||
last_checked=timezone.now(),
|
last_checked=timezone.now(),
|
||||||
bozo=parsed_feed.get("bozo", 0),
|
bozo=parsed_feed.get("bozo", 0),
|
||||||
|
|
|
||||||
21
feeds/migrations/0003_feed_user.py
Normal file
21
feeds/migrations/0003_feed_user.py
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
# Generated by Django 5.0.2 on 2024-02-23 05:38
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('feeds', '0002_alter_author_options_alter_domain_options_and_more'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='feed',
|
||||||
|
name='user',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -7,7 +7,7 @@ from typing import Literal
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import JSONField
|
from django.db.models import JSONField
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Domain(models.Model):
|
class Domain(models.Model):
|
||||||
|
|
@ -129,6 +129,8 @@ class Feed(models.Model):
|
||||||
|
|
||||||
feed_url = models.URLField(unique=True)
|
feed_url = models.URLField(unique=True)
|
||||||
|
|
||||||
|
# The user that added the feed
|
||||||
|
user = models.ForeignKey("auth.User", on_delete=models.SET_NULL, null=True, blank=True)
|
||||||
domain = models.ForeignKey(Domain, on_delete=models.CASCADE)
|
domain = models.ForeignKey(Domain, on_delete=models.CASCADE)
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
modified_at = models.DateTimeField(auto_now=True)
|
modified_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth import login
|
from django.contrib.auth import login
|
||||||
|
|
@ -33,6 +33,7 @@ class IndexView(View):
|
||||||
"keywords": "feed, rss, atom, archive, rss list",
|
"keywords": "feed, rss, atom, archive, rss list",
|
||||||
"author": "TheLovinator",
|
"author": "TheLovinator",
|
||||||
"canonical": "https://feedvault.se/",
|
"canonical": "https://feedvault.se/",
|
||||||
|
"title": "FeedVault",
|
||||||
}
|
}
|
||||||
return HttpResponse(content=template.render(context=context, request=request))
|
return HttpResponse(content=template.render(context=context, request=request))
|
||||||
|
|
||||||
|
|
@ -56,6 +57,7 @@ class FeedView(View):
|
||||||
"keywords": "feed, rss, atom, archive, rss list",
|
"keywords": "feed, rss, atom, archive, rss list",
|
||||||
"author": f"{feed.author_detail.name if feed.author_detail else "FeedVault"}",
|
"author": f"{feed.author_detail.name if feed.author_detail else "FeedVault"}",
|
||||||
"canonical": f"https://feedvault.se/feed/{feed_id}/",
|
"canonical": f"https://feedvault.se/feed/{feed_id}/",
|
||||||
|
"title": f"{feed.title} - FeedVault",
|
||||||
}
|
}
|
||||||
|
|
||||||
return render(request, "feed.html", context)
|
return render(request, "feed.html", context)
|
||||||
|
|
@ -72,10 +74,12 @@ class FeedsView(ListView):
|
||||||
def get_context_data(self, **kwargs) -> dict: # noqa: ANN003
|
def get_context_data(self, **kwargs) -> dict: # noqa: ANN003
|
||||||
"""Get the context data."""
|
"""Get the context data."""
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
context["description"] = "Archive of all feeds"
|
feed_amount: int = Feed.objects.count() or 0
|
||||||
|
context["description"] = f"Archiving {feed_amount} feeds"
|
||||||
context["keywords"] = "feed, rss, atom, archive, rss list"
|
context["keywords"] = "feed, rss, atom, archive, rss list"
|
||||||
context["author"] = "TheLovinator"
|
context["author"] = "TheLovinator"
|
||||||
context["canonical"] = "https://feedvault.se/feeds/"
|
context["canonical"] = "https://feedvault.se/feeds/"
|
||||||
|
context["title"] = "Feeds"
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -95,13 +99,19 @@ class AddView(View):
|
||||||
|
|
||||||
def post(self, request: HttpRequest) -> HttpResponse:
|
def post(self, request: HttpRequest) -> HttpResponse:
|
||||||
"""Add a feed."""
|
"""Add a feed."""
|
||||||
|
if not request.user.is_authenticated:
|
||||||
|
return HttpResponse(content="Not logged in", status=401)
|
||||||
|
|
||||||
|
if not request.user.is_active:
|
||||||
|
return HttpResponse(content="User is not active", status=403)
|
||||||
|
|
||||||
urls: str | None = request.POST.get("urls", None)
|
urls: str | None = request.POST.get("urls", None)
|
||||||
if not urls:
|
if not urls:
|
||||||
return HttpResponse(content="No urls", status=400)
|
return HttpResponse(content="No urls", status=400)
|
||||||
|
|
||||||
# Split the urls by newline.
|
# Split the urls by newline.
|
||||||
for url in urls.split("\n"):
|
for url in urls.split("\n"):
|
||||||
feed: None | Feed = add_feed(url)
|
feed: None | Feed = add_feed(url, request.user)
|
||||||
if not feed:
|
if not feed:
|
||||||
messages.error(request, f"{url} - Failed to add")
|
messages.error(request, f"{url} - Failed to add")
|
||||||
continue
|
continue
|
||||||
|
|
@ -132,13 +142,19 @@ class UploadView(View):
|
||||||
|
|
||||||
def post(self, request: HttpRequest) -> HttpResponse:
|
def post(self, request: HttpRequest) -> HttpResponse:
|
||||||
"""Upload a file."""
|
"""Upload a file."""
|
||||||
|
if not request.user.is_authenticated:
|
||||||
|
return HttpResponse(content="Not logged in", status=401)
|
||||||
|
|
||||||
|
if not request.user.is_active:
|
||||||
|
return HttpResponse(content="User is not active", status=403)
|
||||||
|
|
||||||
file = request.FILES.get("file", None)
|
file = request.FILES.get("file", None)
|
||||||
if not file:
|
if not file:
|
||||||
return HttpResponse(content="No file", status=400)
|
return HttpResponse(content="No file", status=400)
|
||||||
|
|
||||||
# Split the urls by newline.
|
# Split the urls by newline.
|
||||||
for url in file.read().decode("utf-8").split("\n"):
|
for url in file.read().decode("utf-8").split("\n"):
|
||||||
feed: None | Feed = add_feed(url)
|
feed: None | Feed = add_feed(url, request.user)
|
||||||
if not feed:
|
if not feed:
|
||||||
messages.error(request, f"{url} - Failed to add")
|
messages.error(request, f"{url} - Failed to add")
|
||||||
continue
|
continue
|
||||||
|
|
@ -172,6 +188,17 @@ class RegisterView(CreateView):
|
||||||
form_class = UserCreationForm
|
form_class = UserCreationForm
|
||||||
success_url = reverse_lazy("feeds:login")
|
success_url = reverse_lazy("feeds:login")
|
||||||
|
|
||||||
|
# Add context data to the view
|
||||||
|
def get_context_data(self, **kwargs) -> dict: # noqa: ANN003
|
||||||
|
"""Get the context data."""
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context["description"] = "Register a new account"
|
||||||
|
context["keywords"] = "register, account, feed, rss, atom, archive, rss list"
|
||||||
|
context["author"] = "TheLovinator"
|
||||||
|
context["canonical"] = "https://feedvault.se/accounts/register/"
|
||||||
|
context["title"] = "Register"
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
class CustomLogoutView(LogoutView):
|
class CustomLogoutView(LogoutView):
|
||||||
"""Logout view."""
|
"""Logout view."""
|
||||||
|
|
@ -186,6 +213,17 @@ class CustomPasswordChangeView(SuccessMessageMixin, PasswordChangeView):
|
||||||
success_url = reverse_lazy("feeds:index")
|
success_url = reverse_lazy("feeds:index")
|
||||||
success_message = "Your password was successfully updated!"
|
success_message = "Your password was successfully updated!"
|
||||||
|
|
||||||
|
# Add context data to the view
|
||||||
|
def get_context_data(self, **kwargs) -> dict: # noqa: ANN003
|
||||||
|
"""Get the context data."""
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context["description"] = "Change your password"
|
||||||
|
context["keywords"] = "change, password, account, feed, rss, atom, archive, rss list"
|
||||||
|
context["author"] = "TheLovinator"
|
||||||
|
context["canonical"] = "https://feedvault.se/accounts/change-password/"
|
||||||
|
context["title"] = "Change password"
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
class ProfileView(View):
|
class ProfileView(View):
|
||||||
"""Profile page."""
|
"""Profile page."""
|
||||||
|
|
@ -193,11 +231,16 @@ class ProfileView(View):
|
||||||
def get(self, request: HttpRequest) -> HttpResponse:
|
def get(self, request: HttpRequest) -> HttpResponse:
|
||||||
"""Load the profile page."""
|
"""Load the profile page."""
|
||||||
template = loader.get_template(template_name="accounts/profile.html")
|
template = loader.get_template(template_name="accounts/profile.html")
|
||||||
context = {
|
|
||||||
"description": "FeedVault allows users to archive and search their favorite web feeds.",
|
user_feeds = Feed.objects.filter(user=request.user).order_by("-created_at")[:100]
|
||||||
"keywords": "feed, rss, atom, archive, rss list",
|
|
||||||
"author": "TheLovinator",
|
context: dict[str, str | Any] = {
|
||||||
"canonical": "https://feedvault.se/",
|
"description": f"Profile page for {request.user.get_username()}",
|
||||||
|
"keywords": f"profile, account, {request.user.get_username()}",
|
||||||
|
"author": f"{request.user.get_username()}",
|
||||||
|
"canonical": "https://feedvault.se/accounts/profile/",
|
||||||
|
"title": f"{request.user.get_username()}",
|
||||||
|
"user_feeds": user_feeds,
|
||||||
}
|
}
|
||||||
return HttpResponse(content=template.render(context=context, request=request))
|
return HttpResponse(content=template.render(context=context, request=request))
|
||||||
|
|
||||||
|
|
@ -213,5 +256,6 @@ class APIView(View):
|
||||||
"keywords": "feed, rss, atom, archive, rss list",
|
"keywords": "feed, rss, atom, archive, rss list",
|
||||||
"author": "TheLovinator",
|
"author": "TheLovinator",
|
||||||
"canonical": "https://feedvault.se/api/",
|
"canonical": "https://feedvault.se/api/",
|
||||||
|
"title": "API Documentation",
|
||||||
}
|
}
|
||||||
return HttpResponse(content=template.render(context=context, request=request))
|
return HttpResponse(content=template.render(context=context, request=request))
|
||||||
|
|
|
||||||
|
|
@ -3,25 +3,10 @@
|
||||||
<h2>{{ user.username }}</h2>
|
<h2>{{ user.username }}</h2>
|
||||||
<h3>Feeds</h3>
|
<h3>Feeds</h3>
|
||||||
<ul>
|
<ul>
|
||||||
{% for feed in feeds %}
|
{% for feed in user_feeds %}
|
||||||
<li>
|
<li>
|
||||||
<a href='{% url "feeds:feed" feed.id %}'>{{ feed.title }}</a>
|
<a href='{% url "feeds:feed" feed.id %}'>{{ feed.feed_url }}</a>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
<h3>Subscriptions</h3>
|
|
||||||
<ul>
|
|
||||||
{% for subscription in subscriptions %}
|
|
||||||
<li>
|
|
||||||
<a href='{% url "feeds:feed" subscription.id %}'>{{ subscription.title }}</a>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
<h3>Subscribers</h3>
|
|
||||||
<p>
|
|
||||||
<form action="{% url 'feeds:logout' %}" method="post">
|
|
||||||
{% csrf_token %}
|
|
||||||
<button type="submit">Logout</button>
|
|
||||||
</form>
|
|
||||||
</p>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue