diff --git a/backend/clubs/management/commands/populate.py b/backend/clubs/management/commands/populate.py index d3850499e..4bf61f94f 100644 --- a/backend/clubs/management/commands/populate.py +++ b/backend/clubs/management/commands/populate.py @@ -394,6 +394,12 @@ def get_image(url): tag_undergrad, _ = Tag.objects.get_or_create(name="Undergraduate") tag_generic, _ = Tag.objects.get_or_create(name="Generic") + wharton_badge, _ = Badge.objects.get_or_create( + label="Wharton Council", + purpose="Dummy badge to mock Wharton-affiliated clubs", + visible=True, + ) + for i in range(1, 50): club, created = Club.objects.get_or_create( code="z-club-{}".format(i), @@ -406,6 +412,10 @@ def get_image(url): }, ) + if 10 <= i <= 15: + # Make some clubs Wharton-affiliated + club.badges.add(wharton_badge) + if created: club.available_virtually = i % 2 == 0 club.appointment_needed = i % 3 == 0 diff --git a/backend/clubs/management/commands/wharton_council_application.py b/backend/clubs/management/commands/wharton_council_application.py deleted file mode 100644 index 71f02a746..000000000 --- a/backend/clubs/management/commands/wharton_council_application.py +++ /dev/null @@ -1,166 +0,0 @@ -from datetime import datetime - -from django.core.management.base import BaseCommand - -from clubs.models import ( - ApplicationCycle, - ApplicationMultipleChoice, - ApplicationQuestion, - Badge, - Club, - ClubApplication, -) - - -class Command(BaseCommand): - help = "Helper to automatically create the Wharton council club applications." - web_execute = True - - def add_arguments(self, parser): - parser.add_argument( - "application_start_time", - type=str, - help="Date and time at which the centralized application opens.", - ) - parser.add_argument( - "application_end_time", - type=str, - help="Date and time at which the centralized application closes.", - ) - parser.add_argument( - "result_release_time", - type=str, - help="Date and time at which the centralized application results " - "are released.", - ) - parser.add_argument( - "application_cycle", type=str, help="A name for the application cycle" - ) - parser.add_argument( - "--dry-run", - dest="dry_run", - action="store_true", - help="Do not actually create applications.", - ) - parser.add_argument( - "--clubs", - dest="clubs", - type=str, - help="The comma separated list of club codes for which to create the " - "centralized applications.", - ) - parser.set_defaults( - application_start_time="2021-09-04 00:00:00", - application_end_time="2021-09-04 00:00:00", - result_release_time="2021-09-04 00:00:00", - application_cycle="", - dry_run=False, - clubs="", - ) - - def handle(self, *args, **kwargs): - dry_run = kwargs["dry_run"] - club_names = list(map(lambda x: x.strip(), kwargs["clubs"].split(","))) - app_cycle = kwargs["application_cycle"] - clubs = [] - - if not club_names or all(not name for name in club_names): - wc_badge = Badge.objects.filter( - label="Wharton Council", purpose="org", - ).first() - clubs = list(Club.objects.filter(badges=wc_badge)) - else: - clubs = list(Club.objects.filter(code__in=club_names)) - - application_start_time = datetime.strptime( - kwargs["application_start_time"], "%Y-%m-%d %H:%M:%S" - ) - application_end_time = datetime.strptime( - kwargs["application_end_time"], "%Y-%m-%d %H:%M:%S" - ) - result_release_time = datetime.strptime( - kwargs["result_release_time"], "%Y-%m-%d %H:%M:%S" - ) - - prompt_one = ( - "Tell us about a time you took " "initiative or demonstrated leadership" - ) - prompt_two = "Tell us about a time you faced a challenge and how you solved it" - prompt_three = "Tell us about a time you collaborated well in a team" - - cycle, _ = ApplicationCycle.objects.get_or_create( - name=app_cycle, - start_date=application_start_time, - end_date=application_end_time, - ) - - if len(clubs) == 0: - self.stdout.write("No valid club codes provided, returning...") - - for club in clubs: - name = f"{club.name} Application" - if dry_run: - self.stdout.write(f"Would have created application for {club.name}") - else: - self.stdout.write(f"Creating application for {club.name}") - - most_recent = ( - ClubApplication.objects.filter(club=club) - .order_by("-created_at") - .first() - ) - - if most_recent: - # If an application for this club exists, clone it - application = most_recent.make_clone() - application.application_start_time = application_start_time - application.application_end_time = application_end_time - application.result_release_time = result_release_time - application.application_cycle = cycle - application.is_wharton_council = True - application.external_url = ( - f"https://pennclubs.com/club/{club.code}/" - f"application/{application.pk}" - ) - application.save() - else: - # Otherwise, start afresh - application = ClubApplication.objects.create( - name=name, - club=club, - application_start_time=application_start_time, - application_end_time=application_end_time, - result_release_time=result_release_time, - application_cycle=cycle, - is_wharton_council=True, - ) - external_url = ( - f"https://pennclubs.com/club/{club.code}/" - f"application/{application.pk}" - ) - application.external_url = external_url - application.save() - prompt = ( - "Choose one of the following " - "prompts for your personal statement" - ) - prompt_question = ApplicationQuestion.objects.create( - question_type=ApplicationQuestion.MULTIPLE_CHOICE, - application=application, - prompt=prompt, - ) - ApplicationMultipleChoice.objects.create( - value=prompt_one, question=prompt_question - ) - ApplicationMultipleChoice.objects.create( - value=prompt_two, question=prompt_question - ) - ApplicationMultipleChoice.objects.create( - value=prompt_three, question=prompt_question - ) - ApplicationQuestion.objects.create( - question_type=ApplicationQuestion.FREE_RESPONSE, - prompt="Answer the prompt you selected", - word_limit=150, - application=application, - ) diff --git a/backend/clubs/migrations/0094_applicationcycle_release_date.py b/backend/clubs/migrations/0094_applicationcycle_release_date.py new file mode 100644 index 000000000..62ad134f9 --- /dev/null +++ b/backend/clubs/migrations/0094_applicationcycle_release_date.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.18 on 2024-01-11 14:39 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("clubs", "0093_auto_20240106_1153"), + ] + + operations = [ + migrations.AddField( + model_name="applicationcycle", + name="release_date", + field=models.DateTimeField(null=True), + ), + ] diff --git a/backend/clubs/models.py b/backend/clubs/models.py index 5de78ece8..6ea4078f2 100644 --- a/backend/clubs/models.py +++ b/backend/clubs/models.py @@ -1534,6 +1534,7 @@ class ApplicationCycle(models.Model): name = models.CharField(max_length=255) start_date = models.DateTimeField(null=True) end_date = models.DateTimeField(null=True) + release_date = models.DateTimeField(null=True) def __str__(self): return self.name diff --git a/backend/clubs/serializers.py b/backend/clubs/serializers.py index b326eaf91..ac14d5a9f 100644 --- a/backend/clubs/serializers.py +++ b/backend/clubs/serializers.py @@ -101,18 +101,22 @@ def save(self): class ApplicationCycleSerializer(serializers.ModelSerializer): class Meta: model = ApplicationCycle - fields = ["id", "name", "start_date", "end_date"] + fields = ["id", "name", "start_date", "end_date", "release_date"] def validate(self, data): """ - Check that start_date is before end_date. + Check that start_date <= end_date <= release_date """ start_date = data.get("start_date") end_date = data.get("end_date") + release_date = data.get("release_date") if start_date and end_date and start_date >= end_date: raise serializers.ValidationError("Start must be before end.") + if end_date and release_date and end_date >= release_date: + raise serializers.ValidationError("End must be before release.") + return data @@ -1030,6 +1034,7 @@ class Meta: "is_favorite", "is_member", "is_subscribe", + "is_wharton", "membership_count", "recruiting_cycle", "name", @@ -1682,7 +1687,6 @@ class Meta(ClubListSerializer.Meta): "instagram", "is_ghost", "is_request", - "is_wharton", "linkedin", "listserv", "members", diff --git a/backend/clubs/views.py b/backend/clubs/views.py index 27e9bb4ed..5b0ee2ff0 100644 --- a/backend/clubs/views.py +++ b/backend/clubs/views.py @@ -4890,6 +4890,7 @@ def update(self, *args, **kwargs): ) str_start_date = self.request.data.get("start_date").replace("T", " ") str_end_date = self.request.data.get("end_date").replace("T", " ") + str_release_date = self.request.data.get("release_date").replace("T", " ") time_format = "%Y-%m-%d %H:%M:%S%z" start = ( datetime.datetime.strptime(str_start_date, time_format) @@ -4901,142 +4902,45 @@ def update(self, *args, **kwargs): if str_end_date else self.get_object().end_date ) + release = ( + datetime.datetime.strptime(str_release_date, time_format) + if str_release_date + else self.get_object().release_date + ) 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) - app.result_release_time = filler_time + app.result_release_time = release f = ["application_start_time", "application_end_time", "result_release_time"] ClubApplication.objects.bulk_update(applications, f) return super().update(*args, **kwargs) - @action(detail=True, methods=["get"]) - def clubs(self, *args, **kwargs): - """ - Returns clubs in given cycle - --- - - requestBody: {} - responses: - "200": - content: - application/json: - schema: - type: array - items: - type: object - properties: - id: - type: integer - active: - type: boolean - name: - type: string - cycle: - type: string - acceptance_email: - type: string - rejection_email: - type: string - application_start_time: - type: string - application_end_time: - type: string - result_release_time: - type: string - external_url: - type: string - committees: - type: array - items: - type: object - properties: - name: - type: string - questions: - type: array - items: - type: object - properties: - id: - type: integer - question_type: - type: integer - prompt: - type: string - word_limit: - type: integer - multiple_choice: - type: array - items: - type: object - properties: - value: - type: string - committees: - type: array - committee_question: - type: boolean - precedence: - type: integer - club: - type: string - description: - type: string - updated_at: - type: string - club_image_url: - type: string - --- - """ - cycle = self.get_object() - data = ClubApplication.objects.filter( - is_wharton_council=True, application_cycle=cycle, - ) - return Response(ClubApplicationSerializer(data, many=True).data) - - @action(detail=True, methods=["post"]) - def add_clubs(self, *args, **kwargs): + @action(detail=True, methods=["GET"]) + def get_clubs(self, *args, **kwargs): """ - Adds clubs to given cycle + Retrieve clubs associated with given cycle --- requestBody: - content: - application/json: - schema: - type: object - properties: - clubs: - type: array - items: - type: string + content: {} responses: "200": content: {} --- """ cycle = self.get_object() - club_ids = self.request.data.get("clubs") - start = cycle.start_date - end = cycle.end_date - apps = ClubApplication.objects.filter(pk__in=club_ids) - for app in apps: - app.application_cycle = cycle - app.application_start_time = start - app.application_end_time = end - ClubApplication.objects.bulk_update( - apps, - ["application_cycle", "application_start_time", "application_end_time"], + + return Response( + ClubApplication.objects.filter(application_cycle=cycle) + .select_related("club") + .values("club__name", "club__code") ) - return Response([]) - @action(detail=False, methods=["post"]) - def remove_clubs_from_all(self, *args, **kwargs): + @action(detail=True, methods=["PATCH"]) + def edit_clubs(self, *args, **kwargs): """ - Remove selected clubs from any/all cycles + Edit clubs associated with given cycle --- requestBody: content: @@ -5052,15 +4956,98 @@ def remove_clubs_from_all(self, *args, **kwargs): "200": content: {} --- + """ - club_ids = self.request.data.get("clubs", []) - apps = ClubApplication.objects.filter(pk__in=club_ids) - for app in apps: - app.application_cycle = None - ClubApplication.objects.bulk_update( - apps, - ["application_cycle", "application_start_time", "application_end_time"], + cycle = self.get_object() + club_codes = self.request.data.get("clubs") + start = cycle.start_date + end = cycle.end_date + release = cycle.release_date + + # Some apps get deleted + ClubApplication.objects.filter(application_cycle=cycle).exclude( + club__code__in=club_codes + ).delete() + + # Some apps need to be created - use the default Wharton Template + prompt_one = ( + "Tell us about a time you took " "initiative or demonstrated leadership" ) + prompt_two = "Tell us about a time you faced a challenge and how you solved it" + prompt_three = "Tell us about a time you collaborated well in a team" + created_apps_clubs = ( + ClubApplication.objects.filter( + application_cycle=cycle, club__code__in=club_codes + ) + .select_related("club") + .values_list("club__code", flat=True) + ) + creation_pending_clubs = Club.objects.filter( + code__in=set(club_codes) - set(created_apps_clubs) + ) + + for club in creation_pending_clubs: + name = f"{club.name} Application" + most_recent = ( + ClubApplication.objects.filter(club=club) + .order_by("-created_at") + .first() + ) + + if most_recent: + # If an application for this club exists, clone it + application = most_recent.make_clone() + application.application_start_time = start + application.application_end_time = end + application.result_release_time = release + application.application_cycle = cycle + application.is_wharton_council = True + application.external_url = ( + f"https://pennclubs.com/club/{club.code}/" + f"application/{application.pk}" + ) + application.save() + else: + # Otherwise, start afresh + application = ClubApplication.objects.create( + name=name, + club=club, + application_start_time=start, + application_end_time=end, + result_release_time=release, + application_cycle=cycle, + is_wharton_council=True, + ) + external_url = ( + f"https://pennclubs.com/club/{club.code}/" + f"application/{application.pk}" + ) + application.external_url = external_url + application.save() + prompt = ( + "Choose one of the following prompts for your personal statement" + ) + prompt_question = ApplicationQuestion.objects.create( + question_type=ApplicationQuestion.MULTIPLE_CHOICE, + application=application, + prompt=prompt, + ) + ApplicationMultipleChoice.objects.create( + value=prompt_one, question=prompt_question + ) + ApplicationMultipleChoice.objects.create( + value=prompt_two, question=prompt_question + ) + ApplicationMultipleChoice.objects.create( + value=prompt_three, question=prompt_question + ) + ApplicationQuestion.objects.create( + question_type=ApplicationQuestion.FREE_RESPONSE, + prompt="Answer the prompt you selected", + word_limit=150, + application=application, + ) + return Response([]) @action(detail=False, methods=["post"]) diff --git a/frontend/components/Settings/WhartonApplicationCycles.tsx b/frontend/components/Settings/WhartonApplicationCycles.tsx index 5c02eefe5..ddb53aa9b 100644 --- a/frontend/components/Settings/WhartonApplicationCycles.tsx +++ b/frontend/components/Settings/WhartonApplicationCycles.tsx @@ -16,6 +16,7 @@ const fields = ( + ) @@ -59,10 +60,6 @@ const WhartonApplicationCycles = (): ReactElement => { const [clubsSelectedMembership, setClubsSelectedMembership] = useState< ClubOption[] >([]) - const [ - clubsInitialOptionsMembership, - setClubsInitialOptionsMembership, - ] = useState([]) const [clubOptionsMembership, setClubOptionsMembership] = useState< ClubOption[] >([]) @@ -73,27 +70,12 @@ const WhartonApplicationCycles = (): ReactElement => { const closeMembershipModal = (): void => { setEditMembership(false) - // calculate difference between initial and selected - const clubsToRemove = clubsInitialOptionsMembership.filter( - (x) => !clubsSelectedMembership.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 - if (clubsToRemove.length > 0) { - doApiRequest(`/cycles/remove_clubs_from_all/`, { - method: 'POST', - body: { clubs: clubsToRemove.map((x) => x.value) }, - }) - } - if (clubsToAdd.length > 0) { - doApiRequest(`/cycles/${membershipCycle.id}/add_clubs/`, { - method: 'POST', - body: { clubs: clubsToAdd.map((x) => x.value) }, - }) - } + // call /cycles/:id/clubs to set the clubs associated with the cycle + doApiRequest(`/cycles/${membershipCycle.id}/edit_clubs/`, { + method: 'PATCH', + body: { clubs: clubsSelectedMembership.map((x) => x.value) }, + }) } const closeExtensionsModal = (): void => { @@ -125,17 +107,36 @@ const WhartonApplicationCycles = (): ReactElement => { } useEffect(() => { - doApiRequest('/whartonapplications/?format=json') + doApiRequest('/clubs/?format=json') .then((resp) => resp.json()) + .then((data) => data.filter((club) => club.is_wharton)) .then((data) => { setClubOptionsMembership( - data.map((club: ClubApplication) => { - return { label: club.name, value: club.id } + data.map((club) => { + return { label: club.name, value: club.code } }), ) }) }, []) + const refreshMembership = (): void => { + if (membershipCycle && membershipCycle.id != null) { + doApiRequest(`/cycles/${membershipCycle.id}/get_clubs?format=json`) + .then((resp) => resp.json()) + .then((associatedClubs) => { + setClubsSelectedMembership( + associatedClubs.map((data) => { + return { label: data.club__name, value: data.club__code } + }), + ) + }) + } + } + + useEffect(() => { + refreshMembership() + }, [membershipCycle]) + useEffect(() => { doApiRequest('/cycles') .then((resp) => resp.json()) @@ -144,20 +145,6 @@ const WhartonApplicationCycles = (): ReactElement => { }) }) - useEffect(() => { - if (membershipCycle && membershipCycle.id != null) { - doApiRequest(`/cycles/${membershipCycle.id}/clubs?format=json`) - .then((resp) => resp.json()) - .then((data) => { - const initialOptions = data.map((club: ClubApplication) => { - return { label: club.name, value: club.id } - }) - setClubsInitialOptionsMembership(initialOptions) - setClubsSelectedMembership(initialOptions) - }) - } - }, [membershipCycle]) - useEffect(() => { if (extensionsCycle && extensionsCycle.id != null) { doApiRequest(`/cycles/${extensionsCycle.id}/clubs?format=json`) @@ -195,6 +182,7 @@ const WhartonApplicationCycles = (): ReactElement => { { name: 'name' }, { name: 'start_date' }, { name: 'end_date' }, + { name: 'release_date' }, ]} confirmDeletion={true} actions={(object) => ( @@ -222,20 +210,13 @@ const WhartonApplicationCycles = (): ReactElement => { )} /> - + setEditMembership(false)}> {membershipCycle && membershipCycle.name && ( <> - Club Membership for {membershipCycle.name} - {clubOptionsMembership.length === 0 ? ( -

- No club applications are currently active. -
Please visit the{' '} - - Admin Scripts - {' '} - page to initialize new applications for the current cycle. -

- ) : ( + + Club Membership for {membershipCycle.name} Cycle + + { <>
{ paddingTop: '20px', }} > - setClubsSelectedMembership([...e])} + value={clubsSelectedMembership} + options={clubOptionsMembership} + isMulti + isClearable={false} + backspaceRemovesValue={false} + /> +
+ - )} + } )}