diff --git a/apps/api/serializers.py b/apps/api/serializers.py index fe3b382..a8ea309 100644 --- a/apps/api/serializers.py +++ b/apps/api/serializers.py @@ -4,8 +4,7 @@ import logging from rest_framework import serializers -from apps.home.models import Subscription, TrackedContent -# from apps.home.models import RSSFeed, FeedChannel +from apps.home.models import Subscription, TrackedContent, DiscordChannel log = logging.getLogger(__name__) @@ -105,6 +104,14 @@ class DynamicModelSerializer(serializers.ModelSerializer): abstract = True +class DiscordChannelSerializer(DynamicModelSerializer): + """Serializer for the Discord Channel Model.""" + + class Meta: + model = DiscordChannel + fields = ("id", "creation_datetime") + + class SubscriptionSerializer(DynamicModelSerializer): """Serializer for the Subscription Model.""" @@ -113,12 +120,12 @@ class SubscriptionSerializer(DynamicModelSerializer): class Meta: model = Subscription fields = ( - "uuid", "name", "url", "image", "server", + "uuid", "name", "rss_url", "image", "server", "channels", "creation_datetime" ) -class TrackedContent(DynamicModelSerializer): +class TrackedContentSerializer(DynamicModelSerializer): """Serializer for the TrackedContent Model.""" class Meta: diff --git a/apps/api/urls.py b/apps/api/urls.py index 922dd17..b1e5373 100644 --- a/apps/api/urls.py +++ b/apps/api/urls.py @@ -3,27 +3,32 @@ from django.urls import path, include from rest_framework.authtoken.views import obtain_auth_token -from . import views +from .views import ( + DiscordChannel_ListView, + DiscordChannel_DetailView, + Subscription_ListView, + Subscription_DetailView, + TrackedContent_ListView, + TrackedContent_DetailView +) urlpatterns = [ path("api-auth/", include("rest_framework.urls", namespace="rest_framework")), path("api-token-auth/", obtain_auth_token), + path("channel/", include([ + path("", DiscordChannel_ListView.as_view(), name="discordchannel"), + path("/", DiscordChannel_DetailView.as_view(), name="discordchannel-detail") + ])), + path("subscription/", include([ - path("", views.SubscriptionList.as_view(), name="subscription"), - path("/", views.SubscriptionDetail.as_view(), name="subscription-detail") + path("", Subscription_ListView.as_view(), name="subscription"), + path("/", Subscription_DetailView.as_view(), name="subscription-detail") ])), path("tracked/", include([ - path("", views.TrackedContentList.as_view(), name="tracked"), - path("/", views.TrackedContentDetail.as_view(), name="tracked-detail"), - # path("") - ])), - - # path("rssfeed/", include([ - # path("", views.RSSFeedList.as_view(), name="rssfeed"), - # path("/", views.RSSFeedDetail.as_view(), name="rssfeed-detail") - # ])), - # path("feedchannel/", views.FeedChannelListApiView.as_view(), name="feedchannel") + path("", TrackedContent_ListView.as_view(), name="tracked"), + path("/", TrackedContent_DetailView.as_view(), name="tracked-detail") + ])) ] \ No newline at end of file diff --git a/apps/api/views.py b/apps/api/views.py index 84bb6cb..7f97b0c 100644 --- a/apps/api/views.py +++ b/apps/api/views.py @@ -1,90 +1,123 @@ # -*- encoding: utf-8 -*- import logging -import base64 -import httpx -from django.http import HttpResponse -from django_filters import rest_framework as rest_filters -from django.views.decorators.cache import cache_page -from django.utils.decorators import method_decorator -from django.core.files import File -from django.core.files.base import ContentFile -from django.core.files.temp import NamedTemporaryFile from django.db.utils import IntegrityError -from rest_framework.views import APIView -from rest_framework.response import Response +from django_filters import rest_framework as rest_filters from rest_framework import status, permissions, filters, generics +from rest_framework.response import Response from rest_framework.pagination import PageNumberPagination from rest_framework.authentication import SessionAuthentication, TokenAuthentication from rest_framework.parsers import MultiPartParser, FormParser -from asgiref.sync import async_to_sync -from apps.home.models import RSSFeed, FeedChannel -from .serializers import RssFeedSerializer, FeedChannelSerializer +from apps.home.models import DiscordChannel, Subscription, TrackedContent +from .serializers import DiscordChannelSerializer, SubscriptionSerializer, TrackedContentSerializer + log = logging.getLogger(__name__) -class RSSFeedPagination(PageNumberPagination): +class DefaultPagination(PageNumberPagination): + """Default class for pagination in API views.""" + page_size = 10 page_size_query_param = "page_size" max_page_size = 25 -class RSSFeedList(generics.ListAPIView, generics.CreateAPIView): +# Discord Channel Views + +class DiscordChannel_ListView(generics.ListAPIView, generics.CreateAPIView): + """""" + authentication_classes = [SessionAuthentication, TokenAuthentication] permission_classes = [permissions.IsAuthenticated] - pagination_class = RSSFeedPagination - serializer_class = RssFeedSerializer - queryset = RSSFeed.objects.all().order_by("created_at") + pagination_class = DefaultPagination + serializer_class = DiscordChannelSerializer + queryset = DiscordChannel.objects.all().order_by("-creation_datetime") - filter_backends = [filters.SearchFilter, rest_filters.DjangoFilterBackend, filters.OrderingFilter] - filterset_fields = ["uuid", "name", "url", "discord_server_id", "created_at"] - search_fields = ["name"] - ordering_fields = ["created_at"] - - def post(self, request): - serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) - - try: - self.perform_create(serializer) - except IntegrityError: - return Response({"detail": "RSS Feed name must be unique"}, status=status.HTTP_409_CONFLICT, exception=True) - - headers = self.get_success_headers(serializer.data) - return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) + filter_backends = [rest_filters.DjangoFilterBackend, filters.OrderingFilter] + filterset_fields = ["id"] + ordering_fields = ["creation_datetime"] -class RSSFeedDetail(generics.RetrieveUpdateDestroyAPIView): +class DiscordChannel_DetailView(generics.RetrieveDestroyAPIView): + """""" + authentication_classes = [SessionAuthentication, TokenAuthentication] permission_classes = [permissions.IsAuthenticated] parser_classes = [MultiPartParser, FormParser] - serializer_class = RssFeedSerializer - queryset = RSSFeed.objects.all().order_by("-created_at") + serializer_class = DiscordChannelSerializer + queryset = DiscordChannel.objects.all().order_by("-creation_datetime") -class FeedChannelListApiView(generics.ListAPIView): +# Subscription Views + +class Subscription_ListView(generics.ListAPIView, generics.CreateAPIView): + """""" + authentication_classes = [SessionAuthentication, TokenAuthentication] permission_classes = [permissions.IsAuthenticated] - serializer_class = FeedChannelSerializer - - queryset = FeedChannel.objects.all().order_by("-created_at") + pagination_class = DefaultPagination + serializer_class = SubscriptionSerializer + queryset = Subscription.objects.all().order_by("-creation_datetime") filter_backends = [filters.SearchFilter, rest_filters.DjangoFilterBackend, filters.OrderingFilter] - filterset_fields = ["uuid", "discord_channel_id", "discord_server_id", "feeds", "created_at"] - search_fields = ["feeds__name"] - ordering_fields = ["created_at"] + filterset_fields = ["uuid", "name", "rss_url", "server", "channels", "creation_datetime"] + search_fields = ["name"] + ordering_fields = ["creation_datetime"] - def post(self, request): +# def post(self, request): +# serializer = self.get_serializer(data=request.data) +# serializer.is_valid(raise_exception=True) - serializer = self.serializer_class(data=request.data) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status.HTTP_201_CREATED) +# try: +# self.perform_create(serializer) +# except IntegrityError: +# return Response({"detail": "RSS Feed name must be unique"}, status=status.HTTP_409_CONFLICT, exception=True) - return Response(serializer.errors, status.HTTP_400_BAD_REQUEST) +# headers = self.get_success_headers(serializer.data) +# return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) + + +class Subscription_DetailView(generics.RetrieveUpdateDestroyAPIView): + """""" + + authentication_classes = [SessionAuthentication, TokenAuthentication] + permission_classes = [permissions.IsAuthenticated] + parser_classes = [MultiPartParser, FormParser] + + serializer_class = SubscriptionSerializer + queryset = Subscription.objects.all().order_by("-creation_datetime") + + +# Tracked Content Views + +class TrackedContent_ListView(generics.ListAPIView, generics.CreateAPIView): + """""" + + authentication_classes = [SessionAuthentication, TokenAuthentication] + permission_classes = [permissions.IsAuthenticated] + + pagination_class = DefaultPagination + serializer_class = TrackedContentSerializer + queryset = TrackedContent.objects.all().order_by("-creation_datetime") + + filter_backends = [filters.SearchFilter, rest_filters.DjangoFilterBackend, filters.OrderingFilter] + filterset_fields = ["uuid", "subscription", "content_url", "creation_datetime"] + search_fields = ["name"] + ordering_fields = ["creation_datetime"] + + +class TrackedContent_DetailView(generics.RetrieveDestroyAPIView): + """""" + + authentication_classes = [SessionAuthentication, TokenAuthentication] + permission_classes = [permissions.IsAuthenticated] + parser_classes = [MultiPartParser, FormParser] + + serializer_class = TrackedContentSerializer + queryset = TrackedContent.objects.all().order_by("-creation_datetime") diff --git a/apps/home/admin.py b/apps/home/admin.py index 752576e..3e585c0 100644 --- a/apps/home/admin.py +++ b/apps/home/admin.py @@ -2,14 +2,19 @@ from django.contrib import admin -from .models import RSSFeed, FeedChannel -from .models import Subscription, TrackedContent +from .models import DiscordChannel, Subscription, TrackedContent + + +class DiscordChannelAdmin(admin.ModelAdmin): + list_display = [ + "id", "creation_datetime" + ] class SubscriptionAdmin(admin.ModelAdmin): list_display = [ - "uuid", "name", "url", "server", - "channels", "creation_datetime" + "uuid", "name", "rss_url", "server", + "creation_datetime" ] @@ -20,6 +25,7 @@ class TrackedContentAdmin(admin.ModelAdmin): ] +admin.site.register(DiscordChannel, DiscordChannelAdmin) admin.site.register(Subscription, SubscriptionAdmin) admin.site.register(TrackedContent, TrackedContentAdmin) diff --git a/apps/home/migrations/0001_initial.py b/apps/home/migrations/0001_initial.py index 232059d..6f34bfc 100644 --- a/apps/home/migrations/0001_initial.py +++ b/apps/home/migrations/0001_initial.py @@ -1,6 +1,7 @@ -# Generated by Django 5.0.1 on 2024-01-30 20:45 +# Generated by Django 5.0.1 on 2024-02-07 23:19 import apps.home.models +import django.db.models.deletion import django.utils.timezone import uuid from django.db import migrations, models @@ -15,40 +16,45 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='FeedChannel', + name='DiscordChannel', fields=[ - ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('discord_server_id', models.PositiveBigIntegerField(help_text='the discord server id of this item', verbose_name='discord server id')), - ('discord_channel_id', models.PositiveBigIntegerField(help_text='the discord channel id of this item', verbose_name='discord channel id')), - ('created_at', models.DateTimeField(default=django.utils.timezone.now, editable=False, help_text='when this item was created', verbose_name='creation date & time')), + ('id', models.PositiveBigIntegerField(editable=False, help_text='Unique identifier of the channel, provided by Discord.', primary_key=True, serialize=False, verbose_name='id')), + ('creation_datetime', models.DateTimeField(default=django.utils.timezone.now, editable=False, help_text='when this instance was created.', verbose_name='creation datetime')), ], options={ - 'verbose_name': 'Feed Channel', - 'verbose_name_plural': 'Feed Channels', + 'verbose_name': 'discord channel', + 'verbose_name_plural': 'discord channels', + 'get_latest_by': '-creation_datetime', }, ), migrations.CreateModel( - name='RSSFeed', + name='Subscription', fields=[ ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('name', models.CharField(help_text='a human readable nickname for this item', max_length=120, verbose_name='name')), - ('url', models.URLField(help_text='url to the RSS feed', verbose_name='url')), - ('image', models.ImageField(blank=True, help_text='image of the RSS feed', null=True, storage=apps.home.models.OverwriteStorage(), upload_to=apps.home.models.RSSFeedIconPathGenerator(), verbose_name='image')), - ('created_at', models.DateTimeField(default=django.utils.timezone.now, editable=False, help_text='when this item was created', verbose_name='creation date & time')), - ('discord_server_id', models.PositiveBigIntegerField(help_text='the discord server id of this item', verbose_name='discord server id')), + ('name', models.CharField(help_text='Reference name for this subscription (max %(max_length)s chars).', max_length=32, verbose_name='name')), + ('rss_url', models.URLField(help_text='URL of the subscribed to RSS feed.', verbose_name='rss url')), + ('image', models.ImageField(blank=True, default='../static/images/defaultuser.webp', help_text='image of the RSS feed.', null=True, storage=apps.home.models.OverwriteStorage(), upload_to=apps.home.models.Subscription_IconPathGenerator(), verbose_name='image')), + ('creation_datetime', models.DateTimeField(default=django.utils.timezone.now, editable=False, help_text='when this instance was created.', verbose_name='creation datetime')), + ('server', models.PositiveBigIntegerField(help_text='Identifier for the discord server that owns this subscription.', verbose_name='server id')), + ('channels', models.ManyToManyField(help_text='List of Discord Channels acting as targets for subscription content.', to='home.discordchannel', verbose_name='channels')), ], options={ - 'verbose_name': 'RSS Feed', - 'verbose_name_plural': 'RSS Feeds', + 'verbose_name': 'subscription', + 'verbose_name_plural': 'subscriptions', + 'get_latest_by': '-creation_datetime', }, ), - migrations.AddConstraint( - model_name='rssfeed', - constraint=models.UniqueConstraint(fields=('name', 'discord_server_id'), name='unique name & server pair'), + migrations.CreateModel( + name='TrackedContent', + fields=[ + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('content_url', models.URLField(help_text='URL of the tracked content.', verbose_name='content url')), + ('creation_datetime', models.DateTimeField(default=django.utils.timezone.now, editable=False, help_text='when this instance was created.', verbose_name='creation datetime')), + ('subscription', models.ForeignKey(help_text='The subscription that this content originated from.', on_delete=django.db.models.deletion.CASCADE, related_name='tracked_content', to='home.subscription', verbose_name='subscription')), + ], ), - migrations.AddField( - model_name='feedchannel', - name='feeds', - field=models.ManyToManyField(help_text='the feeds to include in this item', related_name='queues', to='home.rssfeed', verbose_name='feeds'), + migrations.AddConstraint( + model_name='subscription', + constraint=models.UniqueConstraint(fields=('name', 'server'), name='unique name & server pair'), ), ] diff --git a/apps/home/models.py b/apps/home/models.py index 4de5aa2..c9d243f 100644 --- a/apps/home/models.py +++ b/apps/home/models.py @@ -38,6 +38,30 @@ class Subscription_IconPathGenerator: return os.path.join("subscriptions", str(instance.uuid), "icon.webp") +class DiscordChannel(models.Model): + """Represents a Discord Channel.""" + + id = models.PositiveBigIntegerField( + verbose_name=_("id"), + help_text=_("Unique identifier of the channel, provided by Discord."), + primary_key=True + ) + creation_datetime = models.DateTimeField( + verbose_name=_("creation datetime"), + help_text=_("when this instance was created."), + default=timezone.now, + editable=False + ) + + class Meta: + verbose_name = _("discord channel") + verbose_name_plural = _("discord channels") + get_latest_by = "-creation_datetime" + + def __str__(self): + return str(self.id) + + class Subscription(models.Model): """Stores relevant data for a user submitted RSS Feed.""" @@ -76,7 +100,11 @@ class Subscription(models.Model): verbose_name=_("server id"), help_text=_("Identifier for the discord server that owns this subscription.") ) - channels = models.ManyToManyField(int) + channels = models.ManyToManyField( + verbose_name=_("channels"), + help_text=_("List of Discord Channels acting as targets for subscription content."), + to=DiscordChannel, + ) class Meta: verbose_name = "subscription" @@ -84,12 +112,17 @@ class Subscription(models.Model): get_latest_by = "-creation_datetime" constraints = [ # Prevent servers from having subscriptions with duplicate names - models.UniqueConstraint(fields=["name", "server_id"], name="unique name & server pair") + models.UniqueConstraint(fields=["name", "server"], name="unique name & server pair") ] def __str__(self): return self.name + def save(self, *args, **kwargs): + new_text = "New " if self._state.adding else "" + log.debug("%sSubscription Saved %s", new_text, self.uuid) + super().save(*args, **kwargs) + class TrackedContent(models.Model): """Tracks content shared from an RSS Feed, to prevent duplicates.""" @@ -110,7 +143,7 @@ class TrackedContent(models.Model): verbose_name=_("content url"), help_text=_("URL of the tracked content.") ) - creation_datetime = models.DatetimeField( + creation_datetime = models.DateTimeField( verbose_name=_("creation datetime"), help_text=_("when this instance was created."), default=timezone.now,