From 3960c66ae267522c578cec4fc2a8687fc38a3916 Mon Sep 17 00:00:00 2001 From: wmuldergov Date: Thu, 23 Apr 2026 11:47:23 -0700 Subject: [PATCH] DBC22-6268: Optimize API performance --- src/webapp/apps/events/serializers.py | 99 ++++++++++++------------- src/webapp/apps/events/views.py | 89 +++++++++++++++++++--- src/webapp/apps/organizations/views.py | 6 +- src/webapp/apps/segments/serializers.py | 12 ++- src/webapp/apps/users/serializers.py | 13 +++- src/webapp/apps/users/views.py | 3 +- 6 files changed, 153 insertions(+), 69 deletions(-) diff --git a/src/webapp/apps/events/serializers.py b/src/webapp/apps/events/serializers.py index 3bd0387d..2ce0d409 100644 --- a/src/webapp/apps/events/serializers.py +++ b/src/webapp/apps/events/serializers.py @@ -43,6 +43,10 @@ class NotesListSerializer(fields.ListField): child = NoteSerializer() def get_attribute(self, instance): + notes_map = self.context.get('notes_map') + if notes_map is not None: + return notes_map.get(instance.id, []) + return Note.current.filter(event=instance.id).order_by('-created') @@ -85,8 +89,14 @@ def validate_notes(self, notes): return notes def get_is_closure(self, obj): - ids = [impact['id'] for impact in obj.impacts] - return TrafficImpact.objects.filter(id__in=ids, closed=True).count() > 0 + closed_ids = self.context.get('closed_impact_ids') + + if closed_ids is None: + ids = [impact['id'] for impact in (obj.impacts or []) if isinstance(impact, dict)] + return TrafficImpact.objects.filter(id__in=ids, closed=True).exists() + + event_impact_ids = [impact['id'] for impact in (obj.impacts or []) if isinstance(impact, dict)] + return any(impact_id in closed_ids for impact_id in event_impact_ids) def get_last_inactivated(self, obj): return obj.meta.get('last_inactivated') @@ -136,27 +146,22 @@ def to_internal_value(self, data): def to_representation(self, instance): obj = super().to_representation(instance) - - # have to remove meta here to avoid sending it, rather than as an - # excluded field, because otherwise meta doesn't get populated on the - # way in through key movement and to_internal_value() - if 'meta' in obj: - del obj['meta'] - - if obj.get('type') == 'ROAD_CONDITION': - if instance.segment: - obj['location']['start']['name'] = instance.segment.name - - obj['polygon'] = instance.geometry.buffer_with_style(.01, end_cap_style=2, join_style=2).coords[0] - - # Only serialize segment for road conditions - if obj.get('type') != 'ROAD_CONDITION' and 'segment' in obj: - del obj['segment'] - - # Only serialize chainup for chainups - if obj.get('type') != 'CHAIN_UP' and 'chainup' in obj: - del obj['chainup'] - + conditions = obj.get('conditions') or [] + + id_to_label = self.context.get('condition_labels') + + if id_to_label is None: + condition_ids = [c if isinstance(c, int) else c.get('id') for c in conditions] + id_to_label = dict(Condition.objects.filter(id__in=condition_ids).values_list('id', 'label')) + + normalized = [] + for condition in conditions: + cid = condition if isinstance(condition, int) else condition.get('id') + label = id_to_label.get(cid) + if label: + normalized.append({'id': cid, 'label': label}) + + obj['conditions'] = normalized return obj def is_automatically_approved(self, data): @@ -320,37 +325,27 @@ def get_first_reported(self, obj): } def to_representation(self, instance): - """Override to normalize conditions to {id, label} objects.""" obj = super().to_representation(instance) - conditions = obj.get('conditions') or [] - if conditions: - condition_ids = [ - condition for condition in conditions - if isinstance(condition, int) - ] - condition_ids.extend([ - condition.get('id') for condition in conditions - if isinstance(condition, dict) and isinstance(condition.get('id'), int) - ]) - id_to_label = dict( - Condition.objects.filter(id__in=condition_ids).values_list('id', 'label') - ) - normalized = [] - for condition in conditions: - if isinstance(condition, int): - label = id_to_label.get(condition) - if label is not None: - normalized.append({'id': condition, 'label': label}) - elif isinstance(condition, dict): - condition_id = condition.get('id') - if isinstance(condition_id, int): - normalized.append({ - 'id': condition_id, - 'label': condition.get('label') or id_to_label.get(condition_id), - }) - obj['conditions'] = normalized - + + id_to_label = self.context.get('condition_labels', {}) + + normalized = [] + for condition in conditions: + c_id = None + c_label = None + + if isinstance(condition, int): + c_id = condition + c_label = id_to_label.get(c_id) + elif isinstance(condition, dict): + c_id = condition.get('id') + c_label = condition.get('label') or id_to_label.get(c_id) + + if c_id and c_label: + normalized.append({'id': c_id, 'label': c_label}) + + obj['conditions'] = normalized return obj class Meta: diff --git a/src/webapp/apps/events/views.py b/src/webapp/apps/events/views.py index 6e106886..84f11346 100644 --- a/src/webapp/apps/events/views.py +++ b/src/webapp/apps/events/views.py @@ -19,28 +19,78 @@ class Events(viewsets.ModelViewSet): - queryset = Event.last.all() + queryset = Event.last.all().select_related( + 'user', + 'segment__route', + 'chainup__route', + 'chainup__area', + 'service_area', + ).prefetch_related( + 'service_area__parent', + 'chainup__area__parent', + ) serializer_class = EventSerializer lookup_field = 'id' permission_classes = [AllowAny] + def get_serializer_context(self): + context = super().get_serializer_context() + + if self.action in ['list', 'retrieve', 'history', 'diffs', 'toggle', 'clear', 'confirm']: + try: + if self.detail: + event_ids = [self.kwargs.get('id')] + else: + queryset = self.filter_queryset(self.get_queryset()) + event_ids = list(queryset.values_list('id', flat=True)) + + first_versions = Event.objects.filter( + id__in=event_ids, + approved=True + ).order_by('id', 'version').distinct('id').select_related('user') + + context['first_reported_map'] = { + v.id: {'user': v.user, 'created': v.created} + for v in first_versions + } + + notes_qs = Note.current.filter(event__in=event_ids).order_by('-created') + notes_map = {} + for note in notes_qs: + if note.event not in notes_map: + notes_map[note.event] = [] + notes_map[note.event].append(note) + context['notes_map'] = notes_map + + except Exception: + context['first_reported_map'] = {} + context['notes_map'] = {} + + context['closed_impact_ids'] = set( + TrafficImpact.objects.filter(closed=True).values_list('id', flat=True) + ) + + context['condition_labels'] = dict( + Condition.objects.values_list('id', 'label') + ) + + return context + @action(detail=True) def history(self, request, id): event = self.get_object() queryset = Event.objects.filter(id=event.id) - serializer = EventHistorySerializer(queryset, many=True) + serializer = EventHistorySerializer(queryset, many=True, context=self.get_serializer_context()) return Response(serializer.data) @action(detail=True) def diffs(self, request, id): event = self.get_object() queryset = Event.objects.filter(id=event.id) - serializer = EventDiffSerializer(queryset, many=True) + serializer = EventDiffSerializer(queryset, many=True, context=self.get_serializer_context()) return Response(serializer.data) def partial_update(self, request, *args, **kwargs): - # need to do this here, otherwise serializer doesn't pick up user via - # default field value return super().partial_update(request, *args, **kwargs) @@ -64,7 +114,13 @@ def validate_allowed_segments(user, segPks): class RoadConditions(Events): - queryset = Event.current.filter(event_type=EventType.ROAD_CONDITION, from_bulk=True) + queryset = Event.current.filter(event_type=EventType.ROAD_CONDITION, from_bulk=True).select_related( + 'user', + 'segment__route', + 'service_area', + ).prefetch_related( + 'service_area__parent', + ) serializer_class = RcSerializer @action(detail=False, methods=['post'], url_path='clear') @@ -84,7 +140,7 @@ def clear_rcs(self, request): for event in existing_events: event.status = 'Inactive' event.save() - cleared_events.append(RcSerializer(event).data) + cleared_events.append(RcSerializer(event, context=self.get_serializer_context()).data) return Response({'status': status.HTTP_202_ACCEPTED, 'data': cleared_events}, status=status.HTTP_202_ACCEPTED) @@ -180,7 +236,13 @@ def bulk_update_rcs(self, request): class ChainUps(Events): - queryset = Event.current.filter(event_type=EventType.CHAIN_UP, from_bulk=True) + queryset = Event.current.filter(event_type=EventType.CHAIN_UP, from_bulk=True).select_related( + 'user', + 'chainup__route', + 'chainup__area', + ).prefetch_related( + 'chainup__area__parent', + ) serializer_class = ChainUpEventSerializer permission_classes = [Approver] @@ -266,7 +328,16 @@ def reconfirm_chainups(self, request): class Pending(viewsets.ModelViewSet): - queryset = Event.pending.all() + queryset = Event.pending.all().select_related( + 'user', + 'segment__route', + 'chainup__route', + 'chainup__area', + ).prefetch_related( + 'service_area', + 'service_area__parent', + 'chainup__area__parent', + ) serializer_class = PendingSerializer lookup_field = 'id' permission_classes = [IsAuthenticated] diff --git a/src/webapp/apps/organizations/views.py b/src/webapp/apps/organizations/views.py index 987fec87..9f20b307 100644 --- a/src/webapp/apps/organizations/views.py +++ b/src/webapp/apps/organizations/views.py @@ -11,7 +11,7 @@ ) class OrganizationAPIView(ModelViewSet): - queryset = Organization.objects.all().order_by('name') + queryset = Organization.objects.all().prefetch_related('users', 'service_areas').order_by('name') serializer_class = OrganizationSerializer permission_classes = [permissions.IsAdminUser] @@ -64,7 +64,7 @@ class ServiceAreaAPIView(ModelViewSet): permission_classes = [permissions.IsAuthenticated] def get_queryset(self): - return ServiceArea.objects.exclude(parent=None).order_by('sortingOrder') + return ServiceArea.objects.exclude(parent=None).select_related('parent').order_by('sortingOrder') @action(detail=True) def boundary(self, request, pk): @@ -83,7 +83,7 @@ class DistrictAPIView(ModelViewSet): permission_classes = [permissions.IsAuthenticated] def get_queryset(self): - return ServiceArea.objects.filter(parent=None).order_by('sortingOrder') + return ServiceArea.objects.filter(parent=None).prefetch_related('children').order_by('sortingOrder') @action(detail=True) def boundary(self, request, pk): diff --git a/src/webapp/apps/segments/serializers.py b/src/webapp/apps/segments/serializers.py index d62225a6..8d1a9319 100644 --- a/src/webapp/apps/segments/serializers.py +++ b/src/webapp/apps/segments/serializers.py @@ -16,8 +16,16 @@ class Meta: exclude = ["geometry"] def get_area(self, obj): - sa = ServiceArea.objects.filter(segments__contains=int(obj.id)).exclude(parent=None).first() - return sa.id if sa else None + if 'segment_to_area' not in self.context: + areas = ServiceArea.objects.exclude(parent=None).order_by('id').values_list('id', 'segments') + mapping = {} + for area_id, segment_ids in areas: + if segment_ids: + for sid in segment_ids: + mapping.setdefault(str(sid), area_id) + self.context['segment_to_area'] = mapping + + return self.context['segment_to_area'].get(str(obj.id)) class ChainUpSerializer(serializers.ModelSerializer): diff --git a/src/webapp/apps/users/serializers.py b/src/webapp/apps/users/serializers.py index 9c47a7be..c8f17d48 100644 --- a/src/webapp/apps/users/serializers.py +++ b/src/webapp/apps/users/serializers.py @@ -11,7 +11,7 @@ class RIDEUserSerializer(serializers.ModelSerializer): class Meta: model = RIDEUser - fields = "__all__" + exclude = ["password", "user_permissions", "groups"] def _get_social_account(self, obj): # prefetch_related makes .all() use the cache, .first() does not @@ -33,7 +33,16 @@ def get_social_provider(self, obj): return social_account.provider def get_is_approver(self, obj): - return obj.is_approver + if obj.is_superuser: + return True + for perm in obj.user_permissions.all(): + if perm.codename == 'approve_ride_events' and perm.content_type.app_label == 'users': + return True + for group in obj.groups.all(): + for perm in group.permissions.all(): + if perm.codename == 'approve_ride_events' and perm.content_type.app_label == 'users': + return True + return False class RIDEGroupSerializer(serializers.ModelSerializer): diff --git a/src/webapp/apps/users/views.py b/src/webapp/apps/users/views.py index 1e00a28d..62b16722 100644 --- a/src/webapp/apps/users/views.py +++ b/src/webapp/apps/users/views.py @@ -51,7 +51,8 @@ class RIDEUserAPIView(ModelViewSet): queryset = RIDEUser.objects.prefetch_related( 'socialaccount_set', 'organizations', - 'user_permissions', + 'user_permissions__content_type', + 'groups__permissions__content_type', ).all() serializer_class = RIDEUserSerializer permission_classes = [permissions.IsAdminUser]