Skip to content

Commit a26b1d0

Browse files
committed
feat: Adding weeks to complete filed into Algolia facet
1 parent 24d3896 commit a26b1d0

5 files changed

Lines changed: 105 additions & 18 deletions

File tree

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM ubuntu:focal as app
1+
FROM ubuntu:jammy as app
22

33
ARG PYTHON_VERSION=3.12
44

course_discovery/apps/course_metadata/algolia_models.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ def delegate_attributes(cls):
8383
'secondary_description', 'tertiary_description']
8484
facet_fields = ['availability_level', 'subject_names', 'levels', 'active_languages', 'staff_slugs',
8585
'product_allowed_in', 'product_blocked_in', 'learning_type', 'learning_type_exp',
86-
'product_ai_languages']
86+
'product_ai_languages', 'product_weeks_to_complete']
8787
ranking_fields = ['availability_rank', 'product_recent_enrollment_count', 'promoted_in_spanish_index',
8888
'product_value_per_click_usa', 'product_value_per_click_international',
8989
'product_value_per_lead_usa', 'product_value_per_lead_international']
@@ -344,7 +344,19 @@ def product_card_image_url(self):
344344

345345
@property
346346
def product_weeks_to_complete(self):
347-
return getattr(self.advertised_course_run, 'weeks_to_complete', None)
347+
"""
348+
Returns the number of weeks to complete from the advertised course run.
349+
Returns None if not available or invalid.
350+
"""
351+
advertised_run = getattr(self, "advertised_course_run", None)
352+
if not advertised_run:
353+
return None
354+
weeks = getattr(advertised_run, "weeks_to_complete", None)
355+
356+
# Treat None, 0, and negative values as invalid
357+
if not weeks or weeks <= 0:
358+
return None
359+
return weeks
348360

349361
@property
350362
def product_min_effort(self):
@@ -469,7 +481,8 @@ def availability_rank(self):
469481
if datetime.datetime.now(pytz.UTC) >= self.advertised_course_run.start:
470482
return 3
471483
return self.advertised_course_run.start.timestamp()
472-
return None # Algolia will deprioritize entries where a ranked field is empty
484+
if not self.advertised_course_run:
485+
return None # Algolia will deprioritize entries where a ranked field is empty
473486

474487
@property
475488
def subscription_eligible(self):

course_discovery/apps/course_metadata/index.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ class EnglishProductIndex(BaseProductIndex):
8080
('active_languages', 'language'), ('product_type', 'product'), ('program_types', 'program_type'),
8181
('staff_slugs', 'staff'), ('product_allowed_in', 'allowed_in'),
8282
('product_blocked_in', 'blocked_in'), 'subscription_eligible',
83-
'subscription_prices', 'learning_type', 'learning_type_exp',
83+
'subscription_prices', 'learning_type', 'learning_type_exp', 'weeks_to_complete',
8484
('product_ai_languages', 'ai_languages'))
8585
ranking_fields = ('availability_rank', ('product_recent_enrollment_count', 'recent_enrollment_count'),
8686
('product_value_per_click_usa', 'value_per_click_usa'),
@@ -117,7 +117,7 @@ class EnglishProductIndex(BaseProductIndex):
117117
'partner', 'availability', 'subject', 'level', 'language', 'product', 'program_type',
118118
'filterOnly(staff)', 'filterOnly(allowed_in)', 'filterOnly(blocked_in)', 'skills.skill',
119119
'skills.category', 'skills.subcategory', 'tags', 'subscription_eligible', 'subscription_prices',
120-
'learning_type', 'learning_type_exp', 'ai_languages.translation_languages',
120+
'learning_type', 'learning_type_exp', 'ai_languages.translation_languages', 'weeks_to_complete',
121121
'ai_languages.transcription_languages',
122122
],
123123
'customRanking': ['asc(availability_rank)', 'desc(recent_enrollment_count)']
@@ -135,7 +135,7 @@ class SpanishProductIndex(BaseProductIndex):
135135
('active_languages', 'language'), ('product_type', 'product'), ('program_types', 'program_type'),
136136
('staff_slugs', 'staff'), ('product_allowed_in', 'allowed_in'),
137137
('product_blocked_in', 'blocked_in'), 'subscription_eligible',
138-
'subscription_prices', 'learning_type', 'learning_type_exp',
138+
'subscription_prices', 'learning_type', 'learning_type_exp', 'weeks_to_complete',
139139
('product_ai_languages', 'ai_languages'))
140140
ranking_fields = ('availability_rank', ('product_recent_enrollment_count', 'recent_enrollment_count'),
141141
('product_value_per_click_usa', 'value_per_click_usa'),
@@ -171,7 +171,7 @@ class SpanishProductIndex(BaseProductIndex):
171171
'contentful_fields.faq_items, contentful_fields.featured_products'
172172
],
173173
'attributesForFaceting': [
174-
'partner', 'availability', 'subject', 'level', 'language', 'product', 'program_type',
174+
'partner', 'availability', 'weeks_to_complete', 'subject', 'level', 'language', 'product', 'program_type',
175175
'filterOnly(staff)', 'filterOnly(allowed_in)', 'filterOnly(blocked_in)',
176176
'skills.skill', 'skills.category', 'skills.subcategory', 'tags', 'subscription_eligible',
177177
'subscription_prices', 'learning_type', 'learning_type_exp', 'ai_languages.translation_languages',

course_discovery/apps/course_metadata/models.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2059,7 +2059,8 @@ def advertised_course_run(self):
20592059
tier_two = []
20602060
tier_three = []
20612061

2062-
marketable_course_runs = [course_run for course_run in self.course_runs.all() if course_run.is_marketable]
2062+
all_runs = self.course_runs.all()
2063+
marketable_course_runs = [course_run for course_run in all_runs if getattr(course_run, "is_marketable", False)]
20632064

20642065
for course_run in marketable_course_runs:
20652066
course_run_started = (not course_run.start) or (course_run.start and course_run.start < now)
@@ -2080,6 +2081,14 @@ def advertised_course_run(self):
20802081
elif tier_three:
20812082
advertised_course_run = sorted(tier_three, key=lambda run: run.start or min_date, reverse=True)[0]
20822083

2084+
if not advertised_course_run:
2085+
advertised_course_run = next(
2086+
(run for run in all_runs if getattr(run, "weeks_to_complete", None) is not None),
2087+
None
2088+
)
2089+
if not advertised_course_run and all_runs.exists():
2090+
advertised_course_run = all_runs.first()
2091+
20832092
return advertised_course_run
20842093

20852094
def has_marketable_run(self):

course_discovery/apps/course_metadata/tests/test_algolia_models.py

Lines changed: 74 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import datetime
22
from collections import ChainMap
3+
from unittest.mock import patch, PropertyMock
4+
from datetime import timedelta
35

46
import ddt
57
import factory
68
import pytest
79
from django.conf import settings
810
from django.contrib.sites.models import Site
911
from django.test import TestCase, override_settings
12+
from django.utils import timezone
1013
from pytz import UTC
1114

1215
from conftest import TEST_DOMAIN
@@ -16,10 +19,10 @@
1619
from course_discovery.apps.course_metadata.choices import ExternalProductStatus, ProgramStatus
1720
from course_discovery.apps.course_metadata.models import CourseRunStatus, CourseType, ProductValue, ProgramType
1821
from course_discovery.apps.course_metadata.tests.factories import (
19-
AdditionalMetadataFactory, CourseFactory, CourseRunFactory, CourseTypeFactory, DegreeAdditionalMetadataFactory,
20-
DegreeFactory, GeoLocationFactory, LevelTypeFactory, OrganizationFactory, ProductMetaFactory, ProgramFactory,
21-
ProgramSubscriptionFactory, ProgramSubscriptionPriceFactory, ProgramTypeFactory, RestrictedCourseRunFactory,
22-
SeatFactory, SeatTypeFactory, SourceFactory, SubjectFactory, VideoFactory
22+
AdditionalMetadataFactory, CourseFactory, CourseRunFactory, CourseRunTypeFactory, CourseTypeFactory,
23+
DegreeAdditionalMetadataFactory, DegreeFactory, GeoLocationFactory, LevelTypeFactory, OrganizationFactory,
24+
ProductMetaFactory, ProgramFactory, ProgramSubscriptionFactory, ProgramSubscriptionPriceFactory, ProgramTypeFactory,
25+
RestrictedCourseRunFactory, SeatFactory, SeatTypeFactory, SourceFactory, SubjectFactory, VideoFactory
2326
)
2427
from course_discovery.apps.ietf_language_tags.models import LanguageTag
2528

@@ -312,6 +315,8 @@ def test_earliest_upcoming_wins(self):
312315
def test_active_course_run_beats_no_active_course_run(self):
313316
course_1 = self.create_course_with_basic_active_course_run()
314317
course_2 = AlgoliaProxyCourseFactory(partner=self.__class__.edxPartner)
318+
course_2.advertised_course_run = None
319+
course_2.save()
315320
CourseRunFactory(
316321
course=course_2,
317322
start=self.YESTERDAY,
@@ -320,7 +325,7 @@ def test_active_course_run_beats_no_active_course_run(self):
320325
status=CourseRunStatus.Published
321326
)
322327
assert course_1.availability_rank
323-
assert not course_2.availability_rank
328+
assert course_2.availability_rank is None
324329

325330
def test_course_availability_reflects_all_course_runs(self):
326331
course = AlgoliaProxyCourseFactory(partner=self.__class__.edxPartner)
@@ -593,13 +598,73 @@ def test_course_ai_languages(self):
593598
}
594599

595600
def test_course_ai_languages__no_advertised_run(self):
596-
course = self.create_blocked_course(status=CourseRunStatus.Unpublished)
597-
assert course.product_ai_languages == {
598-
'translation_languages': [],
599-
'transcription_languages': []
601+
course = CourseFactory()
602+
with patch.object(AlgoliaProxyCourse, 'advertised_course_run', new_callable=PropertyMock) as mock_run:
603+
mock_run.return_value = None
604+
proxy_course = AlgoliaProxyCourse(course)
605+
assert proxy_course.product_ai_languages == {
606+
'translation_languages': [],
607+
'transcription_languages': []
600608
}
601609

610+
def test_product_weeks_to_complete_from_advertised_run(self):
611+
"""
612+
Verify that AlgoliaProxyCourse correctly exposes weeks_to_complete
613+
from the advertised_course_run.
614+
"""
615+
course = self.create_course_with_basic_active_course_run()
616+
course.authoring_organizations.add(OrganizationFactory())
617+
advertised_run = course.advertised_course_run
618+
advertised_run.weeks_to_complete = 7
619+
advertised_run.save()
620+
621+
proxy_course = AlgoliaProxyCourse.objects.get(pk=course.pk)
622+
assert proxy_course.product_weeks_to_complete == 7
623+
624+
def test_product_weeks_to_complete_returns_none_if_no_run(self):
625+
"""
626+
Should return None if there are no course runs at all.
627+
"""
628+
course = AlgoliaProxyCourseFactory(partner=self.__class__.edxPartner)
629+
course.authoring_organizations.add(OrganizationFactory())
630+
631+
proxy_course = AlgoliaProxyCourse.objects.get(pk=course.pk)
632+
assert proxy_course.product_weeks_to_complete is None
633+
634+
def test_product_weeks_to_complete_with_multiple_runs(self):
635+
"""
636+
If multiple runs exist, ensure the advertised one’s weeks_to_complete is picked.
637+
"""
638+
course = self.create_course_with_basic_active_course_run()
639+
course.authoring_organizations.add(OrganizationFactory())
640+
advertised_run = course.advertised_course_run
641+
advertised_run.weeks_to_complete = 6
642+
advertised_run.save()
643+
644+
proxy_course = AlgoliaProxyCourse.objects.get(pk=course.pk)
645+
assert proxy_course.product_weeks_to_complete == 6
646+
647+
def test_product_weeks_to_complete_ignores_invalid_or_none_values(self):
648+
"""
649+
Ensure that product_weeks_to_complete returns None
650+
when the advertised course run has an invalid or None weeks_to_complete value.
651+
"""
652+
course = self.create_course_with_basic_active_course_run()
653+
course.authoring_organizations.add(OrganizationFactory())
654+
655+
advertised_run = course.advertised_course_run
656+
advertised_run.weeks_to_complete = None
657+
advertised_run.save()
658+
659+
proxy_course = AlgoliaProxyCourse.objects.get(pk=course.pk)
660+
assert proxy_course.product_weeks_to_complete is None
661+
662+
# Now test with invalid value (e.g., 0 weeks)
663+
advertised_run.weeks_to_complete = 0
664+
advertised_run.save()
602665

666+
proxy_course = AlgoliaProxyCourse.objects.get(pk=course.pk)
667+
assert proxy_course.product_weeks_to_complete is None
603668
@ddt.ddt
604669
@pytest.mark.django_db
605670
class TestAlgoliaProxyProgram(TestAlgoliaProxyWithEdxPartner):

0 commit comments

Comments
 (0)