From e2de41902e5cb472bbc5225ca956f67db0f8d90b Mon Sep 17 00:00:00 2001 From: aviupadhyayula Date: Sat, 9 Mar 2024 16:09:05 -0800 Subject: [PATCH 01/17] Use subqueries in membership & favorite updates --- .../management/commands/update_club_counts.py | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/backend/clubs/management/commands/update_club_counts.py b/backend/clubs/management/commands/update_club_counts.py index 01ff17676..09274fac6 100644 --- a/backend/clubs/management/commands/update_club_counts.py +++ b/backend/clubs/management/commands/update_club_counts.py @@ -1,5 +1,5 @@ from django.core.management.base import BaseCommand -from django.db.models import Count, Q +from django.db.models import Count, OuterRef, Q, Subquery from clubs.models import Club @@ -9,17 +9,23 @@ class Command(BaseCommand): def handle(self, *args, **kwargs): try: - queryset = Club.objects.all().annotate( - temp_favorite_count=Count("favorite", distinct=True), - temp_membership_count=Count( - "membership", distinct=True, filter=Q(active=True) - ), + favorite_count_subquery = ( + Club.objects.filter(pk=OuterRef("pk")) + .annotate(count=Count("favorite", distinct=True)) + .values("count") + ) + membership_count_subquery = ( + Club.objects.filter(pk=OuterRef("pk")) + .annotate( + count=Count("membership", distinct=True, filter=Q(active=True)) + ) + .values("count") ) - for club in queryset: - club.favorite_count = club.temp_favorite_count - club.membership_count = club.temp_membership_count - Club.objects.bulk_update(queryset, ["favorite_count", "membership_count"]) + Club.objects.update( + favorite_count=Subquery(favorite_count_subquery), + membership_count=Subquery(membership_count_subquery), + ) self.stdout.write( self.style.SUCCESS( From 7646b8788408ca2e2961cb269f776159ef55e101 Mon Sep 17 00:00:00 2001 From: aviupadhyayula Date: Sat, 9 Mar 2024 20:38:18 -0800 Subject: [PATCH 02/17] Refactor club search filter --- backend/clubs/views.py | 244 +++++++++++++++++++++-------------------- 1 file changed, 124 insertions(+), 120 deletions(-) diff --git a/backend/clubs/views.py b/backend/clubs/views.py index ab4e79d67..2e73206fc 100644 --- a/backend/clubs/views.py +++ b/backend/clubs/views.py @@ -322,141 +322,143 @@ class ClubsSearchFilter(filters.BaseFilterBackend): If model is not a Club, expects the model to have a club foreign key to Club. """ - def filter_queryset(self, request, queryset, view): - params = request.GET.dict() + def parse_year(self, field, value, operation, queryset): + if value.isdigit(): + suffix = "" + if operation in {"lt", "gt", "lte", "gte"}: + suffix = f"__{operation}" + return {f"{field}__year{suffix}": int(value)} + if value.lower() in {"none", "null"}: + return {f"{field}__isnull": True} + return {} - def parse_year(field, value, operation, queryset): - if value.isdigit(): - suffix = "" - if operation in {"lt", "gt", "lte", "gte"}: - suffix = f"__{operation}" - return {f"{field}__year{suffix}": int(value)} - if value.lower() in {"none", "null"}: - return {f"{field}__isnull": True} - return {} - - def parse_int(field, value, operation, queryset): - if operation == "in": - values = value.strip().split(",") - sizes = [int(size) for size in values if size] - return {f"{field}__in": sizes} - - if "," in value: - values = [int(x.strip()) for x in value.split(",") if x] - if operation == "and": - for value in values: - queryset = queryset.filter(**{field: value}) - return queryset - return {f"{field}__in": values} - - if value.isdigit(): - suffix = "" - if operation in {"lt", "gt", "lte", "gte"}: - suffix = f"__{operation}" - return {f"{field}{suffix}": int(value)} - if value.lower() in {"none", "null"}: - return {f"{field}__isnull": True} - return {} - - def parse_many_to_many(label, field, value, operation, queryset): - tags = value.strip().split(",") - if operation == "or": - if tags[0].isdigit(): - tags = [int(tag) for tag in tags if tag] - return {f"{field}__id__in": tags} - else: - return {f"{field}__{label}__in": tags} + def parse_int(self, field, value, operation, queryset): + if operation == "in": + values = value.strip().split(",") + sizes = [int(size) for size in values if size] + return {f"{field}__in": sizes} + + if "," in value: + values = [int(x.strip()) for x in value.split(",") if x] + if operation == "and": + for value in values: + queryset = queryset.filter(**{field: value}) + return queryset + return {f"{field}__in": values} + + if value.isdigit(): + suffix = "" + if operation in {"lt", "gt", "lte", "gte"}: + suffix = f"__{operation}" + return {f"{field}{suffix}": int(value)} + if value.lower() in {"none", "null"}: + return {f"{field}__isnull": True} + return {} - if tags[0].isdigit() or operation == "id": + def parse_many_to_many(self, label, field, value, operation, queryset): + tags = value.strip().split(",") + if operation == "or": + if tags[0].isdigit(): tags = [int(tag) for tag in tags if tag] - if settings.BRANDING == "fyh": - queryset = queryset.filter(**{f"{field}__id__in": tags}) - else: - for tag in tags: - queryset = queryset.filter(**{f"{field}__id": tag}) + return {f"{field}__id__in": tags} + else: + return {f"{field}__{label}__in": tags} + + if tags[0].isdigit() or operation == "id": + tags = [int(tag) for tag in tags if tag] + if settings.BRANDING == "fyh": + queryset = queryset.filter(**{f"{field}__id__in": tags}) else: + queryset = queryset.filter(**{f"{field}__id__in": tags}) for tag in tags: - queryset = queryset.filter(**{f"{field}__{label}": tag}) - return queryset + queryset = queryset.filter(**{f"{field}__id": tag}) + else: + for tag in tags: + queryset = queryset.filter(**{f"{field}__{label}": tag}) - def parse_badges(field, value, operation, queryset): - return parse_many_to_many("label", field, value, operation, queryset) + return queryset - def parse_tags(field, value, operation, queryset): - return parse_many_to_many("name", field, value, operation, queryset) + def parse_badges(self, field, value, operation, queryset): + return self.parse_many_to_many("label", field, value, operation, queryset) - def parse_boolean(field, value, operation, queryset): - value = value.strip().lower() + def parse_tags(self, field, value, operation, queryset): + return self.parse_many_to_many("name", field, value, operation, queryset) - if operation == "in": - if set(value.split(",")) == {"true", "false"}: - return + def parse_boolean(self, field, value, operation, queryset): + value = value.strip().lower() - if value in {"true", "yes"}: - boolval = True - elif value in {"false", "no"}: - boolval = False - elif value in {"null", "none"}: - boolval = None - else: + if operation == "in": + if set(value.split(",")) == {"true", "false"}: return - if boolval is None: - return {f"{field}__isnull": True} + if value in {"true", "yes"}: + boolval = True + elif value in {"false", "no"}: + boolval = False + elif value in {"null", "none"}: + boolval = None + else: + return - return {f"{field}": boolval} + if boolval is None: + return {f"{field}__isnull": True} - def parse_string(field, value, operation, queryset): - if operation == "in": - values = [x.strip() for x in value.split(",")] - values = [x for x in values if x] - return {f"{field}__in": values} - return {f"{field}": value} + return {f"{field}": boolval} - def parse_datetime(field, value, operation, queryset): - try: - value = parse(value.strip()) - except (ValueError, OverflowError): - return + def parse_string(self, field, value, operation, queryset): + if operation == "in": + values = [x.strip() for x in value.split(",")] + values = [x for x in values if x] + return {f"{field}__in": values} + return {f"{field}": value} - if operation in {"gt", "lt", "gte", "lte"}: - return {f"{field}__{operation}": value} + def parse_datetime(self, field, value, operation, queryset): + try: + value = parse(value.strip()) + except (ValueError, OverflowError): return - fields = { - "accepting_members": parse_boolean, - "active": parse_boolean, - "application_required": parse_int, - "appointment_needed": parse_boolean, - "approved": parse_boolean, - "available_virtually": parse_boolean, - "badges": parse_badges, - "code": parse_string, - "enables_subscription": parse_boolean, - "favorite_count": parse_int, - "founded": parse_year, - "recruiting_cycle": parse_int, - "size": parse_int, - "tags": parse_tags, - "target_majors": parse_tags, - "target_schools": parse_tags, - "target_years": parse_tags, - "target_students": parse_tags, - "student_types": parse_tags, - } + if operation in {"gt", "lt", "gte", "lte"}: + return {f"{field}__{operation}": value} + return - def parse_fair(field, value, operation, queryset): - try: - value = int(value.strip()) - except ValueError: - return + def parse_fair(self, field, value, operation, queryset): + try: + value = int(value.strip()) + except ValueError: + return - fair = ClubFair.objects.filter(id=value).first() - if fair: - return { - "start_time__gte": fair.start_time, - "end_time__lte": fair.end_time, - } + fair = ClubFair.objects.filter(id=value).first() + if fair: + return { + "start_time__gte": fair.start_time, + "end_time__lte": fair.end_time, + } + + def filter_queryset(self, request, queryset, view): + params = request.GET.dict() + + fields = { + "accepting_members": self.parse_boolean, + "active": self.parse_boolean, + "application_required": self.parse_int, + "appointment_needed": self.parse_boolean, + "approved": self.parse_boolean, + "available_virtually": self.parse_boolean, + "badges": self.parse_badges, + "code": self.parse_string, + "enables_subscription": self.parse_boolean, + "favorite_count": self.parse_int, + "founded": self.parse_year, + "recruiting_cycle": self.parse_int, + "size": self.parse_int, + "tags": self.parse_tags, + "target_majors": self.parse_tags, + "target_schools": self.parse_tags, + "target_years": self.parse_tags, + "target_students": self.parse_tags, + "student_types": self.parse_tags, + } if not queryset.model == Club: fields = {f"club__{k}": v for k, v in fields.items()} @@ -464,10 +466,10 @@ def parse_fair(field, value, operation, queryset): if queryset.model == Event: fields.update( { - "type": parse_int, - "start_time": parse_datetime, - "end_time": parse_datetime, - "fair": parse_fair, + "type": self.parse_int, + "start_time": self.parse_datetime, + "end_time": self.parse_datetime, + "fair": self.parse_fair, } ) @@ -1020,7 +1022,9 @@ class ClubViewSet(XLSXFormatterMixin, viewsets.ModelViewSet): Club.objects.all().prefetch_related("tags").order_by("-favorite_count", "name") ) permission_classes = [ClubPermission | IsSuperuser] - filter_backends = [filters.SearchFilter, ClubsSearchFilter, ClubsOrderingFilter] + + haha = ClubsSearchFilter() + filter_backends = [filters.SearchFilter, haha, ClubsOrderingFilter] search_fields = ["name", "subtitle", "code", "terms"] ordering_fields = ["favorite_count", "name"] ordering = "featured" From e3cc46040be456100b0b0a9184510360281e86e4 Mon Sep 17 00:00:00 2001 From: aviupadhyayula Date: Sat, 9 Mar 2024 20:44:06 -0800 Subject: [PATCH 03/17] Remove errant line --- backend/clubs/views.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/backend/clubs/views.py b/backend/clubs/views.py index 2e73206fc..0602c2737 100644 --- a/backend/clubs/views.py +++ b/backend/clubs/views.py @@ -613,9 +613,11 @@ def current(self, request, *args, **kwargs): --- """ now = timezone.now() + start_time = now - datetime.timedelta(minutes=2) + end_time = now + datetime.timedelta(minutes=2) fair = ClubFair.objects.filter( - start_time__lte=now + datetime.timedelta(minutes=2), - end_time__gte=now - datetime.timedelta(minutes=2), + start_time__lte=start_time, + end_time__gte=end_time, ).first() if fair is None: return Response([]) @@ -1023,8 +1025,7 @@ class ClubViewSet(XLSXFormatterMixin, viewsets.ModelViewSet): ) permission_classes = [ClubPermission | IsSuperuser] - haha = ClubsSearchFilter() - filter_backends = [filters.SearchFilter, haha, ClubsOrderingFilter] + filter_backends = [filters.SearchFilter, ClubsSearchFilter, ClubsOrderingFilter] search_fields = ["name", "subtitle", "code", "terms"] ordering_fields = ["favorite_count", "name"] ordering = "featured" From 63e22e68f39850963bbddffe88c56f1edb3d3119 Mon Sep 17 00:00:00 2001 From: aviupadhyayula Date: Sun, 10 Mar 2024 15:43:55 -0700 Subject: [PATCH 04/17] Use bulk update when updating submission reasons --- backend/clubs/views.py | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/backend/clubs/views.py b/backend/clubs/views.py index 0602c2737..1784c0029 100644 --- a/backend/clubs/views.py +++ b/backend/clubs/views.py @@ -613,11 +613,9 @@ def current(self, request, *args, **kwargs): --- """ now = timezone.now() - start_time = now - datetime.timedelta(minutes=2) - end_time = now + datetime.timedelta(minutes=2) fair = ClubFair.objects.filter( - start_time__lte=start_time, - end_time__gte=end_time, + start_time__lte=now + datetime.timedelta(minutes=2), + end_time__gte=now - datetime.timedelta(minutes=2), ).first() if fair is None: return Response([]) @@ -5512,27 +5510,25 @@ def reason(self, *args, **kwargs): --- """ submissions = self.request.data.get("submissions", []) - pks = list(map(lambda x: x["id"], submissions)) - reasons = list(map(lambda x: x["reason"], submissions)) + pks = [submission["id"] for submission in submissions] + reasons = {submission["id"]: submission["reason"] for submission in submissions} submission_objs = ApplicationSubmission.objects.filter(pk__in=pks) # Invalidate submission viewset cache - app_id = ( - submission_objs.first().application.id if submission_objs.first() else None - ) - if not app_id: + if ( + not submission_objs.exists() + or submission_objs.first().application.id is None + ): return Response({"detail": "No submissions found"}) + app_id = submission_objs.first().application.id key = f"applicationsubmissions:{app_id}" cache.delete(key) - for idx, pk in enumerate(pks): - obj = submission_objs.filter(pk=pk).first() - if obj: - obj.reason = reasons[idx] - obj.save() - else: - return Response({"detail": "Object not found"}) + # Update reasons in a single query + for submission in submission_objs: + submission.reason = reasons[submission.pk] + ApplicationSubmission.objects.bulk_update(submission_objs, ["reason"]) return Response({"detail": "Successfully updated submissions' reasons"}) From d4e85e1753a4bb38e354e783fa4b616b60b7628e Mon Sep 17 00:00:00 2001 From: aviupadhyayula Date: Sun, 10 Mar 2024 17:14:49 -0700 Subject: [PATCH 05/17] Use bulk create for fair creation --- backend/clubs/models.py | 44 +++++++++++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/backend/clubs/models.py b/backend/clubs/models.py index 47c983c7a..49ab9a403 100644 --- a/backend/clubs/models.py +++ b/backend/clubs/models.py @@ -13,7 +13,7 @@ from django.core.exceptions import ValidationError from django.core.mail import EmailMultiAlternatives from django.core.validators import validate_email -from django.db import models, transaction +from django.db import models from django.db.models import Sum from django.dispatch import receiver from django.template.loader import render_to_string @@ -833,21 +833,35 @@ def create_events( club_query = self.participating_clubs.all() if filter is not None: club_query = club_query.filter(filter) - events = [] - with transaction.atomic(): - for club in club_query: - obj, _ = Event.objects.get_or_create( - code=f"fair-{club.code}-{self.id}-{suffix}", - club=club, - type=Event.FAIR, - defaults={ - "name": self.name, - "start_time": start_time, - "end_time": end_time, - }, + + # Find events that already exist + event_codes = [f"fair-{club.code}-{self.id}-{suffix}" for club in club_query] + existing_events = Event.objects.filter(code__in=event_codes).values_list( + "code", flat=True + ) + + new_events = [] + + # Create new events in bulk + for club in club_query: + event_code = f"fair-{club.code}-{self.id}-{suffix}" + if event_code not in existing_events: + new_events.append( + Event( + code=event_code, + club=club, + type=Event.FAIR, + name=self.name, + start_time=start_time, + end_time=end_time, + ) ) - events.append(obj) - return events + + Event.objects.bulk_create(new_events) + + # Return all event objects + events = Event.objects.filter(code__in=event_codes) + return list(events) def __str__(self): fmt = "%b %d, %Y" From 700c1af1fbf08042e1cc4bfe0cae67baf96fd8d0 Mon Sep 17 00:00:00 2001 From: aviupadhyayula Date: Sun, 10 Mar 2024 17:19:22 -0700 Subject: [PATCH 06/17] Formatting changes --- backend/clubs/models.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/backend/clubs/models.py b/backend/clubs/models.py index 49ab9a403..550f5be48 100644 --- a/backend/clubs/models.py +++ b/backend/clubs/models.py @@ -840,9 +840,8 @@ def create_events( "code", flat=True ) - new_events = [] - # Create new events in bulk + new_events = [] for club in club_query: event_code = f"fair-{club.code}-{self.id}-{suffix}" if event_code not in existing_events: @@ -856,7 +855,6 @@ def create_events( end_time=end_time, ) ) - Event.objects.bulk_create(new_events) # Return all event objects From 0de6ca4b682b8abb819a286b4ba1275b9863816a Mon Sep 17 00:00:00 2001 From: aviupadhyayula Date: Sun, 10 Mar 2024 17:28:43 -0700 Subject: [PATCH 07/17] Simplify check in decision reasons update --- backend/clubs/views.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/backend/clubs/views.py b/backend/clubs/views.py index 1784c0029..3698f5289 100644 --- a/backend/clubs/views.py +++ b/backend/clubs/views.py @@ -5516,10 +5516,7 @@ def reason(self, *args, **kwargs): submission_objs = ApplicationSubmission.objects.filter(pk__in=pks) # Invalidate submission viewset cache - if ( - not submission_objs.exists() - or submission_objs.first().application.id is None - ): + if submission_objs.exists(): return Response({"detail": "No submissions found"}) app_id = submission_objs.first().application.id key = f"applicationsubmissions:{app_id}" From 5268d5d3416acb10dd3813573439d3fe72140c1d Mon Sep 17 00:00:00 2001 From: aviupadhyayula Date: Sun, 10 Mar 2024 17:29:23 -0700 Subject: [PATCH 08/17] Fix typo :pensive: --- backend/clubs/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/clubs/views.py b/backend/clubs/views.py index 3698f5289..abd6b68c8 100644 --- a/backend/clubs/views.py +++ b/backend/clubs/views.py @@ -5516,7 +5516,7 @@ def reason(self, *args, **kwargs): submission_objs = ApplicationSubmission.objects.filter(pk__in=pks) # Invalidate submission viewset cache - if submission_objs.exists(): + if not submission_objs.exists(): return Response({"detail": "No submissions found"}) app_id = submission_objs.first().application.id key = f"applicationsubmissions:{app_id}" From f0c2a39e29b7b70ac1caf99023f9ce2965ccd5cc Mon Sep 17 00:00:00 2001 From: aviupadhyayula Date: Sun, 10 Mar 2024 17:31:02 -0700 Subject: [PATCH 09/17] Improve docuemntation clarity --- backend/clubs/views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/clubs/views.py b/backend/clubs/views.py index abd6b68c8..d57879da9 100644 --- a/backend/clubs/views.py +++ b/backend/clubs/views.py @@ -5515,9 +5515,10 @@ def reason(self, *args, **kwargs): submission_objs = ApplicationSubmission.objects.filter(pk__in=pks) - # Invalidate submission viewset cache if not submission_objs.exists(): return Response({"detail": "No submissions found"}) + + # Invalidate submission viewset cache app_id = submission_objs.first().application.id key = f"applicationsubmissions:{app_id}" cache.delete(key) From fae0adb3ed7e5332bb9b36493d4a20c39c263356 Mon Sep 17 00:00:00 2001 From: aviupadhyayula Date: Mon, 11 Mar 2024 16:22:14 -0700 Subject: [PATCH 10/17] Use .exists() in changing applications' status --- backend/clubs/views.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/backend/clubs/views.py b/backend/clubs/views.py index d57879da9..c5e08495e 100644 --- a/backend/clubs/views.py +++ b/backend/clubs/views.py @@ -5453,14 +5453,15 @@ def status(self, *args, **kwargs): submission_pks = self.request.data.get("submissions", []) status = self.request.data.get("status", None) if ( - status in map(lambda x: x[0], ApplicationSubmission.STATUS_TYPES) + status + in [status_type[0] for status_type in ApplicationSubmission.STATUS_TYPES] and len(submission_pks) > 0 ): # Invalidate submission viewset cache submissions = ApplicationSubmission.objects.filter(pk__in=submission_pks) - app_id = submissions.first().application.id if submissions.first() else None - if not app_id: + if not submissions.exists(): return Response({"detail": "No submissions found"}) + app_id = submissions.first().application.id key = f"applicationsubmissions:{app_id}" cache.delete(key) From e42874e85e4714ac00fef69ac7a5ed2f83da2da4 Mon Sep 17 00:00:00 2001 From: aviupadhyayula Date: Mon, 11 Mar 2024 16:30:41 -0700 Subject: [PATCH 11/17] Refactor submission routes a little --- backend/clubs/views.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/backend/clubs/views.py b/backend/clubs/views.py index c5e08495e..f7924066b 100644 --- a/backend/clubs/views.py +++ b/backend/clubs/views.py @@ -5452,15 +5452,14 @@ def status(self, *args, **kwargs): """ submission_pks = self.request.data.get("submissions", []) status = self.request.data.get("status", None) - if ( - status - in [status_type[0] for status_type in ApplicationSubmission.STATUS_TYPES] - and len(submission_pks) > 0 - ): - # Invalidate submission viewset cache + if len(submission_pks) > 0 and status in [ + status_type[0] for status_type in ApplicationSubmission.STATUS_TYPES + ]: submissions = ApplicationSubmission.objects.filter(pk__in=submission_pks) if not submissions.exists(): return Response({"detail": "No submissions found"}) + + # Invalidate submission viewset cache app_id = submissions.first().application.id key = f"applicationsubmissions:{app_id}" cache.delete(key) @@ -5524,7 +5523,6 @@ def reason(self, *args, **kwargs): key = f"applicationsubmissions:{app_id}" cache.delete(key) - # Update reasons in a single query for submission in submission_objs: submission.reason = reasons[submission.pk] ApplicationSubmission.objects.bulk_update(submission_objs, ["reason"]) From d3a178be3d18f234f02265eaf5fd29b54a60c609 Mon Sep 17 00:00:00 2001 From: aviupadhyayula Date: Mon, 11 Mar 2024 16:33:50 -0700 Subject: [PATCH 12/17] Continue to make application submissions a little prettier --- backend/clubs/views.py | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/backend/clubs/views.py b/backend/clubs/views.py index f7924066b..26bd7231f 100644 --- a/backend/clubs/views.py +++ b/backend/clubs/views.py @@ -5452,28 +5452,29 @@ def status(self, *args, **kwargs): """ submission_pks = self.request.data.get("submissions", []) status = self.request.data.get("status", None) - if len(submission_pks) > 0 and status in [ + + if len(submission_pks) == 0 or status not in [ status_type[0] for status_type in ApplicationSubmission.STATUS_TYPES ]: - submissions = ApplicationSubmission.objects.filter(pk__in=submission_pks) - if not submissions.exists(): - return Response({"detail": "No submissions found"}) + return Response({"detail": "Invalid request"}) - # Invalidate submission viewset cache - app_id = submissions.first().application.id - key = f"applicationsubmissions:{app_id}" - cache.delete(key) + submissions = ApplicationSubmission.objects.filter(pk__in=submission_pks) + if not submissions.exists(): + return Response({"detail": "No submissions found"}) - submissions.update(status=status) + # Invalidate submission viewset cache + app_id = submissions.first().application.id + key = f"applicationsubmissions:{app_id}" + cache.delete(key) - return Response( - { - "detail": f"Successfully updated submissions' {submission_pks}" - f"status {status}" - } - ) - else: - return Response({"detail": "Invalid request"}) + submissions.update(status=status) + + return Response( + { + "detail": f"Successfully updated submissions' {submission_pks} " + f"status to {status}" + } + ) @action(detail=False, methods=["post"]) def reason(self, *args, **kwargs): From 65fbf3fb0a3d936f07d4377c07a50e702e1beb43 Mon Sep 17 00:00:00 2001 From: aviupadhyayula Date: Mon, 11 Mar 2024 23:46:41 -0700 Subject: [PATCH 13/17] Use DB to update application times --- backend/clubs/views.py | 46 +++++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/backend/clubs/views.py b/backend/clubs/views.py index 26bd7231f..cda39459a 100644 --- a/backend/clubs/views.py +++ b/backend/clubs/views.py @@ -4902,36 +4902,36 @@ def update(self, *args, **kwargs): """ Updates times for all applications with cycle """ - applications = ClubApplication.objects.filter( - application_cycle=self.get_object() - ) - 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", " ") + cycle = self.get_object() + applications = ClubApplication.objects.filter(application_cycle=cycle) + + new_start = self.request.data.get("start_date").replace("T", " ") + new_end = self.request.data.get("end_date").replace("T", " ") + new_release = 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) - if str_start_date - else self.get_object().start_date + datetime.datetime.strptime(new_start, time_format) + if new_start + else cycle.start_date ) end = ( - datetime.datetime.strptime(str_end_date, time_format) - if str_end_date - else self.get_object().end_date + datetime.datetime.strptime(new_end, time_format) + if new_end + else cycle.end_date ) release = ( - datetime.datetime.strptime(str_release_date, time_format) - if str_release_date - else self.get_object().release_date + datetime.datetime.strptime(new_release, time_format) + if new_release + else cycle.release_date ) - for app in applications: - app.application_start_time = start - if app.application_end_time_exception: - continue - app.application_end_time = end - app.result_release_time = release - f = ["application_start_time", "application_end_time", "result_release_time"] - ClubApplication.objects.bulk_update(applications, f) + + applications.update(application_start_time=start) + # Exclude applications with an extension from end & release update + applications.filter(application_end_time_exception=False).update( + application_end_time=end, result_release_time=release + ) + return super().update(*args, **kwargs) @action(detail=True, methods=["GET"]) From 88d9a2eb170d44a44aefc58a02569c6007ed0fd5 Mon Sep 17 00:00:00 2001 From: aviupadhyayula Date: Tue, 12 Mar 2024 00:46:02 -0700 Subject: [PATCH 14/17] Use update_or_create for membership request creation --- backend/clubs/views.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/backend/clubs/views.py b/backend/clubs/views.py index cda39459a..b9b064ad9 100644 --- a/backend/clubs/views.py +++ b/backend/clubs/views.py @@ -2985,16 +2985,13 @@ def create(self, request, *args, **kwargs): """ If a membership request object already exists, reuse it. """ - club = request.data.get("club", None) - obj = MembershipRequest.objects.filter( - club__code=club, person=request.user - ).first() - if obj is not None: - obj.withdrew = False - obj.save(update_fields=["withdrew"]) - return Response(UserMembershipRequestSerializer(obj).data) - - return super().create(request, *args, **kwargs) + club_code = request.data.get("club", None) + obj, _ = MembershipRequest.objects.update_or_create( + club__code=club_code, + person=request.user, + defaults={"withdrew": False}, + ) + return Response(UserMembershipRequestSerializer(obj).data) def destroy(self, request, *args, **kwargs): """ From d68e52cf5f4cecbb38e78119131ee112502c6cc2 Mon Sep 17 00:00:00 2001 From: Rohan Moniz <60864468+rm03@users.noreply.github.com> Date: Fri, 15 Mar 2024 13:53:49 -0400 Subject: [PATCH 15/17] add back cronjob for updating favorite/membership counts --- k8s/main.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/k8s/main.ts b/k8s/main.ts index de603f726..31421ecfc 100644 --- a/k8s/main.ts +++ b/k8s/main.ts @@ -149,6 +149,13 @@ export class MyChart extends PennLabsChart { secret: fyhSecret, cmd: ["python", "manage.py", "import_paideia_events"], }); + + new CronJob(this, 'update-club-favorite-membership-counts', { + schedule: cronTime.everyDayAt(12), + image: backendImage, + secret: fyhSecret, + cmd: ["python", "manage.py", "update_club_counts"], + }); } } From c1a1ba4bc2e255f3feec3cbcbdd25b3e2a7a6a3f Mon Sep 17 00:00:00 2001 From: aviupadhyayula Date: Tue, 12 Mar 2024 22:41:04 -0700 Subject: [PATCH 16/17] Refactor file upload helper to guard clause pattern --- backend/clubs/views.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/backend/clubs/views.py b/backend/clubs/views.py index b9b064ad9..0fca0a8b9 100644 --- a/backend/clubs/views.py +++ b/backend/clubs/views.py @@ -190,19 +190,20 @@ def file_upload_endpoint_helper(request, code): - obj = get_object_or_404(Club, code=code) - if "file" in request.data and isinstance(request.data["file"], UploadedFile): - asset = Asset.objects.create( - creator=request.user, - club=obj, - file=request.data["file"], - name=request.data["file"].name, - ) - else: + club = get_object_or_404(Club, code=code) + + if "file" not in request.data or not isinstance(request.data["file"], UploadedFile): return Response( {"detail": "No image file was uploaded!"}, status=status.HTTP_400_BAD_REQUEST, ) + + asset = Asset.objects.create( + creator=request.user, + club=club, + file=request.data["file"], + name=request.data["file"].name, + ) return Response({"detail": "Club file uploaded!", "id": asset.id}) From c5a912d0651b46474fc6049530d6a333aeef2636 Mon Sep 17 00:00:00 2001 From: aviupadhyayula Date: Wed, 13 Mar 2024 22:48:59 -0400 Subject: [PATCH 17/17] Use guard clause pattern in ClubFairViewSet.current() --- backend/clubs/views.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/backend/clubs/views.py b/backend/clubs/views.py index 0fca0a8b9..66507a6b6 100644 --- a/backend/clubs/views.py +++ b/backend/clubs/views.py @@ -543,6 +543,7 @@ def filter_queryset(self, request, queryset, view): if "featured" in ordering: if queryset.model == Club: return queryset.order_by("-rank", "-favorite_count", "-id") + return queryset.order_by( "-club__rank", "-club__favorite_count", "-club__id" ) @@ -618,10 +619,11 @@ def current(self, request, *args, **kwargs): start_time__lte=now + datetime.timedelta(minutes=2), end_time__gte=now - datetime.timedelta(minutes=2), ).first() + if fair is None: return Response([]) - else: - return Response([ClubFairSerializer(instance=fair).data]) + + return Response([ClubFairSerializer(instance=fair).data]) @action(detail=True, methods=["get"]) def live(self, *args, **kwargs):