From 9c1c271673d444681a30c0afc644a2fc2ccc46e9 Mon Sep 17 00:00:00 2001 From: Ali Nawaz Date: Mon, 13 May 2024 09:23:49 +0500 Subject: [PATCH] feat: filter restricted runs on APIs --- course_discovery/apps/api/serializers.py | 29 ++++++++++++------- course_discovery/apps/api/utils.py | 4 +++ .../apps/api/v1/views/catalogs.py | 5 ++-- course_discovery/apps/api/v1/views/courses.py | 7 +++-- .../apps/api/v1/views/pathways.py | 7 ++++- .../apps/api/v1/views/programs.py | 9 ++++-- course_discovery/apps/api/v1/views/search.py | 5 ++++ .../apps/course_metadata/algolia_models.py | 19 ++++++++++-- .../apps/course_metadata/index.py | 4 +-- .../apps/course_metadata/models.py | 2 -- .../search_indexes/documents/course.py | 10 +++++-- .../search_indexes/documents/course_run.py | 2 +- .../documents/learner_pathway.py | 13 +++++++-- .../search_indexes/documents/person.py | 2 +- .../search_indexes/documents/program.py | 13 +++++++-- .../search_indexes/serializers/common.py | 10 +++++-- .../search_indexes/serializers/course.py | 11 +++++++ .../apps/course_metadata/utils.py | 2 +- .../apps/learner_pathway/api/v1/views.py | 18 ++++++++++-- 19 files changed, 132 insertions(+), 40 deletions(-) diff --git a/course_discovery/apps/api/serializers.py b/course_discovery/apps/api/serializers.py index b0cff4420e5..007bddf84d7 100644 --- a/course_discovery/apps/api/serializers.py +++ b/course_discovery/apps/api/serializers.py @@ -12,7 +12,7 @@ import pytz import waffle # lint-amnesty, pylint: disable=invalid-django-waffle-import from django.contrib.auth import get_user_model -from django.db.models import Count +from django.db.models import Count, prefetch_related_objects from django.db.models.query import Prefetch from django.utils.text import slugify from django.utils.translation import gettext_lazy as _ @@ -31,7 +31,7 @@ from course_discovery.apps.api.fields import ( HtmlField, ImageField, SlugRelatedFieldWithReadSerializer, SlugRelatedTranslatableField, StdImageSerializerField ) -from course_discovery.apps.api.utils import StudioAPI +from course_discovery.apps.api.utils import StudioAPI, get_forbidden_runs from course_discovery.apps.catalogs.models import Catalog from course_discovery.apps.core.api_client.lms import LMSAPIClient from course_discovery.apps.core.utils import update_instance @@ -1638,8 +1638,15 @@ class CourseWithRecommendationsSerializer(FlexFieldsSerializerMixin, TimestampMo recommendations = serializers.SerializerMethodField() def get_recommendations(self, course): + recommended_courses = course.recommendations() + forbidden_runs = get_forbidden_runs(self.context['request']) + runs = CourseRun.objects.exclude(restricted_run__restriction_type__in=forbidden_runs) + prefetch_related_objects(recommended_courses, Prefetch( + 'course_runs', + queryset=runs.select_related('type').prefetch_related('seats')), + ) return CourseRecommendationSerializer( - course.recommendations(), + recommended_courses, many=True, context={ 'request': self.context.get('request'), @@ -1996,7 +2003,7 @@ def get_organization_logo_override_url(self, obj): return None @classmethod - def prefetch_queryset(cls, partner, queryset=None): + def prefetch_queryset(cls, partner, queryset=None, course_runs=None): # Explicitly check if the queryset is None before selecting related queryset = queryset if queryset is not None else Program.objects.filter(partner=partner) @@ -2020,7 +2027,7 @@ def prefetch_queryset(cls, partner, queryset=None): 'degree__rankings', 'degree__quick_facts', 'labels', - Prefetch('courses', queryset=MinimalProgramCourseSerializer.prefetch_queryset()), + Prefetch('courses', queryset=MinimalProgramCourseSerializer.prefetch_queryset(course_runs=course_runs)), Prefetch('authoring_organizations', queryset=OrganizationSerializer.prefetch_queryset(partner)), ) @@ -2165,8 +2172,8 @@ class MinimalExtendedProgramSerializer(MinimalProgramSerializer): expected_learning_items = serializers.SlugRelatedField(many=True, read_only=True, slug_field='value') @classmethod - def prefetch_queryset(cls, partner, queryset=None): - queryset = super().prefetch_queryset(partner=partner, queryset=queryset) + def prefetch_queryset(cls, partner, queryset=None, course_runs=None): + queryset = super().prefetch_queryset(partner=partner, queryset=queryset, course_runs=course_runs) return queryset.prefetch_related( 'expected_learning_items', @@ -2209,7 +2216,7 @@ class ProgramSerializer(MinimalProgramSerializer): product_source = SourceSerializer(required=False, read_only=True) @classmethod - def prefetch_queryset(cls, partner, queryset=None): + def prefetch_queryset(cls, partner, queryset=None, course_runs=None): """ Prefetch the related objects that will be serialized with a `Program`. @@ -2255,7 +2262,7 @@ def prefetch_queryset(cls, partner, queryset=None): 'instructor_ordering', # We need the full Course prefetch here to get CourseRun information that methods on the Program # model iterate across (e.g. language). These fields aren't prefetched by the minimal Course serializer. - Prefetch('courses', queryset=CourseSerializer.prefetch_queryset(partner=partner)), + Prefetch('courses', queryset=CourseSerializer.prefetch_queryset(partner=partner, course_runs=course_runs)), Prefetch('authoring_organizations', queryset=OrganizationSerializer.prefetch_queryset(partner)), Prefetch('credit_backing_organizations', queryset=OrganizationSerializer.prefetch_queryset(partner)), Prefetch('corporate_endorsements', queryset=CorporateEndorsementSerializer.prefetch_queryset()), @@ -2302,11 +2309,11 @@ class PathwaySerializer(BaseModelSerializer): course_run_statuses = serializers.ReadOnlyField() @classmethod - def prefetch_queryset(cls, partner): + def prefetch_queryset(cls, partner, course_runs=None): queryset = Pathway.objects.filter(partner=partner) return queryset.prefetch_related( - Prefetch('programs', queryset=MinimalProgramSerializer.prefetch_queryset(partner=partner)), + Prefetch('programs', queryset=MinimalProgramSerializer.prefetch_queryset(partner=partner, course_runs=course_runs)), ) class Meta: diff --git a/course_discovery/apps/api/utils.py b/course_discovery/apps/api/utils.py index e93dd1294e3..56bb2c70c7e 100644 --- a/course_discovery/apps/api/utils.py +++ b/course_discovery/apps/api/utils.py @@ -14,6 +14,7 @@ from course_discovery.apps.core.api_client.lms import LMSAPIClient from course_discovery.apps.core.utils import serialize_datetime from course_discovery.apps.course_metadata.models import CourseRun +from course_discovery.apps.course_metadata.choices import CourseRunRestrictionType logger = logging.getLogger(__name__) @@ -198,6 +199,9 @@ def increment_character(character): """ return chr(ord(character) + 1) if character != 'z' else 'a' +def get_forbidden_runs(request): + restriction_list=request.query_params.get('restriction_list', '').split(',') + return list(set(CourseRunRestrictionType.values) - set(restriction_list)) class StudioAPI: """ diff --git a/course_discovery/apps/api/v1/views/catalogs.py b/course_discovery/apps/api/v1/views/catalogs.py index 631b99652e9..92874a75e9e 100644 --- a/course_discovery/apps/api/v1/views/catalogs.py +++ b/course_discovery/apps/api/v1/views/catalogs.py @@ -11,7 +11,7 @@ from course_discovery.apps.api import filters, serializers from course_discovery.apps.api.pagination import ProxiedPagination from course_discovery.apps.api.renderers import CourseRunCSVRenderer -from course_discovery.apps.api.utils import check_catalog_api_access +from course_discovery.apps.api.utils import check_catalog_api_access, get_forbidden_runs from course_discovery.apps.catalogs.models import Catalog from course_discovery.apps.course_metadata.models import CourseRun, CourseType @@ -105,7 +105,8 @@ def courses(self, request, id=None): # pylint: disable=redefined-builtin, unuse if not catalog.include_archived: queryset = queryset.available() course_runs = course_runs.active().enrollable().marketable() - + forbidden_runs = get_forbidden_runs(request) + course_runs = CourseRun.objects.exclude(restricted_run__restriction_type__in=forbidden_runs) queryset = serializers.CatalogCourseSerializer.prefetch_queryset( self.request.site.partner, queryset=queryset, diff --git a/course_discovery/apps/api/v1/views/courses.py b/course_discovery/apps/api/v1/views/courses.py index c8b4640951e..a98eecdb118 100644 --- a/course_discovery/apps/api/v1/views/courses.py +++ b/course_discovery/apps/api/v1/views/courses.py @@ -23,7 +23,7 @@ from course_discovery.apps.api.pagination import ProxiedPagination from course_discovery.apps.api.permissions import IsCourseEditorOrReadOnly from course_discovery.apps.api.serializers import CourseEntitlementSerializer, MetadataWithType -from course_discovery.apps.api.utils import decode_image_data, get_query_param, reviewable_data_has_changed +from course_discovery.apps.api.utils import decode_image_data, get_query_param, reviewable_data_has_changed, get_forbidden_runs from course_discovery.apps.api.v1.exceptions import EditableAndQUnsupported from course_discovery.apps.api.v1.views.course_runs import CourseRunViewSet from course_discovery.apps.course_metadata.choices import CourseRunStatus, ProgramStatus @@ -122,9 +122,11 @@ def get_queryset(self): else: queryset = self.queryset + forbidden_runs = get_forbidden_runs(self.request) if q: queryset = Course.search(q, queryset=queryset) - queryset = self.get_serializer_class().prefetch_queryset(queryset=queryset, partner=partner) + course_runs = CourseRun.objects.exclude(restricted_run__restriction_type__in=forbidden_runs) + queryset = self.get_serializer_class().prefetch_queryset(queryset=queryset, partner=partner, course_runs=course_runs) else: if edit_mode: course_runs = CourseRun.objects.filter_drafts(course__partner=partner) @@ -148,6 +150,7 @@ def get_queryset(self): else: programs = Program.objects.exclude(status=ProgramStatus.Deleted) + course_runs = course_runs.exclude(restricted_run__restriction_type__in=forbidden_runs) queryset = self.get_serializer_class().prefetch_queryset( queryset=queryset, course_runs=course_runs, diff --git a/course_discovery/apps/api/v1/views/pathways.py b/course_discovery/apps/api/v1/views/pathways.py index c428a0074c0..96f79a0944e 100644 --- a/course_discovery/apps/api/v1/views/pathways.py +++ b/course_discovery/apps/api/v1/views/pathways.py @@ -2,14 +2,19 @@ from rest_framework import viewsets from course_discovery.apps.api import serializers +from course_discovery.apps.api.utils import get_forbidden_runs from course_discovery.apps.api.cache import CompressedCacheResponseMixin from course_discovery.apps.api.permissions import ReadOnlyByPublisherUser +from course_discovery.apps.course_metadata.models import CourseRun class PathwayViewSet(CompressedCacheResponseMixin, viewsets.ReadOnlyModelViewSet): permission_classes = (ReadOnlyByPublisherUser,) serializer_class = serializers.PathwaySerializer def get_queryset(self): - queryset = self.get_serializer_class().prefetch_queryset(partner=self.request.site.partner) + forbidden_runs = get_forbidden_runs(self.request) + course_runs = CourseRun.objects.exclude(restricted_run__restriction_type__in=forbidden_runs) + + queryset = self.get_serializer_class().prefetch_queryset(partner=self.request.site.partner, course_runs=course_runs) return queryset.order_by('created') diff --git a/course_discovery/apps/api/v1/views/programs.py b/course_discovery/apps/api/v1/views/programs.py index 575e9209156..5560205a502 100644 --- a/course_discovery/apps/api/v1/views/programs.py +++ b/course_discovery/apps/api/v1/views/programs.py @@ -12,8 +12,8 @@ from course_discovery.apps.api import filters, serializers from course_discovery.apps.api.cache import CompressedCacheResponseMixin from course_discovery.apps.api.pagination import ProxiedPagination -from course_discovery.apps.api.utils import get_query_param -from course_discovery.apps.course_metadata.models import Program +from course_discovery.apps.api.utils import get_query_param, get_forbidden_runs +from course_discovery.apps.course_metadata.models import Program, CourseRun class ProgramViewSet(CompressedCacheResponseMixin, viewsets.ReadOnlyModelViewSet): @@ -46,7 +46,10 @@ def get_queryset(self): queryset = Program.objects.filter(uuid=program_uuid) elif q: queryset = Program.search(q, queryset=queryset) - return self.get_serializer_class().prefetch_queryset(queryset=queryset, partner=partner) + + forbidden_runs = get_forbidden_runs(self.request) + course_runs = CourseRun.objects.exclude(restricted_run__restriction_type__in=forbidden_runs) + return self.get_serializer_class().prefetch_queryset(queryset=queryset, partner=partner, course_runs=course_runs) def get_serializer_context(self): context = super().get_serializer_context() diff --git a/course_discovery/apps/api/v1/views/search.py b/course_discovery/apps/api/v1/views/search.py index f2fbd70fa24..f70de77ef3c 100644 --- a/course_discovery/apps/api/v1/views/search.py +++ b/course_discovery/apps/api/v1/views/search.py @@ -19,6 +19,9 @@ from course_discovery.apps.api.utils import update_query_params_with_body_data from course_discovery.apps.course_metadata.choices import ProgramStatus from course_discovery.apps.course_metadata.models import Person + +from course_discovery.apps.course_metadata.models import * + from course_discovery.apps.course_metadata.search_indexes import documents as search_documents from course_discovery.apps.course_metadata.search_indexes import serializers as search_indexes_serializers from course_discovery.apps.course_metadata.search_indexes.constants import LEARNER_PATHWAY_FEATURE_PARAM @@ -101,6 +104,8 @@ class CourseSearchViewSet(BaseElasticsearchDocumentViewSet): 'subjects': {'field': 'subjects.raw', 'enabled': True}, 'prerequisites': {'field': 'prerequisites', 'enabled': True}, } + + class CourseRunSearchViewSet(FacetQueryFieldsMixin, BaseElasticsearchDocumentViewSet): diff --git a/course_discovery/apps/course_metadata/algolia_models.py b/course_discovery/apps/course_metadata/algolia_models.py index 51bb48ad8b5..579be71e201 100644 --- a/course_discovery/apps/course_metadata/algolia_models.py +++ b/course_discovery/apps/course_metadata/algolia_models.py @@ -12,7 +12,7 @@ from course_discovery.apps.course_metadata.choices import CourseRunStatus, ExternalProductStatus, ProgramStatus from course_discovery.apps.course_metadata.models import ( - AbstractLocationRestrictionModel, Course, CourseType, ProductValue, Program, ProgramType + AbstractLocationRestrictionModel, Course, CourseRun, CourseType, ProductValue, Program, ProgramType ) from course_discovery.apps.course_metadata.utils import transform_skills_data @@ -87,7 +87,8 @@ def _wrap(self, *args, **kwargs): def get_course_availability(course): - all_runs = course.course_runs.filter(status=CourseRunStatus.Published) + all_runs = course.course_runs.all() + all_runs = filter(lambda r: r.status == CourseRunStatus.Published, all_runs) availability = set() for course_run in all_runs: @@ -211,6 +212,13 @@ class AlgoliaProxyCourse(Course, AlgoliaBasicModelFieldsMixin): class Meta: proxy = True + @classmethod + def prefetch_queryset(cls): + return cls.objects.all().prefetch_related( + models.Prefetch( + 'course_runs', queryset=CourseRun.objects.filter(restricted_run__isnull=True) + ) + ) @property def product_type(self): if self.type.slug == CourseType.EXECUTIVE_EDUCATION_2U: @@ -465,6 +473,13 @@ class AlgoliaProxyProgram(Program, AlgoliaBasicModelFieldsMixin): class Meta: proxy = True + @classmethod + def prefetch_queryset(cls): + return cls.objects.all().prefetch_related( + models.Prefetch( + 'courses__course_runs', queryset=CourseRun.objects.filter(restricted_run__isnull=True) + ) + ) @property def product_type(self): if self.is_2u_degree_program: diff --git a/course_discovery/apps/course_metadata/index.py b/course_discovery/apps/course_metadata/index.py index 67baef3d5d2..b924f3123a1 100644 --- a/course_discovery/apps/course_metadata/index.py +++ b/course_discovery/apps/course_metadata/index.py @@ -23,11 +23,11 @@ def get_queryset(self): # pragma: no cover bootcamp_contentful_data = fetch_and_transform_bootcamp_contentful_data() qs1 = [AlgoliaProxyProduct(course, self.language, contentful_data=bootcamp_contentful_data) - for course in AlgoliaProxyCourse.objects.all()] + for course in AlgoliaProxyCourse.prefetch_queryset()] degree_contentful_data = fetch_and_transform_degree_contentful_data() qs2 = [AlgoliaProxyProduct(program, self.language, contentful_data=degree_contentful_data) - for program in AlgoliaProxyProgram.objects.all()] + for program in AlgoliaProxyProgram.prefetch_queryset()] return qs1 + qs2 diff --git a/course_discovery/apps/course_metadata/models.py b/course_discovery/apps/course_metadata/models.py index 314f5570747..b9138920f3e 100644 --- a/course_discovery/apps/course_metadata/models.py +++ b/course_discovery/apps/course_metadata/models.py @@ -1922,7 +1922,6 @@ def recommendations(self): ) .select_related('partner', 'type') .prefetch_related( - Prefetch('course_runs', queryset=CourseRun.objects.select_related('type').prefetch_related('seats')), 'authoring_organizations', '_official_version' ) @@ -1937,7 +1936,6 @@ def recommendations(self): ) .select_related('partner', 'type') .prefetch_related( - Prefetch('course_runs', queryset=CourseRun.objects.select_related('type').prefetch_related('seats')), 'authoring_organizations', '_official_version' ) diff --git a/course_discovery/apps/course_metadata/search_indexes/documents/course.py b/course_discovery/apps/course_metadata/search_indexes/documents/course.py index 3e2c5ae5a3f..4ce26b1f611 100644 --- a/course_discovery/apps/course_metadata/search_indexes/documents/course.py +++ b/course_discovery/apps/course_metadata/search_indexes/documents/course.py @@ -1,10 +1,11 @@ from django.conf import settings +from django.db.models import Prefetch from django_elasticsearch_dsl import Index, fields from opaque_keys.edx.keys import CourseKey from taxonomy.choices import ProductTypes from taxonomy.utils import get_whitelisted_serialized_skills -from course_discovery.apps.course_metadata.models import Course +from course_discovery.apps.course_metadata.models import Course, CourseRun from course_discovery.apps.course_metadata.utils import get_product_skill_names from .analyzers import case_insensitive_keyword @@ -122,9 +123,12 @@ def prepare_partner(self, obj): def prepare_prerequisites(self, obj): return [prerequisite.name for prerequisite in obj.prerequisites.all()] - def get_queryset(self): + def get_queryset(self, forbidden_runs=[]): return super().get_queryset().prefetch_related( - 'course_runs__seats__type', 'course_runs__type', 'course_runs__language').select_related('partner') + Prefetch('course_runs', queryset=CourseRun.objects.exclude(restricted_run__restriction_type__in=forbidden_runs).prefetch_related( + 'seats__type', 'type', 'language' + )) + ).select_related('partner') def prepare_course_type(self, obj): return obj.type.slug diff --git a/course_discovery/apps/course_metadata/search_indexes/documents/course_run.py b/course_discovery/apps/course_metadata/search_indexes/documents/course_run.py index e85df5f9909..fc0a79e6eff 100644 --- a/course_discovery/apps/course_metadata/search_indexes/documents/course_run.py +++ b/course_discovery/apps/course_metadata/search_indexes/documents/course_run.py @@ -137,7 +137,7 @@ def prepare_transcript_languages(self, obj): for language in obj.transcript_languages.all() ] - def get_queryset(self): + def get_queryset(self, forbidden_runs=[]): return filter_visible_runs( super().get_queryset() .select_related('course') diff --git a/course_discovery/apps/course_metadata/search_indexes/documents/learner_pathway.py b/course_discovery/apps/course_metadata/search_indexes/documents/learner_pathway.py index 432d34354c3..d919f2c46c4 100644 --- a/course_discovery/apps/course_metadata/search_indexes/documents/learner_pathway.py +++ b/course_discovery/apps/course_metadata/search_indexes/documents/learner_pathway.py @@ -1,9 +1,12 @@ from django.conf import settings +from django.db.models import Prefetch from django_elasticsearch_dsl import Index, fields from course_discovery.apps.learner_pathway.choices import PathwayStatus from course_discovery.apps.learner_pathway.models import LearnerPathway +from course_discovery.apps.course_metadata.models import CourseRun + from .analyzers import case_insensitive_keyword, edge_ngram_completion, html_strip, synonym_text from .common import BaseDocument, OrganizationsMixin @@ -50,10 +53,16 @@ def prepare_partner(self, obj): def prepare_published(self, obj): return obj.status == PathwayStatus.Active - def get_queryset(self): + def get_queryset(self, forbidden_runs=[]): return super().get_queryset().prefetch_related( 'steps', 'steps__learnerpathwaycourse_set', 'steps__learnerpathwayprogram_set', - 'steps__learnerpathwayblock_set' + 'steps__learnerpathwayblock_set', + Prefetch('steps__learnerpathwaycourse_set__course__course_runs', queryset=CourseRun.objects.exclude( + restricted_run__restriction_type__in=forbidden_runs + )), + Prefetch('steps__learnerpathwayprogram_set__program__courses__course_runs', queryset=CourseRun.objects.exclude( + restricted_run__restriction_type__in=forbidden_runs + )) ) def prepare_skill_names(self, obj): diff --git a/course_discovery/apps/course_metadata/search_indexes/documents/person.py b/course_discovery/apps/course_metadata/search_indexes/documents/person.py index 2394065a9a7..81fc91bd55d 100644 --- a/course_discovery/apps/course_metadata/search_indexes/documents/person.py +++ b/course_discovery/apps/course_metadata/search_indexes/documents/person.py @@ -47,7 +47,7 @@ def prepare_position(self, obj): return [] return [position.title, position.organization_override] - def get_queryset(self): + def get_queryset(self, forbidden_runs=[]): return super().get_queryset().select_related('bio_language') class Django: diff --git a/course_discovery/apps/course_metadata/search_indexes/documents/program.py b/course_discovery/apps/course_metadata/search_indexes/documents/program.py index 359949eb836..cd0c1b1f48e 100644 --- a/course_discovery/apps/course_metadata/search_indexes/documents/program.py +++ b/course_discovery/apps/course_metadata/search_indexes/documents/program.py @@ -1,10 +1,11 @@ from django.conf import settings +from django.db.models import Prefetch from django_elasticsearch_dsl import Index, fields from taxonomy.choices import ProductTypes from taxonomy.utils import get_whitelisted_serialized_skills from course_discovery.apps.course_metadata.choices import ProgramStatus -from course_discovery.apps.course_metadata.models import Degree, Program +from course_discovery.apps.course_metadata.models import Course, CourseRun, Degree, Program from course_discovery.apps.course_metadata.utils import get_product_skill_names from .analyzers import case_insensitive_keyword, edge_ngram_completion, html_strip, synonym_text @@ -121,8 +122,14 @@ def prepare_staff_uuids(self, obj): def prepare_type(self, obj): return obj.type.name_t - def get_queryset(self): - return super().get_queryset().select_related('type').select_related('partner') + def get_queryset(self, forbidden_runs=[]): + return super().get_queryset().select_related('type').select_related('partner').prefetch_related( + Prefetch('courses', queryset=Course.objects.all().prefetch_related( + Prefetch('course_runs', queryset=CourseRun.objects.exclude( + restricted_run__restriction_type__in=forbidden_runs + )) + )) + ) class Django: """ diff --git a/course_discovery/apps/course_metadata/search_indexes/serializers/common.py b/course_discovery/apps/course_metadata/search_indexes/serializers/common.py index 9c577168248..6e273f5ec92 100644 --- a/course_discovery/apps/course_metadata/search_indexes/serializers/common.py +++ b/course_discovery/apps/course_metadata/search_indexes/serializers/common.py @@ -5,7 +5,7 @@ from django_elasticsearch_dsl.registries import registry from course_discovery.apps.core.utils import ElasticsearchUtils, serialize_datetime - +from course_discovery.apps.course_metadata.choices import CourseRunRestrictionType log = logging.getLogger(__name__) @@ -30,6 +30,12 @@ def get_model_object_by_instances(self, instances): Provide Model objects by elasticsearch response instances. Fetches all the incoming instances at once and returns model queryset. """ + # import pdb + # pdb.set_trace() + + restriction_list=self.context['request'].query_params.get('restriction_list', '').split(',') + forbidden = list(set(CourseRunRestrictionType.values) - set(restriction_list)) + if not isinstance(instances, list): instances = [instances] document = None @@ -48,7 +54,7 @@ def get_model_object_by_instances(self, instances): if document and es_pks: try: - _objects = document(hit).get_queryset().filter(pk__in=es_pks) + _objects = document(hit).get_queryset(forbidden_runs=forbidden).filter(pk__in=es_pks) except ObjectDoesNotExist: log.error("Object could not be found in database for SearchResult '%r'.", self) diff --git a/course_discovery/apps/course_metadata/search_indexes/serializers/course.py b/course_discovery/apps/course_metadata/search_indexes/serializers/course.py index 317441ef017..479be302ba5 100644 --- a/course_discovery/apps/course_metadata/search_indexes/serializers/course.py +++ b/course_discovery/apps/course_metadata/search_indexes/serializers/course.py @@ -12,6 +12,7 @@ from course_discovery.apps.api import serializers as cd_serializers from course_discovery.apps.api.serializers import ContentTypeSerializer, CourseWithProgramsSerializer from course_discovery.apps.course_metadata.utils import get_course_run_estimated_hours, get_product_skill_names +from course_discovery.apps.course_metadata.models import CourseRun, Course from course_discovery.apps.edx_elasticsearch_dsl_extensions.serializers import BaseDjangoESDSLFacetSerializer from ..constants import BASE_SEARCH_INDEX_FIELDS, COMMON_IGNORED_FIELDS @@ -229,6 +230,16 @@ class CourseSearchModelSerializer(DocumentDSLSerializerMixin, ContentTypeSeriali Serializer for course model elasticsearch document. """ + + def model_prefetch_queryset(self, queryset): + q = queryset.prefetch_related( + models.Prefetch( + 'course_runs', + queryset=CourseRun.everything.filter(restricted_run__isnull=True) + ) + ) + return q + class Meta(CourseWithProgramsSerializer.Meta): document = CourseDocument fields = ContentTypeSerializer.Meta.fields + CourseWithProgramsSerializer.Meta.fields diff --git a/course_discovery/apps/course_metadata/utils.py b/course_discovery/apps/course_metadata/utils.py index 4c3549f961d..18ab00a350d 100644 --- a/course_discovery/apps/course_metadata/utils.py +++ b/course_discovery/apps/course_metadata/utils.py @@ -28,7 +28,7 @@ from course_discovery.apps.core.models import SalesforceConfiguration from course_discovery.apps.core.utils import serialize_datetime -from course_discovery.apps.course_metadata.choices import CourseRunStatus +from course_discovery.apps.course_metadata.choices import CourseRunStatus, CourseRunRestrictionType from course_discovery.apps.course_metadata.constants import ( DEFAULT_SLUG_FORMAT_ERROR_MSG, HTML_TAGS_ATTRIBUTE_WHITELIST, IMAGE_TYPES, SLUG_FORMAT_REGEX, SUBDIRECTORY_SLUG_FORMAT_REGEX diff --git a/course_discovery/apps/learner_pathway/api/v1/views.py b/course_discovery/apps/learner_pathway/api/v1/views.py index 5deed0923b4..61c38afd707 100644 --- a/course_discovery/apps/learner_pathway/api/v1/views.py +++ b/course_discovery/apps/learner_pathway/api/v1/views.py @@ -1,7 +1,7 @@ """ API Views for learner_pathway app. """ -from django.db.models import Q +from django.db.models import Q, Prefetch from django_filters.rest_framework import DjangoFilterBackend from rest_framework import status from rest_framework.decorators import action @@ -10,6 +10,8 @@ from rest_framework.viewsets import ReadOnlyModelViewSet from course_discovery.apps.api.pagination import ProxiedPagination +from course_discovery.apps.api.utils import get_forbidden_runs +from course_discovery.apps.course_metadata.models import CourseRun from course_discovery.apps.learner_pathway import models from course_discovery.apps.learner_pathway.api import serializers from course_discovery.apps.learner_pathway.api.filters import PathwayUUIDFilter @@ -31,9 +33,21 @@ class LearnerPathwayViewSet(ReadOnlyModelViewSet): # versions of this API should only support the system default, PageNumberPagination. pagination_class = ProxiedPagination + def get_queryset(self): + forbidden_runs = get_forbidden_runs(self.request) + + return self.queryset.prefetch_related( + Prefetch('steps__learnerpathwaycourse_set__course__course_runs', queryset=CourseRun.objects.exclude( + restricted_run__restriction_type__in=forbidden_runs + )), + Prefetch('steps__learnerpathwayprogram_set__program__courses__course_runs', queryset=CourseRun.objects.exclude( + restricted_run__restriction_type__in=forbidden_runs + )) + ) + @action(detail=True) def snapshot(self, request, uuid): - pathway = get_object_or_404(self.queryset, uuid=uuid, status=PathwayStatus.Active.value) + pathway = get_object_or_404(self.get_queryset(), uuid=uuid, status=PathwayStatus.Active.value) serializer = serializers.LearnerPathwaySerializer(pathway, many=False) return Response(data=serializer.data, status=status.HTTP_200_OK)