Full Discord integration
This commit is contained in:
parent
dde32e4b6d
commit
8666a6094e
@ -4,7 +4,7 @@ import logging
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from apps.home.models import Subscription, SubscriptionChannel, TrackedContent
|
||||
from apps.home.models import Subscription, TrackedContent
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@ -105,6 +105,16 @@ 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.
|
||||
@ -112,37 +122,11 @@ class SubscriptionSerializer(DynamicModelSerializer):
|
||||
|
||||
image = serializers.ImageField(required=False)
|
||||
server = serializers.CharField()
|
||||
# targets = SubscriptionTargetSerializer(many=True, required=False)
|
||||
|
||||
class Meta:
|
||||
model = Subscription
|
||||
fields = ("uuid", "name", "rss_url", "image", "server", "creation_datetime")
|
||||
|
||||
|
||||
class SubscriptionChannelSerializerGET(DynamicModelSerializer):
|
||||
"""
|
||||
Serializer for the SubscriptionChannel Model.
|
||||
This serializer should be used with GET requests.
|
||||
"""
|
||||
|
||||
subscription = SubscriptionSerializer()
|
||||
|
||||
class Meta:
|
||||
model = SubscriptionChannel
|
||||
fields = ("uuid", "id", "subscription", "creation_datetime")
|
||||
|
||||
|
||||
class SubscriptionChannelSerializerPOST(DynamicModelSerializer):
|
||||
"""
|
||||
Serializer for the SubscriptionChannel Model.
|
||||
This serializer should be used with POST requests.
|
||||
"""
|
||||
|
||||
subscription = serializers.PrimaryKeyRelatedField(queryset=Subscription.objects.all())
|
||||
|
||||
class Meta:
|
||||
model = SubscriptionChannel
|
||||
fields = ("uuid", "id", "subscription", "creation_datetime")
|
||||
|
||||
fields = ("uuid", "name", "rss_url", "image", "server", "targets", "creation_datetime", "extra_notes")
|
||||
|
||||
class TrackedContentSerializer(DynamicModelSerializer):
|
||||
"""
|
||||
|
@ -6,8 +6,6 @@ from rest_framework.authtoken.views import obtain_auth_token
|
||||
from .views import (
|
||||
Subscription_ListView,
|
||||
Subscription_DetailView,
|
||||
SubscriptionChannel_ListView,
|
||||
SubscriptionChannel_DetailView,
|
||||
TrackedContent_ListView,
|
||||
TrackedContent_DetailView
|
||||
)
|
||||
@ -18,10 +16,6 @@ urlpatterns = [
|
||||
path("api-token-auth/", obtain_auth_token),
|
||||
|
||||
path("subscription/", include([
|
||||
path("channel/", include([
|
||||
path("", SubscriptionChannel_ListView.as_view(), name="subscriptionchannel"),
|
||||
path("<str:pk>/", SubscriptionChannel_DetailView.as_view(), name="subscriptionchannel-detail")
|
||||
])),
|
||||
path("", Subscription_ListView.as_view(), name="subscription"),
|
||||
path("<str:pk>/", Subscription_DetailView.as_view(), name="subscription-detail")
|
||||
])),
|
||||
|
@ -11,11 +11,9 @@ 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, SubscriptionChannel, TrackedContent
|
||||
from apps.home.models import Subscription, TrackedContent
|
||||
from .serializers import (
|
||||
SubscriptionSerializer,
|
||||
SubscriptionChannelSerializerGET,
|
||||
SubscriptionChannelSerializerPOST,
|
||||
TrackedContentSerializer
|
||||
)
|
||||
|
||||
@ -50,8 +48,8 @@ class Subscription_ListView(generics.ListCreateAPIView):
|
||||
queryset = Subscription.objects.all().order_by("-creation_datetime")
|
||||
|
||||
filter_backends = [filters.SearchFilter, rest_filters.DjangoFilterBackend, filters.OrderingFilter]
|
||||
filterset_fields = ["uuid", "name", "rss_url", "server", "creation_datetime"]
|
||||
search_fields = ["name"]
|
||||
filterset_fields = ["uuid", "name", "rss_url", "server", "targets", "creation_datetime", "extra_notes"]
|
||||
search_fields = ["name", "extra_notes"]
|
||||
ordering_fields = ["creation_datetime"]
|
||||
|
||||
def post(self, request):
|
||||
@ -86,88 +84,6 @@ class Subscription_DetailView(generics.RetrieveUpdateDestroyAPIView):
|
||||
queryset = Subscription.objects.all().order_by("-creation_datetime")
|
||||
|
||||
|
||||
# =================================================================================================
|
||||
# SubscriptionChannel Views
|
||||
|
||||
class SubscriptionChannel_ListView(generics.ListCreateAPIView):
|
||||
"""
|
||||
View to provide a list of SubscriptionChannel 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 = SubscriptionChannelSerializer
|
||||
queryset = SubscriptionChannel.objects.all().order_by("-creation_datetime")
|
||||
|
||||
filter_backends = [rest_filters.DjangoFilterBackend, filters.OrderingFilter]
|
||||
filterset_fields = ["uuid", "id", "subscription", "subscription__server"]
|
||||
ordering_fields = ["creation_datetime"]
|
||||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
if self.request.method == "POST":
|
||||
return SubscriptionChannelSerializerPOST(*args, **kwargs)
|
||||
else:
|
||||
return SubscriptionChannelSerializerGET(*args, **kwargs)
|
||||
|
||||
def post(self, request):
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
try:
|
||||
self.perform_create(serializer)
|
||||
except IntegrityError as exc:
|
||||
log.error(exc)
|
||||
return Response(
|
||||
{"detail": "Duplicate or limit reached"},
|
||||
status=status.HTTP_409_CONFLICT,
|
||||
exception=True
|
||||
)
|
||||
|
||||
# Switch to the GET serializer for return data, as it contains more data on the subscription.
|
||||
# We can't modify the POST serializer to contain this data, otherwise it expects it in POST.
|
||||
self.request.method = "GET"
|
||||
serializer = self.get_serializer(serializer.instance)
|
||||
|
||||
headers = self.get_success_headers(serializer.data)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
|
||||
|
||||
|
||||
class SubscriptionChannel_DetailView(generics.RetrieveDestroyAPIView):
|
||||
"""
|
||||
View to provide details on a particular SubscriptionChannel model instances.
|
||||
|
||||
Supports: GET, DELETE
|
||||
"""
|
||||
|
||||
authentication_classes = [SessionAuthentication, TokenAuthentication]
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
parser_classes = [MultiPartParser, FormParser]
|
||||
|
||||
serializer_class = SubscriptionChannelSerializerGET
|
||||
queryset = SubscriptionChannel.objects.all().order_by("-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": "Duplicate or limit reached"},
|
||||
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)
|
||||
|
||||
|
||||
# =================================================================================================
|
||||
# Tracked Content Views
|
||||
|
||||
|
@ -34,6 +34,7 @@ class DiscordUserOAuth2Manager(models.UserManager):
|
||||
flags=user["flags"],
|
||||
locale=user["locale"],
|
||||
mfa_enabled=user["mfa_enabled"],
|
||||
access_token=user["access_token"],
|
||||
**extra_fields
|
||||
)
|
||||
|
||||
|
@ -1,5 +1,7 @@
|
||||
# Generated by Django 5.0.1 on 2024-03-10 22:22
|
||||
# Generated by Django 5.0.1 on 2024-03-11 17:38
|
||||
|
||||
import apps.authentication.managers
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
@ -14,15 +16,22 @@ class Migration(migrations.Migration):
|
||||
migrations.CreateModel(
|
||||
name='DiscordUser',
|
||||
fields=[
|
||||
('id', models.PositiveBigIntegerField(primary_key=True, serialize=False)),
|
||||
('username', models.CharField(max_length=100)),
|
||||
('global_name', models.CharField(max_length=100)),
|
||||
('avatar', models.CharField(max_length=100)),
|
||||
('public_flags', models.IntegerField()),
|
||||
('flags', models.IntegerField()),
|
||||
('locale', models.CharField(max_length=100)),
|
||||
('mfa_enabled', models.BooleanField()),
|
||||
('last_login', models.DateTimeField()),
|
||||
('id', models.PositiveBigIntegerField(help_text="the user's id", primary_key=True, serialize=False)),
|
||||
('username', models.CharField(help_text="the user's username, not unique across the platform", max_length=32, unique=True, verbose_name='username')),
|
||||
('global_name', models.CharField(help_text="the user's display name, if it is set. For bots, this is the application name", max_length=32, verbose_name='nickname')),
|
||||
('avatar', models.CharField(help_text="the user's avatar hash", max_length=64, verbose_name='avatar')),
|
||||
('public_flags', models.IntegerField(help_text="the public flags on a user's account", verbose_name='public flags')),
|
||||
('flags', models.IntegerField(help_text="the flags on a user's account", verbose_name='flags')),
|
||||
('locale', models.CharField(help_text="the user's chosen language option", max_length=16, verbose_name='locale')),
|
||||
('mfa_enabled', models.BooleanField(help_text='whether the user has two factor enabled on their account', verbose_name='mfa enabled')),
|
||||
('last_login', models.DateTimeField(default=django.utils.timezone.now, help_text='datetime of the previous login', verbose_name='last login')),
|
||||
('access_token', models.CharField(help_text='token for the application to make api calls on behalf of the user.', max_length=100, verbose_name='access token')),
|
||||
('is_active', models.BooleanField(default=True, help_text='Use as a "soft delete" rather than deleting the user.', verbose_name='active status')),
|
||||
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
|
||||
('is_superuser', models.BooleanField(default=False, help_text='Designates whether the user has unrestricted site control.', verbose_name='superuser status')),
|
||||
],
|
||||
managers=[
|
||||
('objects', apps.authentication.managers.DiscordUserOAuth2Manager()),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
@ -1,81 +0,0 @@
|
||||
# Generated by Django 5.0.1 on 2024-03-10 23:15
|
||||
|
||||
import apps.authentication.managers
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('authentication', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelManagers(
|
||||
name='discorduser',
|
||||
managers=[
|
||||
('objects', apps.authentication.managers.DiscordUserOAuth2Manager()),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='discorduser',
|
||||
name='is_active',
|
||||
field=models.BooleanField(default=True, help_text='Use as a "soft delete" rather than deleting the user.', verbose_name='active status'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='discorduser',
|
||||
name='is_staff',
|
||||
field=models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='discorduser',
|
||||
name='is_superuser',
|
||||
field=models.BooleanField(default=False, help_text='Designates whether the user has unrestricted site control.', verbose_name='superuser status'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='discorduser',
|
||||
name='avatar',
|
||||
field=models.CharField(help_text="the user's avatar hash", max_length=64, verbose_name='avatar'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='discorduser',
|
||||
name='flags',
|
||||
field=models.IntegerField(help_text="the flags on a user's account", verbose_name='flags'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='discorduser',
|
||||
name='global_name',
|
||||
field=models.CharField(help_text="the user's display name, if it is set. For bots, this is the application name", max_length=32, verbose_name='nickname'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='discorduser',
|
||||
name='id',
|
||||
field=models.PositiveBigIntegerField(help_text="the user's id", primary_key=True, serialize=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='discorduser',
|
||||
name='last_login',
|
||||
field=models.DateTimeField(default=django.utils.timezone.now, help_text='datetime of the previous login', verbose_name='last login'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='discorduser',
|
||||
name='locale',
|
||||
field=models.CharField(help_text="the user's chosen language option", max_length=16, verbose_name='locale'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='discorduser',
|
||||
name='mfa_enabled',
|
||||
field=models.BooleanField(help_text='whether the user has two factor enabled on their account', verbose_name='mfa enabled'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='discorduser',
|
||||
name='public_flags',
|
||||
field=models.IntegerField(help_text="the public flags on a user's account", verbose_name='public flags'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='discorduser',
|
||||
name='username',
|
||||
field=models.CharField(help_text="the user's username, not unique across the platform", max_length=32, unique=True, verbose_name='username'),
|
||||
),
|
||||
]
|
@ -58,6 +58,14 @@ class DiscordUser(models.Model):
|
||||
help_text=_("datetime of the previous login")
|
||||
)
|
||||
|
||||
# Access Token
|
||||
|
||||
access_token = models.CharField(
|
||||
_("access token"),
|
||||
max_length=100,
|
||||
help_text=_("token for the application to make api calls on behalf of the user.")
|
||||
)
|
||||
|
||||
# Custom Attributes
|
||||
|
||||
is_active = models.BooleanField(
|
||||
@ -84,4 +92,8 @@ class DiscordUser(models.Model):
|
||||
return True
|
||||
|
||||
def __str__(self):
|
||||
return self.global_name or self.username
|
||||
return self.global_name or self.username
|
||||
|
||||
@property
|
||||
def avatar_url(self):
|
||||
return f"https://cdn.discordapp.com/avatars/{self.id}/{self.avatar}.webp?size=128"
|
@ -3,13 +3,16 @@
|
||||
from django.urls import path
|
||||
from django.contrib.auth.views import LogoutView
|
||||
|
||||
# from .views import login_view, register_user, profile_view, discord_login, discord_login_redirect
|
||||
from . import views
|
||||
from .views import DiscordLoginAction, DiscordLoginRedirect, Login
|
||||
from .views import DiscordLoginAction, DiscordLoginRedirect, Login, GuildsView, GuildChannelsView
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path("login/", Login.as_view(), name="login"),
|
||||
path("oauth2/login/", DiscordLoginAction.as_view(), name="discord-login"),
|
||||
path("oauth2/login/redirect/", DiscordLoginRedirect.as_view(), name="discord-login-redirect")
|
||||
path("oauth2/login/redirect/", DiscordLoginRedirect.as_view(), name="discord-login-redirect"),
|
||||
path("logout/", LogoutView.as_view(), name="logout"),
|
||||
|
||||
path("guilds/", GuildsView.as_view(), name="guilds"),
|
||||
path("channels/", GuildChannelsView.as_view(), name="channels")
|
||||
|
||||
]
|
||||
|
@ -29,6 +29,7 @@ class DiscordLoginRedirect(View):
|
||||
code = request.GET.get("code")
|
||||
access_token = self.exchange_code_for_token(code)
|
||||
raw_user_data = self.get_raw_user_data(access_token)
|
||||
raw_user_data["access_token"] = access_token
|
||||
|
||||
log.debug(raw_user_data)
|
||||
|
||||
@ -78,3 +79,33 @@ class DiscordLoginRedirect(View):
|
||||
class Login(TemplateView):
|
||||
|
||||
template_name = "accounts/login.html"
|
||||
|
||||
|
||||
class GuildsView(View):
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
|
||||
response = requests.get(
|
||||
url="https://discord.com/api/v6/users/@me/guilds",
|
||||
headers={"Authorization": f"Bearer {request.user.access_token}"}
|
||||
)
|
||||
|
||||
content = response.json()
|
||||
|
||||
return JsonResponse(content, safe=False)
|
||||
|
||||
|
||||
class GuildChannelsView(View):
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
|
||||
guild_id = request.GET.get("guild")
|
||||
|
||||
log.debug("fetching channels from %s using token: %s", guild_id, settings.BOT_TOKEN)
|
||||
|
||||
response = requests.get(
|
||||
url=f"https://discord.com/api/v10/guilds/{guild_id}/channels",
|
||||
headers={"Authorization": f"Bot {settings.BOT_TOKEN}"}
|
||||
)
|
||||
|
||||
return JsonResponse(response.json(), safe=False)
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import Subscription, SubscriptionChannel, TrackedContent
|
||||
from .models import Subscription, TrackedContent
|
||||
|
||||
|
||||
@admin.register(Subscription)
|
||||
@ -13,11 +13,11 @@ class SubscriptionAdmin(admin.ModelAdmin):
|
||||
]
|
||||
|
||||
|
||||
@admin.register(SubscriptionChannel)
|
||||
class SubscriptionAdmin(admin.ModelAdmin):
|
||||
list_display = [
|
||||
"uuid", "id", "subscription", "creation_datetime"
|
||||
]
|
||||
# @admin.register(SubscriptionTarget)
|
||||
# class SubscriptionTargetAdmin(admin.ModelAdmin):
|
||||
# list_display = [
|
||||
# "id", "creation_datetime"
|
||||
# ]
|
||||
|
||||
|
||||
@admin.register(TrackedContent)
|
||||
|
@ -0,0 +1,34 @@
|
||||
# Generated by Django 5.0.1 on 2024-03-11 15:34
|
||||
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('home', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='SubscriptionTarget',
|
||||
fields=[
|
||||
('id', models.PositiveBigIntegerField(help_text='Discord Channel ID', 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': 'subscription target',
|
||||
'verbose_name_plural': 'subscription targets',
|
||||
'get_latest_by': '-creation_date',
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='subscription',
|
||||
name='targets',
|
||||
field=models.ManyToManyField(help_text='Discord channels where subscription content is delivered to.', to='home.subscriptiontarget', verbose_name='targets'),
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='SubscriptionChannel',
|
||||
),
|
||||
]
|
@ -0,0 +1,26 @@
|
||||
# Generated by Django 5.0.1 on 2024-03-11 17:38
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('home', '0002_subscriptiontarget_subscription_targets_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='subscription',
|
||||
name='targets',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='SubscriptionTarget',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='subscription',
|
||||
name='targets',
|
||||
field=models.CharField(default='', help_text='Discord channels where subscription content is delivered to.', max_length=500, verbose_name='targets'),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
18
apps/home/migrations/0004_subscription_extra_notes.py
Normal file
18
apps/home/migrations/0004_subscription_extra_notes.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.0.1 on 2024-03-11 18:24
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('home', '0003_remove_subscription_targets_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='subscription',
|
||||
name='extra_notes',
|
||||
field=models.CharField(blank=True, help_text='Additional user written notes about this item.', max_length=250, null=True, verbose_name='extra notes'),
|
||||
),
|
||||
]
|
@ -39,6 +39,33 @@ 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.
|
||||
# """
|
||||
|
||||
# id = models.PositiveBigIntegerField(
|
||||
# verbose_name=_("id"),
|
||||
# primary_key=True,
|
||||
# help_text=_("Discord Channel ID")
|
||||
# )
|
||||
|
||||
# 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 target"
|
||||
# verbose_name_plural = "subscription targets"
|
||||
# get_latest_by = "-creation_date"
|
||||
|
||||
# def __str__(self):
|
||||
# return str(self.id)
|
||||
|
||||
|
||||
class Subscription(models.Model):
|
||||
"""
|
||||
Represents a stored RSS Feed.
|
||||
@ -80,6 +107,12 @@ class Subscription(models.Model):
|
||||
help_text=_("Identifier for the discord server that owns this subscription.")
|
||||
)
|
||||
|
||||
targets = models.CharField(
|
||||
verbose_name=_("targets"),
|
||||
max_length=500,
|
||||
help_text=_("Discord channels where subscription content is delivered to.")
|
||||
)
|
||||
|
||||
creation_datetime = models.DateTimeField(
|
||||
verbose_name=_("creation datetime"),
|
||||
help_text=_("when this instance was created."),
|
||||
@ -87,6 +120,14 @@ class Subscription(models.Model):
|
||||
editable=False
|
||||
)
|
||||
|
||||
extra_notes = models.CharField(
|
||||
verbose_name=_("extra notes"),
|
||||
max_length=250,
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text=_("Additional user written notes about this item.")
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "subscription"
|
||||
verbose_name_plural = "subscriptions"
|
||||
@ -104,13 +145,13 @@ 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.
|
||||
"""
|
||||
# @property
|
||||
# def channels(self):
|
||||
# """
|
||||
# Returns all SubscriptionChannel objects linked to this Subscription.
|
||||
# """
|
||||
|
||||
return SubscriptionChannel.objects.filter(subscription=self)
|
||||
# return SubscriptionChannel.objects.filter(subscription=self)
|
||||
|
||||
# def save(self, *args, **kwargs):
|
||||
# if Subscription.objects.filter(server=self.server).count() >= 1:
|
||||
@ -119,55 +160,55 @@ class Subscription(models.Model):
|
||||
# super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class SubscriptionChannel(models.Model):
|
||||
"""
|
||||
Represents a Discord TextChannel that should be subject to the content of a Subscription.
|
||||
"""
|
||||
# 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
|
||||
)
|
||||
# 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.")
|
||||
)
|
||||
# # 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
|
||||
)
|
||||
# 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
|
||||
)
|
||||
# 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")
|
||||
]
|
||||
# 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 __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}'"
|
||||
)
|
||||
# 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)
|
||||
# super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class TrackedContent(models.Model):
|
||||
|
@ -99,15 +99,15 @@
|
||||
<!-- ### $App Screen Content ### -->
|
||||
<main class="main-content bg-body-tertiary">
|
||||
<div id="mainContent">
|
||||
|
||||
<!--
|
||||
<div class="row gap-20 masonry pos-r">
|
||||
<div class="masonry-sizer col-md-6"></div>
|
||||
|
||||
<div class="masonry-item col-12">
|
||||
<div class="row gap-20">
|
||||
<div class="row gap-20"> -->
|
||||
|
||||
<!-- Total Subscriptions -->
|
||||
<div class="col-md-3">
|
||||
<!-- <div class="col-md-3">
|
||||
<div class="layers bd bg-body p-20">
|
||||
<div class="layer w-100 mB-10">
|
||||
<h6 class="lh-1">Total Subscriptions</h6>
|
||||
@ -125,10 +125,10 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<!-- Subscription Activity (24hrs) -->
|
||||
<div class="col-md-3">
|
||||
<!-- <div class="col-md-3">
|
||||
<div class="layers bd bg-body p-20">
|
||||
<div class="layer w-100 mB-10">
|
||||
<h6 class="lh-1">Subscription Activity (24hrs)</h6>
|
||||
@ -144,15 +144,15 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
</div>
|
||||
<!-- </div>
|
||||
</div>
|
||||
|
||||
<div class="masonry-item col-12">
|
||||
<hr>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
<div class="container-fluid mT-10 pX-0">
|
||||
<div class="lh-1 pX-5 pB-20 peers">
|
||||
<div class="peer peer-greed">
|
||||
@ -164,7 +164,6 @@
|
||||
New Subscription
|
||||
</button>
|
||||
</div>
|
||||
<!-- bd -->
|
||||
</div>
|
||||
|
||||
<div id="subscriptionContainer" class="row gap-20">
|
||||
@ -211,16 +210,22 @@
|
||||
</div>
|
||||
</div>
|
||||
<div id="navChannelPanel" class="tab-pane fade" role="tabpanel" aria-labelledby="navChannelTab" tabindex="0">
|
||||
<div>
|
||||
<div class="mb-3">
|
||||
<label for="editSubServer" class="form-label">Server</label>
|
||||
<select name="editSubServer" id="editSubServer" class="form-select bdrs-2 bd" required>
|
||||
<option value="">Select a server</option>
|
||||
</select>
|
||||
<div class="form-help">Ensure that PYRSS Bot is a member of the selected server.</div>
|
||||
<div class="invalid-feedback">Please Select a Server.</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="editSubChannels">Channels</label>
|
||||
<select name="editSubChannels" id="editSubChannels" class="form-select bdrs-2 bd" required multiple>
|
||||
<option value="1039201459188805692">CodeHub</option>
|
||||
<option value="753323563381031042">Orange</option>
|
||||
<option value="1204426362794811453">PYRSS Home</option>
|
||||
<option value="410208534861447168">Ryujinx</option>
|
||||
</select>
|
||||
<div class="invalid-feedback">Please Select a Server.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="navExtrasPanel" class="tab-pane fade" role="tabpanel" aria-labelledby="navExtrasTab" tabindex="0">
|
||||
@ -252,38 +257,78 @@
|
||||
{% block javascripts %}
|
||||
<script id="subItemTemplate" type="text/template">
|
||||
<div class="col-md-4 col-xxl-3">
|
||||
<div class="sub-item layers bd bg-body p-20 h-100" data-uuid="">
|
||||
<div class="layer w-100 mb-3">
|
||||
<h6 class="lh-1 sub-name mb-0"></h6>
|
||||
<div class="sub-item layers bd bg-body h-100 rounded-3" data-uuid="">
|
||||
<!-- <div class="layer w-100 bdB pb-4">
|
||||
<div class="d-flex px-4">
|
||||
<span class="h5 lh-1 sub-name mb-0"></span>
|
||||
<img class="sub-img h-100">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layer w-100 mb-3">
|
||||
<!-- <div class="lh-1 fw-bold">UUID</div> -->
|
||||
<div class="layer w-100 py-4 px-4">
|
||||
<p class="sub-desc mb-0"></p>
|
||||
</div>
|
||||
<div class="layer w-100 mb-3 px-4">
|
||||
<span class=sub-uuid></span>
|
||||
</div>
|
||||
<div class="layer w-100 mb-3">
|
||||
<!-- <h6 class="lh-1">RSS URL</h6> -->
|
||||
<div class="layer w-100 mb-3 px-4">
|
||||
<a class="sub-rss"></a>
|
||||
</div>
|
||||
<div class="layer w-100 mb-3">
|
||||
<div class="layer w-100 mb-3 px-4">
|
||||
<img class="sub-img mw-100" height="75">
|
||||
</div>
|
||||
<div class="layer d-flex flex-wrap w-100 mt-auto">
|
||||
<!-- <div class="me-auto">
|
||||
<button class="btn btn-sm bg-body-tertiary bdrs-2 mT-10 mR-5 cur-p draw-border draw-border-primary">
|
||||
<i class="bi bi-link-45deg me-1"></i>
|
||||
Channel Links
|
||||
</button>
|
||||
</div> -->
|
||||
<div class="ms-auto">
|
||||
<button class="sub-edit btn btn-sm bg-body-tertiary bdrs-2 mT-10 cur-p draw-border draw-border-secondary">
|
||||
<div class="layer d-flex flex-wrap w-100 mt-auto bdT pb-3 px-4">
|
||||
<div class="me-auto d-flex ai-c">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input mt-3" style="transform:scale(1.25);">
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button class="sub-edit btn btn-sm bg-body-tertiary bdrs-2 mt-3 cur-p draw-border draw-border-secondary">
|
||||
<i class="bi bi-pencil me-1`"></i>
|
||||
Edit
|
||||
</button>
|
||||
<button class="sub-delete btn btn-sm bg-body-tertiary bdrs-2 mT-10 cur-p draw-border draw-border-danger">
|
||||
<button class="sub-delete btn btn-sm bg-body-tertiary bdrs-2 mt-3 cur-p draw-border draw-border-danger">
|
||||
<i class="bi bi-trash me-1`"></i>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div> -->
|
||||
<div class="layer w-100">
|
||||
<div class="peers p-4 flex-nowrap">
|
||||
<div class="peer peer-greed me-4">
|
||||
<h5 class="sub-name"></h5>
|
||||
<span class="sub-uuid"></span>
|
||||
</div>
|
||||
<div class="peer">
|
||||
<img src="" alt="" class="sub-img">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-50 align-self-start bd mx-4"></div>
|
||||
<div class="layer w-100 p-4">
|
||||
<p class="sub-desc mb-4"></p>
|
||||
<a class="sub-rss"></a>
|
||||
</div>
|
||||
<div class="layer w-100 bdT p-4 mt-auto">
|
||||
<div class="peers ai-c">
|
||||
<div class="peer peer-greed">
|
||||
<div class="form-check form-switch ms-2">
|
||||
<input type="checkbox" name="" id="" class="form-check-input" style="transform: scale(1.5);">
|
||||
</div>
|
||||
</div>
|
||||
<div class="peer">
|
||||
<button class="sub-edit btn bg-body-tertiary rounded-3">
|
||||
<i class="bi bi-pencil me-1"></i>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<div class="peer ms-3">
|
||||
<button class="sub-delete btn bg-body-tertiary rounded-3">
|
||||
<i class="bi bi-trash3 me-1"></i>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -295,17 +340,77 @@
|
||||
template.find(".sub-item").attr("data-uuid", data.uuid);
|
||||
template.find(".sub-name").text(data.name);
|
||||
template.find(".sub-uuid").text(data.uuid);
|
||||
template.find(".sub-rss").text(data.rss_url);
|
||||
template.find(".sub-rss").text(data.rss_url).attr("href", data.rss_url);
|
||||
template.find(".sub-img").attr("src", data.image);
|
||||
template.find(".sub-desc").text(data.extra_notes);
|
||||
template.find(".sub-edit").attr("onclick", `subEditModal("${data.uuid}");`);
|
||||
template.find(".sub-delete").attr("onclick", `unsubscribe("${data.uuid}");`);
|
||||
return template
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
loadGuilds();
|
||||
loadSubscriptions();
|
||||
|
||||
$("#editSubServer").change(function() {
|
||||
loadChannels($(this).find("option:selected").attr("value"));
|
||||
});
|
||||
});
|
||||
|
||||
function loadGuilds() {
|
||||
$.ajax({
|
||||
url: "/guilds",
|
||||
type: "GET",
|
||||
success: function(response) {
|
||||
for (i = 1; i < response.length; i++) {
|
||||
var guild = response[i];
|
||||
$("#editSubServer").append($("<option>", {
|
||||
value: guild.id,
|
||||
text: guild.name
|
||||
}));
|
||||
}
|
||||
},
|
||||
error: function(response) {
|
||||
alert(JSON.stringify(response, null, 4));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function loadChannels(guildID) {
|
||||
$("#editSubChannels").empty();
|
||||
$.ajax({
|
||||
url: `/channels?guild=${guildID}`,
|
||||
type: "GET",
|
||||
success: function(response) {
|
||||
for (i = 1; i < response.length; i++) {
|
||||
var channel = response[i];
|
||||
|
||||
if (channel.type !== 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
var selectedChannelIDs = $("#editSubChannels").attr("data-current").split(";");
|
||||
|
||||
$("#editSubChannels").append($("<option>", {
|
||||
value: channel.id,
|
||||
text: "#" + channel.name,
|
||||
selected: selectedChannelIDs.includes(channel.id.toString())
|
||||
}));
|
||||
}
|
||||
},
|
||||
error: function(response) {
|
||||
console.error(JSON.stringify(response, null, 4));
|
||||
|
||||
if (response.code == "50001") {
|
||||
alert("PYRSS Bot is Missing Access to this Server");
|
||||
}
|
||||
else {
|
||||
alert("unknown error fetching channels " + response.code)
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateSubscriptionCount(difference, overwrite) {
|
||||
const beforeChange = overwrite ? 0 : Number($(".subs-count").text());
|
||||
$(".subs-count").text(beforeChange + difference);
|
||||
@ -340,7 +445,9 @@
|
||||
var modal = $("#subEditModal");
|
||||
|
||||
modal.find("input").val(null);
|
||||
modal.find("textarea").val(null);
|
||||
modal.find("select").val("");
|
||||
$("#editSubChannels").empty();
|
||||
$("#subEditForm").removeClass("was-validated");
|
||||
$("#navDetailsTab").click();
|
||||
|
||||
@ -355,6 +462,8 @@
|
||||
$("#editSubName").val(resp.name);
|
||||
$("#editSubURL").val(resp.rss_url);
|
||||
$("#editSubServer").val(String(resp.server)).trigger("change");
|
||||
$("#editSubChannels").attr("data-current", resp.targets);
|
||||
$("#editSubNotes").val(resp.extra_notes);
|
||||
});
|
||||
|
||||
}
|
||||
@ -378,7 +487,11 @@
|
||||
formData.append("name", $("#editSubName").val());
|
||||
formData.append("rss_url", $("#editSubURL").val());
|
||||
formData.append("server", $("#editSubServer").val());
|
||||
|
||||
formData.append("extra_notes", $("#editSubNotes").val());
|
||||
|
||||
var selectedTargets = $("#editSubChannels option:selected").toArray().map(item => item.value).join(';');
|
||||
formData.append("targets", selectedTargets);
|
||||
|
||||
var imageFile = $("#editSubImage")[0].files[0];
|
||||
if (imageFile) {
|
||||
formData.append("image", imageFile);
|
||||
|
@ -108,10 +108,10 @@
|
||||
<li class="dropdown">
|
||||
<a href="" class="dropdown-toggle no-after peers fxw-nw ai-c lh-1" data-bs-toggle="dropdown">
|
||||
<div class="peer mR-10">
|
||||
<img class="w-2r h-2r bdrs-50p" src="{{ request.user.icon.url }}" style="object-fit: cover;" alt="">
|
||||
<img class="w-2r h-2r bdrs-50p" src="{{ request.user.avatar_url }}" style="object-fit: cover;" alt="">
|
||||
</div>
|
||||
<div class="peer">
|
||||
<span class="fsz-sm">{{ request.user.formal_fullname }}</span>
|
||||
<span class="fsz-sm">{{ request.user }}</span>
|
||||
</div>
|
||||
</a>
|
||||
<ul class="dropdown-menu fsz-sm">
|
||||
|
@ -126,6 +126,7 @@ AUTHENTICATION_BACKENDS = [
|
||||
|
||||
# Discord Related Settings
|
||||
|
||||
BOT_TOKEN = env("BOT_TOKEN")
|
||||
DISCORD_KEY = env("DISCORD_KEY")
|
||||
DISCORD_SECRET = env("DISCORD_SECRET")
|
||||
DISCORD_SCOPES = ["identify", "guilds"]
|
||||
|
Loading…
x
Reference in New Issue
Block a user