From 6b41eba8cf01795ed7fa1db7723ce91f7bcf1151 Mon Sep 17 00:00:00 2001 From: annagav Date: Wed, 20 May 2026 16:39:54 -0400 Subject: [PATCH 1/3] Update Learner Program Record Styling and remove header (#3593) --- frontend/public/scss/learner-records.scss | 14 ++-- frontend/public/src/containers/App.js | 18 ++++- frontend/public/src/containers/App_test.js | 34 ++++++++++ .../pages/records/LearnerRecordsPage.js | 8 +-- .../pages/records/LearnerRecordsPage_test.js | 68 +++++++++++++++++++ 5 files changed, 128 insertions(+), 14 deletions(-) create mode 100644 frontend/public/src/containers/pages/records/LearnerRecordsPage_test.js diff --git a/frontend/public/scss/learner-records.scss b/frontend/public/scss/learner-records.scss index dd5ca72a35..b043be7ea2 100644 --- a/frontend/public/scss/learner-records.scss +++ b/frontend/public/scss/learner-records.scss @@ -20,17 +20,13 @@ } div.learner-record-inst-logo { - padding: 20px 40px; - border: 1px solid $home-page-border-grey; - border-radius: 5px; - align-self: start; - align-items: center; - margin: 0; + display: flex; + padding: 20px 32px 20px 32px; } - img { - width: 200px; - height: auto; + .learner-record-inst-logo img { + height: 30.7569px; + opacity: 1; } } diff --git a/frontend/public/src/containers/App.js b/frontend/public/src/containers/App.js index f4961f8e00..ee946beda5 100644 --- a/frontend/public/src/containers/App.js +++ b/frontend/public/src/containers/App.js @@ -95,6 +95,20 @@ export class App extends React.Component { ) } + isLearnerRecordsPage() { + const { match, location } = this.props + return ( + !!matchPath(location.pathname, { + path: urljoin(match.url, String(routes.learnerRecords)), + exact: false + }) || + !!matchPath(location.pathname, { + path: urljoin(match.url, String(routes.sharedLearnerRecord)), + exact: false + }) + ) + } + render() { const { match, currentUser, cartItemsCount, location } = this.props if (!currentUser) { @@ -104,7 +118,9 @@ export class App extends React.Component { return (
- {!this.isEcomServiceMode() && !this.isCheckoutRelatedPage() && ( + {!this.isEcomServiceMode() && + !this.isCheckoutRelatedPage() && + !this.isLearnerRecordsPage() && (
{ assert.isFalse(inner.find("Header").exists()) }) + it("does not render header on learner record page", async () => { + helper.handleRequestStub.returns(anonymousUser) + renderPage = helper.configureMountRenderer( + App, + InnerApp, + {}, + { + match: { url: routes.root }, + location: { + pathname: "/records/1/" + } + } + ) + const { inner } = await renderPage() + assert.isFalse(inner.find("Header").exists()) + }) + + it("does not render header on shared learner record page", async () => { + helper.handleRequestStub.returns(anonymousUser) + renderPage = helper.configureMountRenderer( + App, + InnerApp, + {}, + { + match: { url: routes.root }, + location: { + pathname: "/records/shared/test-uuid/" + } + } + ) + const { inner } = await renderPage() + assert.isFalse(inner.find("Header").exists()) + }) + it("renders header on dashboard page", async () => { helper.handleRequestStub.returns(anonymousUser) renderPage = helper.configureMountRenderer( diff --git a/frontend/public/src/containers/pages/records/LearnerRecordsPage.js b/frontend/public/src/containers/pages/records/LearnerRecordsPage.js index 37be9ab041..9a6f9a1c52 100644 --- a/frontend/public/src/containers/pages/records/LearnerRecordsPage.js +++ b/frontend/public/src/containers/pages/records/LearnerRecordsPage.js @@ -476,7 +476,7 @@ export class LearnerRecordsPage extends React.Component {

{learnerRecord ? learnerRecord.program.title : - "MITx Online Program Record"} + "MIT Learn Program Record"}

Program Record

@@ -517,7 +517,7 @@ export class LearnerRecordsPage extends React.Component { id="learner-record-school-name" >
-
MITx Online Program Record
+
MIT Learn Program Record

{learnerRecord ? learnerRecord.program.title : null}

@@ -537,8 +537,8 @@ export class LearnerRecordsPage extends React.Component {
MITx Online Logo
diff --git a/frontend/public/src/containers/pages/records/LearnerRecordsPage_test.js b/frontend/public/src/containers/pages/records/LearnerRecordsPage_test.js new file mode 100644 index 0000000000..f16aa86b79 --- /dev/null +++ b/frontend/public/src/containers/pages/records/LearnerRecordsPage_test.js @@ -0,0 +1,68 @@ +// @flow +import { assert } from "chai" +import { shallow } from "enzyme" + +import LearnerRecordsPage, { + LearnerRecordsPage as InnerLearnerRecordsPage +} from "./LearnerRecordsPage" +import { makeLearnerRecord } from "../../../factories/course" +import IntegrationTestHelper from "../../../util/integration_test_helper" + +describe("LearnerRecordsPage", () => { + let helper, renderPage + + beforeEach(() => { + helper = new IntegrationTestHelper() + global.SETTINGS = { + site_name: "MITx Online", + support_email: "support@example.com" + } + + renderPage = helper.configureShallowRenderer( + LearnerRecordsPage, + InnerLearnerRecordsPage, + {}, + { + learnerRecord: null, + isSharedRecord: false, + history: {}, + isLoading: false, + addUserNotification: helper.sandbox.stub(), + forceRequest: helper.sandbox.stub(), + enableRecordSharing: helper.sandbox.stub().resolves({}), + revokeRecordSharing: helper.sandbox.stub().resolves({}), + match: { params: { program: "1" } }, + currentUser: { is_authenticated: true } + } + ) + }) + + afterEach(() => { + helper.cleanup() + delete global.SETTINGS + }) + + it("keeps the records page title banner", async () => { + const { inner } = await renderPage() + const pageHeader = inner.find(".std-page-header").first() + + assert.isTrue(pageHeader.exists()) + assert.equal(pageHeader.find("h1").first().text(), "Program Record") + }) + + it("renders the MIT logo in the learner record header", async () => { + const learnerRecord = makeLearnerRecord(true) + const { inner } = await renderPage({}, { learnerRecord }) + const learnerRecordTable = shallow( + inner.instance().renderLearnerRecordTable(learnerRecord) + ) + + const logo = learnerRecordTable + .find(".learner-record-inst-logo img") + .first() + + assert.isTrue(logo.exists()) + assert.equal(logo.prop("src"), "/static/images/mit-black-logo.png") + assert.equal(logo.prop("alt"), "MIT Logo") + }) +}) From dbd51b56a891b0c5109da5f2c61ffb0db6ff28e7 Mon Sep 17 00:00:00 2001 From: James Kachel Date: Thu, 21 May 2026 13:45:18 -0500 Subject: [PATCH 2/3] Fix consideration for program certificate generation (#3597) --- courses/api.py | 15 +++++++--- courses/api_test.py | 29 +++++++++++++++++++ .../tests/manage_program_certificate_test.py | 11 ++++++- 3 files changed, 50 insertions(+), 5 deletions(-) diff --git a/courses/api.py b/courses/api.py index 224272d934..184faf35f5 100644 --- a/courses/api.py +++ b/courses/api.py @@ -1115,18 +1115,25 @@ def _has_earned_program_cert(user, program): bool: True if a user has earned all the course certificates required for a given program else False """ - program_course_ids = [course[0].id for course in program.courses] + + user_courseruns = CourseRun.objects.filter( + enrollments__user=user, + enrollments__active=True, + enrollments__change_status__isnull=True, + ).filter(course__in=program.courses_qset) cert_courses = Course.objects.filter( - id__in=program_course_ids, + courseruns__in=user_courseruns, courseruns__courseruncertificates__user=user, courseruns__courseruncertificates__is_revoked=False, ) grade_courses = Course.objects.filter( - id__in=program_course_ids, + courseruns__in=user_courseruns.exclude( + enrollment_modes__mode_slug=EDX_ENROLLMENT_VERIFIED_MODE + ), courseruns__grades__user=user, courseruns__grades__passed=True, - ).exclude(courseruns__enrollment_modes__mode_slug=EDX_ENROLLMENT_VERIFIED_MODE) + ) root = ProgramRequirement.get_root_nodes().get(program=program) def _has_earned(node): diff --git a/courses/api_test.py b/courses/api_test.py index 3f4b19f617..f79151682e 100644 --- a/courses/api_test.py +++ b/courses/api_test.py @@ -1641,6 +1641,9 @@ def test_generate_program_certificate_success_single_requirement_course( course_run = CourseRunFactory.create(course=course) course_run.enrollment_modes.set(default_mode_records) course_run.save() + CourseRunEnrollmentFactory.create( + run=course_run, user=user, enrollment_mode=EDX_ENROLLMENT_VERIFIED_MODE + ) CourseRunGradeFactory.create(course_run=course_run, user=user, passed=True, grade=1) CourseRunCertificateFactory.create(user=user, course_run=course_run) @@ -1686,6 +1689,12 @@ def test_generate_program_certificate_success_multiple_required_courses( run.enrollment_modes.set(default_mode_records) run.save() + CourseRunEnrollmentFactory.create_batch( + 3, + run=factory.Iterator(course_runs), + user=user, + enrollment_mode=EDX_ENROLLMENT_VERIFIED_MODE, + ) CourseRunCertificateFactory.create_batch( 3, user=user, course_run=factory.Iterator(course_runs) ) @@ -1995,6 +2004,9 @@ def test_generate_program_certificate_with_subprogram_requirement( # noqa: PLR0 sub_course_run = CourseRunFactory.create(course=sub_course) sub_course_run.enrollment_modes.set(default_mode_records) sub_course_run.save() + CourseRunEnrollmentFactory.create( + run=sub_course_run, user=user, enrollment_mode=EDX_ENROLLMENT_VERIFIED_MODE + ) CourseRunGradeFactory.create( course_run=sub_course_run, user=user, passed=True, grade=1 ) @@ -2065,6 +2077,9 @@ def test_generate_program_certificate_with_revoked_subprogram_certificate( # User completes the sub-program and gets a certificate, but it gets revoked sub_course_run = CourseRunFactory.create(course=sub_course) + CourseRunEnrollmentFactory.create( + run=sub_course_run, user=user, enrollment_mode=EDX_ENROLLMENT_VERIFIED_MODE + ) CourseRunGradeFactory.create( course_run=sub_course_run, user=user, passed=True, grade=1 ) @@ -2122,6 +2137,14 @@ def test_generate_program_certificate_audit_courses(user, default_mode_records): program.add_requirement(cert_course) program.add_requirement(audit_course) + # Add some additional course runs for the audit course. + # This test missed a case - if the course had a mix of runs that were both + # audit-only and not, the certificates wouldn't be generated. + + audit_verified_course_run = CourseRunFactory.create(course=audit_course) + audit_verified_course_run.enrollment_modes.set(default_mode_records) + audit_verified_course_run.save() + ProgramEnrollment.objects.create( user=user, program=program, @@ -3478,6 +3501,9 @@ def test_generate_missing_program_certificates_creates_cert( course_run = CourseRunFactory.create(course=course) course_run.enrollment_modes.set(default_mode_records) course_run.save() + CourseRunEnrollmentFactory.create( + run=course_run, user=user, enrollment_mode=EDX_ENROLLMENT_VERIFIED_MODE + ) ProgramEnrollment.objects.create( user=user, program=program, enrollment_mode=EDX_ENROLLMENT_VERIFIED_MODE @@ -3510,6 +3536,9 @@ def test_generate_missing_program_certificates_idempotent( course_run = CourseRunFactory.create(course=course) course_run.enrollment_modes.set(default_mode_records) course_run.save() + CourseRunEnrollmentFactory.create( + run=course_run, user=user, enrollment_mode=EDX_ENROLLMENT_VERIFIED_MODE + ) ProgramEnrollment.objects.create( user=user, program=program, enrollment_mode=EDX_ENROLLMENT_VERIFIED_MODE diff --git a/courses/management/tests/manage_program_certificate_test.py b/courses/management/tests/manage_program_certificate_test.py index daa3afec55..bac98a2ee2 100644 --- a/courses/management/tests/manage_program_certificate_test.py +++ b/courses/management/tests/manage_program_certificate_test.py @@ -125,10 +125,18 @@ def test_program_certificate_management_revoke_unrevoke_success(user, revoke, un assert certificate.is_revoked is (False if unrevoke else True) # noqa: SIM211 +@pytest.mark.parametrize( + "force_cert", + [ + True, + False, + ], +) def test_program_certificate_management_create( user, program_with_empty_requirements, # noqa: F811 mocker, + force_cert, ): """ Test that create operation for program certificate management command @@ -162,13 +170,14 @@ def test_program_certificate_management_create( create=True, program=program_with_empty_requirements.readable_id, user=user.edx_username, + force=force_cert, ) generated_certificates = ProgramCertificate.objects.filter( user=user, program=program_with_empty_requirements ) - assert generated_certificates.count() == 1 + assert generated_certificates.count() == (1 if force_cert else 0) def test_program_certificate_management_force_create( From a7e754e6f794965c3d1f492c6f476ec4e8968429 Mon Sep 17 00:00:00 2001 From: Doof Date: Thu, 21 May 2026 18:46:26 +0000 Subject: [PATCH 3/3] Release 1.150.7 --- RELEASE.rst | 6 ++++++ main/settings.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/RELEASE.rst b/RELEASE.rst index 6d0cdfa8b5..6c3715eb56 100644 --- a/RELEASE.rst +++ b/RELEASE.rst @@ -1,6 +1,12 @@ Release Notes ============= +Version 1.150.7 +--------------- + +- Fix consideration for program certificate generation (#3597) +- Update Learner Program Record Styling and remove header (#3593) + Version 1.150.6 (Released May 20, 2026) --------------- diff --git a/main/settings.py b/main/settings.py index b8885c7654..ae7204bbfd 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 = "1.150.6" +VERSION = "1.150.7" log = logging.getLogger()