diff --git a/RELEASE.rst b/RELEASE.rst index c637268d3e..3e7d72b1c4 100644 --- a/RELEASE.rst +++ b/RELEASE.rst @@ -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) --------------- diff --git a/cms/serializers.py b/cms/serializers.py index 175e37f994..c1c00dd96d 100644 --- a/cms/serializers.py +++ b/cms/serializers.py @@ -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): @@ -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 @extend_schema_field(serializers.URLField) def get_page_url(self, instance): @@ -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): @@ -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 diff --git a/cms/wagtail_api/schema/serializers.py b/cms/wagtail_api/schema/serializers.py index 62d8660a4f..21955d0d30 100644 --- a/cms/wagtail_api/schema/serializers.py +++ b/cms/wagtail_api/schema/serializers.py @@ -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): diff --git a/courses/serializers/base.py b/courses/serializers/base.py index 92211be92e..7ecf2ab895 100644 --- a/courses/serializers/base.py +++ b/courses/serializers/base.py @@ -1,7 +1,6 @@ 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 @@ -9,14 +8,14 @@ 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 @@ -24,8 +23,10 @@ def get_thumbnail_url(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) diff --git a/courses/serializers/v1/programs_test.py b/courses/serializers/v1/programs_test.py index ad69513946..e906116ac7 100644 --- a/courses/serializers/v1/programs_test.py +++ b/courses/serializers/v1/programs_test.py @@ -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( diff --git a/frontend/public/src/components/CartItemCard.js b/frontend/public/src/components/CartItemCard.js index 0b646be34c..9ded043bef 100644 --- a/frontend/public/src/components/CartItemCard.js +++ b/frontend/public/src/components/CartItemCard.js @@ -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 } @@ -36,7 +38,10 @@ export class CartItemCard extends React.Component { abbreviation = purchasableObject.course_number image = course.page !== null ? ( - + ) : null detailLink = this.renderLink("Course details", pageUrl) statusMessage = courseRunStatusMessage(purchasableObject) @@ -51,7 +56,10 @@ export class CartItemCard extends React.Component { image = purchasableObject.page !== null && purchasableObject.page !== undefined ? ( - + ) : null detailLink = this.renderLink("Program details", pageUrl) statusMessage = null diff --git a/frontend/public/src/components/EnrolledItemCard.js b/frontend/public/src/components/EnrolledItemCard.js index de24fbf41f..c96a10d3c6 100644 --- a/frontend/public/src/components/EnrolledItemCard.js +++ b/frontend/public/src/components/EnrolledItemCard.js @@ -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, @@ -463,25 +465,19 @@ export class EnrolledItemCard extends React.Component< return (
- {enrollment.run.course.feature_image_src && ( -
-
- -
-
- )} - {!enrollment.run.course.feature_image_src && - enrollment.run.course.page && - enrollment.run.course.page.feature_image_src && ( -
-
- + {(() => { + const imgSrc = + enrollment.run.course.feature_image_src || + enrollment.run.course.page?.feature_image_src || + DEFAULT_COURSE_IMG + return ( +
+
+ +
-
- )} + ) + })()}
@@ -632,7 +628,13 @@ export class EnrolledItemCard extends React.Component<
- +
diff --git a/frontend/public/src/containers/pages/CatalogPage.js b/frontend/public/src/containers/pages/CatalogPage.js index 6ea969e5f2..418a4553c0 100644 --- a/frontend/public/src/containers/pages/CatalogPage.js +++ b/frontend/public/src/containers/pages/CatalogPage.js @@ -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, @@ -664,8 +666,11 @@ export class CatalogPage extends React.Component {
@@ -691,8 +696,11 @@ export class CatalogPage extends React.Component {
{program.program_type}
diff --git a/frontend/public/src/containers/pages/CatalogPage_test.js b/frontend/public/src/containers/pages/CatalogPage_test.js index e3f7c44b78..1063cafcff 100644 --- a/frontend/public/src/containers/pages/CatalogPage_test.js +++ b/frontend/public/src/containers/pages/CatalogPage_test.js @@ -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", @@ -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", @@ -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: [ diff --git a/main/settings.py b/main/settings.py index 718824451c..53adcd806e 100644 --- a/main/settings.py +++ b/main/settings.py @@ -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() diff --git a/openapi/specs/v0.yaml b/openapi/specs/v0.yaml index 772446edff..45464a6ae8 100644 --- a/openapi/specs/v0.yaml +++ b/openapi/specs/v0.yaml @@ -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 @@ -5032,6 +5032,7 @@ components: type: string feature_image_src: type: string + nullable: true required: - feature_image_src - id @@ -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 diff --git a/openapi/specs/v1.yaml b/openapi/specs/v1.yaml index f42c9ad59d..1a33493603 100644 --- a/openapi/specs/v1.yaml +++ b/openapi/specs/v1.yaml @@ -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 @@ -5032,6 +5032,7 @@ components: type: string feature_image_src: type: string + nullable: true required: - feature_image_src - id @@ -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 diff --git a/openapi/specs/v2.yaml b/openapi/specs/v2.yaml index f43140b622..0e1fe09e34 100644 --- a/openapi/specs/v2.yaml +++ b/openapi/specs/v2.yaml @@ -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 @@ -5032,6 +5032,7 @@ components: type: string feature_image_src: type: string + nullable: true required: - feature_image_src - id @@ -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