diff --git a/backend/clubs/migrations/0091_clubapplication_application_end_time_exception.py b/backend/clubs/migrations/0091_clubapplication_application_end_time_exception.py new file mode 100644 index 000000000..92377bdb5 --- /dev/null +++ b/backend/clubs/migrations/0091_clubapplication_application_end_time_exception.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.18 on 2023-11-17 22:01 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("clubs", "0090_auto_20230106_1443"), + ] + + operations = [ + migrations.AddField( + model_name="clubapplication", + name="application_end_time_exception", + field=models.BooleanField(blank=True, default=False), + ), + ] diff --git a/backend/clubs/models.py b/backend/clubs/models.py index f0b2c1874..42c172aee 100644 --- a/backend/clubs/models.py +++ b/backend/clubs/models.py @@ -1551,6 +1551,7 @@ class ClubApplication(CloneModel): description = models.TextField(blank=True) application_start_time = models.DateTimeField() application_end_time = models.DateTimeField() + application_end_time_exception = models.BooleanField(default=False, blank=True) name = models.TextField(blank=True) result_release_time = models.DateTimeField() application_cycle = models.ForeignKey( diff --git a/backend/clubs/serializers.py b/backend/clubs/serializers.py index 6dfc1ca74..7b3c51316 100644 --- a/backend/clubs/serializers.py +++ b/backend/clubs/serializers.py @@ -2738,6 +2738,7 @@ class Meta: "rejection_email", "application_start_time", "application_end_time", + "application_end_time_exception", "result_release_time", "external_url", "committees", diff --git a/backend/clubs/views.py b/backend/clubs/views.py index e956cd068..93ef22472 100644 --- a/backend/clubs/views.py +++ b/backend/clubs/views.py @@ -4901,6 +4901,8 @@ def update(self, *args, **kwargs): ) for app in applications: app.application_start_time = start + if app.application_end_time_exception: + continue app.application_end_time = end if app.result_release_time < app.application_end_time: filler_time = app.application_end_time + datetime.timedelta(days=10) @@ -5050,7 +5052,6 @@ def remove_clubs_from_all(self, *args, **kwargs): --- """ club_ids = self.request.data.get("clubs", []) - print(self.request.data) apps = ClubApplication.objects.filter(pk__in=club_ids) for app in apps: app.application_cycle = None @@ -5060,6 +5061,72 @@ def remove_clubs_from_all(self, *args, **kwargs): ) return Response([]) + @action(detail=False, methods=["post"]) + def add_clubs_to_exception(self, *args, **kwargs): + """ + Exempt selected clubs from application cycle deadline + --- + requestBody: + content: + application/json: + schema: + type: object + properties: + clubs: + type: array + items: + type: object: + properties: + id: + type: integer + application_end_time: + type: string + responses: + "200": + content: {} + """ + clubs = self.request.data.get("clubs") + apps = [] + for club in clubs: + app = ClubApplication.objects.get(pk=club["id"]) + apps.append(app) + app.application_end_time = club["end_date"] + app.application_end_time_exception = True + ClubApplication.objects.bulk_update( + apps, ["application_end_time", "application_end_time_exception"], + ) + return Response([]) + + @action(detail=True, methods=["post"]) + def remove_clubs_from_exception(self, *args, **kwargs): + """ + Remove selected clubs from application cycle deadline exemption + --- + requestBody: + content: + application/json: + schema: + type: object + properties: + clubs: + type: array + items: + type: string + responses: + "200": + content: {} + --- + """ + club_ids = self.request.data.get("clubs", []) + apps = ClubApplication.objects.filter(pk__in=club_ids) + for app in apps: + app.application_end_time_exception = False + app.application_end_time = app.application_cycle.end_date + ClubApplication.objects.bulk_update( + apps, ["application_end_time", "application_end_time_exception"], + ) + return Response([]) + class WhartonApplicationAPIView(viewsets.ModelViewSet): """ diff --git a/frontend/components/Settings/WhartonApplicationCycles.tsx b/frontend/components/Settings/WhartonApplicationCycles.tsx index ecbb45f9e..8397b21dd 100644 --- a/frontend/components/Settings/WhartonApplicationCycles.tsx +++ b/frontend/components/Settings/WhartonApplicationCycles.tsx @@ -1,11 +1,13 @@ import { Field } from 'formik' import React, { ReactElement, useEffect } from 'react' +import DatePicker from 'react-datepicker' import Select from 'react-select' +import styled from 'styled-components' import { ClubApplication } from '~/types' import { doApiRequest } from '~/utils' -import { Icon, Loading, Modal } from '../common' +import { Checkbox, Icon, Loading, Modal, Subtitle } from '../common' import { DateTimeField, TextField } from '../FormComponents' import ModelForm from '../ModelForm' @@ -22,6 +24,20 @@ type ClubOption = { value: number } +type ExtensionOption = { + id: number + clubName: string + endDate: Date + exception?: boolean + changed: boolean +} + +const ScrollWrapper = styled.div` + overflow-y: auto; + margin-top: 1rem; + height: 40vh; +` + const WhartonApplicationCycles = (): ReactElement => { const [editMembership, setEditMembership] = React.useState(false) const [membershipCycle, setMembershipCycle] = React.useState({ @@ -29,21 +45,35 @@ const WhartonApplicationCycles = (): ReactElement => { id: null, }) - // use { label: string; value: number; }[] - const [clubsSelected, setClubsSelected] = React.useState([]) - const [clubsInitialOptions, setClubsInitialOptions] = React.useState< + const [editExtensions, setEditExtensions] = React.useState(false) + const [extensionsCycle, setExtensionsCycle] = React.useState({ + name: '', + id: null, + }) + + const [clubsSelectedMembership, setClubsSelectedMembership] = React.useState< ClubOption[] >([]) - const [clubOptions, setClubOptions] = React.useState([]) + const [ + clubsInitialOptionsMembership, + setClubsInitialOptionsMembership, + ] = React.useState([]) + const [clubOptionsMembership, setClubOptionsMembership] = React.useState< + ClubOption[] + >([]) + + const [clubsExtensions, setClubsExtensions] = React.useState< + ExtensionOption[] + >([]) - const closeModal = (): void => { + const closeMembershipModal = (): void => { setEditMembership(false) // calculate difference between initial and selected - const clubsToRemove = clubsInitialOptions.filter( - (x) => !clubsSelected.includes(x), + const clubsToRemove = clubsInitialOptionsMembership.filter( + (x) => !clubsSelectedMembership.includes(x), ) - const clubsToAdd = clubsSelected.filter( - (x) => !clubsInitialOptions.includes(x), + const clubsToAdd = clubsSelectedMembership.filter( + (x) => !clubsInitialOptionsMembership.includes(x), ) // call /cycles/:id/add_clubs and /cycles/remove_clubs_from_all with data.clubs as list of ids @@ -60,17 +90,49 @@ const WhartonApplicationCycles = (): ReactElement => { }) } } + + const closeExtensionsModal = (): void => { + setEditExtensions(false) + // calculate clubs that have changed + const clubsToUpdate = clubsExtensions.filter((x) => x.changed) + // split into clubs with exceptions and clubs without + const clubsExceptions = clubsToUpdate.filter((x) => x.exception) + const clubsNoExceptions = clubsToUpdate.filter((x) => !x.exception) + + // call /cycles/:id/add_clubs and /cycles/remove_clubs_from_all with data.clubs as list of ids + if (clubsExceptions.length > 0) { + doApiRequest(`/cycles/add_clubs_to_exception/`, { + method: 'POST', + body: { + clubs: clubsExceptions.map((x) => { + // eslint-disable-next-line camelcase + return { id: x.id, end_date: x.endDate } + }), + }, + }) + } + if (clubsNoExceptions.length > 0) { + doApiRequest( + `/cycles/${extensionsCycle.id}/remove_clubs_from_exception/`, + { + method: 'POST', + body: { clubs: clubsNoExceptions.map((x) => x.id) }, + }, + ) + } + } + useEffect(() => { doApiRequest('/whartonapplications/?format=json') .then((resp) => resp.json()) .then((data) => { - setClubOptions( + setClubOptionsMembership( data.map((club: ClubApplication) => { return { label: club.name, value: club.id } }), ) }) - }, [clubOptions]) + }, [clubOptionsMembership]) useEffect(() => { if (membershipCycle && membershipCycle.id != null) { @@ -80,13 +142,32 @@ const WhartonApplicationCycles = (): ReactElement => { const initialOptions = data.map((club: ClubApplication) => { return { label: club.name, value: club.id } }) - setClubsInitialOptions(initialOptions) - setClubsSelected(initialOptions) + setClubsInitialOptionsMembership(initialOptions) + setClubsSelectedMembership(initialOptions) }) } }, [membershipCycle]) - if (clubOptions == null) { + useEffect(() => { + if (extensionsCycle && extensionsCycle.id != null) { + doApiRequest(`/cycles/${extensionsCycle.id}/clubs?format=json`) + .then((resp) => resp.json()) + .then((data) => { + const initialOptions = data.map((club: ClubApplication) => { + return { + id: club.id, + clubName: club.name, + endDate: new Date(club.application_end_time), + exception: club.application_end_time_exception, + changed: false, + } + }) + setClubsExtensions(initialOptions) + }) + } + }, [extensionsCycle]) + + if (clubOptionsMembership == null) { return } @@ -102,35 +183,113 @@ const WhartonApplicationCycles = (): ReactElement => { { name: 'end_date' }, ]} actions={(object) => ( - + <> + + + )} /> - + {membershipCycle && membershipCycle.name && ( <> -

- Club Membership for {membershipCycle.name} -

-
+ Club Membership for {membershipCycle.name} +