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
+
+ {
<>