Use FastAPI instead of Django

This commit is contained in:
Joakim Hellsén 2024-05-21 02:43:53 +02:00
commit b462be40af
No known key found for this signature in database
GPG key ID: D196AE66FEBE1DC9
43 changed files with 1105 additions and 1688 deletions

View file

View file

@ -1 +0,0 @@
# Register your models here.

View file

@ -1,6 +0,0 @@
from django.apps import AppConfig
class FeedsConfig(AppConfig):
default_auto_field: str = "django.db.models.BigAutoField"
name = "feeds"

View file

@ -1,224 +0,0 @@
from __future__ import annotations
import logging
from functools import lru_cache
from typing import TYPE_CHECKING, Any, Iterable, Iterator, Self
from django.db.models import Q
from reader import ExceptionInfo, FeedExistsError, FeedNotFoundError, Reader, make_reader
from reader._types import (
EntryForUpdate, # noqa: PLC2701
EntryUpdateIntent,
FeedData,
FeedFilter,
FeedForUpdate, # noqa: PLC2701
FeedUpdateIntent,
SearchType, # noqa: PLC2701
StorageType, # noqa: PLC2701
)
from .models import Entry, Feed
if TYPE_CHECKING:
import datetime
from django.db.models.manager import BaseManager
logger = logging.getLogger(__name__)
class EmptySearch(SearchType): ...
class EntriesForUpdateIterator:
def __init__(self, entries: Iterable[tuple[str, str]]) -> None:
self.entries: Iterator[tuple[str, str]] = iter(entries)
def __iter__(self) -> Self:
return self
def __next__(self) -> EntryForUpdate:
try:
feed_url, entry_id = next(self.entries)
except StopIteration:
raise StopIteration from None
print(f"{feed_url=}, {entry_id=}") # noqa: T201
entry_data: dict[str, Any] | None = (
Entry.objects.filter(Q(feed__url=feed_url) & Q(id=entry_id))
.values("updated", "published", "data_hash", "data_hash_changed")
.first()
)
if not entry_data:
return None
return EntryForUpdate(
updated=entry_data.get("updated"),
published=entry_data.get("published"),
hash=entry_data.get("data_hash"),
hash_changed=entry_data.get("data_hash_changed"),
)
class DjangoStorage(StorageType):
# TODO(TheLovinator): Implement all methods from StorageType.
default_search_cls = EmptySearch
def __enter__(self: DjangoStorage) -> None:
"""Called when Reader is used as a context manager."""
# TODO(TheLovinator): Should we check if we have migrations to apply?
def __exit__(self: DjangoStorage, *_: object) -> None:
"""Called when Reader is used as a context manager."""
# TODO(TheLovinator): Should we close the connection?
def close(self: DjangoStorage) -> None:
"""Called by Reader.close()."""
# TODO(TheLovinator): Should we close the connection?
def add_feed(self, url: str, /, added: datetime.datetime) -> None:
"""Called by Reader.add_feed().
Args:
url: The URL of the feed.
added: The time the feed was added.
Raises:
FeedExistsError: Feed already exists. Bases: FeedError
"""
if Feed.objects.filter(url=url).exists():
msg: str = f"Feed already exists: {url}"
raise FeedExistsError(msg)
feed = Feed(url=url, added=added)
feed.save()
def get_feeds_for_update(self, filter: FeedFilter): # noqa: A002
"""Called by update logic.
Args:
filter: The filter to apply.
Returns:
A lazy iterable.
"""
logger.debug(f"{filter=}") # noqa: G004
feeds: BaseManager[Feed] = Feed.objects.all() # TODO(TheLovinator): Don't get all values, use filter.
for feed in feeds:
yield FeedForUpdate(
url=feed.url,
updated=feed.updated,
http_etag=feed.http_etag,
http_last_modified=feed.http_last_modified,
stale=feed.stale,
last_updated=feed.last_updated,
last_exception=bool(feed.last_exception_type_name),
hash=feed.data_hash,
)
def update_feed(self, intent: FeedUpdateIntent, /) -> None:
"""Called by update logic.
Args:
intent: Data to be passed to Storage when updating a feed.
Raises:
FeedNotFoundError
"""
feed: Feed = Feed.objects.get(url=intent.url)
if feed is None:
msg: str = f"Feed not found: {intent.url}"
raise FeedNotFoundError(msg)
feed.last_updated = intent.last_updated
feed.http_etag = intent.http_etag
feed.http_last_modified = intent.http_last_modified
feed_data: FeedData | None = intent.feed
if feed_data is not None:
feed.title = feed_data.title
feed.link = feed_data.link
feed.author = feed_data.author
feed.subtitle = feed_data.subtitle
feed.version = feed_data.version
if intent.last_exception is not None:
last_exception: ExceptionInfo = intent.last_exception
feed.last_exception_type_name = last_exception.type_name
feed.last_exception_value = last_exception.value_str
feed.last_exception_traceback = last_exception.traceback_str
feed.save()
def set_feed_stale(self, url: str, stale: bool, /) -> None: # noqa: FBT001
"""Used by update logic tests.
Args:
url: The URL of the feed.
stale: Whether the next update should update all entries, regardless of their hash or updated.
Raises:
FeedNotFoundError
"""
feed: Feed = Feed.objects.get(url=url)
if feed is None:
msg: str = f"Feed not found: {url}"
raise FeedNotFoundError(msg)
feed.stale = stale
feed.save()
def get_entries_for_update(self, entries: Iterable[tuple[str, str]], /) -> EntriesForUpdateIterator:
for feed_url, entry_id in entries:
logger.debug(f"{feed_url=}, {entry_id=}") # noqa: G004
entries_list = list(entries)
print(f"{entries_list=}") # noqa: T201
return EntriesForUpdateIterator(entries)
def add_or_update_entries(self, intents: Iterable[EntryUpdateIntent], /) -> None:
"""Called by update logic.
Args:
intents: Data to be passed to Storage when updating a feed.
Raises:
FeedNotFoundError
"""
msg = "Not implemented yet."
raise NotImplementedError(msg)
for intent in intents:
feed_id, entry_id = intent.entry.resource_id
logger.debug(f"{feed_id=}, {entry_id=}") # noqa: G004
# TODO(TheLovinator): Implement this method. Use Entry.objects.get_or_create()/Entry.objects.bulk_create()?
# TODO(TheLovinator): Raise FeedNotFoundError if feed does not exist.
def make_search(self) -> SearchType:
"""Called by Reader.make_search().
Returns:
A Search instance.
"""
return EmptySearch()
@lru_cache(maxsize=1)
def get_reader() -> Reader:
"""Create a Reader instance.
reader = get_reader()
reader.add_feed("https://example.com/feed", added=datetime.datetime.now())
reader.update_feeds()
Returns:
A Reader instance.
"""
return make_reader(
"",
_storage=DjangoStorage(),
search_enabled=False,
)

View file

@ -1,16 +0,0 @@
from typing import TYPE_CHECKING
from django.core.management.base import BaseCommand
if TYPE_CHECKING:
from reader import Reader
class Command(BaseCommand):
help = "Update feeds"
def handle(self, *args, **options) -> None:
from feeds.get_reader import get_reader # noqa: PLC0415
reader: Reader = get_reader()
reader.update_feeds()

View file

@ -1,110 +0,0 @@
# Generated by Django 5.0.6 on 2024-05-20 00:49
import django.db.models.deletion
import feeds.models
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Entry',
fields=[
('id', models.TextField(help_text='The entry id.', primary_key=True, serialize=False)),
('updated', models.DateTimeField(help_text='The date the entry was last updated, according to the feed.', null=True)),
('title', models.TextField(help_text='The title of the entry.', null=True)),
('link', models.TextField(help_text='The URL of the entry.', null=True)),
('author', models.TextField(help_text='The author of the feed.', null=True)),
('published', models.DateTimeField(help_text='The date the entry was published.', null=True)),
('summary', models.TextField(help_text='A summary of the entry.', null=True)),
('read', models.BooleanField(default=False, help_text='Whether the entry has been read.')),
('read_modified', models.DateTimeField(help_text='When read was last modified, None if that never.', null=True)),
('added', models.DateTimeField(help_text='The date when the entry was added (first updated) to reader.', null=True)),
('added_by', models.TextField(help_text="The source of the entry. One of 'feed', 'user'.", null=True)),
('last_updated', models.DateTimeField(help_text='The date when the entry was last retrieved by reader.', null=True)),
('first_updated', models.DateTimeField(help_text='The date when the entry was first retrieved by reader.', null=True)),
('first_updated_epoch', models.DateTimeField(help_text='The date when the entry was first retrieved by reader, as an epoch timestamp.', null=True)),
('feed_order', models.PositiveIntegerField(help_text='The order of the entry in the feed.', null=True)),
('recent_sort', models.PositiveIntegerField(help_text='The order of the entry in the recent list.', null=True)),
('sequence', models.BinaryField(help_text='The sequence of the entry in the feed.', null=True)),
('original_feed', models.TextField(help_text='The URL of the original feed of the entry. If the feed URL never changed, the same as feed_url.', null=True)),
('data_hash', models.TextField(help_text='The hash of the entry data.', null=True)),
('data_hash_changed', models.BooleanField(default=False, help_text='Whether the data hash has changed since the last update.')),
('important', models.BooleanField(default=False, help_text='Whether the entry is important.')),
('important_modified', models.DateTimeField(help_text='When important was last modified, None if that never.', null=True)),
],
),
migrations.CreateModel(
name='Feed',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('url', models.URLField(help_text='The URL of the feed.', unique=True)),
('updated', models.DateTimeField(help_text='The date the feed was last updated, according to the feed.', null=True)),
('title', models.TextField(help_text='The title of the feed.', null=True)),
('link', models.TextField(help_text='The URL of a page associated with the feed.', null=True)),
('author', models.TextField(help_text='The author of the feed.', null=True)),
('subtitle', models.TextField(help_text='A description or subtitle for the feed.', null=True)),
('version', models.TextField(help_text='The version of the feed.', null=True)),
('user_title', models.TextField(help_text='User-defined feed title.', null=True)),
('added', models.DateTimeField(auto_now_add=True, help_text='The date when the feed was added.')),
('last_updated', models.DateTimeField(help_text='The date when the feed was last retrieved by reader.', null=True)),
('last_exception_type_name', models.TextField(help_text='The fully qualified name of the exception type.', null=True)),
('last_exception_value', models.TextField(help_text='The exception value.', null=True)),
('last_exception_traceback', models.TextField(help_text='The exception traceback.', null=True)),
('updates_enabled', models.BooleanField(default=True, help_text='Whether updates are enabled for the feed.')),
('stale', models.BooleanField(default=False, help_text='Whether the next update should update all entries, regardless of their hash or updated.')),
('http_etag', models.TextField(help_text='The HTTP ETag header.', null=True)),
('http_last_modified', models.TextField(help_text='The HTTP Last-Modified header.', null=True)),
('data_hash', models.TextField(help_text='The hash of the feed data.', null=True)),
],
),
migrations.CreateModel(
name='UploadedFeed',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('file', models.FileField(help_text='The file that was uploaded.', upload_to=feeds.models.get_upload_path)),
('original_filename', models.TextField(help_text='The original filename of the file.')),
('created_at', models.DateTimeField(auto_now_add=True, help_text='The time the file was uploaded.')),
('has_been_processed', models.BooleanField(default=False, help_text='Has the file content been added to the archive?')),
('public', models.BooleanField(default=False, help_text='Is the file public?')),
('description', models.TextField(blank=True, help_text='Description added by user.')),
('notes', models.TextField(blank=True, help_text='Notes from admin.')),
],
options={
'verbose_name': 'Uploaded file',
'verbose_name_plural': 'Uploaded files',
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='Enclosure',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('href', models.TextField(help_text='The file URL.')),
('type', models.TextField(help_text='The file content type.', null=True)),
('length', models.PositiveIntegerField(help_text='The file length.', null=True)),
('entry', models.ForeignKey(help_text='The entry this enclosure is for.', on_delete=django.db.models.deletion.CASCADE, related_name='enclosures', to='feeds.entry')),
],
),
migrations.CreateModel(
name='Content',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('value', models.TextField(help_text='The content value.')),
('type', models.TextField(help_text='The content type.', null=True)),
('language', models.TextField(help_text='The content language.', null=True)),
('entry', models.ForeignKey(help_text='The entry this content is for.', on_delete=django.db.models.deletion.CASCADE, related_name='content', to='feeds.entry')),
],
),
migrations.AddField(
model_name='entry',
name='feed',
field=models.ForeignKey(help_text='The feed this entry is from.', on_delete=django.db.models.deletion.CASCADE, related_name='entries', to='feeds.feed'),
),
]

View file

@ -1,18 +0,0 @@
# Generated by Django 5.0.6 on 2024-05-20 01:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('feeds', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='feed',
name='data_hash',
field=models.BinaryField(help_text='The hash of the feed data.', null=True),
),
]

View file

@ -1,131 +0,0 @@
"""These models are used to store the data from https://reader.readthedocs.io/en/latest/api.html#reader.Feed."""
from __future__ import annotations
import typing
import uuid
from pathlib import Path
from django.db import models
def get_upload_path(instance: UploadedFeed, filename: str) -> str:
"""Don't save the file with the original filename."""
ext: str = Path(filename).suffix
unix_time: int = int(instance.created_at.timestamp())
filename = f"{unix_time}-{uuid.uuid4().hex}{ext}"
return f"uploads/{filename}"
class UploadedFeed(models.Model):
"""A file uploaded to the server by a user."""
file = models.FileField(upload_to=get_upload_path, help_text="The file that was uploaded.")
original_filename = models.TextField(help_text="The original filename of the file.")
created_at = models.DateTimeField(auto_now_add=True, help_text="The time the file was uploaded.")
has_been_processed = models.BooleanField(default=False, help_text="Has the file content been added to the archive?")
public = models.BooleanField(default=False, help_text="Is the file public?")
description = models.TextField(blank=True, help_text="Description added by user.")
notes = models.TextField(blank=True, help_text="Notes from admin.")
class Meta:
"""Meta information for the uploaded file model."""
ordering: typing.ClassVar[list[str]] = ["-created_at"]
verbose_name: str = "Uploaded file"
verbose_name_plural: str = "Uploaded files"
def __str__(self: UploadedFeed) -> str:
return f"{self.original_filename} - {self.created_at}"
class Feed(models.Model):
url = models.URLField(unique=True, help_text="The URL of the feed.")
updated = models.DateTimeField(help_text="The date the feed was last updated, according to the feed.", null=True)
title = models.TextField(help_text="The title of the feed.", null=True)
link = models.TextField(help_text="The URL of a page associated with the feed.", null=True)
author = models.TextField(help_text="The author of the feed.", null=True)
subtitle = models.TextField(help_text="A description or subtitle for the feed.", null=True)
version = models.TextField(help_text="The version of the feed.", null=True)
user_title = models.TextField(help_text="User-defined feed title.", null=True)
added = models.DateTimeField(help_text="The date when the feed was added.", auto_now_add=True)
last_updated = models.DateTimeField(help_text="The date when the feed was last retrieved by reader.", null=True)
last_exception_type_name = models.TextField(help_text="The fully qualified name of the exception type.", null=True)
last_exception_value = models.TextField(help_text="The exception value.", null=True)
last_exception_traceback = models.TextField(help_text="The exception traceback.", null=True)
updates_enabled = models.BooleanField(help_text="Whether updates are enabled for the feed.", default=True)
stale = models.BooleanField(
help_text="Whether the next update should update all entries, regardless of their hash or updated.",
default=False,
)
http_etag = models.TextField(help_text="The HTTP ETag header.", null=True)
http_last_modified = models.TextField(help_text="The HTTP Last-Modified header.", null=True)
data_hash = models.BinaryField(help_text="The hash of the feed data.", null=True)
def __str__(self) -> str:
return f"{self.title} ({self.url})" if self.title else self.url
class Entry(models.Model):
feed = models.ForeignKey(
Feed, on_delete=models.CASCADE, help_text="The feed this entry is from.", related_name="entries"
)
id = models.TextField(primary_key=True, help_text="The entry id.")
updated = models.DateTimeField(help_text="The date the entry was last updated, according to the feed.", null=True)
title = models.TextField(help_text="The title of the entry.", null=True)
link = models.TextField(help_text="The URL of the entry.", null=True)
author = models.TextField(help_text="The author of the feed.", null=True)
published = models.DateTimeField(help_text="The date the entry was published.", null=True)
summary = models.TextField(help_text="A summary of the entry.", null=True)
read = models.BooleanField(help_text="Whether the entry has been read.", default=False)
read_modified = models.DateTimeField(help_text="When read was last modified, None if that never.", null=True)
added = models.DateTimeField(help_text="The date when the entry was added (first updated) to reader.", null=True)
added_by = models.TextField(help_text="The source of the entry. One of 'feed', 'user'.", null=True)
last_updated = models.DateTimeField(help_text="The date when the entry was last retrieved by reader.", null=True)
first_updated = models.DateTimeField(help_text="The date when the entry was first retrieved by reader.", null=True)
first_updated_epoch = models.DateTimeField(
help_text="The date when the entry was first retrieved by reader, as an epoch timestamp.", null=True
)
feed_order = models.PositiveIntegerField(help_text="The order of the entry in the feed.", null=True)
recent_sort = models.PositiveIntegerField(help_text="The order of the entry in the recent list.", null=True)
sequence = models.BinaryField(help_text="The sequence of the entry in the feed.", null=True)
original_feed = models.TextField(
help_text="The URL of the original feed of the entry. If the feed URL never changed, the same as feed_url.",
null=True,
)
data_hash = models.TextField(help_text="The hash of the entry data.", null=True)
data_hash_changed = models.BooleanField(
help_text="Whether the data hash has changed since the last update.", default=False
)
important = models.BooleanField(help_text="Whether the entry is important.", default=False)
important_modified = models.DateTimeField(
help_text="When important was last modified, None if that never.", null=True
)
def __str__(self) -> str:
return f"{self.title} ({self.link})" if self.title and self.link else self.id
class Content(models.Model):
entry = models.ForeignKey(
Entry, on_delete=models.CASCADE, help_text="The entry this content is for.", related_name="content"
)
value = models.TextField(help_text="The content value.")
type = models.TextField(help_text="The content type.", null=True)
language = models.TextField(help_text="The content language.", null=True)
def __str__(self) -> str:
max_length = 50
return self.value[:max_length] + "..." if len(self.value) > max_length else self.value
class Enclosure(models.Model):
entry = models.ForeignKey(
Entry, on_delete=models.CASCADE, help_text="The entry this enclosure is for.", related_name="enclosures"
)
href = models.TextField(help_text="The file URL.")
type = models.TextField(help_text="The file content type.", null=True)
length = models.PositiveIntegerField(help_text="The file length.", null=True)
def __str__(self) -> str:
return self.href

View file

@ -1 +0,0 @@
# Create your tests here.

View file

@ -1,26 +0,0 @@
from __future__ import annotations
from django.contrib.sitemaps import GenericSitemap
from django.urls import include, path
from feedvault.sitemaps import StaticViewSitemap
from .models import Feed
from .views import AddView, FeedsView, FeedView, IndexView, SearchView, UploadView
app_name: str = "feeds"
sitemaps = {
"static": StaticViewSitemap,
"feeds": GenericSitemap({"queryset": Feed.objects.all(), "date_field": "created_at"}),
}
urlpatterns: list = [
path(route="", view=IndexView.as_view(), name="index"),
path("__debug__/", include("debug_toolbar.urls")),
path(route="feed/<int:feed_id>/", view=FeedView.as_view(), name="feed"),
path(route="feeds/", view=FeedsView.as_view(), name="feeds"),
path(route="add/", view=AddView.as_view(), name="add"),
path(route="upload/", view=UploadView.as_view(), name="upload"),
path(route="search/", view=SearchView.as_view(), name="search"),
]

View file

@ -1,175 +0,0 @@
from __future__ import annotations
import logging
from typing import TYPE_CHECKING, Any
from django.contrib import messages
from django.core.paginator import EmptyPage, Page, Paginator
from django.db.models.manager import BaseManager
from django.http import HttpRequest, HttpResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.template import loader
from django.views import View
from reader import InvalidFeedURLError
from feeds.get_reader import get_reader
from feeds.models import Entry, Feed, UploadedFeed
if TYPE_CHECKING:
from django.core.files.uploadedfile import UploadedFile
from django.db.models.manager import BaseManager
from reader import Reader
logger: logging.Logger = logging.getLogger(__name__)
class HtmxHttpRequest(HttpRequest):
htmx: Any
class IndexView(View):
"""Index path."""
def get(self, request: HttpRequest) -> HttpResponse:
"""Load the index page."""
template = loader.get_template(template_name="index.html")
context: dict[str, str] = {
"description": "FeedVault allows users to archive and search their favorite web feeds.",
"keywords": "feed, rss, atom, archive, rss list",
"author": "TheLovinator",
"canonical": "https://feedvault.se/",
"title": "FeedVault",
}
return HttpResponse(content=template.render(context=context, request=request))
class FeedView(View):
"""A single feed."""
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: # noqa: ANN002, ANN003
"""Load the feed page."""
feed_id: str = kwargs.get("feed_id", None)
if not feed_id:
return HttpResponse(content="No id", status=400)
feed: Feed = get_object_or_404(Feed, pk=feed_id)
entries: BaseManager[Entry] = Entry.objects.filter(feed=feed).order_by("-added")[:100]
context: dict[str, Any] = {
"feed": feed,
"entries": entries,
"description": f"{feed.subtitle}" or f"Archive of {feed.url}",
"keywords": "feed, rss, atom, archive, rss list",
"author": f"{feed.author}" or "FeedVault",
"canonical": f"https://feedvault.se/feed/{feed.pk}/",
"title": f"{feed.title}" or "FeedVault",
}
return render(request=request, template_name="feed.html", context=context)
class FeedsView(View):
"""All feeds."""
def get(self, request: HtmxHttpRequest) -> HttpResponse:
"""All feeds."""
feeds: BaseManager[Feed] = Feed.objects.only("id", "url")
paginator = Paginator(object_list=feeds, per_page=100)
page_number = int(request.GET.get("page", default=1))
try:
pages: Page = paginator.get_page(page_number)
except EmptyPage:
return HttpResponse("")
context: dict[str, str | Page | int] = {
"feeds": pages,
"description": "An archive of web feeds",
"keywords": "feed, rss, atom, archive, rss list",
"author": "TheLovinator",
"canonical": "https://feedvault.se/feeds/",
"title": "Feeds",
"page": page_number,
}
template_name = "partials/feeds.html" if request.htmx else "feeds.html"
return render(request, template_name, context)
class AddView(View):
"""Add a feed."""
def get(self, request: HttpRequest) -> HttpResponse:
"""Load the index page."""
template = loader.get_template(template_name="index.html")
context: dict[str, str] = {
"description": "FeedVault allows users to archive and search their favorite web feeds.",
"keywords": "feed, rss, atom, archive, rss list",
"author": "TheLovinator",
"canonical": "https://feedvault.se/",
}
return HttpResponse(content=template.render(context=context, request=request))
def post(self, request: HttpRequest) -> HttpResponse:
"""Add a feed."""
urls: str | None = request.POST.get("urls", None)
if not urls:
return HttpResponse(content="No urls", status=400)
reader: Reader = get_reader()
for url in urls.split("\n"):
clean_url: str = url.strip()
try:
reader.add_feed(clean_url)
messages.success(request, f"Added {clean_url}")
except InvalidFeedURLError:
logger.exception("Error adding %s", clean_url)
messages.error(request, f"Error adding {clean_url}")
messages.success(request, "Feeds added")
return redirect("feeds:index")
class UploadView(View):
"""Upload a file."""
def post(self, request: HttpRequest) -> HttpResponse:
"""Upload a file."""
file: UploadedFile | None = request.FILES.get("file", None)
if not file:
return HttpResponse(content="No file", status=400)
# Save file to media folder
UploadedFeed.objects.create(user=request.user, file=file, original_filename=file.name)
# Render the index page.
messages.success(request, f"{file.name} uploaded")
messages.info(request, "If the file was marked as public, it will be shown on the feeds page. ")
return redirect("feeds:index")
class SearchView(View):
"""Search view."""
def get(self, request: HtmxHttpRequest) -> HttpResponse:
"""Load the search page."""
query: str | None = request.GET.get("q", None)
if not query:
return FeedsView().get(request)
# TODO(TheLovinator): #20 Search more fields
# https://github.com/TheLovinator1/FeedVault/issues/20
feeds: BaseManager[Feed] = Feed.objects.filter(url__icontains=query).order_by("-added")[:100]
context = {
"feeds": feeds,
"description": f"Search results for {query}",
"keywords": f"feed, rss, atom, archive, rss list, {query}",
"author": "TheLovinator",
"canonical": f"https://feedvault.se/search/?q={query}",
"title": f"Search results for {query}",
"query": query,
}
return render(request, "search.html", context)