Full Discord integration
This commit is contained in:
parent
dde32e4b6d
commit
8666a6094e
@ -4,7 +4,7 @@ import logging
|
|||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from apps.home.models import Subscription, SubscriptionChannel, TrackedContent
|
from apps.home.models import Subscription, TrackedContent
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -105,6 +105,16 @@ class DynamicModelSerializer(serializers.ModelSerializer):
|
|||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
|
|
||||||
|
# class SubscriptionTargetSerializer(DynamicModelSerializer):
|
||||||
|
# """
|
||||||
|
# Serializer for the Subscription Target Model.
|
||||||
|
# """
|
||||||
|
|
||||||
|
# class Meta:
|
||||||
|
# model = SubscriptionTarget
|
||||||
|
# fields = ("id", "creation_datetime")
|
||||||
|
|
||||||
|
|
||||||
class SubscriptionSerializer(DynamicModelSerializer):
|
class SubscriptionSerializer(DynamicModelSerializer):
|
||||||
"""
|
"""
|
||||||
Serializer for the Subscription Model.
|
Serializer for the Subscription Model.
|
||||||
@ -112,37 +122,11 @@ class SubscriptionSerializer(DynamicModelSerializer):
|
|||||||
|
|
||||||
image = serializers.ImageField(required=False)
|
image = serializers.ImageField(required=False)
|
||||||
server = serializers.CharField()
|
server = serializers.CharField()
|
||||||
|
# targets = SubscriptionTargetSerializer(many=True, required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Subscription
|
model = Subscription
|
||||||
fields = ("uuid", "name", "rss_url", "image", "server", "creation_datetime")
|
fields = ("uuid", "name", "rss_url", "image", "server", "targets", "creation_datetime", "extra_notes")
|
||||||
|
|
||||||
|
|
||||||
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")
|
|
||||||
|
|
||||||
|
|
||||||
class TrackedContentSerializer(DynamicModelSerializer):
|
class TrackedContentSerializer(DynamicModelSerializer):
|
||||||
"""
|
"""
|
||||||
|
@ -6,8 +6,6 @@ from rest_framework.authtoken.views import obtain_auth_token
|
|||||||
from .views import (
|
from .views import (
|
||||||
Subscription_ListView,
|
Subscription_ListView,
|
||||||
Subscription_DetailView,
|
Subscription_DetailView,
|
||||||
SubscriptionChannel_ListView,
|
|
||||||
SubscriptionChannel_DetailView,
|
|
||||||
TrackedContent_ListView,
|
TrackedContent_ListView,
|
||||||
TrackedContent_DetailView
|
TrackedContent_DetailView
|
||||||
)
|
)
|
||||||
@ -18,10 +16,6 @@ urlpatterns = [
|
|||||||
path("api-token-auth/", obtain_auth_token),
|
path("api-token-auth/", obtain_auth_token),
|
||||||
|
|
||||||
path("subscription/", include([
|
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("", Subscription_ListView.as_view(), name="subscription"),
|
||||||
path("<str:pk>/", Subscription_DetailView.as_view(), name="subscription-detail")
|
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.authentication import SessionAuthentication, TokenAuthentication
|
||||||
from rest_framework.parsers import MultiPartParser, FormParser
|
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 (
|
from .serializers import (
|
||||||
SubscriptionSerializer,
|
SubscriptionSerializer,
|
||||||
SubscriptionChannelSerializerGET,
|
|
||||||
SubscriptionChannelSerializerPOST,
|
|
||||||
TrackedContentSerializer
|
TrackedContentSerializer
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -50,8 +48,8 @@ class Subscription_ListView(generics.ListCreateAPIView):
|
|||||||
queryset = Subscription.objects.all().order_by("-creation_datetime")
|
queryset = Subscription.objects.all().order_by("-creation_datetime")
|
||||||
|
|
||||||
filter_backends = [filters.SearchFilter, rest_filters.DjangoFilterBackend, filters.OrderingFilter]
|
filter_backends = [filters.SearchFilter, rest_filters.DjangoFilterBackend, filters.OrderingFilter]
|
||||||
filterset_fields = ["uuid", "name", "rss_url", "server", "creation_datetime"]
|
filterset_fields = ["uuid", "name", "rss_url", "server", "targets", "creation_datetime", "extra_notes"]
|
||||||
search_fields = ["name"]
|
search_fields = ["name", "extra_notes"]
|
||||||
ordering_fields = ["creation_datetime"]
|
ordering_fields = ["creation_datetime"]
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
@ -86,88 +84,6 @@ class Subscription_DetailView(generics.RetrieveUpdateDestroyAPIView):
|
|||||||
queryset = Subscription.objects.all().order_by("-creation_datetime")
|
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
|
# Tracked Content Views
|
||||||
|
|
||||||
|
@ -34,6 +34,7 @@ class DiscordUserOAuth2Manager(models.UserManager):
|
|||||||
flags=user["flags"],
|
flags=user["flags"],
|
||||||
locale=user["locale"],
|
locale=user["locale"],
|
||||||
mfa_enabled=user["mfa_enabled"],
|
mfa_enabled=user["mfa_enabled"],
|
||||||
|
access_token=user["access_token"],
|
||||||
**extra_fields
|
**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
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
@ -14,15 +16,22 @@ class Migration(migrations.Migration):
|
|||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='DiscordUser',
|
name='DiscordUser',
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.PositiveBigIntegerField(primary_key=True, serialize=False)),
|
('id', models.PositiveBigIntegerField(help_text="the user's id", primary_key=True, serialize=False)),
|
||||||
('username', models.CharField(max_length=100)),
|
('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(max_length=100)),
|
('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(max_length=100)),
|
('avatar', models.CharField(help_text="the user's avatar hash", max_length=64, verbose_name='avatar')),
|
||||||
('public_flags', models.IntegerField()),
|
('public_flags', models.IntegerField(help_text="the public flags on a user's account", verbose_name='public flags')),
|
||||||
('flags', models.IntegerField()),
|
('flags', models.IntegerField(help_text="the flags on a user's account", verbose_name='flags')),
|
||||||
('locale', models.CharField(max_length=100)),
|
('locale', models.CharField(help_text="the user's chosen language option", max_length=16, verbose_name='locale')),
|
||||||
('mfa_enabled', models.BooleanField()),
|
('mfa_enabled', models.BooleanField(help_text='whether the user has two factor enabled on their account', verbose_name='mfa enabled')),
|
||||||
('last_login', models.DateTimeField()),
|
('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")
|
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
|
# Custom Attributes
|
||||||
|
|
||||||
is_active = models.BooleanField(
|
is_active = models.BooleanField(
|
||||||
@ -84,4 +92,8 @@ class DiscordUser(models.Model):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def __str__(self):
|
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.urls import path
|
||||||
from django.contrib.auth.views import LogoutView
|
from django.contrib.auth.views import LogoutView
|
||||||
|
|
||||||
# from .views import login_view, register_user, profile_view, discord_login, discord_login_redirect
|
from .views import DiscordLoginAction, DiscordLoginRedirect, Login, GuildsView, GuildChannelsView
|
||||||
from . import views
|
|
||||||
from .views import DiscordLoginAction, DiscordLoginRedirect, Login
|
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("login/", Login.as_view(), name="login"),
|
path("login/", Login.as_view(), name="login"),
|
||||||
path("oauth2/login/", DiscordLoginAction.as_view(), name="discord-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")
|
code = request.GET.get("code")
|
||||||
access_token = self.exchange_code_for_token(code)
|
access_token = self.exchange_code_for_token(code)
|
||||||
raw_user_data = self.get_raw_user_data(access_token)
|
raw_user_data = self.get_raw_user_data(access_token)
|
||||||
|
raw_user_data["access_token"] = access_token
|
||||||
|
|
||||||
log.debug(raw_user_data)
|
log.debug(raw_user_data)
|
||||||
|
|
||||||
@ -78,3 +79,33 @@ class DiscordLoginRedirect(View):
|
|||||||
class Login(TemplateView):
|
class Login(TemplateView):
|
||||||
|
|
||||||
template_name = "accounts/login.html"
|
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 django.contrib import admin
|
||||||
|
|
||||||
from .models import Subscription, SubscriptionChannel, TrackedContent
|
from .models import Subscription, TrackedContent
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Subscription)
|
@admin.register(Subscription)
|
||||||
@ -13,11 +13,11 @@ class SubscriptionAdmin(admin.ModelAdmin):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@admin.register(SubscriptionChannel)
|
# @admin.register(SubscriptionTarget)
|
||||||
class SubscriptionAdmin(admin.ModelAdmin):
|
# class SubscriptionTargetAdmin(admin.ModelAdmin):
|
||||||
list_display = [
|
# list_display = [
|
||||||
"uuid", "id", "subscription", "creation_datetime"
|
# "id", "creation_datetime"
|
||||||
]
|
# ]
|
||||||
|
|
||||||
|
|
||||||
@admin.register(TrackedContent)
|
@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"
|
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):
|
class Subscription(models.Model):
|
||||||
"""
|
"""
|
||||||
Represents a stored RSS Feed.
|
Represents a stored RSS Feed.
|
||||||
@ -80,6 +107,12 @@ class Subscription(models.Model):
|
|||||||
help_text=_("Identifier for the discord server that owns this subscription.")
|
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(
|
creation_datetime = models.DateTimeField(
|
||||||
verbose_name=_("creation datetime"),
|
verbose_name=_("creation datetime"),
|
||||||
help_text=_("when this instance was created."),
|
help_text=_("when this instance was created."),
|
||||||
@ -87,6 +120,14 @@ class Subscription(models.Model):
|
|||||||
editable=False
|
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:
|
class Meta:
|
||||||
verbose_name = "subscription"
|
verbose_name = "subscription"
|
||||||
verbose_name_plural = "subscriptions"
|
verbose_name_plural = "subscriptions"
|
||||||
@ -104,13 +145,13 @@ class Subscription(models.Model):
|
|||||||
log.debug("%sSubscription Saved %s", new_text, self.uuid)
|
log.debug("%sSubscription Saved %s", new_text, self.uuid)
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
@property
|
# @property
|
||||||
def channels(self):
|
# def channels(self):
|
||||||
"""
|
# """
|
||||||
Returns all SubscriptionChannel objects linked to this Subscription.
|
# 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):
|
# def save(self, *args, **kwargs):
|
||||||
# if Subscription.objects.filter(server=self.server).count() >= 1:
|
# if Subscription.objects.filter(server=self.server).count() >= 1:
|
||||||
@ -119,55 +160,55 @@ class Subscription(models.Model):
|
|||||||
# super().save(*args, **kwargs)
|
# super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class SubscriptionChannel(models.Model):
|
# class SubscriptionChannel(models.Model):
|
||||||
"""
|
# """
|
||||||
Represents a Discord TextChannel that should be subject to the content of a Subscription.
|
# Represents a Discord TextChannel that should be subject to the content of a Subscription.
|
||||||
"""
|
# """
|
||||||
|
|
||||||
uuid = models.UUIDField(
|
# uuid = models.UUIDField(
|
||||||
primary_key=True,
|
# primary_key=True,
|
||||||
default=uuid4,
|
# default=uuid4,
|
||||||
editable=False
|
# editable=False
|
||||||
)
|
# )
|
||||||
|
|
||||||
# The ID is not auto generated, but rather be obtained from discord.
|
# # The ID is not auto generated, but rather be obtained from discord.
|
||||||
id = models.PositiveBigIntegerField(
|
# id = models.PositiveBigIntegerField(
|
||||||
verbose_name=_("id"),
|
# verbose_name=_("id"),
|
||||||
help_text=_("Identifier of the channel, provided by Discord.")
|
# help_text=_("Identifier of the channel, provided by Discord.")
|
||||||
)
|
# )
|
||||||
|
|
||||||
subscription = models.ForeignKey(
|
# subscription = models.ForeignKey(
|
||||||
verbose_name=_("subscription"),
|
# verbose_name=_("subscription"),
|
||||||
help_text=_("The subscription of this instance."),
|
# help_text=_("The subscription of this instance."),
|
||||||
to=Subscription,
|
# to=Subscription,
|
||||||
on_delete=models.CASCADE
|
# on_delete=models.CASCADE
|
||||||
)
|
# )
|
||||||
|
|
||||||
creation_datetime = models.DateTimeField(
|
# creation_datetime = models.DateTimeField(
|
||||||
verbose_name=_("creation datetime"),
|
# verbose_name=_("creation datetime"),
|
||||||
help_text=_("when this instance was created."),
|
# help_text=_("when this instance was created."),
|
||||||
default=timezone.now,
|
# default=timezone.now,
|
||||||
editable=False
|
# editable=False
|
||||||
)
|
# )
|
||||||
|
|
||||||
class Meta:
|
# class Meta:
|
||||||
verbose_name = "subscription channel"
|
# verbose_name = "subscription channel"
|
||||||
verbose_name_plural = "subscription channels"
|
# verbose_name_plural = "subscription channels"
|
||||||
get_latest_by = "-creation_date"
|
# get_latest_by = "-creation_date"
|
||||||
constraints = [
|
# constraints = [
|
||||||
models.UniqueConstraint(fields=["id", "subscription"], name="unique id & sub pair")
|
# models.UniqueConstraint(fields=["id", "subscription"], name="unique id & sub pair")
|
||||||
]
|
# ]
|
||||||
|
|
||||||
def __str__(self):
|
# def __str__(self):
|
||||||
return str(self.id)
|
# return str(self.id)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
# def save(self, *args, **kwargs):
|
||||||
if SubscriptionChannel.objects.filter(subscription=self.subscription).count() >= 4:
|
# if SubscriptionChannel.objects.filter(subscription=self.subscription).count() >= 4:
|
||||||
raise IntegrityError(
|
# raise IntegrityError(
|
||||||
f"SubscriptionChannel limit reached for subscription '{self.subscription}'"
|
# f"SubscriptionChannel limit reached for subscription '{self.subscription}'"
|
||||||
)
|
# )
|
||||||
|
|
||||||
super().save(*args, **kwargs)
|
# super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class TrackedContent(models.Model):
|
class TrackedContent(models.Model):
|
||||||
|
@ -99,15 +99,15 @@
|
|||||||
<!-- ### $App Screen Content ### -->
|
<!-- ### $App Screen Content ### -->
|
||||||
<main class="main-content bg-body-tertiary">
|
<main class="main-content bg-body-tertiary">
|
||||||
<div id="mainContent">
|
<div id="mainContent">
|
||||||
|
<!--
|
||||||
<div class="row gap-20 masonry pos-r">
|
<div class="row gap-20 masonry pos-r">
|
||||||
<div class="masonry-sizer col-md-6"></div>
|
<div class="masonry-sizer col-md-6"></div>
|
||||||
|
|
||||||
<div class="masonry-item col-12">
|
<div class="masonry-item col-12">
|
||||||
<div class="row gap-20">
|
<div class="row gap-20"> -->
|
||||||
|
|
||||||
<!-- Total Subscriptions -->
|
<!-- Total Subscriptions -->
|
||||||
<div class="col-md-3">
|
<!-- <div class="col-md-3">
|
||||||
<div class="layers bd bg-body p-20">
|
<div class="layers bd bg-body p-20">
|
||||||
<div class="layer w-100 mB-10">
|
<div class="layer w-100 mB-10">
|
||||||
<h6 class="lh-1">Total Subscriptions</h6>
|
<h6 class="lh-1">Total Subscriptions</h6>
|
||||||
@ -125,10 +125,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> -->
|
||||||
|
|
||||||
<!-- Subscription Activity (24hrs) -->
|
<!-- Subscription Activity (24hrs) -->
|
||||||
<div class="col-md-3">
|
<!-- <div class="col-md-3">
|
||||||
<div class="layers bd bg-body p-20">
|
<div class="layers bd bg-body p-20">
|
||||||
<div class="layer w-100 mB-10">
|
<div class="layer w-100 mB-10">
|
||||||
<h6 class="lh-1">Subscription Activity (24hrs)</h6>
|
<h6 class="lh-1">Subscription Activity (24hrs)</h6>
|
||||||
@ -144,15 +144,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> -->
|
||||||
|
|
||||||
</div>
|
<!-- </div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="masonry-item col-12">
|
<div class="masonry-item col-12">
|
||||||
<hr>
|
<hr>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> -->
|
||||||
<div class="container-fluid mT-10 pX-0">
|
<div class="container-fluid mT-10 pX-0">
|
||||||
<div class="lh-1 pX-5 pB-20 peers">
|
<div class="lh-1 pX-5 pB-20 peers">
|
||||||
<div class="peer peer-greed">
|
<div class="peer peer-greed">
|
||||||
@ -164,7 +164,6 @@
|
|||||||
New Subscription
|
New Subscription
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<!-- bd -->
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="subscriptionContainer" class="row gap-20">
|
<div id="subscriptionContainer" class="row gap-20">
|
||||||
@ -211,16 +210,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="navChannelPanel" class="tab-pane fade" role="tabpanel" aria-labelledby="navChannelTab" tabindex="0">
|
<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>
|
<label for="editSubServer" class="form-label">Server</label>
|
||||||
<select name="editSubServer" id="editSubServer" class="form-select bdrs-2 bd" required>
|
<select name="editSubServer" id="editSubServer" class="form-select bdrs-2 bd" required>
|
||||||
<option value="">Select a server</option>
|
<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="1039201459188805692">CodeHub</option>
|
||||||
<option value="753323563381031042">Orange</option>
|
<option value="753323563381031042">Orange</option>
|
||||||
<option value="1204426362794811453">PYRSS Home</option>
|
<option value="1204426362794811453">PYRSS Home</option>
|
||||||
<option value="410208534861447168">Ryujinx</option>
|
<option value="410208534861447168">Ryujinx</option>
|
||||||
</select>
|
</select>
|
||||||
<div class="invalid-feedback">Please Select a Server.</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="navExtrasPanel" class="tab-pane fade" role="tabpanel" aria-labelledby="navExtrasTab" tabindex="0">
|
<div id="navExtrasPanel" class="tab-pane fade" role="tabpanel" aria-labelledby="navExtrasTab" tabindex="0">
|
||||||
@ -252,38 +257,78 @@
|
|||||||
{% block javascripts %}
|
{% block javascripts %}
|
||||||
<script id="subItemTemplate" type="text/template">
|
<script id="subItemTemplate" type="text/template">
|
||||||
<div class="col-md-4 col-xxl-3">
|
<div class="col-md-4 col-xxl-3">
|
||||||
<div class="sub-item layers bd bg-body p-20 h-100" data-uuid="">
|
<div class="sub-item layers bd bg-body h-100 rounded-3" data-uuid="">
|
||||||
<div class="layer w-100 mb-3">
|
<!-- <div class="layer w-100 bdB pb-4">
|
||||||
<h6 class="lh-1 sub-name mb-0"></h6>
|
<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>
|
||||||
<div class="layer w-100 mb-3">
|
<div class="layer w-100 py-4 px-4">
|
||||||
<!-- <div class="lh-1 fw-bold">UUID</div> -->
|
<p class="sub-desc mb-0"></p>
|
||||||
|
</div>
|
||||||
|
<div class="layer w-100 mb-3 px-4">
|
||||||
<span class=sub-uuid></span>
|
<span class=sub-uuid></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="layer w-100 mb-3">
|
<div class="layer w-100 mb-3 px-4">
|
||||||
<!-- <h6 class="lh-1">RSS URL</h6> -->
|
|
||||||
<a class="sub-rss"></a>
|
<a class="sub-rss"></a>
|
||||||
</div>
|
</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">
|
<img class="sub-img mw-100" height="75">
|
||||||
</div>
|
</div>
|
||||||
<div class="layer d-flex flex-wrap w-100 mt-auto">
|
<div class="layer d-flex flex-wrap w-100 mt-auto bdT pb-3 px-4">
|
||||||
<!-- <div class="me-auto">
|
<div class="me-auto d-flex ai-c">
|
||||||
<button class="btn btn-sm bg-body-tertiary bdrs-2 mT-10 mR-5 cur-p draw-border draw-border-primary">
|
<div class="form-check">
|
||||||
<i class="bi bi-link-45deg me-1"></i>
|
<input type="checkbox" class="form-check-input mt-3" style="transform:scale(1.25);">
|
||||||
Channel Links
|
</div>
|
||||||
</button>
|
</div>
|
||||||
</div> -->
|
<div>
|
||||||
<div class="ms-auto">
|
<button class="sub-edit btn btn-sm bg-body-tertiary bdrs-2 mt-3 cur-p draw-border draw-border-secondary">
|
||||||
<button class="sub-edit btn btn-sm bg-body-tertiary bdrs-2 mT-10 cur-p draw-border draw-border-secondary">
|
|
||||||
<i class="bi bi-pencil me-1`"></i>
|
<i class="bi bi-pencil me-1`"></i>
|
||||||
Edit
|
Edit
|
||||||
</button>
|
</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>
|
<i class="bi bi-trash me-1`"></i>
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -295,17 +340,77 @@
|
|||||||
template.find(".sub-item").attr("data-uuid", data.uuid);
|
template.find(".sub-item").attr("data-uuid", data.uuid);
|
||||||
template.find(".sub-name").text(data.name);
|
template.find(".sub-name").text(data.name);
|
||||||
template.find(".sub-uuid").text(data.uuid);
|
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-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-edit").attr("onclick", `subEditModal("${data.uuid}");`);
|
||||||
template.find(".sub-delete").attr("onclick", `unsubscribe("${data.uuid}");`);
|
template.find(".sub-delete").attr("onclick", `unsubscribe("${data.uuid}");`);
|
||||||
return template
|
return template
|
||||||
}
|
}
|
||||||
|
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
|
loadGuilds();
|
||||||
loadSubscriptions();
|
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) {
|
function updateSubscriptionCount(difference, overwrite) {
|
||||||
const beforeChange = overwrite ? 0 : Number($(".subs-count").text());
|
const beforeChange = overwrite ? 0 : Number($(".subs-count").text());
|
||||||
$(".subs-count").text(beforeChange + difference);
|
$(".subs-count").text(beforeChange + difference);
|
||||||
@ -340,7 +445,9 @@
|
|||||||
var modal = $("#subEditModal");
|
var modal = $("#subEditModal");
|
||||||
|
|
||||||
modal.find("input").val(null);
|
modal.find("input").val(null);
|
||||||
|
modal.find("textarea").val(null);
|
||||||
modal.find("select").val("");
|
modal.find("select").val("");
|
||||||
|
$("#editSubChannels").empty();
|
||||||
$("#subEditForm").removeClass("was-validated");
|
$("#subEditForm").removeClass("was-validated");
|
||||||
$("#navDetailsTab").click();
|
$("#navDetailsTab").click();
|
||||||
|
|
||||||
@ -355,6 +462,8 @@
|
|||||||
$("#editSubName").val(resp.name);
|
$("#editSubName").val(resp.name);
|
||||||
$("#editSubURL").val(resp.rss_url);
|
$("#editSubURL").val(resp.rss_url);
|
||||||
$("#editSubServer").val(String(resp.server)).trigger("change");
|
$("#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("name", $("#editSubName").val());
|
||||||
formData.append("rss_url", $("#editSubURL").val());
|
formData.append("rss_url", $("#editSubURL").val());
|
||||||
formData.append("server", $("#editSubServer").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];
|
var imageFile = $("#editSubImage")[0].files[0];
|
||||||
if (imageFile) {
|
if (imageFile) {
|
||||||
formData.append("image", imageFile);
|
formData.append("image", imageFile);
|
||||||
|
@ -108,10 +108,10 @@
|
|||||||
<li class="dropdown">
|
<li class="dropdown">
|
||||||
<a href="" class="dropdown-toggle no-after peers fxw-nw ai-c lh-1" data-bs-toggle="dropdown">
|
<a href="" class="dropdown-toggle no-after peers fxw-nw ai-c lh-1" data-bs-toggle="dropdown">
|
||||||
<div class="peer mR-10">
|
<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>
|
||||||
<div class="peer">
|
<div class="peer">
|
||||||
<span class="fsz-sm">{{ request.user.formal_fullname }}</span>
|
<span class="fsz-sm">{{ request.user }}</span>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<ul class="dropdown-menu fsz-sm">
|
<ul class="dropdown-menu fsz-sm">
|
||||||
|
@ -126,6 +126,7 @@ AUTHENTICATION_BACKENDS = [
|
|||||||
|
|
||||||
# Discord Related Settings
|
# Discord Related Settings
|
||||||
|
|
||||||
|
BOT_TOKEN = env("BOT_TOKEN")
|
||||||
DISCORD_KEY = env("DISCORD_KEY")
|
DISCORD_KEY = env("DISCORD_KEY")
|
||||||
DISCORD_SECRET = env("DISCORD_SECRET")
|
DISCORD_SECRET = env("DISCORD_SECRET")
|
||||||
DISCORD_SCOPES = ["identify", "guilds"]
|
DISCORD_SCOPES = ["identify", "guilds"]
|
||||||
|
Loading…
x
Reference in New Issue
Block a user