Skip to content

Commit

Permalink
initial template frontend + backend
Browse files Browse the repository at this point in the history
  • Loading branch information
rm03 committed Oct 15, 2024
1 parent a4c1b2a commit c9f36cb
Show file tree
Hide file tree
Showing 11 changed files with 236 additions and 5 deletions.
6 changes: 6 additions & 0 deletions backend/clubs/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
Cart,
Club,
ClubApplication,
ClubApprovalResponseTemplate,
ClubFair,
ClubFairBooth,
ClubFairRegistration,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
42 changes: 42 additions & 0 deletions backend/clubs/migrations/0117_clubapprovalresponsetemplate.py
Original file line number Diff line number Diff line change
@@ -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,
),
),
],
),
]
18 changes: 18 additions & 0 deletions backend/clubs/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
20 changes: 20 additions & 0 deletions backend/clubs/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
Badge,
Club,
ClubApplication,
ClubApprovalResponseTemplate,
ClubFair,
ClubFairBooth,
ClubVisit,
Expand Down Expand Up @@ -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")
2 changes: 2 additions & 0 deletions backend/clubs/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
BadgeClubViewSet,
BadgeViewSet,
ClubApplicationViewSet,
ClubApprovalResponseTemplateViewSet,
ClubBoothsViewSet,
ClubEventViewSet,
ClubFairViewSet,
Expand Down Expand Up @@ -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")
Expand Down
11 changes: 11 additions & 0 deletions backend/clubs/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@
Cart,
Club,
ClubApplication,
ClubApprovalResponseTemplate,
ClubFair,
ClubFairBooth,
ClubFairRegistration,
Expand Down Expand Up @@ -158,6 +159,7 @@
AuthenticatedMembershipSerializer,
BadgeSerializer,
ClubApplicationSerializer,
ClubApprovalResponseTemplateSerializer,
ClubBoothSerializer,
ClubConstitutionSerializer,
ClubFairSerializer,
Expand Down Expand Up @@ -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.
Expand Down
37 changes: 36 additions & 1 deletion frontend/components/ClubPage/ClubApprovalDialog.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -36,6 +37,7 @@ const ClubApprovalDialog = ({ club }: Props): ReactElement | null => {
const [loading, setLoading] = useState<boolean>(false)
const [confirmModal, setConfirmModal] = useState<ConfirmParams | null>(null)
const [fairs, setFairs] = useState<ClubFair[]>([])
const [templates, setTemplates] = useState<Template[]>([])

const canApprove = apiCheckPermission('clubs.approve_club')
const seeFairStatus = apiCheckPermission('clubs.see_fair_status')
Expand All @@ -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 (
Expand Down Expand Up @@ -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."
></textarea>
<div className="field is-grouped mb-3">
<div className="control is-expanded">
<Select
isClearable
placeholder="Select a template"
options={templates.map((template) => ({
value: template.id,
label: template.title,
content: template.content,
}))}
onChange={(selectedOption) => {
selectedOption
? setComment(selectedOption.content)
: setComment('')
}}
/>
</div>
<div className="control">
<button
className="button is-primary"
onClick={() => router.push('/admin/templates')}
>
<Icon name="edit" />
Edit Templates
</button>
</div>
</div>
</>
)}
<div className="buttons">
Expand Down
28 changes: 25 additions & 3 deletions frontend/components/Settings/QueueTab.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import Link from 'next/link'
import { useRouter } from 'next/router'
import { ReactElement, useEffect, useState } from 'react'
import Select from 'react-select'
import styled from 'styled-components'

import { CLUB_ROUTE } from '../../constants'
import { Club } from '../../types'
import { Club, Template } from '../../types'
import { apiCheckPermission, doApiRequest } from '../../utils'
import {
OBJECT_NAME_PLURAL,
Expand All @@ -21,13 +22,15 @@ type QueueTableModalProps = {
closeModal: () => void
bulkAction: (comment: string) => void
isApproving: boolean
templates: Template[]
}

const QueueTableModal = ({
show,
closeModal,
bulkAction,
isApproving,
templates,
}: QueueTableModalProps): ReactElement => {
const [comment, setComment] = useState<string>('')
return (
Expand All @@ -45,6 +48,18 @@ const QueueTableModal = ({
notes will be emailed to the requesters when you{' '}
{isApproving ? 'approve' : 'reject'} these requests.
</div>
<Select
isClearable
placeholder="Select a template"
options={templates.map((template) => ({
value: template.id,
label: template.title,
content: template.content,
}))}
onChange={(selectedOption) => {
selectedOption ? setComment(selectedOption.content) : setComment('')
}}
/>
<textarea
value={comment}
onChange={(e) => setComment(e.target.value)}
Expand All @@ -68,10 +83,11 @@ const QueueTableModal = ({

type QueueTableProps = {
clubs: Club[] | null
templates: Template[]
}
/* TODO: refactor with Table component when render and search
functionality are disconnected */
const QueueTable = ({ clubs }: QueueTableProps): ReactElement => {
const QueueTable = ({ clubs, templates }: QueueTableProps): ReactElement => {
const router = useRouter()
const [selectedCodes, setSelectedCodes] = useState<string[]>([])
const [showModal, setShowModal] = useState<boolean>(false)
Expand Down Expand Up @@ -106,6 +122,7 @@ const QueueTable = ({ clubs }: QueueTableProps): ReactElement => {
closeModal={() => setShowModal(false)}
bulkAction={bulkAction}
isApproving={approve}
templates={templates}
/>
<QueueTableHeader>
<QueueTableHeaderText>
Expand Down Expand Up @@ -238,6 +255,7 @@ const QueueTab = (): ReactElement => {
const [rejectedClubs, setRejectedClubs] = useState<Club[] | null>(null)
const [inactiveClubs, setInactiveClubs] = useState<Club[] | null>(null)
const [allClubs, setAllClubs] = useState<boolean[] | null>(null)
const [templates, setTemplates] = useState<Template[]>([])
const canApprove = apiCheckPermission('clubs.approve_club')

useEffect(() => {
Expand All @@ -261,6 +279,10 @@ const QueueTab = (): ReactElement => {
doApiRequest('/clubs/directory/?format=json')
.then((resp) => resp.json())
.then((data) => setAllClubs(data.map((club: Club) => club.approved)))

doApiRequest('/templates/?format=json')
.then((resp) => resp.json())
.then(setTemplates)
}
}, [])

Expand Down Expand Up @@ -327,7 +349,7 @@ const QueueTab = (): ReactElement => {
{approvedClubsCount} Approved {OBJECT_NAME_TITLE}
</li>
</ul>
<QueueTable clubs={pendingClubs} />
<QueueTable clubs={pendingClubs} templates={templates} />
<SmallTitle>Other Clubs</SmallTitle>
<div className="mt-3 mb-3">
The table below shows a list of {OBJECT_NAME_PLURAL} that have been
Expand Down
59 changes: 59 additions & 0 deletions frontend/components/Settings/TemplatesTab.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Field } from 'formik'
import moment from 'moment-timezone'
import React, { ReactElement } from 'react'

import { Template } from '../../types'
import { OBJECT_NAME_SINGULAR } from '../../utils/branding'
import { Text } from '../common'
import { TextField } from '../FormComponents'
import ModelForm from '../ModelForm'

type TemplatesTabProps = {
templates: Template[]
}

export default function TemplatesTab({
templates,
}: TemplatesTabProps): ReactElement {
return (
<>
<Text>
You can use this page to manage {OBJECT_NAME_SINGULAR} approval response
templates. Since your account has the required permissions, you are able
to view this page.
</Text>
<ModelForm
baseUrl="/templates/"
initialData={templates}
fields={
<>
<Field
name="title"
as={TextField}
required
helpText={`The title of the ${OBJECT_NAME_SINGULAR} approval response template. This will be shown in the template dropdown menu.`}
/>
<Field name="content" as={TextField} type="textarea" required />
</>
}
tableFields={[
{ name: 'title', label: 'Title' },
{ name: 'content', label: 'Content' },
{ name: 'author', label: 'Author' },
{
name: 'created_at',
label: 'Date Created',
converter: (field) => moment(field).format('MMMM Do, YYYY'),
},
{
name: 'updated_at',
label: 'Last Updated',
converter: (field) => moment(field).format('MMMM Do, YYYY'),
},
]}
noun="Template"
confirmDeletion={true}
/>
</>
)
}
Loading

0 comments on commit c9f36cb

Please sign in to comment.