diff --git a/course_discovery/apps/api/serializers.py b/course_discovery/apps/api/serializers.py index 04635cfa999..69ec718c3c3 100644 --- a/course_discovery/apps/api/serializers.py +++ b/course_discovery/apps/api/serializers.py @@ -2308,7 +2308,9 @@ 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, course_runs=course_runs)), + Prefetch('programs', queryset=MinimalProgramSerializer.prefetch_queryset( + partner=partner, course_runs=course_runs + )), ) class Meta: diff --git a/course_discovery/apps/api/tests/test_serializers.py b/course_discovery/apps/api/tests/test_serializers.py index 9c8581ee46a..ec023e9adf5 100644 --- a/course_discovery/apps/api/tests/test_serializers.py +++ b/course_discovery/apps/api/tests/test_serializers.py @@ -2347,9 +2347,9 @@ def test_detail_fields_in_response(self, is_post_request): 'staff': MinimalPersonSerializer(course_run.staff, many=True, context={'request': request}).data, 'content_language': course_run.language.code if course_run.language else None, - 'restriction_type': course_run.restricted_run.restriction_type if hasattr(course_run, 'restricted_run') else None - - + 'restriction_type': ( + course_run.restricted_run.restriction_type if hasattr(course_run, 'restricted_run') else None + ) }], 'uuid': str(course.uuid), 'subjects': [subject.name for subject in course.subjects.all()], @@ -2420,7 +2420,9 @@ def get_expected_data(cls, course, course_run, course_skill, seat): 'estimated_hours': get_course_run_estimated_hours(course_run), 'first_enrollable_paid_seat_price': course_run.first_enrollable_paid_seat_price or 0.0, 'is_enrollable': course_run.is_enrollable, - 'restriction_type': course_run.restricted_run.restriction_type if hasattr(course_run, 'restricted_run') else None + 'restriction_type': ( + course_run.restricted_run.restriction_type if hasattr(course_run, 'restricted_run') else None + ) }], 'uuid': str(course.uuid), 'subjects': [subject.name for subject in course.subjects.all()], @@ -2552,8 +2554,9 @@ def get_expected_data(cls, course_run, course_skill, request): 'first_enrollable_paid_seat_sku': course_run.first_enrollable_paid_seat_sku(), 'first_enrollable_paid_seat_price': course_run.first_enrollable_paid_seat_price, 'is_enrollable': course_run.is_enrollable, - 'restriction_type': course_run.restricted_run.restriction_type if hasattr(course_run, 'restricted_run') else None, - + 'restriction_type': ( + course_run.restricted_run.restriction_type if hasattr(course_run, 'restricted_run') else None + ) } diff --git a/course_discovery/apps/api/utils.py b/course_discovery/apps/api/utils.py index ebac57f3b1d..1818ef62d9a 100644 --- a/course_discovery/apps/api/utils.py +++ b/course_discovery/apps/api/utils.py @@ -13,8 +13,8 @@ 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 +from course_discovery.apps.course_metadata.models import CourseRun logger = logging.getLogger(__name__) @@ -199,10 +199,12 @@ def increment_character(character): """ return chr(ord(character) + 1) if character != 'z' else 'a' + def get_excluded_restriction_types(request): - include_restricted=request.query_params.get('include_restricted', '').split(',') + include_restricted = request.query_params.get('include_restricted', '').split(',') return list(set(CourseRunRestrictionType.values) - set(include_restricted)) + class StudioAPI: """ A convenience class for talking to the Studio API - designed to allow subclassing by the publisher django app, diff --git a/course_discovery/apps/api/v1/tests/test_views/test_catalogs.py b/course_discovery/apps/api/v1/tests/test_views/test_catalogs.py index 87a9d00c9fe..ab071ba81a8 100644 --- a/course_discovery/apps/api/v1/tests/test_views/test_catalogs.py +++ b/course_discovery/apps/api/v1/tests/test_views/test_catalogs.py @@ -21,7 +21,7 @@ from course_discovery.apps.course_metadata.choices import CourseRunStatus from course_discovery.apps.course_metadata.models import Course, CourseType from course_discovery.apps.course_metadata.tests.factories import ( - CourseRunFactory, SeatFactory, SeatTypeFactory, SubjectFactory, RestrictedCourseRunFactory + CourseRunFactory, RestrictedCourseRunFactory, SeatFactory, SeatTypeFactory, SubjectFactory ) from course_discovery.conftest import get_course_run_states @@ -346,7 +346,7 @@ def test_courses_with_restricted_runs(self, include_restriction_param): course__title='ABC Test Course With Archived', end=future, enrollment_end=future ) restricted_course_run = CourseRunFactory.create( - course = course_run.course, + course=course_run.course, course__title='ABC Test Course With Archived', end=future, enrollment_end=future, status=CourseRunStatus.Published ) diff --git a/course_discovery/apps/api/v1/tests/test_views/test_course_runs.py b/course_discovery/apps/api/v1/tests/test_views/test_course_runs.py index a7e5fae2259..b0c8bd4a7eb 100644 --- a/course_discovery/apps/api/v1/tests/test_views/test_course_runs.py +++ b/course_discovery/apps/api/v1/tests/test_views/test_course_runs.py @@ -1211,13 +1211,10 @@ def test_list_sorted_by_course_start_date(self): self.serialize_course_run(CourseRun.objects.all().order_by('start'), many=True) ) - @ddt.data(True, False) def test_list_include_restricted(self, include_restriction_param): - restricted_run = CourseRunFactory(course__partner=self.partner) RestrictedCourseRunFactory(course_run=restricted_run, restriction_type='custom-b2c') - url = reverse('api:v1:course_run-list') if include_restriction_param: url += '?include_restricted=custom-b2c' @@ -1230,11 +1227,11 @@ def test_list_include_restricted(self, include_restriction_param): if include_restriction_param: assert restricted_run.key in restrieved_keys else: - assert not restricted_run.key in restrieved_keys + assert restricted_run.key not in restrieved_keys @ddt.data(True, False) def test_list_query_include_restricted(self, include_restriction_param): - course_runs = CourseRunFactory.create_batch(3, title='Some cool title', course__partner=self.partner) + CourseRunFactory.create_batch(3, title='Some cool title', course__partner=self.partner) CourseRunFactory(title='non-cool title') restricted_run = CourseRunFactory(title='Some cool title', course__partner=self.partner) RestrictedCourseRunFactory(course_run=restricted_run, restriction_type='custom-b2c') diff --git a/course_discovery/apps/api/v1/tests/test_views/test_courses.py b/course_discovery/apps/api/v1/tests/test_views/test_courses.py index fa4257764c9..33787abac2f 100644 --- a/course_discovery/apps/api/v1/tests/test_views/test_courses.py +++ b/course_discovery/apps/api/v1/tests/test_views/test_courses.py @@ -35,7 +35,7 @@ from course_discovery.apps.course_metadata.tests.factories import ( CourseEditorFactory, CourseEntitlementFactory, CourseFactory, CourseLocationRestrictionFactory, CourseRunFactory, CourseTypeFactory, GeoLocationFactory, LevelTypeFactory, OrganizationFactory, ProductValueFactory, ProgramFactory, - SeatFactory, SeatTypeFactory, SourceFactory, SubjectFactory, RestrictedCourseRunFactory + RestrictedCourseRunFactory, SeatFactory, SeatTypeFactory, SourceFactory, SubjectFactory ) from course_discovery.apps.course_metadata.toggles import IS_SUBDIRECTORY_SLUG_FORMAT_ENABLED from course_discovery.apps.course_metadata.utils import data_modified_timestamp_update, ensure_draft_world @@ -280,8 +280,16 @@ def test_course_runs_are_ordered(self): @ddt.data(True, False) def test_course_runs_restriction(self, include_restriction_param): - run_restricted = CourseRunFactory(course=self.course, start=datetime.datetime(2033, 1, 1, tzinfo=pytz.UTC), status=CourseRunStatus.Published) - run_not_restricted = CourseRunFactory(course=self.course, start=datetime.datetime(2033, 1, 1, tzinfo=pytz.UTC), status=CourseRunStatus.Unpublished) + run_restricted = CourseRunFactory( + course=self.course, + start=datetime.datetime(2033, 1, 1, tzinfo=pytz.UTC), + status=CourseRunStatus.Published + ) + run_not_restricted = CourseRunFactory( + course=self.course, + start=datetime.datetime(2033, 1, 1, tzinfo=pytz.UTC), + status=CourseRunStatus.Unpublished + ) RestrictedCourseRunFactory(course_run=run_restricted, restriction_type='custom-b2c') SeatFactory(course_run=run_restricted) SeatFactory(course_run=run_not_restricted) @@ -300,14 +308,24 @@ def test_course_runs_restriction(self, include_restriction_param): self.assertEqual(response.data['advertised_course_run_uuid'], None) else: self.assertEqual(set(response.data['course_run_keys']), {run_not_restricted.key, run_restricted.key}) - self.assertEqual(set(response.data['course_run_statuses']), {run_not_restricted.status, run_restricted.status}) + self.assertEqual( + set(response.data['course_run_statuses']), + {run_not_restricted.status, run_restricted.status} + ) self.assertEqual(len(response.data['course_runs']), 2) self.assertEqual(response.data['advertised_course_run_uuid'], run_restricted.uuid) - def test_course_runs_restriction_param(self): - run_restricted = CourseRunFactory(course=self.course, start=datetime.datetime(2033, 1, 1, tzinfo=pytz.UTC), status=CourseRunStatus.Published) - run_not_restricted = CourseRunFactory(course=self.course, start=datetime.datetime(2033, 1, 1, tzinfo=pytz.UTC), status=CourseRunStatus.Unpublished) + run_restricted = CourseRunFactory( + course=self.course, + start=datetime.datetime(2033, 1, 1, tzinfo=pytz.UTC), + status=CourseRunStatus.Published + ) + run_not_restricted = CourseRunFactory( + course=self.course, + start=datetime.datetime(2033, 1, 1, tzinfo=pytz.UTC), + status=CourseRunStatus.Unpublished + ) RestrictedCourseRunFactory(course_run=run_restricted, restriction_type='custom-b2c') SeatFactory(course_run=run_restricted) diff --git a/course_discovery/apps/api/v1/tests/test_views/test_programs.py b/course_discovery/apps/api/v1/tests/test_views/test_programs.py index 7dbb3089c5a..1d2fc4caf5e 100644 --- a/course_discovery/apps/api/v1/tests/test_views/test_programs.py +++ b/course_discovery/apps/api/v1/tests/test_views/test_programs.py @@ -2,7 +2,6 @@ import urllib.parse from unittest import mock -import ddt import pytest import pytz from django.test import RequestFactory @@ -14,15 +13,16 @@ from course_discovery.apps.api.v1.views.programs import ProgramViewSet from course_discovery.apps.core.tests.factories import USER_PASSWORD, UserFactory from course_discovery.apps.core.tests.helpers import make_image_file -from course_discovery.apps.course_metadata.choices import ProgramStatus, CourseRunStatus +from course_discovery.apps.course_metadata.choices import CourseRunStatus, ProgramStatus from course_discovery.apps.course_metadata.models import CourseType, Program, ProgramType from course_discovery.apps.course_metadata.tests.factories import ( CorporateEndorsementFactory, CourseFactory, CourseRunFactory, CurriculumCourseMembershipFactory, CurriculumFactory, CurriculumProgramMembershipFactory, DegreeAdditionalMetadataFactory, DegreeFactory, EndorsementFactory, ExpectedLearningItemFactory, JobOutlookItemFactory, OrganizationFactory, PersonFactory, ProgramFactory, - ProgramTypeFactory, VideoFactory, RestrictedCourseRunFactory + ProgramTypeFactory, RestrictedCourseRunFactory, VideoFactory ) + @pytest.mark.django_db @pytest.mark.usefixtures('django_cache') class TestProgramViewSet(SerializationMixin): @@ -55,9 +55,9 @@ def create_program(self, courses=None, program_type=None, include_restricted_run if courses is None: courses = [CourseFactory(partner=self.partner)] course_run = CourseRunFactory(course=courses[0], staff=[person]) - + if include_restricted_run: - RestrictedCourseRunFactory(course_run=course_run, restriction_type='custom-b2c') + RestrictedCourseRunFactory(course_run=course_run, restriction_type='custom-b2c') # pylint: disable=possibly-used-before-assignment if program_type is None: program_type = ProgramTypeFactory() @@ -234,7 +234,6 @@ def test_list_restricted_runs(self, include_restriction_param): assert not resp.data['results'][0]['courses'][0]['course_run_statuses'] assert resp.data['results'][0]['course_run_statuses'] == [] - def test_extended_query_param_fields(self): """ Verify that the `extended` query param will result in an extended amount of fields returned. """ for _ in range(3): diff --git a/course_discovery/apps/api/v1/tests/test_views/test_search.py b/course_discovery/apps/api/v1/tests/test_views/test_search.py index 682478201f4..28ee484468e 100644 --- a/course_discovery/apps/api/v1/tests/test_views/test_search.py +++ b/course_discovery/apps/api/v1/tests/test_views/test_search.py @@ -3,7 +3,6 @@ import sys import urllib.parse import uuid -from itertools import product import ddt import factory @@ -24,10 +23,10 @@ CourseRunSearchDocumentSerializer, CourseRunSearchModelSerializer, LimitedAggregateSearchSerializer ) from course_discovery.apps.course_metadata.tests.factories import ( - CourseFactory, CourseRunFactory, OrganizationFactory, PersonFactory, PositionFactory, ProgramFactory, SeatFactory, RestrictedCourseRunFactory, SeatTypeFactory + CourseFactory, CourseRunFactory, OrganizationFactory, PersonFactory, PositionFactory, ProgramFactory, + RestrictedCourseRunFactory, SeatFactory, SeatTypeFactory ) from course_discovery.apps.ietf_language_tags.utils import serialize_language - from course_discovery.apps.learner_pathway.models import LearnerPathway from course_discovery.apps.learner_pathway.tests.factories import LearnerPathwayStepFactory from course_discovery.apps.publisher.tests import factories as publisher_factories @@ -124,10 +123,10 @@ def test_search(self, path, serializer): self.assert_successful_search(path=path, serializer=serializer) @ddt.data( - [list_path, True], - [list_path, False], - [detailed_path, True], - [detailed_path, False] + [list_path, True], + [list_path, False], + [detailed_path, True], + [detailed_path, False] ) @ddt.unpack def test_search_restricted_runs(self, path, include_restriction_param): @@ -151,7 +150,6 @@ def test_search_restricted_runs(self, path, include_restriction_param): assert not response.data['results'] assert response.data['count'] == 0 - def test_faceted_search(self): """ Verify the view returns results and facets. """ course_run, response_data = self.assert_successful_search(path=self.faceted_path) @@ -343,7 +341,6 @@ def process_response(self, response): assert objects['count'] > 0 return objects - @ddt.data(True, False) def test_results_restricted_runs(self, include_restriced_param): CourseFactory( @@ -379,7 +376,11 @@ def test_results_restricted_runs(self, include_restriced_param): endpoint = self.list_path if include_restriced_param: endpoint += '?include_restricted=custom-b2c' - response = self.get_response(query={'key.raw': self.desired_key}, endpoint=endpoint, path_has_params=include_restriced_param) + response = self.get_response( + query={'key.raw': self.desired_key}, + endpoint=endpoint, + path_has_params=include_restriced_param + ) assert response.status_code == 200 response_data = response.json() @@ -389,12 +390,13 @@ def test_results_restricted_runs(self, include_restriced_param): assert len(response_data["results"][0]["course_runs"]) == 1 assert set(response_data["results"][0]["languages"]) == {serialize_language(course_run.language)} assert set(response_data["results"][0]["seat_types"]) == {'audit'} - else: + else: assert len(response_data["results"][0]["course_runs"]) == 2 - assert set(response_data["results"][0]["languages"]) == {serialize_language(course_run.language), serialize_language(course_run_restricted.language)} + assert set(response_data["results"][0]["languages"]) == { + serialize_language(course_run.language), serialize_language(course_run_restricted.language) + } assert set(response_data["results"][0]["seat_types"]) == {'audit', 'verified'} - def test_results_only_include_specific_key_objects(self): """ Verify the search results only include items with 'key' set to 'course:edX+DemoX'. """ diff --git a/course_discovery/apps/api/v1/views/course_runs.py b/course_discovery/apps/api/v1/views/course_runs.py index 49655617735..f9283b5dce9 100644 --- a/course_discovery/apps/api/v1/views/course_runs.py +++ b/course_discovery/apps/api/v1/views/course_runs.py @@ -19,7 +19,9 @@ from course_discovery.apps.api.pagination import ProxiedPagination from course_discovery.apps.api.permissions import IsCourseRunEditorOrDjangoOrReadOnly from course_discovery.apps.api.serializers import MetadataWithRelatedChoices -from course_discovery.apps.api.utils import StudioAPI, get_query_param, reviewable_data_has_changed, get_excluded_restriction_types +from course_discovery.apps.api.utils import ( + StudioAPI, get_excluded_restriction_types, get_query_param, reviewable_data_has_changed +) from course_discovery.apps.api.v1.exceptions import EditableAndQUnsupported from course_discovery.apps.core.utils import SearchQuerySetWrapper from course_discovery.apps.course_metadata.choices import CourseRunStatus @@ -99,7 +101,6 @@ def get_queryset(self): if self.request.method == 'GET': queryset = queryset.exclude(restricted_run__restriction_type__in=excluded_restriction_types) - if q: queryset = SearchQuerySetWrapper( CourseRun.search(q).filter('term', partner=partner.short_code).exclude( diff --git a/course_discovery/apps/api/v1/views/courses.py b/course_discovery/apps/api/v1/views/courses.py index cbaac48482b..931d2dc8863 100644 --- a/course_discovery/apps/api/v1/views/courses.py +++ b/course_discovery/apps/api/v1/views/courses.py @@ -23,7 +23,9 @@ 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, get_excluded_restriction_types +from course_discovery.apps.api.utils import ( + decode_image_data, get_excluded_restriction_types, get_query_param, reviewable_data_has_changed +) 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 @@ -126,7 +128,9 @@ def get_queryset(self): if q: queryset = Course.search(q, queryset=queryset) course_runs = CourseRun.objects.exclude(restricted_run__restriction_type__in=excluded_restriction_types) - queryset = self.get_serializer_class().prefetch_queryset(queryset=queryset, partner=partner, course_runs=course_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) diff --git a/course_discovery/apps/api/v1/views/pathways.py b/course_discovery/apps/api/v1/views/pathways.py index 05d290af8e7..5cbde6d66c9 100644 --- a/course_discovery/apps/api/v1/views/pathways.py +++ b/course_discovery/apps/api/v1/views/pathways.py @@ -2,12 +2,12 @@ from rest_framework import viewsets from course_discovery.apps.api import serializers -from course_discovery.apps.api.utils import get_excluded_restriction_types from course_discovery.apps.api.cache import CompressedCacheResponseMixin from course_discovery.apps.api.permissions import ReadOnlyByPublisherUser - +from course_discovery.apps.api.utils import get_excluded_restriction_types from course_discovery.apps.course_metadata.models import CourseRun + class PathwayViewSet(CompressedCacheResponseMixin, viewsets.ReadOnlyModelViewSet): permission_classes = (ReadOnlyByPublisherUser,) serializer_class = serializers.PathwaySerializer @@ -16,5 +16,8 @@ def get_queryset(self): excluded_restriction_types = get_excluded_restriction_types(self.request) course_runs = CourseRun.objects.exclude(restricted_run__restriction_type__in=excluded_restriction_types) - queryset = self.get_serializer_class().prefetch_queryset(partner=self.request.site.partner, course_runs=course_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 63a590e1a78..4cbd17d0b94 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, get_excluded_restriction_types -from course_discovery.apps.course_metadata.models import Program, CourseRun +from course_discovery.apps.api.utils import get_excluded_restriction_types, get_query_param +from course_discovery.apps.course_metadata.models import CourseRun, Program class ProgramViewSet(CompressedCacheResponseMixin, viewsets.ReadOnlyModelViewSet): @@ -46,10 +46,14 @@ def get_queryset(self): queryset = Program.objects.filter(uuid=program_uuid) elif q: queryset = Program.search(q, queryset=queryset) - + excluded_restriction_types = get_excluded_restriction_types(self.request) course_runs = CourseRun.objects.exclude(restricted_run__restriction_type__in=excluded_restriction_types) - return self.get_serializer_class().prefetch_queryset(queryset=queryset, partner=partner, course_runs=course_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 e8ef8668aa5..fb1e7a0095d 100644 --- a/course_discovery/apps/api/v1/views/search.py +++ b/course_discovery/apps/api/v1/views/search.py @@ -16,7 +16,7 @@ from rest_framework.views import APIView from course_discovery.apps.api import serializers -from course_discovery.apps.api.utils import update_query_params_with_body_data, get_excluded_restriction_types +from course_discovery.apps.api.utils import get_excluded_restriction_types, 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.search_indexes import documents as search_documents diff --git a/course_discovery/apps/course_metadata/algolia_models.py b/course_discovery/apps/course_metadata/algolia_models.py index 34164a4f68a..6945ce87a66 100644 --- a/course_discovery/apps/course_metadata/algolia_models.py +++ b/course_discovery/apps/course_metadata/algolia_models.py @@ -238,6 +238,7 @@ def prefetch_queryset(cls): 'course_runs', queryset=CourseRun.objects.filter(restricted_run__isnull=True) ) ) + @property def product_type(self): if self.type.slug == CourseType.EXECUTIVE_EDUCATION_2U: @@ -485,6 +486,7 @@ def prefetch_queryset(cls): '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/models.py b/course_discovery/apps/course_metadata/models.py index 216da63cf3c..0c4cdc1beb0 100644 --- a/course_discovery/apps/course_metadata/models.py +++ b/course_discovery/apps/course_metadata/models.py @@ -1909,20 +1909,25 @@ def advertised_course_run(self): def has_marketable_run(self): return any(run.is_marketable for run in self.course_runs.all()) - def recommendations(self, excluded_restriction_types=[]): + def recommendations(self, excluded_restriction_types=None): """ Recommended set of courses for upsell after finishing a course. Returns de-duped list of Courses that: A) belong in the same program as given Course B) share the same subject AND same organization (or at least one) in priority of A over B """ + if excluded_restriction_types is None: + excluded_restriction_types = [] + program_courses = list( Course.objects.filter( programs__in=self.programs.all() ) .select_related('partner', 'type') .prefetch_related( - Prefetch('course_runs', queryset=CourseRun.objects.exclude(restricted_run__restriction_type__in=excluded_restriction_types).select_related('type').prefetch_related('seats')), + Prefetch('course_runs', queryset=CourseRun.objects.exclude( + restricted_run__restriction_type__in=excluded_restriction_types + ).select_related('type').prefetch_related('seats')), 'authoring_organizations', '_official_version' ) @@ -1937,7 +1942,9 @@ def recommendations(self, excluded_restriction_types=[]): ) .select_related('partner', 'type') .prefetch_related( - Prefetch('course_runs', queryset=CourseRun.objects.exclude(restricted_run__restriction_type__in=excluded_restriction_types).select_related('type').prefetch_related('seats')), + Prefetch('course_runs', queryset=CourseRun.objects.exclude( + restricted_run__restriction_type__in=excluded_restriction_types + ).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 2a661afa91d..12d6ad2e6ee 100644 --- a/course_discovery/apps/course_metadata/search_indexes/documents/course.py +++ b/course_discovery/apps/course_metadata/search_indexes/documents/course.py @@ -123,10 +123,15 @@ def prepare_partner(self, obj): def prepare_prerequisites(self, obj): return [prerequisite.name for prerequisite in obj.prerequisites.all()] - def get_queryset(self, excluded_restriction_types=[]): + def get_queryset(self, excluded_restriction_types=None): + if excluded_restriction_types is None: + excluded_restriction_types = [] + return super().get_queryset().prefetch_related( - Prefetch('course_runs', queryset=CourseRun.objects.exclude(restricted_run__restriction_type__in=excluded_restriction_types).prefetch_related( - 'seats__type', 'type', 'language', 'restricted_run', + Prefetch('course_runs', queryset=CourseRun.objects.exclude( + restricted_run__restriction_type__in=excluded_restriction_types + ).prefetch_related( + 'seats__type', 'type', 'language', 'restricted_run', )) ).select_related('partner') 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 a057b31f279..657ac6d735e 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 @@ -143,7 +143,7 @@ def prepare_transcript_languages(self, obj): for language in obj.transcript_languages.all() ] - def get_queryset(self, excluded_restriction_types=[]): + def get_queryset(self, excluded_restriction_types=None): # pylint: disable=unused-argument 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 013bd68f380..f2cdad7b0d7 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,12 +1,9 @@ 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 @@ -53,7 +50,7 @@ def prepare_partner(self, obj): def prepare_published(self, obj): return obj.status == PathwayStatus.Active - def get_queryset(self, excluded_restriction_types=[]): + def get_queryset(self, excluded_restriction_types=None): # pylint: disable=unused-argument return super().get_queryset().prefetch_related( 'steps', 'steps__learnerpathwaycourse_set', 'steps__learnerpathwayprogram_set', 'steps__learnerpathwayblock_set', 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 d8774cbfce4..77bc5f11405 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, excluded_restriction_types=[]): + def get_queryset(self, excluded_restriction_types=None): # pylint: disable=unused-argument 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 fe9982ebe46..4c1e29a0f38 100644 --- a/course_discovery/apps/course_metadata/search_indexes/documents/program.py +++ b/course_discovery/apps/course_metadata/search_indexes/documents/program.py @@ -122,7 +122,10 @@ def prepare_staff_uuids(self, obj): def prepare_type(self, obj): return obj.type.name_t - def get_queryset(self, excluded_restriction_types=[]): + def get_queryset(self, excluded_restriction_types=None): + if excluded_restriction_types is None: + excluded_restriction_types = [] + 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( 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 1c929816875..a61640006bd 100644 --- a/course_discovery/apps/course_metadata/search_indexes/serializers/common.py +++ b/course_discovery/apps/course_metadata/search_indexes/serializers/common.py @@ -4,8 +4,9 @@ from django.utils.dateparse import parse_datetime from django_elasticsearch_dsl.registries import registry -from course_discovery.apps.core.utils import ElasticsearchUtils, serialize_datetime from course_discovery.apps.api.utils import get_excluded_restriction_types +from course_discovery.apps.core.utils import ElasticsearchUtils, serialize_datetime + log = logging.getLogger(__name__) @@ -51,7 +52,9 @@ def get_model_object_by_instances(self, instances): if document and es_pks: try: - _objects = document(hit).get_queryset(excluded_restriction_types=excluded_restriction_types).filter(pk__in=es_pks) + _objects = document(hit).get_queryset( + excluded_restriction_types=excluded_restriction_types + ).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/tests/test_algolia_models.py b/course_discovery/apps/course_metadata/tests/test_algolia_models.py index bfb63c66342..25c7b629b27 100644 --- a/course_discovery/apps/course_metadata/tests/test_algolia_models.py +++ b/course_discovery/apps/course_metadata/tests/test_algolia_models.py @@ -11,16 +11,16 @@ from conftest import TEST_DOMAIN from course_discovery.apps.core.models import Currency, Partner from course_discovery.apps.core.tests.factories import PartnerFactory, SiteFactory -from course_discovery.apps.course_metadata.algolia_models import AlgoliaProxyCourse, AlgoliaProxyProgram, AlgoliaProxyProduct -from course_discovery.apps.course_metadata.index import EnglishProductIndex - +from course_discovery.apps.course_metadata.algolia_models import ( + AlgoliaProxyCourse, AlgoliaProxyProgram +) from course_discovery.apps.course_metadata.choices import ExternalProductStatus, ProgramStatus from course_discovery.apps.course_metadata.models import CourseRunStatus, CourseType, ProductValue, ProgramType from course_discovery.apps.course_metadata.tests.factories import ( AdditionalMetadataFactory, CourseFactory, CourseRunFactory, CourseTypeFactory, DegreeAdditionalMetadataFactory, DegreeFactory, GeoLocationFactory, LevelTypeFactory, OrganizationFactory, ProductMetaFactory, ProgramFactory, - ProgramSubscriptionFactory, ProgramSubscriptionPriceFactory, ProgramTypeFactory, RestrictedCourseRunFactory, SeatFactory, SeatTypeFactory, - SourceFactory, SubjectFactory, VideoFactory + ProgramSubscriptionFactory, ProgramSubscriptionPriceFactory, ProgramTypeFactory, RestrictedCourseRunFactory, + SeatFactory, SeatTypeFactory, SourceFactory, SubjectFactory, VideoFactory ) from course_discovery.apps.ietf_language_tags.models import LanguageTag diff --git a/course_discovery/apps/course_metadata/utils.py b/course_discovery/apps/course_metadata/utils.py index 18ab00a350d..4c3549f961d 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, CourseRunRestrictionType +from course_discovery.apps.course_metadata.choices import CourseRunStatus 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/serializers.py b/course_discovery/apps/learner_pathway/api/serializers.py index 23d0410c3e7..a33724b831c 100644 --- a/course_discovery/apps/learner_pathway/api/serializers.py +++ b/course_discovery/apps/learner_pathway/api/serializers.py @@ -3,9 +3,10 @@ """ from rest_framework import serializers +from course_discovery.apps.api.utils import get_excluded_restriction_types from course_discovery.apps.course_metadata.choices import CourseRunStatus from course_discovery.apps.learner_pathway import models -from course_discovery.apps.api.utils import get_excluded_restriction_types + class LearnerPathwayCourseMinimalSerializer(serializers.ModelSerializer): """ @@ -20,7 +21,11 @@ class Meta: def get_course_runs(self, obj): excluded_restriction_types = get_excluded_restriction_types(self.context['request']) - return list(obj.course.course_runs.filter(status=CourseRunStatus.Published).exclude(restricted_run__restriction_type__in=excluded_restriction_types).values('key')) + return list(obj.course.course_runs.filter( + status=CourseRunStatus.Published + ).exclude( + restricted_run__restriction_type__in=excluded_restriction_types + ).values('key')) class LearnerPathwayCourseSerializer(LearnerPathwayCourseMinimalSerializer): diff --git a/course_discovery/apps/learner_pathway/api/v1/tests/test_views.py b/course_discovery/apps/learner_pathway/api/v1/tests/test_views.py index 9f3ab44a7b8..d798872cf8f 100644 --- a/course_discovery/apps/learner_pathway/api/v1/tests/test_views.py +++ b/course_discovery/apps/learner_pathway/api/v1/tests/test_views.py @@ -2,11 +2,9 @@ import ddt from django.test import Client, TestCase -from django.urls import reverse from pytest import mark from rest_framework import status -from course_discovery.apps import learner_pathway from course_discovery.apps.core.tests.factories import UserFactory from course_discovery.apps.course_metadata.tests.factories import CourseRunFactory, RestrictedCourseRunFactory from course_discovery.apps.learner_pathway.choices import PathwayStatus @@ -178,10 +176,10 @@ def test_learner_pathway_restricted_runs(self, add_restriction_param): status='published', ) RestrictedCourseRunFactory(course_run=restricted_run, restriction_type='custom-b2c') - url = f'/api/v1/learner-pathway/' + url = '/api/v1/learner-pathway/' if add_restriction_param: url += '?include_restricted=custom-b2c' - + api_response = self.client.get(url) data = api_response.json() assert len(data['results'][0]['steps'][0]['courses'][0]['course_runs']) == 2 if add_restriction_param else 1 diff --git a/course_discovery/apps/learner_pathway/api/v1/views.py b/course_discovery/apps/learner_pathway/api/v1/views.py index d849a4853d6..5e8031fb6a2 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, Prefetch +from django.db.models import Q from django_filters.rest_framework import DjangoFilterBackend from rest_framework import status from rest_framework.decorators import action @@ -10,8 +10,6 @@ from rest_framework.viewsets import ReadOnlyModelViewSet from course_discovery.apps.api.pagination import ProxiedPagination -from course_discovery.apps.api.utils import get_excluded_restriction_types -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 diff --git a/course_discovery/apps/learner_pathway/models.py b/course_discovery/apps/learner_pathway/models.py index 68873d5a763..7278f612a3f 100644 --- a/course_discovery/apps/learner_pathway/models.py +++ b/course_discovery/apps/learner_pathway/models.py @@ -299,13 +299,22 @@ def get_skills(self) -> [str]: return program_skills - def get_linked_courses_and_course_runs(self, excluded_restriction_types=[]) -> [dict]: + def get_linked_courses_and_course_runs(self, excluded_restriction_types=None) -> [dict]: """ Returns list of dict where each dict contains a course key linked with program and all its course runs """ + if excluded_restriction_types is None: + excluded_restriction_types = [] + courses = [] for course in self.program.courses.all(): - course_runs = list(course.course_runs.filter(status=CourseRunStatus.Published).exclude(restricted_run__restriction_type__in=excluded_restriction_types).values('key')) + course_runs = list( + course.course_runs.filter( + status=CourseRunStatus.Published + ).exclude( + restricted_run__restriction_type__in=excluded_restriction_types + ).values('key') + ) courses.append({"key": course.key, "course_runs": course_runs}) return courses