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

Version 0.141.2
---------------

- 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 +27 to 29
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 was not updated to handle a null feature_image_src, causing a missing image for courses in the program enrollment drawer.
Severity: MEDIUM

Suggested Fix

Update ProgramCourseInfoCard.js to provide a fallback to a default image when course.feature_image_src is null or falsy. This can be done by using a logical OR operator, for example: const imageUrl = course.feature_image_src || DEFAULT_COURSE_IMG;.

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#L27-L29

Potential issue: The `get_feature_image_src` method in serializers like
`BaseCoursePageSerializer` was modified to return `None` instead of a default image path
if no image exists. While several frontend components were updated to handle this change
by providing a fallback image, the `ProgramCourseInfoCard.js` component was missed. This
component checks `if (course.feature_image_src)` before rendering an image. When the API
returns `null` for this field, the condition fails, and no image is rendered at all.
This causes a missing image in the UI for courses within the program enrollment drawer
that do not have a specific feature image set.

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
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 = "0.141.2"

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
5 changes: 3 additions & 2 deletions openapi/specs/v1.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
5 changes: 3 additions & 2 deletions openapi/specs/v2.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