diff --git a/apps/api/serializers.py b/apps/api/serializers.py index d0e4123..e80e5dc 100644 --- a/apps/api/serializers.py +++ b/apps/api/serializers.py @@ -4,7 +4,7 @@ import logging from rest_framework import serializers -from apps.home.models import Subscription, TrackedContent +from apps.home.models import Subscription, SavedGuilds from apps.authentication.models import UserServerLink log = logging.getLogger(__name__) @@ -106,16 +106,6 @@ class DynamicModelSerializer(serializers.ModelSerializer): abstract = True -# class SubscriptionTargetSerializer(DynamicModelSerializer): -# """ -# Serializer for the Subscription Target Model. -# """ - -# class Meta: -# model = SubscriptionTarget -# fields = ("id", "creation_datetime") - - class SubscriptionSerializer(DynamicModelSerializer): """ Serializer for the Subscription Model. @@ -130,16 +120,6 @@ class SubscriptionSerializer(DynamicModelSerializer): fields = ("uuid", "name", "rss_url", "image", "server", "targets", "creation_datetime", "extra_notes", "active") -class TrackedContentSerializer(DynamicModelSerializer): - """ - Serializer for the TrackedContent Model. - """ - - class Meta: - model = TrackedContent - fields = ("uuid", "content_url", "subscription", "creation_datetime") - - class UserServerLinkSerializer(DynamicModelSerializer): """ Serializer for the UserServerLink Model. @@ -148,3 +128,13 @@ class UserServerLinkSerializer(DynamicModelSerializer): class Meta: model = UserServerLink fields = ("id", "server_id", "user", "name", "icon", "icon_url", "permissions") + + +class SavedGuildSerializer(DynamicModelSerializer): + """ + Serializer for the SavedGuild model. + """ + + class Meta: + model = SavedGuilds + fields = ("id", "guild_id", "name", "icon") diff --git a/apps/api/urls.py b/apps/api/urls.py index 35043a1..6c312de 100644 --- a/apps/api/urls.py +++ b/apps/api/urls.py @@ -6,13 +6,13 @@ from rest_framework.authtoken.views import obtain_auth_token from .views import ( Subscription_ListView, Subscription_DetailView, - TrackedContent_ListView, - TrackedContent_DetailView, UserServerLink_ListView, - UserServerLink_DetailView + UserServerLink_DetailView, + SavedGuild_ListView, + SavedGuild_DetailView ) - + urlpatterns = [ path("api-auth/", include("rest_framework.urls", namespace="rest_framework")), path("api-token-auth/", obtain_auth_token), @@ -22,13 +22,13 @@ urlpatterns = [ path("/", Subscription_DetailView.as_view(), name="subscription-detail") ])), - path("tracked/", include([ - path("", TrackedContent_ListView.as_view(), name="tracked"), - path("/", TrackedContent_DetailView.as_view(), name="tracked-detail") - ])), - path("serverlink/", include([ path("", UserServerLink_ListView.as_view(), name="serverlink"), path("/", UserServerLink_DetailView.as_view(), name="serverlink-detail") ])), + + path("saved-guilds/", include([ + path("", SavedGuild_ListView.as_view(), name="saved-guilds"), + path("/", SavedGuild_DetailView.as_view(), name="saved-guilds-detail") + ])), ] \ No newline at end of file diff --git a/apps/api/views.py b/apps/api/views.py index 3d7290e..c735cdc 100644 --- a/apps/api/views.py +++ b/apps/api/views.py @@ -11,12 +11,12 @@ from rest_framework.pagination import PageNumberPagination from rest_framework.authentication import SessionAuthentication, TokenAuthentication from rest_framework.parsers import MultiPartParser, FormParser -from apps.home.models import Subscription, TrackedContent +from apps.home.models import Subscription, SavedGuilds from apps.authentication.models import UserServerLink from .serializers import ( SubscriptionSerializer, - TrackedContentSerializer, - UserServerLinkSerializer + UserServerLinkSerializer, + SavedGuildSerializer ) log = logging.getLogger(__name__) @@ -92,61 +92,6 @@ class Subscription_DetailView(generics.RetrieveUpdateDestroyAPIView): # .order_by("-creation_datetime") -# ================================================================================================= -# Tracked Content Views - -class TrackedContent_ListView(generics.ListCreateAPIView): - """ - View to provide a list of TrackedContent model instances. - Can also be used to create a new instance. - - Supports: GET, POST - """ - - 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"] - - 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": "Tracked content 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) - - -class TrackedContent_DetailView(generics.RetrieveDestroyAPIView): - """ - View to provide details on a particular TrackedContent model instances. - - Supports: GET, DELETE - """ - - authentication_classes = [SessionAuthentication, TokenAuthentication] - permission_classes = [permissions.IsAuthenticated] - parser_classes = [MultiPartParser, FormParser] - - serializer_class = TrackedContentSerializer - queryset = TrackedContent.objects.all().order_by("-creation_datetime") - - # ================================================================================================= # UserServerLinks Views @@ -224,3 +169,59 @@ class UserServerLink_DetailView(generics.RetrieveDestroyAPIView): serializer_class = UserServerLinkSerializer queryset = UserServerLink.objects.all() + + +# ================================================================================================= +# SavedGuild Views + +class SavedGuild_ListView(generics.ListCreateAPIView): + """ + View to provide a list of SavedGuild model instances. + Can also be used to create a new instance. + + Supports: GET, POST + """ + + authentication_classes = [SessionAuthentication, TokenAuthentication] + permission_classes = [permissions.IsAuthenticated] + + pagination_class = None + serializer_class = SavedGuildSerializer + + filter_backends = [filters.SearchFilter, rest_filters.DjangoFilterBackend, filters.OrderingFilter] + filterset_fields = ["id", "guild_id", "name", "icon"] + search_fields = ["name"] + + def get_queryset(self): + return SavedGuilds.objects.all() + + 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": "SavedGuild 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) + + +class SavedGuild_DetailView(generics.RetrieveDestroyAPIView): + """ + View to provide details on a particular UserServerLink model instances. + + Supports: GET, DELETE + """ + + authentication_classes = [SessionAuthentication, TokenAuthentication] + permission_classes = [permissions.IsAuthenticated] + parser_classes = [MultiPartParser, FormParser] + + serializer_class = SavedGuildSerializer + queryset = SavedGuilds.objects.all() diff --git a/apps/authentication/urls.py b/apps/authentication/urls.py index 4053cc3..cbc0d5c 100644 --- a/apps/authentication/urls.py +++ b/apps/authentication/urls.py @@ -3,7 +3,7 @@ from django.urls import path from django.contrib.auth.views import LogoutView -from .views import DiscordLoginAction, DiscordLoginRedirect, Login, GuildsView, GuildChannelsView +from .views import DiscordLoginAction, DiscordLoginRedirect, Login, GuildsView, GuildChannelsView, SaveGuildView urlpatterns = [ @@ -13,6 +13,7 @@ urlpatterns = [ path("logout/", LogoutView.as_view(), name="logout"), path("guilds/", GuildsView.as_view(), name="guilds"), - path("channels/", GuildChannelsView.as_view(), name="channels") + path("channels/", GuildChannelsView.as_view(), name="channels"), + path("save-guild/", SaveGuildView.as_view(), name="save-guild") ] diff --git a/apps/authentication/views.py b/apps/authentication/views.py index 4eed246..02f3468 100644 --- a/apps/authentication/views.py +++ b/apps/authentication/views.py @@ -11,6 +11,7 @@ from django.shortcuts import render, redirect from django.contrib.auth import authenticate, login from .models import UserServerLink +from apps.home.models import SavedGuilds log = logging.getLogger(__name__) @@ -97,35 +98,10 @@ class GuildsView(View): ) content = response.json() - self.create_server_links(request.user, content) + print(response, content) return JsonResponse(content, safe=False) - def create_server_links(self, user, content: list[dict]): - """ - Creates objects representing the user's discord servers, storing - server info and the user's permissions within. - - Parameters - ---------- - content : list[dict] - Raw data for the servers. - """ - - servers = [ - UserServerLink( - server_id=server["id"], - user=user, - name=server["name"], - permissions=server["permissions"], - icon=server["icon"] - ) - for server in content - ] - - UserServerLink.objects.filter(user=user).delete() - UserServerLink.objects.bulk_create(servers) - class GuildChannelsView(View): @@ -139,5 +115,30 @@ class GuildChannelsView(View): url=f"{settings.DISCORD_API_URL}/guilds/{guild_id}/channels", headers={"Authorization": f"Bot {settings.BOT_TOKEN}"} ) + print(response, response.json()) return JsonResponse(response.json(), safe=False) + + +class SaveGuildView(View): + + def get(self, request, *args, **kwargs): + + guild_id = request.GET.get("id") + + if guild_id: + return SavedGuilds.objects.filter(id=guild_id) + + return SavedGuilds.objects.all() + + def post(self, request, *args, **kwargs): + + data = request.POST + + guild = SavedGuilds.objects.get_or_create( + id=data["id"], + name=data["name"], + icon=data["icon"] + ) + + return JsonResponse(data) diff --git a/apps/home/admin.py b/apps/home/admin.py index 8c9a8fb..1196528 100644 --- a/apps/home/admin.py +++ b/apps/home/admin.py @@ -2,7 +2,7 @@ from django.contrib import admin -from .models import Subscription, TrackedContent +from .models import Subscription, SavedGuilds @admin.register(Subscription) @@ -13,17 +13,8 @@ class SubscriptionAdmin(admin.ModelAdmin): ] -# @admin.register(SubscriptionTarget) -# class SubscriptionTargetAdmin(admin.ModelAdmin): -# list_display = [ -# "id", "creation_datetime" -# ] - - -@admin.register(TrackedContent) -class TrackedContentAdmin(admin.ModelAdmin): +@admin.register(SavedGuilds) +class SavedGuildAdmin(admin.ModelAdmin): list_display = [ - "uuid", "content_url", "subscription", - "creation_datetime" + "id", "name", "icon" ] - \ No newline at end of file diff --git a/apps/home/migrations/0007_savedguilds_delete_trackedcontent.py b/apps/home/migrations/0007_savedguilds_delete_trackedcontent.py new file mode 100644 index 0000000..3aac7a9 --- /dev/null +++ b/apps/home/migrations/0007_savedguilds_delete_trackedcontent.py @@ -0,0 +1,24 @@ +# Generated by Django 5.0.1 on 2024-04-02 11:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('home', '0006_alter_subscription_image'), + ] + + operations = [ + migrations.CreateModel( + name='SavedGuilds', + fields=[ + ('id', models.PositiveBigIntegerField(primary_key=True, serialize=False)), + ('name', models.CharField(max_length=128)), + ('icon', models.CharField(max_length=128)), + ], + ), + migrations.DeleteModel( + name='TrackedContent', + ), + ] diff --git a/apps/home/migrations/0008_savedguilds_guild_id_alter_savedguilds_id.py b/apps/home/migrations/0008_savedguilds_guild_id_alter_savedguilds_id.py new file mode 100644 index 0000000..bddf308 --- /dev/null +++ b/apps/home/migrations/0008_savedguilds_guild_id_alter_savedguilds_id.py @@ -0,0 +1,24 @@ +# Generated by Django 5.0.1 on 2024-04-02 16:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('home', '0007_savedguilds_delete_trackedcontent'), + ] + + operations = [ + migrations.AddField( + model_name='savedguilds', + name='guild_id', + field=models.PositiveBigIntegerField(default=1), + preserve_default=False, + ), + migrations.AlterField( + model_name='savedguilds', + name='id', + field=models.AutoField(primary_key=True, serialize=False), + ), + ] diff --git a/apps/home/migrations/0009_alter_savedguilds_guild_id.py b/apps/home/migrations/0009_alter_savedguilds_guild_id.py new file mode 100644 index 0000000..74bad53 --- /dev/null +++ b/apps/home/migrations/0009_alter_savedguilds_guild_id.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.1 on 2024-04-02 16:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('home', '0008_savedguilds_guild_id_alter_savedguilds_id'), + ] + + operations = [ + migrations.AlterField( + model_name='savedguilds', + name='guild_id', + field=models.CharField(max_length=128), + ), + ] diff --git a/apps/home/models.py b/apps/home/models.py index 886edaa..52e7561 100644 --- a/apps/home/models.py +++ b/apps/home/models.py @@ -39,31 +39,26 @@ class IconPathGenerator: return Path(instance.__class__.__name__.lower()) / str(instance.uuid) / "icon.webp" -# class SubscriptionTarget(models.Model): -# """ -# Represents a Discord TextChannel that should be subject to the content of a Subscription. -# """ +class SavedGuilds(models.Model): + """ + + """ -# id = models.PositiveBigIntegerField( -# verbose_name=_("id"), -# primary_key=True, -# help_text=_("Discord Channel ID") -# ) + id = models.AutoField( + primary_key=True + ) -# creation_datetime = models.DateTimeField( -# verbose_name=_("creation datetime"), -# help_text=_("when this instance was created."), -# default=timezone.now, -# editable=False -# ) + guild_id = models.CharField( + max_length=128 + ) -# class Meta: -# verbose_name = "subscription target" -# verbose_name_plural = "subscription targets" -# get_latest_by = "-creation_date" + name = models.CharField( + max_length=128 + ) -# def __str__(self): -# return str(self.id) + icon = models.CharField( + max_length=128 + ) class Subscription(models.Model): @@ -155,126 +150,3 @@ class Subscription(models.Model): log.debug("%sSubscription Saved %s", new_text, self.uuid) super().save(*args, **kwargs) - # @property - # def channels(self): - # """ - # Returns all SubscriptionChannel objects linked to this Subscription. - # """ - - # return SubscriptionChannel.objects.filter(subscription=self) - - # def save(self, *args, **kwargs): - # if Subscription.objects.filter(server=self.server).count() >= 1: - # raise IntegrityError(f"Subscription limit reached for server '{self.server}'") - - # super().save(*args, **kwargs) - - -# class SubscriptionChannel(models.Model): -# """ -# Represents a Discord TextChannel that should be subject to the content of a Subscription. -# """ - -# uuid = models.UUIDField( -# primary_key=True, -# default=uuid4, -# editable=False -# ) - -# # The ID is not auto generated, but rather be obtained from discord. -# id = models.PositiveBigIntegerField( -# verbose_name=_("id"), -# help_text=_("Identifier of the channel, provided by Discord.") -# ) - -# subscription = models.ForeignKey( -# verbose_name=_("subscription"), -# help_text=_("The subscription of this instance."), -# to=Subscription, -# on_delete=models.CASCADE -# ) - -# creation_datetime = models.DateTimeField( -# verbose_name=_("creation datetime"), -# help_text=_("when this instance was created."), -# default=timezone.now, -# editable=False -# ) - -# class Meta: -# verbose_name = "subscription channel" -# verbose_name_plural = "subscription channels" -# get_latest_by = "-creation_date" -# constraints = [ -# models.UniqueConstraint(fields=["id", "subscription"], name="unique id & sub pair") -# ] - -# def __str__(self): -# return str(self.id) - -# def save(self, *args, **kwargs): -# if SubscriptionChannel.objects.filter(subscription=self.subscription).count() >= 4: -# raise IntegrityError( -# f"SubscriptionChannel limit reached for subscription '{self.subscription}'" -# ) - -# super().save(*args, **kwargs) - - -class TrackedContent(models.Model): - """ - Tracks content shared from an RSS Feed Subscription. - Content is tracked to help prevent duplicate content. - """ - - uuid = models.UUIDField( - primary_key=True, - default=uuid4, - editable=False - ) - - subscription = models.ForeignKey( - verbose_name=_("subscription"), - help_text=_("The subscription that this content originated from."), - to=Subscription, - on_delete=models.CASCADE, - related_name="tracked_content" - ) - - content_url = models.URLField( - verbose_name=_("content url"), - help_text=_("URL of the tracked content.") - ) - - creation_datetime = models.DateTimeField( - verbose_name=_("creation datetime"), - help_text=_("when this instance was created."), - default=timezone.now, - editable=False - ) - - def __str__(self): - return str(self.content_url) - - -@receiver(pre_save, sender=TrackedContent) -def maintain_item_cap(sender, instance, **kwargs): - """" - Delete the latest tracked content, if the total under - the same subscription reaches 100. - """ - - log.debug("checking if tracked content can be deleted.") - - queryset = sender.objects.filter(subscription=instance.subscription) - total_tracked = queryset.count() - - if total_tracked < 100: - log.debug("tracked content cannot be deleted, less than 100 items: %s", total_tracked) - return - - oldest = queryset.earliest() - - log.info("tracked content limit exceeded, deleting oldest item with uuid: %s", oldest.uuid) - - oldest.delete() diff --git a/apps/static/js/api.js b/apps/static/js/api.js index 33c00dd..5dd8669 100644 --- a/apps/static/js/api.js +++ b/apps/static/js/api.js @@ -1,3 +1,59 @@ +function getSavedGuilds() { + return new Promise(function(resolve, reject) { + $.ajax({ + url: "/api/saved-guilds/", + type: "GET", + beforeSend: function(xhr) { + xhr.setRequestHeader("X-CSRFToken", CSRF_MiddlewareToken); + }, + success: function(response) { + resolve(response); + }, + error: function(response) { + reject(response); + } + }); + }); +} + +function getSavedGuild(id) { + return new Promise(function(resolve, reject) { + $.ajax({ + url: `/api/saved-guilds/${id}/`, + type: "GET", + beforeSend: function(xhr) { + xhr.setRequestHeader("X-CSRFToken", CSRF_MiddlewareToken); + }, + success: function(response) { + resolve(response); + }, + error: function(response) { + reject(response); + } + }); + }); +} + +function newSavedGuild(formData) { + return new Promise(function(resolve, reject) { + $.ajax({ + url: "/api/saved-guilds/", + type: "POST", + data: formData, + processData: false, + contentType: false, + beforeSend: function(xhr) { + xhr.setRequestHeader("X-CSRFToken", CSRF_MiddlewareToken); + }, + success: function(response) { + resolve(response); + }, + error: function(response) { + reject(response); + } + }); + }); +} function getSubscriptions() { return new Promise(function(resolve, reject) { diff --git a/apps/static/js/subscriptions.js b/apps/static/js/subscriptions.js index 2bc9c32..6bcdc94 100644 --- a/apps/static/js/subscriptions.js +++ b/apps/static/js/subscriptions.js @@ -74,7 +74,7 @@ function loadGuilds() { for (i = 1; i < response.length; i++) { var guild = response[i]; $("#editSubServer").append($("