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
6 changes: 6 additions & 0 deletions RELEASE.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
Release Notes
=============

Version 1.141.3
---------------

- Add filter to context to filter enrollable courseruns (#3377)
- correct default course / program image behavior (#3351)

Version 0.141.1 (Released March 11, 2026)
---------------

Expand Down
32 changes: 12 additions & 20 deletions cms/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,12 @@
from __future__ import annotations

import bleach
from django.templatetags.static import static
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers

from cms import models
from cms.api import get_wagtail_img_src
from cms.models import FlexiblePricingRequestForm, ProgramPage
from courses.constants import DEFAULT_COURSE_IMG_PATH


class BaseCoursePageSerializer(serializers.ModelSerializer):
Expand All @@ -22,14 +20,12 @@ class BaseCoursePageSerializer(serializers.ModelSerializer):
effort = serializers.SerializerMethodField()
length = serializers.SerializerMethodField()

@extend_schema_field(str)
@extend_schema_field(serializers.CharField(allow_null=True))
def get_feature_image_src(self, instance):
"""Serializes the source of the feature_image"""
feature_img_src = None
"""Serializes the source of the feature_image, or None if not set."""
if hasattr(instance, "feature_image"):
feature_img_src = get_wagtail_img_src(instance.feature_image)

return feature_img_src or static(DEFAULT_COURSE_IMG_PATH)
return get_wagtail_img_src(instance.feature_image) or None
return None
Comment on lines 26 to +28
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: The ProgramCourseInfoCard.js component is not handling the new null value for feature_image_src, causing it to render without an image instead of using a default fallback.
Severity: MEDIUM

Suggested Fix

Update ProgramCourseInfoCard.js to handle a null value for feature_image_src. When the value is null, the component should render a default fallback image, similar to the implementation in EnrolledItemCard.js.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: cms/serializers.py#L26-L28

Potential issue: The serializer method `get_feature_image_src` was changed to return
`None` when a course lacks a feature image. While several frontend components were
updated to handle this by using a default image, the `ProgramCourseInfoCard.js`
component was not. This component's conditional check `if (course.feature_image_src)`
will evaluate to false when the value is `null`, causing it to render no image element
at all. This results in a missing image in the program enrollment drawer, which is a
functional regression.

Did we get this right? 👍 / 👎 to inform future reviews.


@extend_schema_field(serializers.URLField)
def get_page_url(self, instance):
Expand Down Expand Up @@ -280,14 +276,12 @@ def _get_financial_assistance_url(self, page, slug):
"""Helper method to construct financial assistance URL"""
return f"{page.get_url()}{slug}/" if page and slug else ""

@extend_schema_field(str)
@extend_schema_field(serializers.CharField(allow_null=True))
def get_feature_image_src(self, instance):
"""Serializes the source of the feature_image"""
feature_img_src = None
"""Serializes the source of the feature_image, or None if not set."""
if hasattr(instance, "feature_image"):
feature_img_src = get_wagtail_img_src(instance.feature_image)

return feature_img_src or static(DEFAULT_COURSE_IMG_PATH)
return get_wagtail_img_src(instance.feature_image) or None
return None

@extend_schema_field(serializers.URLField)
def get_page_url(self, instance):
Expand Down Expand Up @@ -385,14 +379,12 @@ class InstructorPageSerializer(serializers.ModelSerializer):

feature_image_src = serializers.SerializerMethodField()

@extend_schema_field(str)
@extend_schema_field(serializers.CharField(allow_null=True))
def get_feature_image_src(self, instance):
"""Serializes the source of the feature_image"""
feature_img_src = None
"""Serializes the source of the feature_image, or None if not set."""
if hasattr(instance, "feature_image"):
feature_img_src = get_wagtail_img_src(instance.feature_image)

return feature_img_src or static(DEFAULT_COURSE_IMG_PATH)
return get_wagtail_img_src(instance.feature_image) or None
return None

class Meta:
model = models.InstructorPage
Expand Down
2 changes: 1 addition & 1 deletion cms/wagtail_api/schema/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ class FacultySerializer(serializers.Serializer):
instructor_title = serializers.CharField()
instructor_bio_short = serializers.CharField()
instructor_bio_long = serializers.CharField()
feature_image_src = serializers.CharField()
feature_image_src = serializers.CharField(allow_null=True)


class PriceItemSerializer(serializers.Serializer):
Expand Down
11 changes: 6 additions & 5 deletions courses/serializers/base.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,32 @@
from urllib.parse import urljoin

from django.conf import settings
from django.templatetags.static import static
from rest_framework import serializers

from courses import models


def get_thumbnail_url(page):
"""
Get the thumbnail URL or else return a default image URL.
Get the thumbnail URL or return None if no image is configured.

Args:
page (cms.models.ProductPage): A product page

Returns:
str:
A page URL
str | None:
A fully-qualified page image URL, or None if no image is set.
"""
relative_url = (
page.feature_image.file.url
if page
and page.feature_image
and page.feature_image.file
and page.feature_image.file.url
else static("images/mit-dome.png")
else None
)
if relative_url is None:
return None
return urljoin(settings.SITE_BASE_URL, relative_url)


Expand Down
8 changes: 5 additions & 3 deletions courses/serializers/v1/programs_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -417,12 +417,14 @@ def test_learner_record_serializer(
assert course_0_payload == serialized_data["program"]["courses"][0]


def test_program_serializer_returns_default_image():
"""If the program has no page, we should still get a featured_image_url."""
def test_program_serializer_returns_null_image_when_no_page():
"""If the program has no page, feature_image_src should be None (null)."""

program = ProgramFactory.create(page=None)
page_data = ProgramSerializer(program).data["page"]

assert "feature_image_src" in ProgramSerializer(program).data["page"]
assert "feature_image_src" in page_data
assert page_data["feature_image_src"] is None


@pytest.mark.parametrize(
Expand Down
10 changes: 10 additions & 0 deletions courses/serializers/v2/courses.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,16 @@ class CourseWithCourseRunsSerializer(CourseSerializer):
def get_courseruns(self, instance):
# Use prefetched course runs to preserve prefetched products
courseruns = instance.courseruns.all()
if hasattr(instance, "prefetched_courseruns"):
courseruns = instance.prefetched_courseruns
# Filter by enrollable status if context parameter is present
if "courserun_is_enrollable" in self.context:
courseruns = [
run
for run in courseruns
if getattr(run, "is_enrollable", False)
== bool(self.context["courserun_is_enrollable"])
]
if "org_id" in self.context:
courseruns = [
run
Expand Down
5 changes: 5 additions & 0 deletions courses/views/v2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,11 @@ def get_serializer_context(self):
added_context["include_programs"] = True
if qp.get("include_approved_financial_aid"):
added_context["include_approved_financial_aid"] = True
if qp.get("courserun_is_enrollable") is not None:
# Add courserun_is_enrollable to context if present in query params
added_context["courserun_is_enrollable"] = (
qp.get("courserun_is_enrollable", "").lower() == "true"
)
if qp.get("org_id") or qp.get("contract_id"):
# If an org or contract is specified, we extract the user's contracts
# according to those params, so filtering on some course run data is
Expand Down
1 change: 0 additions & 1 deletion courses/views/v2/views_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,6 @@ def test_get_courses(
params = {"page_size": 100}

courses = Course.objects.order_by("title").prefetch_related("departments")

if include_finaid is not None:
mock_context["include_approved_financial_aid"] = include_finaid
params["include_approved_financial_aid"] = include_finaid
Expand Down
12 changes: 10 additions & 2 deletions frontend/public/src/components/CartItemCard.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import React from "react"
import type { Product } from "../flow/cartTypes"
import { courseRunStatusMessage } from "../lib/courseApi"

const DEFAULT_COURSE_IMG = "/static/images/mit-dome.png"

type Props = {
product: Product
}
Expand Down Expand Up @@ -36,7 +38,10 @@ export class CartItemCard extends React.Component<Props> {
abbreviation = purchasableObject.course_number
image =
course.page !== null ? (
<img src={course.page.feature_image_src} alt="" />
<img
src={course.page.feature_image_src || DEFAULT_COURSE_IMG}
alt=""
/>
) : null
detailLink = this.renderLink("Course details", pageUrl)
statusMessage = courseRunStatusMessage(purchasableObject)
Expand All @@ -51,7 +56,10 @@ export class CartItemCard extends React.Component<Props> {
image =
purchasableObject.page !== null &&
purchasableObject.page !== undefined ? (
<img src={purchasableObject.page.feature_image_src} alt="" />
<img
src={purchasableObject.page.feature_image_src || DEFAULT_COURSE_IMG}
alt=""
/>
) : null
detailLink = this.renderLink("Program details", pageUrl)
statusMessage = null
Expand Down
40 changes: 21 additions & 19 deletions frontend/public/src/components/EnrolledItemCard.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/* global SETTINGS:false */
import React from "react"
import moment from "moment"

const DEFAULT_COURSE_IMG = "/static/images/mit-dome.png"
import {
parseDateString,
formatPrettyDateTimeAmPmTz,
Expand Down Expand Up @@ -463,25 +465,19 @@ export class EnrolledItemCard extends React.Component<
return (
<div className="enrolled-item container card" key={enrollment.run.id}>
<div className="row flex-grow-1 enrolled-item-info">
{enrollment.run.course.feature_image_src && (
<div className="col-12 col-md-auto p-0">
<div className="img-container">
<img src={enrollment.run.course.feature_image_src} alt="" />
</div>
</div>
)}
{!enrollment.run.course.feature_image_src &&
enrollment.run.course.page &&
enrollment.run.course.page.feature_image_src && (
<div className="col-12 col-md-auto p-0">
<div className="img-container">
<img
src={enrollment.run.course.page.feature_image_src}
alt=""
/>
{(() => {
const imgSrc =
enrollment.run.course.feature_image_src ||
enrollment.run.course.page?.feature_image_src ||
DEFAULT_COURSE_IMG
return (
<div className="col-12 col-md-auto p-0">
<div className="img-container">
<img src={imgSrc} alt="" />
</div>
</div>
</div>
)}
)
})()}

<div className="col-12 col-md course-card-text-details d-grid">
<div className="d-flex justify-content-between flex-nowrap w-100">
Expand Down Expand Up @@ -632,7 +628,13 @@ export class EnrolledItemCard extends React.Component<
<div className="row flex-grow-1 enrolled-item-info">
<div className="col-12 col-md-auto p-0">
<div className="img-container">
<img src={enrollment.program.page.feature_image_src} alt="" />
<img
src={
enrollment.program.page?.feature_image_src ||
DEFAULT_COURSE_IMG
}
alt=""
/>
</div>
</div>

Expand Down
16 changes: 12 additions & 4 deletions frontend/public/src/containers/pages/CatalogPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import React from "react"
import { CSSTransition, TransitionGroup } from "react-transition-group"
import { getStartDateText } from "../../lib/util"

const DEFAULT_COURSE_IMG = "/static/images/mit-dome.png"

import {
coursesCountSelector,
coursesSelector,
Expand Down Expand Up @@ -664,8 +666,11 @@ export class CatalogPage extends React.Component<Props> {
<a href={course.page.page_url} key={course.id}>
<div className="col catalog-item">
<img
src={course?.page?.feature_image_src}
key={course.id + course?.page?.feature_image_src}
src={course?.page?.feature_image_src || DEFAULT_COURSE_IMG}
key={
course.id +
(course?.page?.feature_image_src || DEFAULT_COURSE_IMG)
}
alt=""
/>
<div className="catalog-item-description">
Expand All @@ -691,8 +696,11 @@ export class CatalogPage extends React.Component<Props> {
<div className="col catalog-item">
<div className="program-image-and-badge">
<img
src={program?.page?.feature_image_src}
key={program.id + program?.page?.feature_image_src}
src={program?.page?.feature_image_src || DEFAULT_COURSE_IMG}
key={
program.id +
(program?.page?.feature_image_src || DEFAULT_COURSE_IMG)
}
alt=""
/>
<div className="program-type-badge">{program.program_type}</div>
Expand Down
7 changes: 3 additions & 4 deletions frontend/public/src/containers/pages/CatalogPage_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ const displayedCourse = {
}
],
page: {
feature_image_src: "/static/images/mit-dome.png",
feature_image_src: null,
page_url: "/courses/course-v1:edX+E2E-101/",
financial_assistance_form_url: "",
description: "E2E Test Course",
Expand All @@ -64,7 +64,7 @@ const displayedCourse = {
}
],
page: {
feature_image_src: "/static/images/mit-dome.png",
feature_image_src: null,
page_url: "/courses/course-v1:edX+E2E-101/",
financial_assistance_form_url: "",
description: "E2E Test Course",
Expand Down Expand Up @@ -126,8 +126,7 @@ const displayedProgram = {
}
],
page: {
feature_image_src:
"http://mitxonline.odl.local:8013/static/images/mit-dome.png"
feature_image_src: null
},
program_type: "Series",
departments: [
Expand Down
2 changes: 1 addition & 1 deletion main/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
from main.sentry import init_sentry
from openapi.settings_spectacular import open_spectacular_settings

VERSION = "0.141.1"
VERSION = "1.141.3"

log = logging.getLogger()

Expand Down
5 changes: 3 additions & 2 deletions openapi/specs/v0.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3785,7 +3785,7 @@ components:
properties:
feature_image_src:
type: string
description: Serializes the source of the feature_image
nullable: true
readOnly: true
page_url:
type: string
Expand Down Expand Up @@ -5032,6 +5032,7 @@ components:
type: string
feature_image_src:
type: string
nullable: true
required:
- feature_image_src
- id
Expand Down Expand Up @@ -6465,7 +6466,7 @@ components:
properties:
feature_image_src:
type: string
description: Serializes the source of the feature_image
nullable: true
readOnly: true
page_url:
type: string
Expand Down
Loading
Loading