Skip to content

Commit e0ffe20

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

File tree

6 files changed

+105
-20
lines changed

6 files changed

+105
-20
lines changed

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 datetime import timedelta
4+
from unittest.mock import PropertyMock, patch
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):

docs/conf.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@
1111
import datetime
1212
import os
1313

14-
15-
1614
# on_rtd is whether we are on readthedocs.org
1715
on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
1816

0 commit comments

Comments
 (0)