Skip to content

Commit 32c4829

Browse files
authored
Change "next_run" to "best_run" and refactor how next_start_date is calculated (#2586)
1 parent e73eedf commit 32c4829

File tree

7 files changed

+115
-48
lines changed

7 files changed

+115
-48
lines changed

learning_resources/etl/edx_shared_test.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ def test_sync_edx_course_files( # noqa: PLR0913
8080
)
8181
course.refresh_from_db()
8282
if published:
83-
assert course.next_run in runs
83+
assert course.best_run in runs
8484
keys.extend(
8585
[f"20220101/{s3_prefix}/{run.run_id}.tar.gz" for run in runs]
8686
if source != ETLSource.oll.name
@@ -102,7 +102,7 @@ def test_sync_edx_course_files( # noqa: PLR0913
102102
assert mock_load_content_files.call_count == (4 if published else 0)
103103
if published:
104104
for course in courses:
105-
mock_load_content_files.assert_any_call(course.next_run, fake_data)
105+
mock_load_content_files.assert_any_call(course.best_run, fake_data)
106106
mock_log.assert_not_called()
107107

108108

@@ -113,7 +113,7 @@ def test_sync_edx_course_files_matching_checksum(
113113

114114
run = LearningResourceFactory.create(
115115
is_course=True, create_runs=True, etl_source=ETLSource.mitxonline.name
116-
).next_run
116+
).best_run
117117
run.learning_resource.runs.exclude(id=run.id).first()
118118
run.checksum = "123"
119119
run.save()
@@ -156,7 +156,7 @@ def test_sync_edx_course_files_invalid_tarfile(
156156
published=True,
157157
create_runs=True,
158158
)
159-
run = course.next_run
159+
run = course.best_run
160160
key = f"20220101/courses/{run.run_id}.tar.gz"
161161
bucket = (
162162
mock_mitxonline_learning_bucket
@@ -231,7 +231,7 @@ def test_sync_edx_course_files_error(
231231
published=True,
232232
create_runs=True,
233233
)
234-
run = course.next_run
234+
run = course.best_run
235235
key = f"20220101/courses/{run.run_id}.tar.gz"
236236
bucket = (
237237
mock_mitxonline_learning_bucket

learning_resources/etl/loaders.py

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
Video,
4646
VideoChannel,
4747
VideoPlaylist,
48+
now_in_utc,
4849
)
4950
from learning_resources.utils import (
5051
add_parent_topics_to_learning_resource,
@@ -137,16 +138,12 @@ def load_run_dependent_values(
137138
Returns:
138139
tuple[datetime.time | None, list[Decimal], str]: date, prices, and availability
139140
"""
140-
next_upcoming_run = resource.next_run
141-
if next_upcoming_run:
142-
resource.next_start_date = next_upcoming_run.start_date
143-
else:
144-
resource.next_start_date = None
145-
best_run = (
146-
next_upcoming_run
147-
or resource.runs.filter(published=True).order_by("-start_date").first()
148-
)
149-
if best_run:
141+
now = now_in_utc()
142+
best_run = resource.best_run
143+
if resource.published and best_run:
144+
resource.next_start_date = max(
145+
best_run.start_date or best_run.enrollment_start or now, now
146+
)
150147
resource.availability = best_run.availability
151148
resource.prices = (
152149
best_run.prices
@@ -165,6 +162,8 @@ def load_run_dependent_values(
165162
resource.time_commitment = best_run.time_commitment
166163
resource.min_weekly_hours = best_run.min_weekly_hours
167164
resource.max_weekly_hours = best_run.max_weekly_hours
165+
else:
166+
resource.next_start_date = None
168167
resource.save()
169168
return ResourceNextRunConfig(
170169
next_start_date=resource.next_start_date,

learning_resources/etl/loaders_test.py

Lines changed: 60 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99

1010
import pytest
1111
from django.forms.models import model_to_dict
12-
from django.utils import timezone
1312

1413
from learning_resources.constants import (
1514
CURRENCY_USD,
@@ -364,11 +363,11 @@ def test_load_program_bad_platform(mocker):
364363
@pytest.mark.parametrize("course_exists", [True, False])
365364
@pytest.mark.parametrize("is_published", [True, False])
366365
@pytest.mark.parametrize("is_run_published", [True, False])
367-
@pytest.mark.parametrize("blocklisted", [True, False])
366+
@pytest.mark.parametrize("blocklisted", [False, True])
368367
@pytest.mark.parametrize("delivery", [LearningResourceDelivery.hybrid.name, None])
369368
@pytest.mark.parametrize("has_upcoming_run", [True, False])
370369
@pytest.mark.parametrize("has_departments", [True, False])
371-
def test_load_course( # noqa: PLR0913,PLR0912,PLR0915
370+
def test_load_course( # noqa: PLR0913,PLR0912,PLR0915, C901
372371
mock_upsert_tasks,
373372
course_exists,
374373
is_published,
@@ -391,30 +390,45 @@ def test_load_course( # noqa: PLR0913,PLR0912,PLR0915
391390

392391
learning_resource.published = is_published
393392

394-
start_date = (
395-
timezone.now() + timedelta(10)
396-
if has_upcoming_run
397-
else timezone.now() - timedelta(1)
398-
)
399-
old_start_date = timezone.now() - timedelta(365)
393+
now = now_in_utc()
394+
start_date = now + timedelta(10) if has_upcoming_run else now - timedelta(10)
395+
end_date = start_date + timedelta(30)
396+
old_start_date = now - timedelta(365)
397+
old_end_date = old_start_date + timedelta(30)
400398

401399
if course_exists:
402400
run = LearningResourceRunFactory.create(
403-
learning_resource=learning_resource, published=True, start_date=start_date
401+
learning_resource=learning_resource,
402+
published=True,
403+
enrollment_start=start_date - timedelta(30),
404+
start_date=start_date,
405+
end_date=end_date,
406+
enrollment_end=end_date - timedelta(30),
404407
)
405408
old_run = LearningResourceRunFactory.create(
406409
learning_resource=learning_resource,
407410
published=True,
408411
start_date=old_start_date,
412+
end_date=old_end_date,
413+
enrollment_start=old_start_date - timedelta(30),
414+
enrollment_end=old_end_date - timedelta(30),
409415
)
410-
learning_resource.runs.set([run])
416+
learning_resource.runs.set([run, old_run])
411417
learning_resource.save()
412418
else:
413-
run = LearningResourceRunFactory.build(start_date=start_date)
419+
run = LearningResourceRunFactory.build(
420+
start_date=start_date,
421+
end_date=end_date,
422+
enrollment_start=start_date - timedelta(30),
423+
enrollment_end=end_date - timedelta(30),
424+
)
414425
old_run = LearningResourceRunFactory.build(
415426
learning_resource=learning_resource,
416427
published=True,
417428
start_date=old_start_date,
429+
end_date=old_end_date,
430+
enrollment_start=old_start_date - timedelta(30),
431+
enrollment_end=old_end_date - timedelta(30),
418432
)
419433
assert Course.objects.count() == (1 if course_exists else 0)
420434
if has_departments:
@@ -451,7 +465,7 @@ def test_load_course( # noqa: PLR0913,PLR0912,PLR0915
451465
{
452466
"run_id": run.run_id,
453467
"enrollment_start": run.enrollment_start,
454-
"start_date": start_date,
468+
"start_date": run.start_date,
455469
"end_date": run.end_date,
456470
"prices": [
457471
{"amount": Decimal("0.00"), "currency": CURRENCY_USD},
@@ -468,10 +482,11 @@ def test_load_course( # noqa: PLR0913,PLR0912,PLR0915
468482
result = load_course(props, blocklist, [], config=CourseLoaderConfig(prune=True))
469483
assert result.professional is True
470484

471-
expected_next_start_date = (
472-
start_date if has_upcoming_run and is_run_published else None
473-
)
474-
assert result.next_start_date == expected_next_start_date
485+
if is_published and is_run_published and not blocklisted:
486+
if has_upcoming_run:
487+
assert result.next_start_date == start_date
488+
else:
489+
assert result.next_start_date.date() == now.date()
475490
assert result.prices == (
476491
[Decimal("0.00"), Decimal("49.00")]
477492
if is_run_published and result.certification
@@ -1769,6 +1784,34 @@ def test_load_run_dependent_values(certification):
17691784
assert getattr(result, key) == getattr(course, key) == getattr(run, key)
17701785

17711786

1787+
def test_load_run_dependent_values_resets_next_start_date():
1788+
"""Test that next_start_date is reset to None when best_run becomes None"""
1789+
# Create a published course with an existing next_start_date
1790+
previous_date = now_in_utc() + timedelta(days=5)
1791+
course = LearningResourceFactory.create(
1792+
is_course=True,
1793+
published=True,
1794+
next_start_date=previous_date, # Course previously had a start date
1795+
)
1796+
1797+
# Ensure course has no runs, so best_run will return None
1798+
course.runs.all().update(published=False)
1799+
assert course.best_run is None
1800+
1801+
# Verify the course initially has a next_start_date
1802+
assert course.next_start_date == previous_date
1803+
1804+
# Call load_run_dependent_values
1805+
result = load_run_dependent_values(course)
1806+
1807+
# Refresh course from database
1808+
course.refresh_from_db()
1809+
1810+
# Verify that next_start_date was reset to None
1811+
assert result.next_start_date is None
1812+
assert course.next_start_date is None
1813+
1814+
17721815
@pytest.mark.parametrize(
17731816
("is_scholar_course", "tag_counts", "expected_score"),
17741817
[

learning_resources/etl/openedx.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -503,7 +503,7 @@ def _parse_program_instructors_topics(program):
503503
for course in courses:
504504
topics.extend([{"name": topic.name} for topic in course.topics.all()])
505505
run = (
506-
course.next_run
506+
course.best_run
507507
or course.runs.filter(published=True).order_by("-start_date").first()
508508
)
509509
if run:

learning_resources/models.py

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
PrivacyLevel,
3232
)
3333
from main.models import TimestampedModel, TimestampedModelQuerySet
34-
from main.utils import checksum_for_content
34+
from main.utils import checksum_for_content, now_in_utc
3535

3636
if TYPE_CHECKING:
3737
from django.contrib.auth import get_user_model
@@ -526,13 +526,38 @@ def audience(self) -> str | None:
526526
return None
527527

528528
@cached_property
529-
def next_run(self) -> Optional["LearningResourceRun"]:
530-
"""Returns the next run for the learning resource"""
531-
return (
532-
self.runs.filter(Q(published=True) & Q(start_date__gt=timezone.now()))
533-
.order_by("start_date")
529+
def best_run(self) -> Optional["LearningResourceRun"]:
530+
"""Returns the most current/upcoming enrollable run for the learning resource"""
531+
published_runs = self.runs.filter(published=True)
532+
now = now_in_utc()
533+
# Find the most recent run with a currently active enrollment period
534+
best_lr_run = (
535+
published_runs.filter(
536+
(
537+
Q(enrollment_start__lte=now)
538+
| (Q(enrollment_start__isnull=True) & Q(start_date__lte=now))
539+
)
540+
& (
541+
Q(enrollment_end__gt=now)
542+
| (Q(enrollment_end__isnull=True) & Q(end_date__gt=now))
543+
)
544+
)
545+
.order_by("start_date", "end_date")
534546
.first()
535547
)
548+
if not best_lr_run:
549+
# If no current enrollable run found, find the next upcoming run
550+
best_lr_run = (
551+
self.runs.filter(Q(published=True) & Q(start_date__gte=timezone.now()))
552+
.order_by("start_date")
553+
.first()
554+
)
555+
if not best_lr_run:
556+
# If current_run is still null, return the run with the latest start date
557+
best_lr_run = (
558+
self.runs.filter(Q(published=True)).order_by("-start_date").first()
559+
)
560+
return best_lr_run
536561

537562
@cached_property
538563
def views_count(self) -> int:

vector_search/tasks.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,8 @@ def start_embed_resources(self, indexes, skip_content_files, overwrite):
135135
.order_by("id")
136136
):
137137
run = (
138-
course.next_run
139-
if course.next_run
138+
course.best_run
139+
if course.best_run
140140
else course.runs.filter(published=True)
141141
.order_by("-start_date")
142142
.first()
@@ -225,8 +225,8 @@ def embed_learning_resources_by_id(self, ids, skip_content_files, overwrite):
225225
if not skip_content_files and resource_type == COURSE_TYPE:
226226
for course in embed_resources.order_by("id"):
227227
run = (
228-
course.next_run
229-
if course.next_run
228+
course.best_run
229+
if course.best_run
230230
else course.runs.filter(published=True)
231231
.order_by("-start_date")
232232
.first()

vector_search/tasks_test.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -249,9 +249,9 @@ def test_embed_learning_resources_by_id(mocker, mocked_celery):
249249
assert sorted(resource_ids) == sorted(embedded_resource_ids)
250250

251251

252-
def test_embedded_content_from_next_run(mocker, mocked_celery):
252+
def test_embedded_content_from_best_run(mocker, mocked_celery):
253253
"""
254-
Content files to embed should come from next course run
254+
Content files to embed should come from best course run
255255
"""
256256

257257
mocker.patch("vector_search.tasks.load_course_blocklist", return_value=[])
@@ -267,10 +267,10 @@ def test_embedded_content_from_next_run(mocker, mocked_celery):
267267
start_date=datetime.datetime.now(tz=datetime.UTC) + datetime.timedelta(days=2),
268268
)
269269

270-
next_run_contentfiles = [
270+
best_run_contentfiles = [
271271
cf.id
272272
for cf in ContentFileFactory.create_batch(
273-
3, run=course.learning_resource.next_run
273+
3, run=course.learning_resource.best_run
274274
)
275275
]
276276
# create contentfiles using the other run
@@ -286,7 +286,7 @@ def test_embedded_content_from_next_run(mocker, mocked_celery):
286286
)
287287

288288
generate_embeddings_mock.si.assert_called_with(
289-
next_run_contentfiles,
289+
best_run_contentfiles,
290290
"content_file",
291291
True, # noqa: FBT003
292292
)

0 commit comments

Comments
 (0)