Add command for updating feeds
This commit is contained in:
parent
7005490bf4
commit
d04fe12f80
10 changed files with 116 additions and 13 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -158,3 +158,4 @@ cython_debug/
|
||||||
# FeedVault directories
|
# FeedVault directories
|
||||||
data/
|
data/
|
||||||
media/
|
media/
|
||||||
|
staticfiles/
|
||||||
|
|
|
||||||
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
|
|
@ -50,6 +50,7 @@
|
||||||
"localbattle",
|
"localbattle",
|
||||||
"localdomain",
|
"localdomain",
|
||||||
"lscr",
|
"lscr",
|
||||||
|
"makemigrations",
|
||||||
"malformedurl",
|
"malformedurl",
|
||||||
"meowning",
|
"meowning",
|
||||||
"mmcdole",
|
"mmcdole",
|
||||||
|
|
|
||||||
19
README.md
19
README.md
|
|
@ -30,6 +30,25 @@ Please create a new issue before submitting a big pull request. I am probably ok
|
||||||
|
|
||||||
Try to minimize the number of dependencies you add to the project. If you need to add a new dependency, please create an issue first.
|
Try to minimize the number of dependencies you add to the project. If you need to add a new dependency, please create an issue first.
|
||||||
|
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
- [Python](https://www.python.org/)
|
||||||
|
- [Poetry](https://python-poetry.org/)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
poetry install
|
||||||
|
poetry shell
|
||||||
|
python manage.py test
|
||||||
|
python manage.py collectstatic
|
||||||
|
python manage.py makemigrations
|
||||||
|
python manage.py migrate
|
||||||
|
python manage.py runserver
|
||||||
|
|
||||||
|
# Update feeds
|
||||||
|
python manage.py update_feeds
|
||||||
|
```
|
||||||
|
|
||||||
## Contact
|
## Contact
|
||||||
|
|
||||||
For any inquiries or support, please create an issue on GitHub.
|
For any inquiries or support, please create an issue on GitHub.
|
||||||
|
|
|
||||||
|
|
@ -335,25 +335,34 @@ def populate_feed(url: str | None, user: AbstractBaseUser | AnonymousUser) -> Fe
|
||||||
return feed
|
return feed
|
||||||
|
|
||||||
|
|
||||||
def grab_entries(feed: Feed) -> None:
|
def grab_entries(feed: Feed) -> None | list[Entry]:
|
||||||
"""Grab the entries from a feed.
|
"""Grab the entries from a feed.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
feed: The feed to grab the entries from.
|
feed: The feed to grab the entries from.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The entries that were added. If no entries were added, None is returned.
|
||||||
"""
|
"""
|
||||||
|
# Set the last checked time to now.
|
||||||
|
feed.last_checked = timezone.now()
|
||||||
|
feed.save()
|
||||||
|
|
||||||
|
entries_added: list[Entry] = []
|
||||||
# Parse the feed.
|
# Parse the feed.
|
||||||
parsed_feed: dict | None = parse_feed(url=feed.feed_url)
|
parsed_feed: dict | None = parse_feed(url=feed.feed_url)
|
||||||
if not parsed_feed:
|
if not parsed_feed:
|
||||||
return
|
return None
|
||||||
|
|
||||||
entries = parsed_feed.get("entries", [])
|
entries = parsed_feed.get("entries", [])
|
||||||
for entry in entries:
|
for entry in entries:
|
||||||
added_entry: Entry | None = add_entry(feed=feed, entry=entry)
|
added_entry: Entry | None = add_entry(feed=feed, entry=entry)
|
||||||
if not added_entry:
|
if not added_entry:
|
||||||
continue
|
continue
|
||||||
|
entries_added.append(added_entry)
|
||||||
|
|
||||||
logger.info("Grabbed entries for feed: %s", feed)
|
logger.info("Added entries: %s", entries_added)
|
||||||
return
|
return entries_added
|
||||||
|
|
||||||
|
|
||||||
def add_url(url: str, user: AbstractBaseUser | AnonymousUser) -> FeedAddResult:
|
def add_url(url: str, user: AbstractBaseUser | AnonymousUser) -> FeedAddResult:
|
||||||
0
feedvault/management/commands/__init__.py
Normal file
0
feedvault/management/commands/__init__.py
Normal file
37
feedvault/management/commands/update_feeds.py
Normal file
37
feedvault/management/commands/update_feeds.py
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand, no_translations
|
||||||
|
from django.db.models import Q
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from feedvault.feeds import grab_entries
|
||||||
|
from feedvault.models import Entry, Feed
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = "Check for new entries in feeds"
|
||||||
|
requires_migrations_checks = True
|
||||||
|
|
||||||
|
@no_translations
|
||||||
|
def handle(self, *args, **options) -> None: # noqa: ANN002, ANN003, ARG002
|
||||||
|
new_entries: int = 0
|
||||||
|
|
||||||
|
# Grab feeds that haven't been checked in 15 minutes OR haven't been checked at all
|
||||||
|
for feed in Feed.objects.filter(
|
||||||
|
Q(last_checked__lte=timezone.now() - timedelta(minutes=15)) | Q(last_checked__isnull=True),
|
||||||
|
):
|
||||||
|
entries: None | list[Entry] = grab_entries(feed)
|
||||||
|
if not entries:
|
||||||
|
self.stdout.write(f"No new entries for {feed.title}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
self.stdout.write(f"Updated {feed}")
|
||||||
|
self.stdout.write(f"Added {len(entries)} new entries for {feed}")
|
||||||
|
new_entries += len(entries)
|
||||||
|
|
||||||
|
if new_entries:
|
||||||
|
self.stdout.write(self.style.SUCCESS(f"Successfully updated feeds. Added {new_entries} new entries"))
|
||||||
|
|
||||||
|
self.stdout.write("No new entries found")
|
||||||
|
|
@ -293,7 +293,7 @@ class Entry(models.Model):
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
"""Return string representation of the entry."""
|
"""Return string representation of the entry."""
|
||||||
return f"{self.feed} - {self.title}"
|
return f"{self.feed.feed_url} - {self.title}"
|
||||||
|
|
||||||
|
|
||||||
def get_upload_path(instance: UserUploadedFile, filename: str) -> str:
|
def get_upload_path(instance: UserUploadedFile, filename: str) -> str:
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,9 @@ from dotenv import find_dotenv, load_dotenv
|
||||||
|
|
||||||
load_dotenv(dotenv_path=find_dotenv(), verbose=True)
|
load_dotenv(dotenv_path=find_dotenv(), verbose=True)
|
||||||
|
|
||||||
|
# Is True when running tests, used for not spamming Discord when new users are created
|
||||||
|
TESTING: bool = len(sys.argv) > 1 and sys.argv[1] == "test"
|
||||||
|
|
||||||
DEBUG: bool = os.getenv(key="DEBUG", default="True").lower() == "true"
|
DEBUG: bool = os.getenv(key="DEBUG", default="True").lower() == "true"
|
||||||
BASE_DIR: Path = Path(__file__).resolve().parent.parent
|
BASE_DIR: Path = Path(__file__).resolve().parent.parent
|
||||||
SECRET_KEY: str = os.getenv("SECRET_KEY", default="")
|
SECRET_KEY: str = os.getenv("SECRET_KEY", default="")
|
||||||
|
|
@ -42,7 +45,12 @@ ROOT_URLCONF = "feedvault.urls"
|
||||||
WSGI_APPLICATION = "feedvault.wsgi.application"
|
WSGI_APPLICATION = "feedvault.wsgi.application"
|
||||||
NINJA_PAGINATION_PER_PAGE = 1000
|
NINJA_PAGINATION_PER_PAGE = 1000
|
||||||
STATIC_URL = "static/"
|
STATIC_URL = "static/"
|
||||||
STATIC_ROOT: Path = BASE_DIR / "static"
|
STATIC_ROOT: Path = BASE_DIR / "staticfiles"
|
||||||
|
STATICFILES_STORAGE = (
|
||||||
|
"django.contrib.staticfiles.storage.StaticFilesStorage"
|
||||||
|
if TESTING
|
||||||
|
else "whitenoise.storage.CompressedManifestStaticFilesStorage"
|
||||||
|
)
|
||||||
STATIC_ROOT.mkdir(parents=True, exist_ok=True)
|
STATIC_ROOT.mkdir(parents=True, exist_ok=True)
|
||||||
MEDIA_URL = "media/"
|
MEDIA_URL = "media/"
|
||||||
MEDIA_ROOT: Path = BASE_DIR / "media"
|
MEDIA_ROOT: Path = BASE_DIR / "media"
|
||||||
|
|
@ -50,8 +58,6 @@ MEDIA_ROOT.mkdir(parents=True, exist_ok=True)
|
||||||
LOGIN_REDIRECT_URL = "/"
|
LOGIN_REDIRECT_URL = "/"
|
||||||
LOGOUT_REDIRECT_URL = "/"
|
LOGOUT_REDIRECT_URL = "/"
|
||||||
|
|
||||||
# Is True when running tests, used for not spamming Discord when new users are created
|
|
||||||
TESTING: bool = len(sys.argv) > 1 and sys.argv[1] == "test"
|
|
||||||
|
|
||||||
INSTALLED_APPS: list[str] = [
|
INSTALLED_APPS: list[str] = [
|
||||||
"feedvault.apps.FeedVaultConfig",
|
"feedvault.apps.FeedVaultConfig",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
|
@ -7,7 +8,7 @@ from django.http.response import HttpResponse
|
||||||
from django.test import Client, TestCase
|
from django.test import Client, TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from feedvault.models import Domain, Entry, Feed
|
from feedvault.models import Domain, Entry, Feed, UserUploadedFile
|
||||||
from feedvault.stats import get_db_size
|
from feedvault.stats import get_db_size
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
|
@ -63,17 +64,46 @@ class TestFeedsPage(TestCase):
|
||||||
|
|
||||||
|
|
||||||
class TestAddPage(TestCase):
|
class TestAddPage(TestCase):
|
||||||
|
def setUp(self) -> None:
|
||||||
|
"""Create a test user."""
|
||||||
|
self.user: User = User.objects.create_user(
|
||||||
|
username="testuser",
|
||||||
|
email="hello@feedvault.se",
|
||||||
|
password="testpassword", # noqa: S106
|
||||||
|
)
|
||||||
|
|
||||||
|
self.client.force_login(user=self.user)
|
||||||
|
|
||||||
def test_add_page(self) -> None:
|
def test_add_page(self) -> None:
|
||||||
"""Test if the add page is accessible."""
|
"""Test if the add page is accessible."""
|
||||||
response: HttpResponse = self.client.get(reverse("add"))
|
response: HttpResponse = self.client.post(reverse("add"), {"urls": "https://feedvault.se/feed.xml"})
|
||||||
assert response.status_code == 200, f"Expected 200, got {response.status_code}"
|
assert response.status_code == 200, f"Expected 200, got {response.status_code}"
|
||||||
|
|
||||||
|
|
||||||
class TestUploadPage(TestCase):
|
class TestUploadPage(TestCase):
|
||||||
|
def setUp(self) -> None:
|
||||||
|
"""Create a test user."""
|
||||||
|
self.user: User = User.objects.create_user(
|
||||||
|
username="testuser",
|
||||||
|
email="hello@feedvault.se",
|
||||||
|
password="testpassword", # noqa: S106
|
||||||
|
)
|
||||||
|
|
||||||
|
self.client.force_login(user=self.user)
|
||||||
|
|
||||||
def test_upload_page(self) -> None:
|
def test_upload_page(self) -> None:
|
||||||
"""Test if the upload page is accessible."""
|
"""Test if the upload page is accessible."""
|
||||||
response: HttpResponse = self.client.get(reverse("upload"))
|
# Check the amounts of files in the database
|
||||||
assert response.status_code == 200, f"Expected 200, got {response.status_code}"
|
assert UserUploadedFile.objects.count() == 0, f"Expected 0, got {UserUploadedFile.objects.count()}"
|
||||||
|
|
||||||
|
# Open this file and upload it
|
||||||
|
current_file = __file__
|
||||||
|
with Path(current_file).open("rb") as file:
|
||||||
|
response: HttpResponse = self.client.post(reverse("upload"), {"file": file})
|
||||||
|
assert response.status_code == 200, f"Expected 200, got {response.status_code}: {response.content}"
|
||||||
|
|
||||||
|
# Check if the file is in the database
|
||||||
|
assert UserUploadedFile.objects.count() == 1, f"Expected 1, got {UserUploadedFile.objects.count()}"
|
||||||
|
|
||||||
|
|
||||||
class TestRobotsPage(TestCase):
|
class TestRobotsPage(TestCase):
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ from django.views import View
|
||||||
from django.views.generic.edit import CreateView
|
from django.views.generic.edit import CreateView
|
||||||
from django.views.generic.list import ListView
|
from django.views.generic.list import ListView
|
||||||
|
|
||||||
from feedvault.add_feeds import add_url
|
from feedvault.feeds import add_url
|
||||||
from feedvault.models import Domain, Entry, Feed, FeedAddResult, UserUploadedFile
|
from feedvault.models import Domain, Entry, Feed, FeedAddResult, UserUploadedFile
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue