From c9f36cb32beb8097755338c37566022fd026a658 Mon Sep 17 00:00:00 2001 From: Rohan Moniz <60864468+rm03@users.noreply.github.com> Date: Tue, 15 Oct 2024 01:23:07 -0400 Subject: [PATCH] initial template frontend + backend --- backend/clubs/admin.py | 6 ++ .../0117_clubapprovalresponsetemplate.py | 42 +++++++++++++ backend/clubs/models.py | 18 ++++++ backend/clubs/serializers.py | 20 +++++++ backend/clubs/urls.py | 2 + backend/clubs/views.py | 11 ++++ .../ClubPage/ClubApprovalDialog.tsx | 37 +++++++++++- frontend/components/Settings/QueueTab.tsx | 28 ++++++++- frontend/components/Settings/TemplatesTab.tsx | 59 +++++++++++++++++++ frontend/pages/admin/[[...slug]].tsx | 11 +++- frontend/types.ts | 7 +++ 11 files changed, 236 insertions(+), 5 deletions(-) create mode 100644 backend/clubs/migrations/0117_clubapprovalresponsetemplate.py create mode 100644 frontend/components/Settings/TemplatesTab.tsx diff --git a/backend/clubs/admin.py b/backend/clubs/admin.py index e48ba1678..73046a164 100644 --- a/backend/clubs/admin.py +++ b/backend/clubs/admin.py @@ -25,6 +25,7 @@ Cart, Club, ClubApplication, + ClubApprovalResponseTemplate, ClubFair, ClubFairBooth, ClubFairRegistration, @@ -415,6 +416,10 @@ class ApplicationSubmissionAdmin(admin.ModelAdmin): list_display = ("user", "id", "created_at", "status") +class ClubApprovalResponseTemplateAdmin(admin.ModelAdmin): + search_fields = ("title", "content") + + admin.site.register(Asset) admin.site.register(ApplicationCommittee) admin.site.register(ApplicationExtension) @@ -460,3 +465,4 @@ class ApplicationSubmissionAdmin(admin.ModelAdmin): admin.site.register(TicketTransferRecord) admin.site.register(Cart) admin.site.register(ApplicationCycle) +admin.site.register(ClubApprovalResponseTemplate, ClubApprovalResponseTemplateAdmin) diff --git a/backend/clubs/migrations/0117_clubapprovalresponsetemplate.py b/backend/clubs/migrations/0117_clubapprovalresponsetemplate.py new file mode 100644 index 000000000..64e296864 --- /dev/null +++ b/backend/clubs/migrations/0117_clubapprovalresponsetemplate.py @@ -0,0 +1,42 @@ +# Generated by Django 5.0.4 on 2024-10-15 05:12 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("clubs", "0116_alter_club_approved_on_and_more"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="ClubApprovalResponseTemplate", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("title", models.CharField(max_length=255)), + ("content", models.TextField()), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "author", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="templates", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + ] diff --git a/backend/clubs/models.py b/backend/clubs/models.py index 87848e2c4..4ff70142b 100644 --- a/backend/clubs/models.py +++ b/backend/clubs/models.py @@ -1998,6 +1998,24 @@ def send_confirmation_email(self): ) +class ClubApprovalResponseTemplate(models.Model): + """ + Represents a (rejection) template for site administrators to use + during the club approval process. + """ + + author = models.ForeignKey( + get_user_model(), on_delete=models.SET_NULL, null=True, related_name="templates" + ) + title = models.CharField(max_length=255) + content = models.TextField() + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def __str__(self): + return self.title + + @receiver(models.signals.pre_delete, sender=Asset) def asset_delete_cleanup(sender, instance, **kwargs): if instance.file: diff --git a/backend/clubs/serializers.py b/backend/clubs/serializers.py index c477a9c8f..eb70bbb2c 100644 --- a/backend/clubs/serializers.py +++ b/backend/clubs/serializers.py @@ -31,6 +31,7 @@ Badge, Club, ClubApplication, + ClubApprovalResponseTemplate, ClubFair, ClubFairBooth, ClubVisit, @@ -3000,3 +3001,22 @@ class WritableClubFairSerializer(ClubFairSerializer): class Meta(ClubFairSerializer.Meta): pass + + +class ClubApprovalResponseTemplateSerializer(serializers.ModelSerializer): + author = serializers.SerializerMethodField("get_author") + + def get_author(self, obj): + return obj.author.get_full_name() + + def create(self, validated_data): + validated_data["author"] = self.context["request"].user + return super().create(validated_data) + + def update(self, instance, validated_data): + validated_data.pop("author", "") + return super().update(instance, validated_data) + + class Meta: + model = ClubApprovalResponseTemplate + fields = ("id", "author", "title", "content", "created_at", "updated_at") diff --git a/backend/clubs/urls.py b/backend/clubs/urls.py index 4395aa838..4a1006cad 100644 --- a/backend/clubs/urls.py +++ b/backend/clubs/urls.py @@ -12,6 +12,7 @@ BadgeClubViewSet, BadgeViewSet, ClubApplicationViewSet, + ClubApprovalResponseTemplateViewSet, ClubBoothsViewSet, ClubEventViewSet, ClubFairViewSet, @@ -92,6 +93,7 @@ basename="wharton", ) router.register(r"submissions", ApplicationSubmissionUserViewSet, basename="submission") +router.register(r"templates", ClubApprovalResponseTemplateViewSet, basename="templates") clubs_router = routers.NestedSimpleRouter(router, r"clubs", lookup="club") clubs_router.register(r"members", MemberViewSet, basename="club-members") diff --git a/backend/clubs/views.py b/backend/clubs/views.py index f7a93f6d4..4c7276265 100644 --- a/backend/clubs/views.py +++ b/backend/clubs/views.py @@ -95,6 +95,7 @@ Cart, Club, ClubApplication, + ClubApprovalResponseTemplate, ClubFair, ClubFairBooth, ClubFairRegistration, @@ -158,6 +159,7 @@ AuthenticatedMembershipSerializer, BadgeSerializer, ClubApplicationSerializer, + ClubApprovalResponseTemplateSerializer, ClubBoothSerializer, ClubConstitutionSerializer, ClubFairSerializer, @@ -7387,6 +7389,15 @@ def get_queryset(self): ).order_by("-created_at") +class ClubApprovalResponseTemplateViewSet(viewsets.ModelViewSet): + serializer_class = ClubApprovalResponseTemplateSerializer + permission_classes = [IsSuperuser] + lookup_field = "id" + + def get_queryset(self): + return ClubApprovalResponseTemplate.objects.all().order_by("-created_at") + + class ScriptExecutionView(APIView): """ View and execute Django management scripts using these endpoints. diff --git a/frontend/components/ClubPage/ClubApprovalDialog.tsx b/frontend/components/ClubPage/ClubApprovalDialog.tsx index 6ce2c975f..d826baaeb 100644 --- a/frontend/components/ClubPage/ClubApprovalDialog.tsx +++ b/frontend/components/ClubPage/ClubApprovalDialog.tsx @@ -1,9 +1,10 @@ import { useRouter } from 'next/router' import { ReactElement, useEffect, useState } from 'react' +import Select from 'react-select' import { CLUB_SETTINGS_ROUTE } from '~/constants/routes' -import { Club, ClubFair, MembershipRank, UserInfo } from '../../types' +import { Club, ClubFair, MembershipRank, Template, UserInfo } from '../../types' import { apiCheckPermission, doApiRequest, @@ -36,6 +37,7 @@ const ClubApprovalDialog = ({ club }: Props): ReactElement | null => { const [loading, setLoading] = useState(false) const [confirmModal, setConfirmModal] = useState(null) const [fairs, setFairs] = useState([]) + const [templates, setTemplates] = useState([]) const canApprove = apiCheckPermission('clubs.approve_club') const seeFairStatus = apiCheckPermission('clubs.see_fair_status') @@ -54,6 +56,12 @@ const ClubApprovalDialog = ({ club }: Props): ReactElement | null => { .then((resp) => resp.json()) .then(setFairs) } + + if (canApprove) { + doApiRequest('/templates/?format=json') + .then((resp) => resp.json()) + .then(setTemplates) + } }, []) return ( @@ -200,6 +208,33 @@ const ClubApprovalDialog = ({ club }: Props): ReactElement | null => { className="textarea mb-4" placeholder="Enter approval or rejection notes here! Your notes will be emailed to the requester when you approve or reject this request." > +
+
+ ({ + value: template.id, + label: template.title, + content: template.content, + }))} + onChange={(selectedOption) => { + selectedOption ? setComment(selectedOption.content) : setComment('') + }} + />