Skip to content

Commit c49675f

Browse files
feat: introduce new marketable field (#4493)
* feat: introduce new marketable field * test: handle all cases in is_marketable_external test * refactor: code cleanup * fix: use correct review state * chore: revert numQueries and try prefetch
1 parent c66c37b commit c49675f

File tree

4 files changed

+104
-12
lines changed

4 files changed

+104
-12
lines changed

course_discovery/apps/api/serializers.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -961,7 +961,7 @@ def prefetch_queryset(cls, queryset=None):
961961
# queryset passed in happens to be empty.
962962
queryset = queryset if queryset is not None else CourseRun.objects.all()
963963

964-
return queryset.select_related('course', 'type').prefetch_related(
964+
return queryset.select_related('course', 'type', 'course__type').prefetch_related(
965965
'_official_version',
966966
'course__partner',
967967
'restricted_run',
@@ -973,7 +973,7 @@ class Meta:
973973
fields = ('key', 'uuid', 'title', 'external_key', 'fixed_price_usd', 'image', 'short_description',
974974
'marketing_url', 'seats', 'start', 'end', 'go_live_date', 'enrollment_start', 'enrollment_end',
975975
'weeks_to_complete', 'pacing_type', 'type', 'restriction_type', 'run_type', 'status', 'is_enrollable',
976-
'is_marketable', 'term', 'availability', 'variant_id')
976+
'is_marketable', 'is_marketable_external', 'term', 'availability', 'variant_id')
977977

978978
def get_marketing_url(self, obj):
979979
include_archived = self.context.get('include_archived')
@@ -1080,6 +1080,7 @@ def prefetch_queryset(cls, queryset=None):
10801080

10811081
return queryset.select_related(
10821082
'course__level_type',
1083+
'course__type',
10831084
'course__video__image',
10841085
'course__additional_metadata',
10851086
'language',
@@ -1167,7 +1168,7 @@ class CourseRunWithProgramsSerializer(CourseRunSerializer):
11671168
def prefetch_queryset(cls, queryset=None):
11681169
queryset = super().prefetch_queryset(queryset=queryset)
11691170

1170-
return queryset.select_related('course').prefetch_related(
1171+
return queryset.select_related('course', 'course__type').prefetch_related(
11711172
Prefetch('course__programs', queryset=(
11721173
Program.objects.select_related('type', 'partner').prefetch_related('excluded_course_runs', 'courses')
11731174
))

course_discovery/apps/api/tests/test_serializers.py

Lines changed: 78 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
from course_discovery.apps.core.tests.mixins import ElasticsearchTestMixin, LMSAPIClientMixin
4343
from course_discovery.apps.core.utils import serialize_datetime
4444
from course_discovery.apps.course_metadata.choices import CourseRunStatus, ProgramStatus
45-
from course_discovery.apps.course_metadata.models import AbstractLocationRestrictionModel, CourseReview
45+
from course_discovery.apps.course_metadata.models import AbstractLocationRestrictionModel, CourseReview, CourseType
4646
from course_discovery.apps.course_metadata.search_indexes.documents import (
4747
CourseDocument, CourseRunDocument, LearnerPathwayDocument, PersonDocument, ProgramDocument
4848
)
@@ -55,14 +55,14 @@
5555
from course_discovery.apps.course_metadata.tests.factories import (
5656
AdditionalMetadataFactory, AdditionalPromoAreaFactory, CertificateInfoFactory, CollaboratorFactory,
5757
CorporateEndorsementFactory, CourseEditorFactory, CourseEntitlementFactory, CourseFactory,
58-
CourseLocationRestrictionFactory, CourseRunFactory, CourseSkillsFactory, CurriculumCourseMembershipFactory,
59-
CurriculumFactory, CurriculumProgramMembershipFactory, DegreeAdditionalMetadataFactory, DegreeCostFactory,
60-
DegreeDeadlineFactory, DegreeFactory, EndorsementFactory, ExpectedLearningItemFactory, FactFactory,
61-
IconTextPairingFactory, ImageFactory, JobOutlookItemFactory, OrganizationFactory, PathwayFactory,
62-
PersonAreaOfExpertiseFactory, PersonFactory, PersonSocialNetworkFactory, PositionFactory, PrerequisiteFactory,
63-
ProgramFactory, ProgramLocationRestrictionFactory, ProgramSkillFactory, ProgramSubscriptionFactory,
64-
ProgramSubscriptionPriceFactory, ProgramTypeFactory, RankingFactory, SeatFactory, SeatTypeFactory,
65-
SpecializationFactory, SubjectFactory, TopicFactory, VideoFactory
58+
CourseLocationRestrictionFactory, CourseRunFactory, CourseSkillsFactory, CourseTypeFactory,
59+
CurriculumCourseMembershipFactory, CurriculumFactory, CurriculumProgramMembershipFactory,
60+
DegreeAdditionalMetadataFactory, DegreeCostFactory, DegreeDeadlineFactory, DegreeFactory, EndorsementFactory,
61+
ExpectedLearningItemFactory, FactFactory, IconTextPairingFactory, ImageFactory, JobOutlookItemFactory,
62+
OrganizationFactory, PathwayFactory, PersonAreaOfExpertiseFactory, PersonFactory, PersonSocialNetworkFactory,
63+
PositionFactory, PrerequisiteFactory, ProgramFactory, ProgramLocationRestrictionFactory, ProgramSkillFactory,
64+
ProgramSubscriptionFactory, ProgramSubscriptionPriceFactory, ProgramTypeFactory, RankingFactory, SeatFactory,
65+
SeatTypeFactory, SpecializationFactory, SubjectFactory, TopicFactory, VideoFactory
6666
)
6767
from course_discovery.apps.course_metadata.utils import get_course_run_estimated_hours
6868
from course_discovery.apps.ietf_language_tags.models import LanguageTag
@@ -636,6 +636,7 @@ def get_expected_data(cls, course_run, request):
636636
'external_key': course_run.external_key,
637637
'is_enrollable': course_run.is_enrollable,
638638
'is_marketable': course_run.is_marketable,
639+
'is_marketable_external': course_run.is_marketable_external,
639640
'availability': course_run.availability,
640641
'variant_id': str(course_run.variant_id),
641642
'fixed_price_usd': str(course_run.fixed_price_usd),
@@ -647,6 +648,7 @@ def get_expected_data(cls, course_run, request):
647648
}
648649

649650

651+
@ddt.ddt
650652
class MinimalCourseRunSerializerTests(MinimalCourseRunBaseTestSerializer):
651653

652654
def test_data(self):
@@ -656,6 +658,73 @@ def test_data(self):
656658
expected = self.get_expected_data(course_run, request)
657659
assert serializer.data == expected
658660

661+
@ddt.data(
662+
{
663+
'course_type': CourseType.EXECUTIVE_EDUCATION_2U,
664+
'status': CourseRunStatus.Reviewed,
665+
'is_marketable': False,
666+
'has_future_start_date': True,
667+
'expected_is_marketable_external': True
668+
},
669+
{
670+
'course_type': CourseType.EXECUTIVE_EDUCATION_2U,
671+
'status': CourseRunStatus.InternalReview,
672+
'is_marketable': False,
673+
'has_future_start_date': True,
674+
'expected_is_marketable_external': False
675+
},
676+
{
677+
'course_type': CourseType.EXECUTIVE_EDUCATION_2U,
678+
'status': CourseRunStatus.LegalReview,
679+
'is_marketable': False,
680+
'has_future_start_date': True,
681+
'expected_is_marketable_external': False
682+
},
683+
{
684+
'course_type': CourseType.EXECUTIVE_EDUCATION_2U,
685+
'status': CourseRunStatus.Unpublished,
686+
'is_marketable': False,
687+
'has_future_start_date': True,
688+
'expected_is_marketable_external': False
689+
},
690+
{
691+
'course_type': CourseType.PROFESSIONAL,
692+
'status': CourseRunStatus.InternalReview,
693+
'is_marketable': False,
694+
'has_future_start_date': True,
695+
'expected_is_marketable_external': False
696+
},
697+
{
698+
'course_type': CourseType.VERIFIED_AUDIT,
699+
'status': CourseRunStatus.Published,
700+
'is_marketable': True,
701+
'has_future_start_date': False,
702+
'expected_is_marketable_external': True
703+
},
704+
)
705+
@ddt.unpack
706+
def test_is_marketable_external(
707+
self, course_type, status, is_marketable,
708+
has_future_start_date, expected_is_marketable_external
709+
):
710+
course_type_instance = CourseTypeFactory(slug=course_type)
711+
current_time = datetime.datetime.now(tz=UTC)
712+
start_date = current_time + datetime.timedelta(
713+
days=10) if has_future_start_date else current_time - datetime.timedelta(days=10)
714+
go_live_date = current_time + datetime.timedelta(days=5) # Assuming go_live_date is 5 days in the future
715+
716+
course_run = CourseRunFactory(
717+
course=CourseFactory(type=course_type_instance),
718+
status=status,
719+
start=start_date,
720+
go_live_date=go_live_date
721+
)
722+
seat = SeatFactory(course_run=course_run, type=SeatTypeFactory.verified())
723+
course_run.seats.set([seat])
724+
725+
assert course_run.is_marketable == is_marketable
726+
assert course_run.is_marketable_external == expected_is_marketable_external
727+
659728
def test_get_lms_course_url(self):
660729
partner = PartnerFactory()
661730
course_key = 'course-v1:testX+test1.23+2018T1'

course_discovery/apps/course_metadata/models.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2969,6 +2969,27 @@ def is_marketable(self):
29692969
is_published = self.status == CourseRunStatus.Published
29702970
return is_published and self.seats.exists() and bool(self.marketing_url)
29712971

2972+
@property
2973+
def is_marketable_external(self):
2974+
"""
2975+
Determines if the course_run is suitable for external marketing.
2976+
2977+
If already marketable, simply return self.is_marketable.
2978+
2979+
Else, a course run is deemed suitable for external marketing if it is an
2980+
executive education (EE) course, the discovery service status is
2981+
'Reviewed', and the course go_live_date & start_date is in the future.
2982+
"""
2983+
if self.is_marketable:
2984+
return self.is_marketable
2985+
is_exec_ed_course = self.course.type.slug == CourseType.EXECUTIVE_EDUCATION_2U
2986+
if is_exec_ed_course:
2987+
current_time = datetime.datetime.now(pytz.UTC)
2988+
is_reviewed = self.status == CourseRunStatus.Reviewed
2989+
has_future_go_live_date = self.go_live_date and self.go_live_date > current_time
2990+
return is_reviewed and self.is_upcoming() and has_future_go_live_date
2991+
return False
2992+
29722993
@property
29732994
def is_active(self):
29742995
"""

course_discovery/apps/course_metadata/search_indexes/documents/course_run.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ def get_queryset(self, excluded_restriction_types=None): # pylint: disable=unus
148148
return filter_visible_runs(
149149
super().get_queryset()
150150
.select_related('course')
151+
.select_related('course__type')
151152
.prefetch_related('seats__type')
152153
.prefetch_related('transcript_languages')
153154
)

0 commit comments

Comments
 (0)