From d86f5c150798913cd9d560badcd86b3b96ba06c8 Mon Sep 17 00:00:00 2001 From: Troy Sankey Date: Tue, 21 Jan 2025 12:44:39 -0800 Subject: [PATCH] feat: add parent_course query parameter to basic content-metadata endpoint ENT-9840 --- .../apps/api/v1/views/content_metadata.py | 70 +++++++++++++++++-- 1 file changed, 65 insertions(+), 5 deletions(-) diff --git a/enterprise_catalog/apps/api/v1/views/content_metadata.py b/enterprise_catalog/apps/api/v1/views/content_metadata.py index 216da5e5c..45d8f1426 100644 --- a/enterprise_catalog/apps/api/v1/views/content_metadata.py +++ b/enterprise_catalog/apps/api/v1/views/content_metadata.py @@ -1,10 +1,13 @@ import uuid +from django.db.models import Q +from django.shortcuts import get_object_or_404 from edx_rest_framework_extensions.auth.jwt.authentication import ( JwtAuthentication, ) from rest_framework import permissions, viewsets from rest_framework.authentication import SessionAuthentication +from rest_framework.decorators import action from rest_framework.renderers import JSONRenderer from rest_framework_xml.renderers import XMLRenderer @@ -12,6 +15,7 @@ PageNumberWithSizePagination, ) from enterprise_catalog.apps.api.v1.serializers import ContentMetadataSerializer +from enterprise_catalog.apps.catalog.constants import COURSE_RUN from enterprise_catalog.apps.catalog.models import ContentMetadata @@ -47,16 +51,72 @@ class ContentMetadataView(viewsets.ReadOnlyModelViewSet): queryset = ContentMetadata.objects.all() pagination_class = PageNumberWithSizePagination + @property + def pk_is_object_id(self): + return self.kwargs.get('pk', '').isdigit() + + @property + def pk_is_content_uuid(self): + return not self.pk_is_object_id and is_valid_uuid(self.kwargs.get('pk')) + + @property + def pk_is_content_key(self): + return not self.pk_is_object_id and not self.pk_is_content_uuid + def get_queryset(self, **kwargs): """ Returns all content metadata objects filtered by an optional request query param (LIST) ``content_identifiers`` """ - content_filters = self.request.query_params.getlist('content_identifiers') queryset = self.queryset + + # Find all directly requested content + content_filters = self.request.query_params.getlist('content_identifiers') + queryset_direct = None if content_filters: content_uuids, content_keys = partition(is_valid_uuid, content_filters) - if content_keys: - queryset = queryset.filter(content_key__in=content_filters) - if content_uuids: - queryset = queryset.filter(content_uuid__in=content_filters) + queryset_direct = self.queryset.filter( + Q(content_uuid__in=content_uuids) | Q(content_key__in=content_keys) + ) + queryset = queryset_direct + + # If ``?parent_course=true`` was passed, exclude course runs. + if self.request.query_params.get('parent_course', False): + query_filters = ~Q(content_type=COURSE_RUN) + # If ``?content_identifiers=`` was passed, follow any matched course run objects back up + # to their parent courses and include those in the response. + if content_filters: + parent_content_keys = ( + record[0] for record in + queryset_direct.filter(content_type=COURSE_RUN).values_list('parent_content_key') + ) + all_content_keys_to_find = content_keys+parent_content_keys + query_filters &= ( + Q(content_uuid__in=content_uuids) | Q(content_key__in=all_content_keys_to_find) + ) + queryset = self.queryset.filter(query_filters) + return queryset + + def retrieve(self, request, *args, pk=None, **kwargs): + """ + Override to + """ + # Support alternative pk types besisdes just the raw object IDs (which are completely opaque + # to API clients). + obj = None + if self.pk_is_content_uuid: + obj = get_object_or_404(self.queryset, content_uuid=pk) + if self.pk_is_content_key: + obj = get_object_or_404(self.queryset, content_key=pk) + + # Coerce course runs to courses if requested. + if self.request.query_params.get('parent_course', False): + if not obj: + obj = get_object_or_404(self.queryset, id=pk) + if obj.content_type == COURSE_RUN: + obj = get_object_or_404(self.queryset, content_key=obj.parent_content_key) + + # Finally, call super's retrieve() which has more DRF guts that are best not duplicated in + # this codebase. + pk_to_retrieve = obj.id if obj else pk + return super().retrieve(request, *args, pk=pk_to_retrieve, **kwargs)