Skip to content

Commit f6aa06d

Browse files
authored
Add best_run_id to REST/search API results (#2696)
1 parent 47675dd commit f6aa06d

File tree

9 files changed

+276
-26
lines changed

9 files changed

+276
-26
lines changed

frontends/api/src/generated/v0/api.ts

Lines changed: 48 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontends/api/src/generated/v1/api.ts

Lines changed: 48 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

learning_resources/models.py

Lines changed: 57 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,7 @@ def for_serialization(self, *, user: Optional["User"] = None):
367367
queryset=LearningResourceRun.objects.filter(published=True)
368368
.order_by("start_date", "enrollment_start", "id")
369369
.for_serialization(),
370+
to_attr="_published_runs",
370371
),
371372
Prefetch(
372373
"parents",
@@ -537,36 +538,68 @@ def audience(self) -> str | None:
537538
@cached_property
538539
def best_run(self) -> Optional["LearningResourceRun"]:
539540
"""Returns the most current/upcoming enrollable run for the learning resource"""
540-
published_runs = self.runs.filter(published=True)
541+
if hasattr(self, "_published_runs"):
542+
published_runs = self._published_runs
543+
else:
544+
published_runs = list(self.runs.filter(published=True))
545+
546+
if not published_runs:
547+
return None
548+
541549
now = now_in_utc()
550+
542551
# Find the most recent run with a currently active enrollment period
543-
best_lr_run = (
544-
published_runs.filter(
545-
(
546-
Q(enrollment_start__lte=now)
547-
| (Q(enrollment_start__isnull=True) & Q(start_date__lte=now))
548-
)
549-
& (
550-
Q(enrollment_end__gt=now)
551-
| (Q(enrollment_end__isnull=True) & Q(end_date__gt=now))
552+
enrollable_runs = [
553+
run
554+
for run in published_runs
555+
if (
556+
(run.enrollment_start and run.enrollment_start <= now)
557+
or (
558+
not run.enrollment_start
559+
and run.start_date
560+
and run.start_date <= now
552561
)
553562
)
554-
.order_by("start_date", "end_date")
555-
.first()
556-
)
557-
if not best_lr_run:
558-
# If no current enrollable run found, find the next upcoming run
559-
best_lr_run = (
560-
self.runs.filter(Q(published=True) & Q(start_date__gte=timezone.now()))
561-
.order_by("start_date")
562-
.first()
563+
and (
564+
(run.enrollment_end and run.enrollment_end > now)
565+
or (not run.enrollment_end and run.end_date and run.end_date > now)
563566
)
564-
if not best_lr_run:
565-
# If current_run is still null, return the run with the latest start date
566-
best_lr_run = (
567-
self.runs.filter(Q(published=True)).order_by("-start_date").first()
567+
]
568+
if enrollable_runs:
569+
return min(
570+
enrollable_runs,
571+
key=lambda r: (
572+
r.start_date or timezone.now(),
573+
r.end_date or timezone.now(),
574+
),
568575
)
569-
return best_lr_run
576+
577+
# If no enrollable runs found, find the next upcoming run
578+
upcoming_runs = [
579+
run
580+
for run in published_runs
581+
if run.start_date and run.start_date >= timezone.now()
582+
]
583+
if upcoming_runs:
584+
return min(upcoming_runs, key=lambda r: r.start_date)
585+
586+
# No enrollable/upcoming runs, return run with the latest start date
587+
runs_with_dates = [run for run in published_runs if run.start_date]
588+
if runs_with_dates:
589+
return max(runs_with_dates, key=lambda r: r.start_date)
590+
591+
return published_runs[0] if published_runs else None
592+
593+
@cached_property
594+
def published_runs(self) -> list["LearningResourceRun"]:
595+
"""Return a list of published runs for the resource"""
596+
if hasattr(self, "_published_runs"):
597+
return self._published_runs
598+
return list(
599+
self.runs.filter(published=True)
600+
.order_by("start_date", "enrollment_start", "id")
601+
.for_serialization()
602+
)
570603

571604
@cached_property
572605
def views_count(self) -> int:

learning_resources/serializers.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -900,7 +900,9 @@ class LearningResourceBaseSerializer(serializers.ModelSerializer, WriteableTopic
900900
read_only=True,
901901
)
902902
resource_prices = LearningResourcePriceSerializer(read_only=True, many=True)
903-
runs = LearningResourceRunSerializer(read_only=True, many=True, allow_null=True)
903+
runs = LearningResourceRunSerializer(
904+
source="published_runs", read_only=True, many=True, allow_null=True
905+
)
904906
image = serializers.SerializerMethodField()
905907
learning_path_parents = MicroLearningPathRelationshipSerializer(
906908
many=True, read_only=True
@@ -915,13 +917,21 @@ class LearningResourceBaseSerializer(serializers.ModelSerializer, WriteableTopic
915917
format = serializers.ListField(child=FormatSerializer(), read_only=True)
916918
pace = serializers.ListField(child=PaceSerializer(), read_only=True)
917919
children = serializers.SerializerMethodField(allow_null=True)
920+
best_run_id = serializers.SerializerMethodField(allow_null=True)
918921

919922
@extend_schema_field(LearningResourceRelationshipChildField(allow_null=True))
920923
def get_children(self, instance):
921924
return LearningResourceRelationshipChildField(
922925
instance.children, many=True, read_only=True
923926
).data
924927

928+
def get_best_run_id(self, instance) -> int | None:
929+
"""Return the best run id for the resource, if it has runs"""
930+
best_run = instance.best_run
931+
if best_run:
932+
return best_run.id
933+
return None
934+
925935
def get_resource_category(self, instance) -> str:
926936
"""Return the resource category of the resource"""
927937
if instance.resource_type in [

learning_resources/serializers_test.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,7 @@ def test_learning_resource_serializer( # noqa: PLR0913
324324
"ocw_topics": sorted(resource.ocw_topics),
325325
"runs": [
326326
serializers.LearningResourceRunSerializer(instance=run).data
327-
for run in resource.runs.all()
327+
for run in resource.published_runs
328328
],
329329
detail_key: detail_serializer_cls(instance=getattr(resource, detail_key)).data,
330330
"views": resource.views.count(),
@@ -351,6 +351,7 @@ def test_learning_resource_serializer( # noqa: PLR0913
351351
"max_weekly_hours": resource.max_weekly_hours,
352352
"min_weeks": resource.min_weeks,
353353
"max_weeks": resource.max_weeks,
354+
"best_run_id": resource.best_run.id if resource.best_run else None,
354355
}
355356

356357

@@ -857,6 +858,11 @@ def test_instructors_display():
857858
load_instructors(
858859
run, [{"full_name": instructor.full_name} for instructor in instructors]
859860
)
861+
# Clear cached properties so they pick up the new run with instructors
862+
if hasattr(resource, "_published_runs"):
863+
delattr(resource, "_published_runs")
864+
if hasattr(resource, "published_runs"):
865+
del resource.__dict__["published_runs"]
860866
serialized_resource = serializers.LearningResourceSerializer(resource).data
861867
metadata_serializer = serializers.LearningResourceMetadataDisplaySerializer(
862868
serialized_resource

learning_resources_search/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,7 @@ class FilterConfig:
300300
"location": {"type": "keyword"},
301301
},
302302
},
303+
"best_run_id": {"type": "long"},
303304
"next_start_date": {"type": "date"},
304305
"resource_age_date": {"type": "date"},
305306
"featured_rank": {"type": "float"},

0 commit comments

Comments
 (0)