Merge app and project, use SQLite instead and

This commit is contained in:
Joakim Hellsén 2024-03-15 13:35:00 +01:00
commit 4c16d14e61
No known key found for this signature in database
GPG key ID: D196AE66FEBE1DC9
29 changed files with 221 additions and 454 deletions

View file

@ -1,4 +1,5 @@
POSTGRES_USER=
POSTGRES_PASSWORD=
POSTGRES_DB=feedvault
DEBUG=True
SECRET_KEY=
EMAIL_HOST_USER=
EMAIL_HOST_PASSWORD=
DISCORD_WEBHOOK_URL=

View file

@ -15,29 +15,15 @@ jobs:
ADMIN_EMAIL: 4153203+TheLovinator1@users.noreply.github.com
EMAIL_HOST_USER: ${{ secrets.EMAIL_HOST_USER }}
EMAIL_HOST_PASSWORD: ${{ secrets.EMAIL_HOST_PASSWORD }}
POSTGRES_PASSWORD: githubtest
POSTGRES_HOST: 127.0.0.1
POSTGRES_USER: feedvault
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: ${{ env.POSTGRES_PASSWORD }}
POSTGRES_HOST: ${{ env.POSTGRES_HOST }}
POSTGRES_USER: ${{ env.POSTGRES_USER }}
ports:
- 5432:5432
options: --health-cmd pg_isready --health-interval 1s --health-timeout 5s --health-retries 5
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: 3.12
- run: pipx install poetry
- run: pipx inject poetry poetry-plugin-export
- run: poetry install
- run: poetry run python manage.py migrate
# - run: poetry run python manage.py test
- run: poetry run python manage.py test
build:
runs-on: ubuntu-latest
permissions:

View file

@ -2,7 +2,7 @@
_A seed vault for your feeds._
FeedVault is an open-source web application that allows users to archive and search their favorite RSS, Atom, and JSON feeds. With FeedVault, users can effortlessly add their favorite feeds, ensuring they have a centralized location for accessing and preserving valuable content.
[FeedVault](https://feedvault.se/) is an open-source web application that allows users to archive and search their favorite RSS, Atom, and JSON feeds. With FeedVault, users can effortlessly add their favorite feeds, ensuring they have a centralized location for accessing and preserving valuable content.
## Features
@ -17,7 +17,7 @@ _Note: Some features are currently in development._
## Usage
- Visit the FeedVault website.
- [Visit the FeedVault website](https://feedvault.se/).
- Sign up for an account or log in if you already have one.
- Add your favorite feeds to start archiving content.
- Explore, manage, and enjoy your centralized feed archive.

View file

@ -1,26 +1,22 @@
services:
# Django - Web framework
feedvault: &feedvault
feedvault:
container_name: feedvault
image: ghcr.io/thelovinator1/feedvault:latest
user: "1000:1000"
restart: always
networks:
- feedvault_db
- feedvault_web
environment:
- SECRET_KEY=${SECRET_KEY}
- DEBUG=${DEBUG}
- ADMIN_EMAIL=${ADMIN_EMAIL}
- EMAIL_HOST_USER=${EMAIL_HOST_USER}
- EMAIL_HOST_PASSWORD=${EMAIL_HOST_PASSWORD}
- POSTGRES_HOST=feedvault_postgres
- POSTGRES_PORT=5432
- POSTGRES_USER=feedvault
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- DISCORD_WEBHOOK_URL=${DISCORD_WEBHOOK_URL}
volumes:
- /mnt/Fourteen/Docker/FeedVault/staticfiles:/app/staticfiles
- /mnt/Fourteen/Docker/FeedVault/media:/app/media
- /mnt/Fourteen/Docker/FeedVault/data:/app/data
# Nginx - Reverse proxy
web:
@ -51,27 +47,8 @@ services:
environment:
- TUNNEL_URL=http://feedvault_web:80
# Postgres - Database
postgres:
container_name: feedvault_postgres
image: postgres:16
user: "1000:1000"
ports:
- 5432:5432
restart: always
environment:
- POSTGRES_USER=feedvault
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=feedvault
volumes:
- /mnt/Fourteen/Docker/FeedVault/Postgres:/var/lib/postgresql/data
networks:
- feedvault_db
networks:
feedvault_tunnel:
driver: bridge
feedvault_db:
driver: bridge
feedvault_web:
driver: bridge

View file

@ -1,8 +0,0 @@
from django.apps import AppConfig
class FeedsConfig(AppConfig):
"""This Django app is responsible for managing the feeds."""
default_auto_field = "django.db.models.BigAutoField"
name = "feeds"

View file

@ -1,53 +0,0 @@
# Generated by Django 5.0.2 on 2024-02-23 05:27
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('feeds', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='author',
options={'ordering': ['name'], 'verbose_name': 'Author', 'verbose_name_plural': 'Authors'},
),
migrations.AlterModelOptions(
name='domain',
options={'ordering': ['name'], 'verbose_name': 'Domain', 'verbose_name_plural': 'Domains'},
),
migrations.AlterModelOptions(
name='entry',
options={'ordering': ['-created_parsed'], 'verbose_name': 'Entry', 'verbose_name_plural': 'Entries'},
),
migrations.AlterModelOptions(
name='feed',
options={'ordering': ['-created_at'], 'verbose_name': 'Feed', 'verbose_name_plural': 'Feeds'},
),
migrations.AlterModelOptions(
name='generator',
options={'ordering': ['name'], 'verbose_name': 'Feed generator', 'verbose_name_plural': 'Feed generators'},
),
migrations.AlterModelOptions(
name='links',
options={'ordering': ['href'], 'verbose_name': 'Link', 'verbose_name_plural': 'Links'},
),
migrations.AlterModelOptions(
name='publisher',
options={'ordering': ['name'], 'verbose_name': 'Publisher', 'verbose_name_plural': 'Publishers'},
),
migrations.AlterUniqueTogether(
name='author',
unique_together={('name', 'email', 'href')},
),
migrations.AlterUniqueTogether(
name='links',
unique_together={('href', 'rel')},
),
migrations.AlterUniqueTogether(
name='publisher',
unique_together={('name', 'email', 'href')},
),
]

View file

@ -1,21 +0,0 @@
# 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),
),
]

View file

@ -1,58 +0,0 @@
from __future__ import annotations
from django.contrib.sitemaps import GenericSitemap
from django.contrib.sitemaps.views import sitemap
from django.urls import URLPattern, path
from django.views.decorators.cache import cache_page
from feeds import views
from feeds.models import Domain, Feed
from feeds.sitemaps import StaticViewSitemap
from .views import APIView, CustomLoginView, CustomLogoutView, ProfileView, RegisterView
app_name: str = "feeds"
sitemaps = {
"static": StaticViewSitemap,
"feeds": GenericSitemap({"queryset": Feed.objects.all(), "date_field": "created_at"}),
"domains": GenericSitemap({"queryset": Domain.objects.all(), "date_field": "created_at"}),
}
# Normal pages
urlpatterns: list[URLPattern] = [
path(route="", view=views.IndexView.as_view(), name="index"),
path(route="feed/<int:feed_id>/", view=views.FeedView.as_view(), name="feed"),
path(route="feeds/", view=views.FeedsView.as_view(), name="feeds"),
path(route="add", view=views.AddView.as_view(), name="add"),
path(route="upload", view=views.UploadView.as_view(), name="upload"),
path(route="robots.txt", view=cache_page(timeout=60 * 60 * 365)(views.RobotsView.as_view()), name="robots"),
path(
"sitemap.xml",
sitemap,
{"sitemaps": sitemaps},
name="django.contrib.sitemaps.views.sitemap",
),
path(route="domains/", view=views.DomainsView.as_view(), name="domains"),
path(route="domain/<int:domain_id>/", view=views.DomainView.as_view(), name="domain"),
]
# API urls
urlpatterns += [
path(route="api/", view=APIView.as_view(), name="api"),
path(route="api/feeds/", view=views.APIFeedsView.as_view(), name="api_feeds"),
path(route="api/feeds/<int:feed_id>/", view=views.APIFeedView.as_view(), name="api_feeds_id"),
path(route="api/feeds/<int:feed_id>/entries/", view=views.APIFeedEntriesView.as_view(), name="api_feed_entries"),
path(route="api/entries/", view=views.APIEntriesView.as_view(), name="api_entries"),
path(route="api/entries/<int:entry_id>/", view=views.APIEntryView.as_view(), name="api_entries_id"),
]
# Account urls
urlpatterns += [
path(route="accounts/login/", view=CustomLoginView.as_view(), name="login"),
path(route="accounts/register/", view=RegisterView.as_view(), name="register"),
path(route="accounts/logout/", view=CustomLogoutView.as_view(), name="logout"),
# path(route="accounts/change-password/", view=CustomPasswordChangeView.as_view(), name="change_password"),
path(route="accounts/profile/", view=ProfileView.as_view(), name="profile"),
]

View file

@ -10,7 +10,7 @@ import feedparser
from django.utils import timezone
from feedparser import FeedParserDict
from feeds.models import Author, Domain, Entry, Feed, Generator, Publisher
from feedvault.models import Author, Domain, Entry, Feed, Generator, Publisher
if TYPE_CHECKING:
from django.contrib.auth.models import AbstractBaseUser, AnonymousUser
@ -129,7 +129,7 @@ def parse_feed(url: str | None) -> dict | None:
Returns:
The parsed feed.
"""
# TODO(TheLovinator): Backup the feed URL to a cloudflare worker. # noqa: TD003
# TODO(TheLovinator): Backup the feed URL. # noqa: TD003
if not url:
return None
@ -299,7 +299,7 @@ def add_feed(url: str | None, user: AbstractBaseUser | AnonymousUser) -> Feed |
try:
feed.save()
except Exception:
logger.exception("Error saving feed: %s", feed)
logger.exception("Got exception while saving feed: %s", url)
return None
entries = parsed_feed.get("entries", [])

8
feedvault/apps.py Normal file
View file

@ -0,0 +1,8 @@
from django.apps import AppConfig
class FeedVaultConfig(AppConfig):
"""FeedVault app configuration."""
default_auto_field: str = "django.db.models.BigAutoField"
name: str = "feedvault"

View file

@ -15,7 +15,7 @@ def add_global_context(request: HttpRequest) -> dict[str, str | int]: # noqa: A
Returns:
A dictionary with the global context.
"""
from feeds.stats import get_db_size # noqa: PLC0415
from feedvault.stats import get_db_size # noqa: PLC0415
from .models import Feed # noqa: PLC0415

View file

@ -1,6 +1,7 @@
# Generated by Django 5.0.2 on 2024-02-19 02:47
# Generated by Django 5.0.3 on 2024-03-15 01:27
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
@ -9,20 +10,10 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Author',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('modified_at', models.DateTimeField(auto_now=True)),
('name', models.TextField(blank=True)),
('href', models.TextField(blank=True)),
('email', models.TextField(blank=True)),
],
),
migrations.CreateModel(
name='Domain',
fields=[
@ -36,21 +27,14 @@ class Migration(migrations.Migration):
('hidden_at', models.DateTimeField(blank=True, null=True)),
('hidden_reason', models.TextField(blank=True)),
],
options={
'verbose_name': 'Domain',
'verbose_name_plural': 'Domains',
'ordering': ['name'],
},
),
migrations.CreateModel(
name='Links',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('modified_at', models.DateTimeField(auto_now=True)),
('rel', models.TextField(blank=True)),
('type', models.TextField(blank=True)),
('href', models.TextField(blank=True)),
('title', models.TextField(blank=True)),
],
),
migrations.CreateModel(
name='Publisher',
name='Author',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
@ -59,6 +43,12 @@ class Migration(migrations.Migration):
('href', models.TextField(blank=True)),
('email', models.TextField(blank=True)),
],
options={
'verbose_name': 'Author',
'verbose_name_plural': 'Authors',
'ordering': ['name'],
'unique_together': {('name', 'email', 'href')},
},
),
migrations.CreateModel(
name='Generator',
@ -71,9 +61,47 @@ class Migration(migrations.Migration):
('version', models.TextField(blank=True)),
],
options={
'verbose_name': 'Feed generator',
'verbose_name_plural': 'Feed generators',
'ordering': ['name'],
'unique_together': {('name', 'version', 'href')},
},
),
migrations.CreateModel(
name='Links',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('modified_at', models.DateTimeField(auto_now=True)),
('rel', models.TextField(blank=True)),
('type', models.TextField(blank=True)),
('href', models.TextField(blank=True)),
('title', models.TextField(blank=True)),
],
options={
'verbose_name': 'Link',
'verbose_name_plural': 'Links',
'ordering': ['href'],
'unique_together': {('href', 'rel')},
},
),
migrations.CreateModel(
name='Publisher',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('modified_at', models.DateTimeField(auto_now=True)),
('name', models.TextField(blank=True)),
('href', models.TextField(blank=True)),
('email', models.TextField(blank=True)),
],
options={
'verbose_name': 'Publisher',
'verbose_name_plural': 'Publishers',
'ordering': ['name'],
'unique_together': {('name', 'email', 'href')},
},
),
migrations.CreateModel(
name='Feed',
fields=[
@ -123,11 +151,17 @@ class Migration(migrations.Migration):
('ttl', models.TextField(blank=True)),
('updated', models.TextField(blank=True)),
('updated_parsed', models.DateTimeField(blank=True, null=True)),
('author_detail', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='feeds', to='feeds.author')),
('domain', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='feeds.domain')),
('generator_detail', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='feeds', to='feeds.generator')),
('publisher_detail', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='feeds', to='feeds.publisher')),
('author_detail', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='feeds', to='feedvault.author')),
('domain', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='feedvault.domain')),
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
('generator_detail', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='feeds', to='feedvault.generator')),
('publisher_detail', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='feeds', to='feedvault.publisher')),
],
options={
'verbose_name': 'Feed',
'verbose_name_plural': 'Feeds',
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='Entry',
@ -159,9 +193,14 @@ class Migration(migrations.Migration):
('title_detail', models.JSONField(blank=True, null=True)),
('updated', models.TextField(blank=True)),
('updated_parsed', models.DateTimeField(blank=True, null=True)),
('author_detail', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='entries', to='feeds.author')),
('feed', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='feeds.feed')),
('publisher_detail', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='entries', to='feeds.publisher')),
('author_detail', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='entries', to='feedvault.author')),
('feed', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='feedvault.feed')),
('publisher_detail', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='entries', to='feedvault.publisher')),
],
options={
'verbose_name': 'Entry',
'verbose_name_plural': 'Entries',
'ordering': ['-created_parsed'],
},
),
]

View file

@ -7,22 +7,43 @@ from dotenv import find_dotenv, load_dotenv
load_dotenv(dotenv_path=find_dotenv(), verbose=True)
# Run Django in debug mode
DEBUG: bool = os.getenv(key="DEBUG", default="True").lower() == "true"
BASE_DIR: Path = Path(__file__).resolve().parent.parent
# The secret key is used for cryptographic signing, and should be set to a unique, unpredictable value.
SECRET_KEY: str = os.getenv("SECRET_KEY", default="")
ADMINS: list[tuple[str, str]] = [("Joakim Hellsén", "django@feedvault.se")]
ALLOWED_HOSTS: list[str] = [".feedvault.se", ".localhost", "127.0.0.1"]
CSRF_COOKIE_DOMAIN = ".feedvault.se"
CSRF_TRUSTED_ORIGINS: list[str] = ["https://feedvault.se", "https://www.feedvault.se"]
TIME_ZONE = "Europe/Stockholm"
USE_TZ = True
USE_I18N = False
LANGUAGE_CODE = "en-us"
DECIMAL_SEPARATOR = ","
THOUSAND_SEPARATOR = " "
EMAIL_HOST = "smtp.gmail.com"
EMAIL_PORT = 587
EMAIL_USE_TLS = True
EMAIL_HOST_USER: str = os.getenv(key="EMAIL_HOST_USER", default="webmaster@localhost")
EMAIL_HOST_PASSWORD: str = os.getenv(key="EMAIL_HOST_PASSWORD", default="")
EMAIL_SUBJECT_PREFIX = "[FeedVault] "
EMAIL_USE_LOCALTIME = True
EMAIL_TIMEOUT = 10
DEFAULT_FROM_EMAIL: str = os.getenv(key="EMAIL_HOST_USER", default="webmaster@localhost")
SERVER_EMAIL: str = os.getenv(key="EMAIL_HOST_USER", default="webmaster@localhost")
USE_X_FORWARDED_HOST = True
INTERNAL_IPS: list[str] = ["127.0.0.1", "localhost"]
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
SITE_ID = 1
PASSWORD_HASHERS: list[str] = ["django.contrib.auth.hashers.Argon2PasswordHasher"]
ROOT_URLCONF = "feedvault.urls"
WSGI_APPLICATION = "feedvault.wsgi.application"
INSTALLED_APPS: list[str] = [
"feeds.apps.FeedsConfig",
"feedvault.apps.FeedVaultConfig",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"django.contrib.sitemaps",
]
@ -36,22 +57,15 @@ MIDDLEWARE: list[str] = [
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
ROOT_URLCONF = "feedvault.urls"
WSGI_APPLICATION = "feedvault.wsgi.application"
# Database
# https://docs.djangoproject.com/en/5.0/ref/settings/#databases
DATABASES: dict[str, dict[str, str]] = {
database_folder: Path = BASE_DIR / "data"
database_folder.mkdir(parents=True, exist_ok=True)
DATABASES: dict[str, dict[str, str | Path | bool]] = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": "feedvault",
"USER": os.getenv(key="POSTGRES_USER", default=""),
"PASSWORD": os.getenv(key="POSTGRES_PASSWORD", default=""),
"HOST": os.getenv(key="POSTGRES_HOST", default=""),
"PORT": os.getenv(key="POSTGRES_PORT", default="5432"),
"ENGINE": "django.db.backends.sqlite3",
"NAME": database_folder / "feedvault.sqlite3",
"ATOMIC_REQUESTS": True,
},
}
@ -86,7 +100,7 @@ TEMPLATES = [
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
"feeds.context_processors.add_global_context",
"feedvault.context_processors.add_global_context",
],
"loaders": [
(
@ -100,62 +114,3 @@ TEMPLATES = [
},
},
]
# A list of all the people who get code error notifications. When DEBUG=False and a view raises an exception, Django
ADMINS: list[tuple[str, str]] = [("Joakim Hellsén", "django@feedvault.se")]
# A list of strings representing the host/domain names that this Django site can serve.
# .feedvault.se will match *.feedvault.se and feedvault.se
ALLOWED_HOSTS: list[str] = [".feedvault.se", ".localhost", "127.0.0.1"]
CSRF_COOKIE_DOMAIN = ".feedvault.se"
CSRF_TRUSTED_ORIGINS: list[str] = ["https://feedvault.se", "https://www.feedvault.se"]
# The time zone that Django will use to display datetimes in templates and to interpret datetimes entered in forms
TIME_ZONE = "Europe/Stockholm"
# If datetimes will be timezone-aware by default. If True, Django will use timezone-aware datetimes internally.
USE_TZ = True
# Don't use Django's translation system
USE_I18N = False
# Decides which translation is served to all users.
LANGUAGE_CODE = "en-us"
# Default decimal separator used when formatting decimal numbers.
DECIMAL_SEPARATOR = ","
# Use a space as the thousand separator instead of a comma
THOUSAND_SEPARATOR = " "
# Use gmail for sending emails
EMAIL_HOST = "smtp.gmail.com"
EMAIL_PORT = 587
EMAIL_USE_TLS = True
EMAIL_HOST_USER: str = os.getenv(key="EMAIL_HOST_USER", default="webmaster@localhost")
EMAIL_HOST_PASSWORD: str = os.getenv(key="EMAIL_HOST_PASSWORD", default="")
EMAIL_SUBJECT_PREFIX = "[FeedVault] "
EMAIL_USE_LOCALTIME = True
EMAIL_TIMEOUT = 10
DEFAULT_FROM_EMAIL: str = os.getenv(key="EMAIL_HOST_USER", default="webmaster@localhost")
SERVER_EMAIL: str = os.getenv(key="EMAIL_HOST_USER", default="webmaster@localhost")
# Use the X-Forwarded-Host header
# USE_X_FORWARDED_HOST = True
# Set the Referrer Policy HTTP header on all responses that do not already have one.
# SECURE_REFERRER_POLICY = "strict-origin-when-cross-origin"
# Internal IPs that are allowed to see debug views
INTERNAL_IPS: list[str] = ["127.0.0.1", "localhost"]
STATIC_URL = "static/"
STATIC_ROOT: Path = BASE_DIR / "staticfiles"
STATICFILES_DIRS: list[Path] = [BASE_DIR / "static"]
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
# Our site ID
SITE_ID = 1
PASSWORD_HASHERS: list[str] = ["django.contrib.auth.hashers.Argon2PasswordHasher"]

View file

@ -10,10 +10,10 @@ class StaticViewSitemap(Sitemap):
changefreq: str = "daily"
priority: float = 0.5
def items(self: StaticViewSitemap) -> list[str]:
def items(self: StaticViewSitemap) -> list[str]: # noqa: PLR6301
"""Return all the items in the sitemap."""
return ["feeds:index", "feeds:feeds", "feeds:domains"]
def location(self, item: str) -> str:
def location(self: StaticViewSitemap, item: str) -> str: # noqa: PLR6301
"""Return the location of the item."""
return reverse(item)

View file

@ -21,13 +21,18 @@ def get_db_size() -> str:
logger.debug("Got db_size from cache")
return db_size
# Get SQLite database size
with connection.cursor() as cursor:
cursor.execute("SELECT pg_size_pretty(pg_database_size(current_database()))")
row = cursor.fetchone()
cursor.execute("PRAGMA page_size")
page_size_result = cursor.fetchone()
page_size = page_size_result[0] if page_size_result else None
db_size = "0 MB" if row is None else str(row[0])
cursor.execute("PRAGMA page_count")
page_count_result = cursor.fetchone()
page_count = page_count_result[0] if page_count_result else None
db_size = page_size * page_count if page_size and page_count else None
# Store value in cache for 15 minutes
cache.set("db_size", db_size, 60 * 15)
return db_size
return f"{db_size / 1024 / 1024:.2f} MB" if db_size is not None else "0 MB"

View file

@ -1,5 +1,48 @@
from django.urls import include, path
from __future__ import annotations
urlpatterns = [
path("", include("feeds.urls")),
from django.contrib.sitemaps import GenericSitemap
from django.contrib.sitemaps.views import sitemap
from django.urls import URLPattern, path
from django.views.decorators.cache import cache_page
from feedvault import views
from feedvault.models import Domain, Feed
from feedvault.sitemaps import StaticViewSitemap
from feedvault.views import APIView, CustomLoginView, CustomLogoutView, ProfileView, RegisterView
app_name: str = "feedvault"
sitemaps = {
"static": StaticViewSitemap,
"feeds": GenericSitemap({"queryset": Feed.objects.all(), "date_field": "created_at"}),
"domains": GenericSitemap({"queryset": Domain.objects.all(), "date_field": "created_at"}),
}
urlpatterns: list[URLPattern] = [
path(route="", view=views.IndexView.as_view(), name="index"),
path(route="feed/<int:feed_id>/", view=views.FeedView.as_view(), name="feed"),
path(route="feeds/", view=views.FeedsView.as_view(), name="feeds"),
path(route="add", view=views.AddView.as_view(), name="add"),
path(route="upload", view=views.UploadView.as_view(), name="upload"),
path(route="robots.txt", view=cache_page(timeout=60 * 60 * 365)(views.RobotsView.as_view()), name="robots"),
path(
"sitemap.xml",
sitemap,
{"sitemaps": sitemaps},
name="django.contrib.sitemaps.views.sitemap",
),
path(route="domains/", view=views.DomainsView.as_view(), name="domains"),
path(route="domain/<int:domain_id>/", view=views.DomainView.as_view(), name="domain"),
path(route="api/", view=APIView.as_view(), name="api"),
path(route="api/feeds/", view=views.APIFeedsView.as_view(), name="api_feeds"),
path(route="api/feeds/<int:feed_id>/", view=views.APIFeedView.as_view(), name="api_feeds_id"),
path(route="api/feeds/<int:feed_id>/entries/", view=views.APIFeedEntriesView.as_view(), name="api_feed_entries"),
path(route="api/entries/", view=views.APIEntriesView.as_view(), name="api_entries"),
path(route="api/entries/<int:entry_id>/", view=views.APIEntryView.as_view(), name="api_entries_id"),
path(route="accounts/login/", view=CustomLoginView.as_view(), name="login"),
path(route="accounts/register/", view=RegisterView.as_view(), name="register"),
path(route="accounts/logout/", view=CustomLogoutView.as_view(), name="logout"),
# path(route="accounts/change-password/", view=CustomPasswordChangeView.as_view(), name="change_password"),
path(route="accounts/profile/", view=ProfileView.as_view(), name="profile"),
]

View file

@ -17,8 +17,8 @@ from django.views import View
from django.views.generic.edit import CreateView
from django.views.generic.list import ListView
from feeds.add_feeds import add_feed
from feeds.models import Domain, Entry, Feed
from feedvault.add_feeds import add_feed
from feedvault.models import Domain, Entry, Feed
if TYPE_CHECKING:
from django.contrib.auth.models import User
@ -188,7 +188,7 @@ class RegisterView(CreateView):
template_name = "accounts/register.html"
form_class = UserCreationForm
success_url = reverse_lazy("feeds:login")
success_url = reverse_lazy("login")
# Add context data to the view
def get_context_data(self, **kwargs) -> dict: # noqa: ANN003
@ -205,14 +205,14 @@ class RegisterView(CreateView):
class CustomLogoutView(LogoutView):
"""Logout view."""
next_page = "feeds:index" # Redirect to index after logout
next_page = "index" # Redirect to index after logout
class CustomPasswordChangeView(SuccessMessageMixin, PasswordChangeView):
"""Custom password change view."""
template_name = "accounts/change_password.html"
success_url = reverse_lazy("feeds:index")
success_url = reverse_lazy("index")
success_message = "Your password was successfully updated!"
# Add context data to the view

125
poetry.lock generated
View file

@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
[[package]]
name = "argon2-cffi"
@ -302,13 +302,13 @@ six = ">=1.13.0"
[[package]]
name = "json5"
version = "0.9.17"
version = "0.9.22"
description = "A Python implementation of the JSON5 data format."
optional = false
python-versions = ">=3.8"
files = [
{file = "json5-0.9.17-py2.py3-none-any.whl", hash = "sha256:f8ec1ecf985951d70f780f6f877c4baca6a47b6e61e02c4cd190138d10a7805a"},
{file = "json5-0.9.17.tar.gz", hash = "sha256:717d99d657fa71b7094877b1d921b1cce40ab444389f6d770302563bb7dfd9ae"},
{file = "json5-0.9.22-py3-none-any.whl", hash = "sha256:6621007c70897652f8b5d03885f732771c48d1925591ad989aa80c7e0e5ad32f"},
{file = "json5-0.9.22.tar.gz", hash = "sha256:b729bde7650b2196a35903a597d2b704b8fdf8648bfb67368cfb79f1174a17bd"},
]
[package.extras]
@ -316,13 +316,13 @@ dev = ["hypothesis"]
[[package]]
name = "packaging"
version = "23.2"
version = "24.0"
description = "Core utilities for Python packages"
optional = false
python-versions = ">=3.7"
files = [
{file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"},
{file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"},
{file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"},
{file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"},
]
[[package]]
@ -336,104 +336,6 @@ files = [
{file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
]
[[package]]
name = "psycopg"
version = "3.1.18"
description = "PostgreSQL database adapter for Python"
optional = false
python-versions = ">=3.7"
files = [
{file = "psycopg-3.1.18-py3-none-any.whl", hash = "sha256:4d5a0a5a8590906daa58ebd5f3cfc34091377354a1acced269dd10faf55da60e"},
{file = "psycopg-3.1.18.tar.gz", hash = "sha256:31144d3fb4c17d78094d9e579826f047d4af1da6a10427d91dfcfb6ecdf6f12b"},
]
[package.dependencies]
psycopg-binary = {version = "3.1.18", optional = true, markers = "implementation_name != \"pypy\" and extra == \"binary\""}
typing-extensions = ">=4.1"
tzdata = {version = "*", markers = "sys_platform == \"win32\""}
[package.extras]
binary = ["psycopg-binary (==3.1.18)"]
c = ["psycopg-c (==3.1.18)"]
dev = ["black (>=24.1.0)", "codespell (>=2.2)", "dnspython (>=2.1)", "flake8 (>=4.0)", "mypy (>=1.4.1)", "types-setuptools (>=57.4)", "wheel (>=0.37)"]
docs = ["Sphinx (>=5.0)", "furo (==2022.6.21)", "sphinx-autobuild (>=2021.3.14)", "sphinx-autodoc-typehints (>=1.12)"]
pool = ["psycopg-pool"]
test = ["anyio (>=3.6.2,<4.0)", "mypy (>=1.4.1)", "pproxy (>=2.7)", "pytest (>=6.2.5)", "pytest-cov (>=3.0)", "pytest-randomly (>=3.5)"]
[[package]]
name = "psycopg-binary"
version = "3.1.18"
description = "PostgreSQL database adapter for Python -- C optimisation distribution"
optional = false
python-versions = ">=3.7"
files = [
{file = "psycopg_binary-3.1.18-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5c323103dfa663b88204cf5f028e83c77d7a715f9b6f51d2bbc8184b99ddd90a"},
{file = "psycopg_binary-3.1.18-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:887f8d856c91510148be942c7acd702ccf761a05f59f8abc123c22ab77b5a16c"},
{file = "psycopg_binary-3.1.18-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d322ba72cde4ca2eefc2196dad9ad7e52451acd2f04e3688d590290625d0c970"},
{file = "psycopg_binary-3.1.18-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:489aa4fe5a0b653b68341e9e44af247dedbbc655326854aa34c163ef1bcb3143"},
{file = "psycopg_binary-3.1.18-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55ff0948457bfa8c0d35c46e3a75193906d1c275538877ba65907fd67aa059ad"},
{file = "psycopg_binary-3.1.18-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b15e3653c82384b043d820fc637199b5c6a36b37fa4a4943e0652785bb2bad5d"},
{file = "psycopg_binary-3.1.18-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f8ff3bc08b43f36fdc24fedb86d42749298a458c4724fb588c4d76823ac39f54"},
{file = "psycopg_binary-3.1.18-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:1729d0e3dfe2546d823841eb7a3d003144189d6f5e138ee63e5227f8b75276a5"},
{file = "psycopg_binary-3.1.18-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:13bcd3742112446037d15e360b27a03af4b5afcf767f5ee374ef8f5dd7571b31"},
{file = "psycopg_binary-3.1.18-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:320047e3d3554b857e16c2b6b615a85e0db6a02426f4d203a4594a2f125dfe57"},
{file = "psycopg_binary-3.1.18-cp310-cp310-win_amd64.whl", hash = "sha256:888a72c2aca4316ca6d4a619291b805677bae99bba2f6e31a3c18424a48c7e4d"},
{file = "psycopg_binary-3.1.18-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4e4de16a637ec190cbee82e0c2dc4860fed17a23a35f7a1e6dc479a5c6876722"},
{file = "psycopg_binary-3.1.18-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6432047b8b24ef97e3fbee1d1593a0faaa9544c7a41a2c67d1f10e7621374c83"},
{file = "psycopg_binary-3.1.18-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d684227ef8212e27da5f2aff9d4d303cc30b27ac1702d4f6881935549486dd5"},
{file = "psycopg_binary-3.1.18-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:67284e2e450dc7a9e4d76e78c0bd357dc946334a3d410defaeb2635607f632cd"},
{file = "psycopg_binary-3.1.18-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c9b6bd7fb5c6638cb32469674707649b526acfe786ba6d5a78ca4293d87bae4"},
{file = "psycopg_binary-3.1.18-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7121acc783c4e86d2d320a7fb803460fab158a7f0a04c5e8c5d49065118c1e73"},
{file = "psycopg_binary-3.1.18-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e28ff8f3de7b56588c2a398dc135fd9f157d12c612bd3daa7e6ba9872337f6f5"},
{file = "psycopg_binary-3.1.18-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c84a0174109f329eeda169004c7b7ca2e884a6305acab4a39600be67f915ed38"},
{file = "psycopg_binary-3.1.18-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:531381f6647fc267383dca88dbe8a70d0feff433a8e3d0c4939201fea7ae1b82"},
{file = "psycopg_binary-3.1.18-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b293e01057e63c3ac0002aa132a1071ce0fdb13b9ee2b6b45d3abdb3525c597d"},
{file = "psycopg_binary-3.1.18-cp311-cp311-win_amd64.whl", hash = "sha256:780a90bcb69bf27a8b08bc35b958e974cb6ea7a04cdec69e737f66378a344d68"},
{file = "psycopg_binary-3.1.18-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:87dd9154b757a5fbf6d590f6f6ea75f4ad7b764a813ae04b1d91a70713f414a1"},
{file = "psycopg_binary-3.1.18-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f876ebbf92db70125f6375f91ab4bc6b27648aa68f90d661b1fc5affb4c9731c"},
{file = "psycopg_binary-3.1.18-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:258d2f0cb45e4574f8b2fe7c6d0a0e2eb58903a4fd1fbaf60954fba82d595ab7"},
{file = "psycopg_binary-3.1.18-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd27f713f2e5ef3fd6796e66c1a5203a27a30ecb847be27a78e1df8a9a5ae68c"},
{file = "psycopg_binary-3.1.18-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c38a4796abf7380f83b1653c2711cb2449dd0b2e5aca1caa75447d6fa5179c69"},
{file = "psycopg_binary-3.1.18-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2f7f95746efd1be2dc240248cc157f4315db3fd09fef2adfcc2a76e24aa5741"},
{file = "psycopg_binary-3.1.18-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4085f56a8d4fc8b455e8f44380705c7795be5317419aa5f8214f315e4205d804"},
{file = "psycopg_binary-3.1.18-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:2e2484ae835dedc80cdc7f1b1a939377dc967fed862262cfd097aa9f50cade46"},
{file = "psycopg_binary-3.1.18-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:3c2b039ae0c45eee4cd85300ef802c0f97d0afc78350946a5d0ec77dd2d7e834"},
{file = "psycopg_binary-3.1.18-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f54978c4b646dec77fefd8485fa82ec1a87807f334004372af1aaa6de9539a5"},
{file = "psycopg_binary-3.1.18-cp312-cp312-win_amd64.whl", hash = "sha256:9ffcbbd389e486d3fd83d30107bbf8b27845a295051ccabde240f235d04ed921"},
{file = "psycopg_binary-3.1.18-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c76659ae29a84f2c14f56aad305dd00eb685bd88f8c0a3281a9a4bc6bd7d2aa7"},
{file = "psycopg_binary-3.1.18-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c7afcd6f1d55992f26d9ff7b0bd4ee6b475eb43aa3f054d67d32e09f18b0065"},
{file = "psycopg_binary-3.1.18-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:639dd78ac09b144b0119076783cb64e1128cc8612243e9701d1503c816750b2e"},
{file = "psycopg_binary-3.1.18-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e1cf59e0bb12e031a48bb628aae32df3d0c98fd6c759cb89f464b1047f0ca9c8"},
{file = "psycopg_binary-3.1.18-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e262398e5d51563093edf30612cd1e20fedd932ad0994697d7781ca4880cdc3d"},
{file = "psycopg_binary-3.1.18-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:59701118c7d8842e451f1e562d08e8708b3f5d14974eefbce9374badd723c4ae"},
{file = "psycopg_binary-3.1.18-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:dea4a59da7850192fdead9da888e6b96166e90608cf39e17b503f45826b16f84"},
{file = "psycopg_binary-3.1.18-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:4575da95fc441244a0e2ebaf33a2b2f74164603341d2046b5cde0a9aa86aa7e2"},
{file = "psycopg_binary-3.1.18-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:812726266ab96de681f2c7dbd6b734d327f493a78357fcc16b2ac86ff4f4e080"},
{file = "psycopg_binary-3.1.18-cp37-cp37m-win_amd64.whl", hash = "sha256:3e7ce4d988112ca6c75765c7f24c83bdc476a6a5ce00878df6c140ca32c3e16d"},
{file = "psycopg_binary-3.1.18-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:02bd4da45d5ee9941432e2e9bf36fa71a3ac21c6536fe7366d1bd3dd70d6b1e7"},
{file = "psycopg_binary-3.1.18-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:39242546383f6b97032de7af30edb483d237a0616f6050512eee7b218a2aa8ee"},
{file = "psycopg_binary-3.1.18-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d46ae44d66bf6058a812467f6ae84e4e157dee281bfb1cfaeca07dee07452e85"},
{file = "psycopg_binary-3.1.18-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ad35ac7fd989184bf4d38a87decfb5a262b419e8ba8dcaeec97848817412c64a"},
{file = "psycopg_binary-3.1.18-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:247474af262bdd5559ee6e669926c4f23e9cf53dae2d34c4d991723c72196404"},
{file = "psycopg_binary-3.1.18-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ebecbf2406cd6875bdd2453e31067d1bd8efe96705a9489ef37e93b50dc6f09"},
{file = "psycopg_binary-3.1.18-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:1859aeb2133f5ecdd9cbcee155f5e38699afc06a365f903b1512c765fd8d457e"},
{file = "psycopg_binary-3.1.18-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:da917f6df8c6b2002043193cb0d74cc173b3af7eb5800ad69c4e1fbac2a71c30"},
{file = "psycopg_binary-3.1.18-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:9e24e7b6a68a51cc3b162d0339ae4e1263b253e887987d5c759652f5692b5efe"},
{file = "psycopg_binary-3.1.18-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e252d66276c992319ed6cd69a3ffa17538943954075051e992143ccbf6dc3d3e"},
{file = "psycopg_binary-3.1.18-cp38-cp38-win_amd64.whl", hash = "sha256:5d6e860edf877d4413e4a807e837d55e3a7c7df701e9d6943c06e460fa6c058f"},
{file = "psycopg_binary-3.1.18-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eea5f14933177ffe5c40b200f04f814258cc14b14a71024ad109f308e8bad414"},
{file = "psycopg_binary-3.1.18-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:824a1bfd0db96cc6bef2d1e52d9e0963f5bf653dd5bc3ab519a38f5e6f21c299"},
{file = "psycopg_binary-3.1.18-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a87e9eeb80ce8ec8c2783f29bce9a50bbcd2e2342a340f159c3326bf4697afa1"},
{file = "psycopg_binary-3.1.18-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91074f78a9f890af5f2c786691575b6b93a4967ad6b8c5a90101f7b8c1a91d9c"},
{file = "psycopg_binary-3.1.18-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e05f6825f8db4428782135e6986fec79b139210398f3710ed4aa6ef41473c008"},
{file = "psycopg_binary-3.1.18-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f68ac2364a50d4cf9bb803b4341e83678668f1881a253e1224574921c69868c"},
{file = "psycopg_binary-3.1.18-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7ac1785d67241d5074f8086705fa68e046becea27964267ab3abd392481d7773"},
{file = "psycopg_binary-3.1.18-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:cd2a9f7f0d4dacc5b9ce7f0e767ae6cc64153264151f50698898c42cabffec0c"},
{file = "psycopg_binary-3.1.18-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:3e4b0bb91da6f2238dbd4fbb4afc40dfb4f045bb611b92fce4d381b26413c686"},
{file = "psycopg_binary-3.1.18-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:74e498586b72fb819ca8ea82107747d0cb6e00ae685ea6d1ab3f929318a8ce2d"},
{file = "psycopg_binary-3.1.18-cp39-cp39-win_amd64.whl", hash = "sha256:d4422af5232699f14b7266a754da49dc9bcd45eba244cf3812307934cd5d6679"},
]
[[package]]
name = "pycparser"
version = "2.21"
@ -704,17 +606,6 @@ notebook = ["ipywidgets (>=6)"]
slack = ["slack-sdk"]
telegram = ["requests"]
[[package]]
name = "typing-extensions"
version = "4.9.0"
description = "Backported and Experimental Type Hints for Python 3.8+"
optional = false
python-versions = ">=3.8"
files = [
{file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"},
{file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"},
]
[[package]]
name = "tzdata"
version = "2024.1"
@ -729,4 +620,4 @@ files = [
[metadata]
lock-version = "2.0"
python-versions = "^3.12"
content-hash = "1fee2dcd23f6eebc52722c0bb70c478026ec820e2999ebfd63295fc1d0395f08"
content-hash = "dd4d8ba16bb5e34d2e0f94009d4ea86de094a6b1d6d1af3e6b69c14e881ccf3e"

View file

@ -7,10 +7,9 @@ readme = "README.md"
[tool.poetry.dependencies]
python = "^3.12"
django = {extras = ["argon2"], version = "^5.0.2"}
django = { extras = ["argon2"], version = "^5.0.3" }
python-dotenv = "^1.0.1"
feedparser = "^6.0.11"
psycopg = {extras = ["binary"], version = "^3.1.18"}
gunicorn = "^21.2.0"
[tool.poetry.group.dev.dependencies]
@ -26,17 +25,21 @@ exclude = ["migrations"]
fix = true
unsafe-fixes = true
preview = true
line-length = 120
lint.select = ["ALL"]
lint.ignore = [
"PLR6301", # Checks for the presence of unused self parameter in methods definitions
"CPY001", # Missing copyright notice at top of file
"ERA001", # Found commented-out code
"FIX002", # Line contains TODO
"D104", # Missing docstring in public package # TODO(TheLovinator): Fix this
"D100", # Missing docstring in public module # TODO(TheLovinator): Fix this
# https://github.com/TheLovinator1/panso.se/issues/25
"D100", # Checks for undocumented public module definitions.
"D101", # Checks for undocumented public class definitions.
"D102", # Checks for undocumented public method definitions.
"D104", # Missing docstring in public package.
"D105", # Missing docstring in magic method.
"D106", # Checks for undocumented public class definitions, for nested classes.
"COM812", # Checks for the absence of trailing commas.
"ISC001", # Checks for implicitly concatenated strings on a single line.
]
line-length = 120
[tool.ruff.lint.pydocstyle]
convention = "google"
@ -51,6 +54,5 @@ convention = "google"
"PLR6301", # Checks for the presence of unused self parameter in methods definitions.
]
[tool.djlint]
format_attribute_template_tags = true

View file

@ -1,7 +1,7 @@
{% extends "base.html" %}
{% block content %}
<p>
You can register <a href='{% url "feeds:register" %}'>here</a>.
You can register <a href="{% url 'register' %}">here</a>.
</p>
<h2>Login</h2>
<form method="post">

View file

@ -82,7 +82,7 @@
{% endif %}
<span class="title">
<h1>
<a href='{% url "feeds:index" %}'>FeedVault</a>
<a href="{% url 'index' %}">FeedVault</a>
</h1>
</span>
<div class="leftright">
@ -104,21 +104,21 @@
<small>
<div class="leftright">
<div class="left">
<a href='{% url "feeds:index" %}'>Home</a> |
<a href='{% url "feeds:domains" %}'>Domains</a> |
<a href='{% url "feeds:feeds" %}'>Feeds</a> |
<a href='{% url "feeds:api" %}'>API</a>
<a href="{% url 'index' %}">Home</a> |
<a href="{% url 'domains' %}">Domains</a> |
<a href="{% url 'feeds' %}">Feeds</a> |
<a href="{% url 'api' %}">API</a>
</div>
<div class="right">
<a href="https://github.com/TheLovinator1/FeedVault">GitHub</a> |
<a href="https://github.com/sponsors/TheLovinator1">Donate</a>
<!-- Show login if not logged in -->
{% if not user.is_authenticated %}
| <a href='{% url "feeds:login" %}'>Login</a>
| <a href="{% url 'login' %}">Login</a>
{% endif %}
<!-- Show username if logged in -->
{% if user.is_authenticated %}
| <a href='{% url "feeds:profile" %}'>{{ user.username }}</a>
| <a href="{% url 'profile' %}">{{ user.username }}</a>
{% endif %}
</div>
</div>

View file

@ -6,7 +6,7 @@
<ul>
{% for feed in feeds %}
<li>
<a href="/feed/{{ feed.id }}">{{ feed.feed_url }}</a>
<a href="{% url 'feed' feed.id %}">{{ feed.feed_url }}</a>
</li>
{% empty %}
<li>Found no feeds for this domain.</li>

View file

@ -9,7 +9,7 @@
{% for domain in domains %}
{% if not domain.hidden %}
<li>
<a href="/domain/{{ domain.id }}">{{ domain.url }}</a> - {{ domain.created_at|date }}
<a href="{% url 'domain' domain.id %}">{{ domain.url }}</a> - {{ domain.created_at|date }}
</li>
{% endif %}
{% empty %}

View file

@ -5,7 +5,7 @@
{% for feed in feeds %}
<li>{{ feed.feed_url }} - {{ feed.created_at|date }}</li>
<li>
<a href="/feed/{{ feed.id }}">View</a>
<a href="{% url 'feed' feed.id %}">View</a>
</li>
{% empty %}
<li>No feeds yet.</li>

View file

@ -5,7 +5,7 @@
<p>
Input the URLs of the feeds you wish to archive below. You can add as many as needed, and access them through the website or API. Alternatively, include links to .opml files, and the feeds within will be archived.
</p>
<form action='{% url "feeds:add" %}' method='post'>
<form action="/add" method='post'>
{% csrf_token %}
<textarea id="urls" name="urls" rows="5" cols="50" required></textarea>
<button type="submit">Add feeds</button>
@ -18,7 +18,7 @@
</p>
<form enctype="multipart/form-data"
method="post"
action="{% url 'feeds:upload' %}">
action="{% url 'upload' %}">
{% csrf_token %}
<p>
<input type="file" name="file" id="file" required>
@ -31,7 +31,7 @@
FeedVault is a service that archives web feeds. It allows users to access and search for historical content from various websites. The service is designed to preserve the history of the web and provide a reliable source for accessing content that may no longer be available on the original websites.
</p>
<p>
You need to <a href='{% url "feeds:login" %}'>login</a> or <a href='{% url "feeds:register" %}'>register</a> to add new feeds or upload files.
You need to <a href="{% url 'login' %}">login</a> or <a href="{% url 'register' %}">register</a> to add new feeds or upload files.
</p>
{% endif %}
<h2>FAQ</h2>