diff --git a/.env.example b/.env.example index 9719a75..71cb6e4 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,5 @@ -POSTGRES_USER= -POSTGRES_PASSWORD= -POSTGRES_DB=feedvault +DEBUG=True SECRET_KEY= +EMAIL_HOST_USER= +EMAIL_HOST_PASSWORD= +DISCORD_WEBHOOK_URL= diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index a850421..3a1e175 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -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: diff --git a/README.md b/README.md index cc39306..5ee8bbf 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/docker-compose.yml b/docker-compose.yml index 1f4aca2..51f2857 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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 diff --git a/feeds/apps.py b/feeds/apps.py deleted file mode 100644 index ffe3011..0000000 --- a/feeds/apps.py +++ /dev/null @@ -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" diff --git a/feeds/migrations/0002_alter_author_options_alter_domain_options_and_more.py b/feeds/migrations/0002_alter_author_options_alter_domain_options_and_more.py deleted file mode 100644 index db7e033..0000000 --- a/feeds/migrations/0002_alter_author_options_alter_domain_options_and_more.py +++ /dev/null @@ -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')}, - ), - ] diff --git a/feeds/migrations/0003_feed_user.py b/feeds/migrations/0003_feed_user.py deleted file mode 100644 index bda67ee..0000000 --- a/feeds/migrations/0003_feed_user.py +++ /dev/null @@ -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), - ), - ] diff --git a/feeds/migrations/__init__.py b/feeds/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/feeds/urls.py b/feeds/urls.py deleted file mode 100644 index 7f21844..0000000 --- a/feeds/urls.py +++ /dev/null @@ -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//", 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//", 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//", view=views.APIFeedView.as_view(), name="api_feeds_id"), - path(route="api/feeds//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//", 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"), -] diff --git a/feeds/add_feeds.py b/feedvault/add_feeds.py similarity index 97% rename from feeds/add_feeds.py rename to feedvault/add_feeds.py index 22602e3..e5ec665 100644 --- a/feeds/add_feeds.py +++ b/feedvault/add_feeds.py @@ -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", []) diff --git a/feedvault/apps.py b/feedvault/apps.py new file mode 100644 index 0000000..249f139 --- /dev/null +++ b/feedvault/apps.py @@ -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" diff --git a/feeds/context_processors.py b/feedvault/context_processors.py similarity index 90% rename from feeds/context_processors.py rename to feedvault/context_processors.py index 9f5860d..ad6a73b 100644 --- a/feeds/context_processors.py +++ b/feedvault/context_processors.py @@ -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 diff --git a/feeds/migrations/0001_initial.py b/feedvault/migrations/0001_initial.py similarity index 82% rename from feeds/migrations/0001_initial.py rename to feedvault/migrations/0001_initial.py index 86fa6d6..73eb5bd 100644 --- a/feeds/migrations/0001_initial.py +++ b/feedvault/migrations/0001_initial.py @@ -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'], + }, ), ] diff --git a/feeds/__init__.py b/feedvault/migrations/__init__.py similarity index 100% rename from feeds/__init__.py rename to feedvault/migrations/__init__.py diff --git a/feeds/models.py b/feedvault/models.py similarity index 100% rename from feeds/models.py rename to feedvault/models.py diff --git a/feedvault/settings.py b/feedvault/settings.py index c4b784a..c5cb350 100644 --- a/feedvault/settings.py +++ b/feedvault/settings.py @@ -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"] diff --git a/feeds/sitemaps.py b/feedvault/sitemaps.py similarity index 74% rename from feeds/sitemaps.py rename to feedvault/sitemaps.py index f290878..a1587b0 100644 --- a/feeds/sitemaps.py +++ b/feedvault/sitemaps.py @@ -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) diff --git a/feeds/stats.py b/feedvault/stats.py similarity index 52% rename from feeds/stats.py rename to feedvault/stats.py index 94c1337..2e30698 100644 --- a/feeds/stats.py +++ b/feedvault/stats.py @@ -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" diff --git a/feeds/tests.py b/feedvault/tests.py similarity index 100% rename from feeds/tests.py rename to feedvault/tests.py diff --git a/feedvault/urls.py b/feedvault/urls.py index cd8f684..b819839 100644 --- a/feedvault/urls.py +++ b/feedvault/urls.py @@ -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//", 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//", 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//", view=views.APIFeedView.as_view(), name="api_feeds_id"), + path(route="api/feeds//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//", 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"), ] diff --git a/feeds/views.py b/feedvault/views.py similarity index 98% rename from feeds/views.py rename to feedvault/views.py index dc1cd36..aebe606 100644 --- a/feeds/views.py +++ b/feedvault/views.py @@ -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 diff --git a/poetry.lock b/poetry.lock index 9ef1ae6..8d6a9ce 100644 --- a/poetry.lock +++ b/poetry.lock @@ -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" diff --git a/pyproject.toml b/pyproject.toml index 5e25cae..b29eefa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 + "CPY001", # Missing copyright notice at top of file + "ERA001", # Found commented-out code + "FIX002", # Line contains TODO + "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 diff --git a/templates/accounts/login.html b/templates/accounts/login.html index b48f0d3..e750ef1 100644 --- a/templates/accounts/login.html +++ b/templates/accounts/login.html @@ -1,7 +1,7 @@ {% extends "base.html" %} {% block content %}

- You can register here. + You can register here.

Login

diff --git a/templates/base.html b/templates/base.html index ef695a6..5e6d69f 100644 --- a/templates/base.html +++ b/templates/base.html @@ -82,7 +82,7 @@ {% endif %}

- FeedVault + FeedVault

@@ -104,21 +104,21 @@
- Home | - Domains | - Feeds | - API + Home | + Domains | + Feeds | + API
GitHub | Donate {% if not user.is_authenticated %} - | Login + | Login {% endif %} {% if user.is_authenticated %} - | {{ user.username }} + | {{ user.username }} {% endif %}
diff --git a/templates/domain.html b/templates/domain.html index 6fe780a..54b8e83 100644 --- a/templates/domain.html +++ b/templates/domain.html @@ -6,7 +6,7 @@
    {% for feed in feeds %}
  • - {{ feed.feed_url }} + {{ feed.feed_url }}
  • {% empty %}
  • Found no feeds for this domain.
  • diff --git a/templates/domains.html b/templates/domains.html index ce4ff2c..d682fa9 100644 --- a/templates/domains.html +++ b/templates/domains.html @@ -9,7 +9,7 @@ {% for domain in domains %} {% if not domain.hidden %}
  • - {{ domain.url }} - {{ domain.created_at|date }} + {{ domain.url }} - {{ domain.created_at|date }}
  • {% endif %} {% empty %} diff --git a/templates/feeds.html b/templates/feeds.html index 18fedc5..27883f5 100644 --- a/templates/feeds.html +++ b/templates/feeds.html @@ -5,7 +5,7 @@ {% for feed in feeds %}
  • {{ feed.feed_url }} - {{ feed.created_at|date }}
  • - View + View
  • {% empty %}
  • No feeds yet.
  • diff --git a/templates/index.html b/templates/index.html index 527a346..db18f60 100644 --- a/templates/index.html +++ b/templates/index.html @@ -5,7 +5,7 @@

    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.

    - + {% csrf_token %} @@ -18,7 +18,7 @@

    + action="{% url 'upload' %}"> {% csrf_token %}

    @@ -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.

    - You need to login or register to add new feeds or upload files. + You need to login or register to add new feeds or upload files.

    {% endif %}

    FAQ