storing guilds first, then subscriptions

This commit is contained in:
Corban-Lee Jones 2024-04-02 17:28:42 +01:00
parent 73ee984747
commit 05e90c64e4
14 changed files with 383 additions and 304 deletions

View File

@ -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")

View File

@ -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("<str:pk>/", Subscription_DetailView.as_view(), name="subscription-detail")
])),
path("tracked/", include([
path("", TrackedContent_ListView.as_view(), name="tracked"),
path("<str:pk>/", TrackedContent_DetailView.as_view(), name="tracked-detail")
])),
path("serverlink/", include([
path("", UserServerLink_ListView.as_view(), name="serverlink"),
path("<int:pk>/", UserServerLink_DetailView.as_view(), name="serverlink-detail")
])),
path("saved-guilds/", include([
path("", SavedGuild_ListView.as_view(), name="saved-guilds"),
path("<int:pk>/", SavedGuild_DetailView.as_view(), name="saved-guilds-detail")
])),
]

View File

@ -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()

View File

@ -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")
]

View File

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

View File

@ -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"
]

View File

@ -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',
),
]

View File

@ -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),
),
]

View File

@ -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),
),
]

View File

@ -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()

View File

@ -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) {

View File

@ -74,7 +74,7 @@ function loadGuilds() {
for (i = 1; i < response.length; i++) {
var guild = response[i];
$("#editSubServer").append($("<option>", {
value: guild.id,
value: guild.guild_id,
text: guild.name
}));
}

View File

@ -0,0 +1,19 @@
<form id="serverForm" novalidate>
<div class="modal fade" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
test
</div>
<div class="modal-body">
<select name="serverOptions" id="serverOptions" class="select-2" data-dropdownparent="#serverForm .modal"></select>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">Submit</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
</div>
</div>
</div>
</div>
</form>

View File

@ -19,34 +19,9 @@
<div class="peers as-s ai-s w-100">
<div class="peer bg-body-secondary">
<div class="p-2 layers border-end h-100">
<div id="serverList" class="p-2 layers border-end h-100">
<div class="layer mb-2">
<button type="button" class="rounded-3 p-1 bg-body bd">
<img src="https://cdn.discordapp.com/icons/136501320340209664/bc41eb01d667196c17e05c045f357268.webp?size=80" alt="" width="50">
</button>
</div>
<div class="layer mb-2">
<button type="button" class="rounded-3 p-1 bg-body bd">
<img src="https://cdn.discordapp.com/icons/136501320340209664/bc41eb01d667196c17e05c045f357268.webp?size=80" alt="" width="50">
</button>
</div>
<div class="layer mb-2">
<button type="button" class="rounded-3 p-1 bg-body bd">
<img src="https://cdn.discordapp.com/icons/136501320340209664/bc41eb01d667196c17e05c045f357268.webp?size=80" alt="" width="50">
</button>
</div>
<div class="layer mb-2">
<button type="button" class="rounded-3 p-1 bg-body bd">
<img src="https://cdn.discordapp.com/icons/136501320340209664/bc41eb01d667196c17e05c045f357268.webp?size=80" alt="" width="50">
</button>
</div>
<div class="layer mb-2">
<button type="button" class="rounded-3 p-1 bg-body bd">
<img src="https://cdn.discordapp.com/icons/136501320340209664/bc41eb01d667196c17e05c045f357268.webp?size=80" alt="" width="50">
</button>
</div>
<div class="layer mb-2">
<button type="button" class="btn btn-secondary rounded-3 p-1 bd" onclick="javascript: alert('add new server (not implemented)');">
<button type="button" id="newServerBtn" class="btn btn-secondary rounded-3 p-1 bd" onclick="javascript: openServerModal();">
<span class="d-flex jc-c ai-c" style="width: 50px; height: 50px;">
<i class="bi bi-plus-lg fs-4"></i>
</span>
@ -55,7 +30,7 @@
</div>
</div>
<div class="peer-greed">
<header class="px-4 py-3 bg-body-secondary">
<header class="px-4 py-3 border-bottom">
<div class="peers">
<div class="peer-greed">
<div class="peers ai-c">
@ -69,7 +44,7 @@
</div>
</div>
<div class="peer d-flex as-s ai-c">
<button type="button" class="btn btn-primary">New Subscription</button>
<button type="button" id="newSubscriptionBtn" class="btn btn-primary">New Subscription</button>
</div>
</div>
</header>
@ -115,7 +90,114 @@
</div>
</main>
{% include "home/includes/servermodal.html" %}
{% endblock content %}
<!-- Specific Page JS goes HERE -->
{% block javascripts %}{% endblock javascripts %}
{% block javascripts %}
<script id="serverItemTemplate" type="text/template">
<div class="layer mb-2">
<button type="button" class="rounded-3 p-1 bg-body bd">
<img src="" class="rounded-3" alt="" width="50" height="50">
<!-- https://cdn.discordapp.com/icons/136501320340209664/bc41eb01d667196c17e05c045f357268.webp?size=80 -->
</button>
</div>
</script>
<script src="{% static 'js/api.js' %}"></script>
<script>
function openServerModal() {
$("#newServerBtn").prop("disabled", true);
$('#serverOptions').val(null).trigger('change');
$("#serverOptions").empty();
$.ajax({
url: "/guilds",
type: "GET",
success: function(response) {
for (i = 1; i < response.length; i++) {
var guild = response[i];
$("#serverOptions").append($("<option>", {
value: guild.id,
text: guild.name,
"data-icon": guild.icon
}));
}
$("#newServerBtn").prop("disabled", false);
$("#serverForm .modal").modal("show");
},
error: function(response) {
$("#newServerBtn").prop("disabled", false);
alert(JSON.stringify(response, null, 4));
}
});
};
function addServer(serverName, serverId, serverIconHash) {
var formData = new FormData();
formData.append("name", serverName);
formData.append("guild_id", serverId);
formData.append("icon", serverIconHash);
newSavedGuild(formData)
.then(resp => {
alert(JSON.stringify(resp, null, 4));
})
.catch(err => {
alert(JSON.stringify(err, null, 4));
})
addServerTemplate(serverId, serverIconHash);
}
$(document).ready(function() {
getSavedGuilds()
.then(resp => {
alert(JSON.stringify(resp, null, 4));
for (i=0; i < resp.length; i++) {
var guild = resp[i];
addServerTemplate(guild.guild_id, guild.icon);
}
})
.catch(err => {
alert(JSON.stringify(err, null, 4));
})
});
function addServerTemplate(serverId, serverIconHash) {
template = $($("#serverItemTemplate").html());
template.find("img").attr("src", `https://cdn.discordapp.com/icons/${serverId}/${serverIconHash}.webp?size=80`);
$("#serverList").prepend(template);
}
$("#serverForm").on("submit", function(event) {
event.preventDefault();
var selectedOption = $("#serverOptions option:selected");
serverName = selectedOption.text();
serverId = selectedOption.val();
serverIconHash = selectedOption.attr("data-icon");
addServer(serverName, serverId, serverIconHash);
// $.ajax({
// url: "",
// type: "GET",
// success: function(response) {
// alert(JSON.stringify(response, null, 4));
// },
// error: function(response) {
// alert(JSON.stringify(response, null, 4));
// }
// });
$("#serverForm .modal").modal("hide");
alert(`${serverName} ${serverId} ${serverIconHash}`);
});
</script>
{% endblock javascripts %}