Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions frontends/api/src/generated/v0/api.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

48 changes: 48 additions & 0 deletions frontends/api/src/generated/v1/api.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

81 changes: 57 additions & 24 deletions learning_resources/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,7 @@ def for_serialization(self, *, user: Optional["User"] = None):
queryset=LearningResourceRun.objects.filter(published=True)
.order_by("start_date", "enrollment_start", "id")
.for_serialization(),
to_attr="_published_runs",
),
Prefetch(
"parents",
Expand Down Expand Up @@ -537,36 +538,68 @@ def audience(self) -> str | None:
@cached_property
def best_run(self) -> Optional["LearningResourceRun"]:
"""Returns the most current/upcoming enrollable run for the learning resource"""
published_runs = self.runs.filter(published=True)
if hasattr(self, "_published_runs"):
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The inclusion of best_run_id in the serializer introduced lots of n+1 query errors, which is why this property needed to be refactored.

published_runs = self._published_runs
else:
published_runs = list(self.runs.filter(published=True))

if not published_runs:
return None

now = now_in_utc()

# Find the most recent run with a currently active enrollment period
best_lr_run = (
published_runs.filter(
(
Q(enrollment_start__lte=now)
| (Q(enrollment_start__isnull=True) & Q(start_date__lte=now))
)
& (
Q(enrollment_end__gt=now)
| (Q(enrollment_end__isnull=True) & Q(end_date__gt=now))
enrollable_runs = [
run
for run in published_runs
if (
(run.enrollment_start and run.enrollment_start <= now)
or (
not run.enrollment_start
and run.start_date
and run.start_date <= now
)
)
.order_by("start_date", "end_date")
.first()
)
if not best_lr_run:
# If no current enrollable run found, find the next upcoming run
best_lr_run = (
self.runs.filter(Q(published=True) & Q(start_date__gte=timezone.now()))
.order_by("start_date")
.first()
and (
(run.enrollment_end and run.enrollment_end > now)
or (not run.enrollment_end and run.end_date and run.end_date > now)
)
if not best_lr_run:
# If current_run is still null, return the run with the latest start date
best_lr_run = (
self.runs.filter(Q(published=True)).order_by("-start_date").first()
]
if enrollable_runs:
return min(
enrollable_runs,
key=lambda r: (
r.start_date or timezone.now(),
r.end_date or timezone.now(),
),
)
return best_lr_run

# If no enrollable runs found, find the next upcoming run
upcoming_runs = [
run
for run in published_runs
if run.start_date and run.start_date >= timezone.now()
]
if upcoming_runs:
return min(upcoming_runs, key=lambda r: r.start_date)

# No enrollable/upcoming runs, return run with the latest start date
runs_with_dates = [run for run in published_runs if run.start_date]
if runs_with_dates:
return max(runs_with_dates, key=lambda r: r.start_date)

return published_runs[0] if published_runs else None

@cached_property
def published_runs(self) -> list["LearningResourceRun"]:
"""Return a list of published runs for the resource"""
if hasattr(self, "_published_runs"):
return self._published_runs
return list(
self.runs.filter(published=True)
.order_by("start_date", "enrollment_start", "id")
.for_serialization()
)

@cached_property
def views_count(self) -> int:
Expand Down
12 changes: 11 additions & 1 deletion learning_resources/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -900,7 +900,9 @@ class LearningResourceBaseSerializer(serializers.ModelSerializer, WriteableTopic
read_only=True,
)
resource_prices = LearningResourcePriceSerializer(read_only=True, many=True)
runs = LearningResourceRunSerializer(read_only=True, many=True, allow_null=True)
runs = LearningResourceRunSerializer(
source="published_runs", read_only=True, many=True, allow_null=True
)
image = serializers.SerializerMethodField()
learning_path_parents = MicroLearningPathRelationshipSerializer(
many=True, read_only=True
Expand All @@ -915,13 +917,21 @@ class LearningResourceBaseSerializer(serializers.ModelSerializer, WriteableTopic
format = serializers.ListField(child=FormatSerializer(), read_only=True)
pace = serializers.ListField(child=PaceSerializer(), read_only=True)
children = serializers.SerializerMethodField(allow_null=True)
best_run_id = serializers.SerializerMethodField(allow_null=True)

@extend_schema_field(LearningResourceRelationshipChildField(allow_null=True))
def get_children(self, instance):
return LearningResourceRelationshipChildField(
instance.children, many=True, read_only=True
).data

def get_best_run_id(self, instance) -> int | None:
"""Return the best run id for the resource, if it has runs"""
best_run = instance.best_run
if best_run:
return best_run.id
return None

def get_resource_category(self, instance) -> str:
"""Return the resource category of the resource"""
if instance.resource_type in [
Expand Down
8 changes: 7 additions & 1 deletion learning_resources/serializers_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,7 @@ def test_learning_resource_serializer( # noqa: PLR0913
"ocw_topics": sorted(resource.ocw_topics),
"runs": [
serializers.LearningResourceRunSerializer(instance=run).data
for run in resource.runs.all()
for run in resource.published_runs
],
detail_key: detail_serializer_cls(instance=getattr(resource, detail_key)).data,
"views": resource.views.count(),
Expand All @@ -351,6 +351,7 @@ def test_learning_resource_serializer( # noqa: PLR0913
"max_weekly_hours": resource.max_weekly_hours,
"min_weeks": resource.min_weeks,
"max_weeks": resource.max_weeks,
"best_run_id": resource.best_run.id if resource.best_run else None,
}


Expand Down Expand Up @@ -857,6 +858,11 @@ def test_instructors_display():
load_instructors(
run, [{"full_name": instructor.full_name} for instructor in instructors]
)
# Clear cached properties so they pick up the new run with instructors
if hasattr(resource, "_published_runs"):
delattr(resource, "_published_runs")
if hasattr(resource, "published_runs"):
del resource.__dict__["published_runs"]
serialized_resource = serializers.LearningResourceSerializer(resource).data
metadata_serializer = serializers.LearningResourceMetadataDisplaySerializer(
serialized_resource
Expand Down
1 change: 1 addition & 0 deletions learning_resources_search/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,7 @@ class FilterConfig:
"location": {"type": "keyword"},
},
},
"best_run_id": {"type": "long"},
"next_start_date": {"type": "date"},
"resource_age_date": {"type": "date"},
"featured_rank": {"type": "float"},
Expand Down
Loading
Loading