From 8ba02adbdb9c7d80417e1b0d7d2cb8026d52093d Mon Sep 17 00:00:00 2001 From: Zaman Afzal Date: Thu, 6 Aug 2026 17:15:41 +0500 Subject: [PATCH 1/4] Use CMS Certificate Title for program verifiable credentials (#3698) Co-authored-by: Claude Opus 4.7 --- courses/api.py | 4 +++- courses/api_test.py | 42 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/courses/api.py b/courses/api.py index 8b22c9046b..5aeffb84bf 100644 --- a/courses/api.py +++ b/courses/api.py @@ -1779,7 +1779,9 @@ def get_verifiable_credentials_payload( program = certificate.program program_page = program.program_page url = get_learn_product_url("programs", program.readable_id) - certificate_name = certificate.program.title + certificate_name = ( + certificate_page.product_name or "" + ).strip() or certificate.program.title activity_start_date = ProgramEnrollment.all_objects.get( user_id=certificate.user_id, program=program ).created_on.strftime("%Y-%m-%dT%H:%M:%SZ") diff --git a/courses/api_test.py b/courses/api_test.py index 2617ae243a..5d919369bd 100644 --- a/courses/api_test.py +++ b/courses/api_test.py @@ -3377,6 +3377,7 @@ def test_program_certificate_verifiable_credentials( mock_certificate_page = Mock() mock_certificate_page.verifiable_credential_criteria = "mock_credential_data" mock_certificate_page.should_provision_verifiable_credential = True + mock_certificate_page.product_name = "Test Program Certificate" mocker.patch("courses.api.get_certificate_page", return_value=mock_certificate_page) courses = CourseFactory.create_batch(3) course_runs = CourseRunFactory.create_batch(3, course=factory.Iterator(courses)) @@ -3588,6 +3589,9 @@ def test_program_certificate_verifiable_credentials_signing_payload( mock_certificate_page = Mock() mock_certificate_page.verifiable_credential_criteria = "mock_credential_data" + # The verifiable credential name should come from the CMS "Certificate Title" + # (product_name), not the program title. + mock_certificate_page.product_name = "Universal AI" payload = get_verifiable_credentials_payload(program_cert, mock_certificate_page) # Assert the expected payload structure @@ -3630,8 +3634,8 @@ def test_program_certificate_verifiable_credentials_signing_payload( "criteria": { "narrative": mock_certificate_page.verifiable_credential_criteria }, - "description": "Jane Smith has successfully completed all modules and earned a Program Certificate in Data Science MicroMasters.", - "name": "Data Science MicroMasters", + "description": "Jane Smith has successfully completed all modules and earned a Program Certificate in Universal AI.", + "name": "Universal AI", "image": { "id": "https://example.com/program-thumbnail.jpg", "type": "Image", @@ -3644,6 +3648,40 @@ def test_program_certificate_verifiable_credentials_signing_payload( assert payload == expected_payload +@pytest.mark.parametrize("product_name", ["", " "]) +@patch("courses.api.ProgramEnrollment.all_objects.get") +@patch("courses.api.get_thumbnail_url") +def test_program_verifiable_credential_name_falls_back_to_program_title( + mock_get_thumbnail_url, mock_enrollment_get, product_name, settings, mocker +): + """The VC name falls back to the program title when product_name is blank.""" + mocker.patch("hubspot_sync.task_helpers.sync_hubspot_user") + mocker.patch("hubspot_sync.api.upsert_custom_properties") + + mock_enrollment = Mock() + mock_enrollment.created_on = datetime( + 2024, 2, 20, 14, 45, 0, tzinfo=ZoneInfo("UTC") + ) + mock_enrollment_get.return_value = mock_enrollment + mock_get_thumbnail_url.return_value = "" + + settings.ENVIRONMENT = "production" + + program_cert = ProgramCertificateFactory.create() + program_cert.program.title = "Data Science MicroMasters" + program_cert.program.save() + + mock_certificate_page = Mock() + mock_certificate_page.verifiable_credential_criteria = "mock_credential_data" + mock_certificate_page.product_name = product_name + + payload = get_verifiable_credentials_payload(program_cert, mock_certificate_page) + + achievement = payload["credentialSubject"]["achievement"] + assert achievement["name"] == "Data Science MicroMasters" + assert "Data Science MicroMasters" in achievement["description"] + + @pytest.mark.parametrize( "keep_failed_enrollments,flag_enabled,expected_behavior", # noqa: PT006 [ From 5151d654716a0207e281660edb5e106f4cf03491 Mon Sep 17 00:00:00 2001 From: Tobias Macey Date: Thu, 6 Aug 2026 08:39:40 -0400 Subject: [PATCH 2/4] Add zizmor pre-commit hook and 7-day uv dependency delay (#3808) Co-authored-by: Claude Sonnet 5 --- .pre-commit-config.yaml | 5 ++++ pyproject.toml | 49 +++++++++++++++++++++++++++++++++++++++ uv.lock | 51 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 417ae6f245..e67de1005f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -65,6 +65,11 @@ repos: - id: actionlint name: actionlint description: Runs actionlint to lint GitHub Actions workflow files + - repo: https://github.com/zizmorcore/zizmor-pre-commit + rev: v1.29.0 + hooks: + - id: zizmor + args: [--no-progress, --min-severity=high, --min-confidence=medium] - repo: local hooks: - id: drf-serializer-orm-check diff --git a/pyproject.toml b/pyproject.toml index 356e768fd8..2d10889ab9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -117,6 +117,55 @@ dev = [ [tool.uv] package = false default-groups = "all" +exclude-newer = "7d" +required-version = ">=0.9.17" + +[tool.uv.exclude-newer-package] +ol-concourse = "0d" +django-aqueduct = "0d" +edx-sysadmin = "0d" +edx-username-changer = "0d" +ol-openedx-ai-static-translations = "0d" +ol-openedx-auto-select-language = "0d" +ol-openedx-canvas-integration = "0d" +ol-openedx-chat = "0d" +ol-openedx-chat-xblock = "0d" +ol-openedx-checkout-external = "0d" +ol-openedx-course-export = "0d" +ol-openedx-course-outline-api = "0d" +ol-openedx-course-structure-api = "0d" +ol-openedx-course-sync = "0d" +ol-openedx-course-translations = "0d" +ol-openedx-events-handler = "0d" +ol-openedx-feedback = "0d" +ol-openedx-git-auto-export = "0d" +ol-openedx-logging = "0d" +ol-openedx-lti-utilities = "0d" +ol-openedx-otel-monitoring = "0d" +ol-openedx-rapid-response-reports = "0d" +ol-openedx-sentry = "0d" +ol-openedx-uai-content-customization = "0d" +ol-social-auth = "0d" +openedx-companion-auth = "0d" +rapid-response-xblock = "0d" +mitol-django-common = "0d" +mitol-django-mail = "0d" +mitol-django-authentication = "0d" +mitol-django-digitalcredentials = "0d" +mitol-django-geoip = "0d" +mitol-django-google-sheets = "0d" +mitol-django-google-sheets-deferrals = "0d" +mitol-django-google-sheets-refunds = "0d" +mitol-django-hubspot-api = "0d" +mitol-django-oauth-toolkit-extensions = "0d" +mitol-django-olposthog = "0d" +mitol-django-openedx = "0d" +mitol-django-payment-gateway = "0d" +mitol-django-transcoding = "0d" +mitol-django-apigateway = "0d" +mitol-django-observability = "0d" +mitol-django-scim = "0d" +mitol-drf-lint = "0d" [tool.uv.sources] diff --git a/uv.lock b/uv.lock index 7a50f0c214..fcdfde401f 100644 --- a/uv.lock +++ b/uv.lock @@ -2,6 +2,57 @@ version = 1 revision = 3 requires-python = "==3.11.*" +[options] +exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. +exclude-newer-span = "P7D" + +[options.exclude-newer-package] +mitol-django-google-sheets = { timestamp = "0001-01-01T00:00:00Z", span = "PT0S" } +ol-openedx-uai-content-customization = { timestamp = "0001-01-01T00:00:00Z", span = "PT0S" } +ol-openedx-canvas-integration = { timestamp = "0001-01-01T00:00:00Z", span = "PT0S" } +ol-concourse = { timestamp = "0001-01-01T00:00:00Z", span = "PT0S" } +mitol-django-olposthog = { timestamp = "0001-01-01T00:00:00Z", span = "PT0S" } +mitol-django-oauth-toolkit-extensions = { timestamp = "0001-01-01T00:00:00Z", span = "PT0S" } +ol-social-auth = { timestamp = "0001-01-01T00:00:00Z", span = "PT0S" } +mitol-django-payment-gateway = { timestamp = "0001-01-01T00:00:00Z", span = "PT0S" } +ol-openedx-ai-static-translations = { timestamp = "0001-01-01T00:00:00Z", span = "PT0S" } +mitol-django-google-sheets-refunds = { timestamp = "0001-01-01T00:00:00Z", span = "PT0S" } +ol-openedx-chat-xblock = { timestamp = "0001-01-01T00:00:00Z", span = "PT0S" } +ol-openedx-events-handler = { timestamp = "0001-01-01T00:00:00Z", span = "PT0S" } +ol-openedx-feedback = { timestamp = "0001-01-01T00:00:00Z", span = "PT0S" } +ol-openedx-logging = { timestamp = "0001-01-01T00:00:00Z", span = "PT0S" } +ol-openedx-rapid-response-reports = { timestamp = "0001-01-01T00:00:00Z", span = "PT0S" } +mitol-django-observability = { timestamp = "0001-01-01T00:00:00Z", span = "PT0S" } +mitol-django-authentication = { timestamp = "0001-01-01T00:00:00Z", span = "PT0S" } +ol-openedx-lti-utilities = { timestamp = "0001-01-01T00:00:00Z", span = "PT0S" } +ol-openedx-checkout-external = { timestamp = "0001-01-01T00:00:00Z", span = "PT0S" } +mitol-django-geoip = { timestamp = "0001-01-01T00:00:00Z", span = "PT0S" } +ol-openedx-git-auto-export = { timestamp = "0001-01-01T00:00:00Z", span = "PT0S" } +ol-openedx-chat = { timestamp = "0001-01-01T00:00:00Z", span = "PT0S" } +mitol-django-hubspot-api = { timestamp = "0001-01-01T00:00:00Z", span = "PT0S" } +openedx-companion-auth = { timestamp = "0001-01-01T00:00:00Z", span = "PT0S" } +ol-openedx-course-sync = { timestamp = "0001-01-01T00:00:00Z", span = "PT0S" } +mitol-django-digitalcredentials = { timestamp = "0001-01-01T00:00:00Z", span = "PT0S" } +mitol-django-common = { timestamp = "0001-01-01T00:00:00Z", span = "PT0S" } +ol-openedx-sentry = { timestamp = "0001-01-01T00:00:00Z", span = "PT0S" } +rapid-response-xblock = { timestamp = "0001-01-01T00:00:00Z", span = "PT0S" } +mitol-django-mail = { timestamp = "0001-01-01T00:00:00Z", span = "PT0S" } +mitol-django-google-sheets-deferrals = { timestamp = "0001-01-01T00:00:00Z", span = "PT0S" } +ol-openedx-course-translations = { timestamp = "0001-01-01T00:00:00Z", span = "PT0S" } +ol-openedx-course-outline-api = { timestamp = "0001-01-01T00:00:00Z", span = "PT0S" } +mitol-django-transcoding = { timestamp = "0001-01-01T00:00:00Z", span = "PT0S" } +edx-username-changer = { timestamp = "0001-01-01T00:00:00Z", span = "PT0S" } +ol-openedx-auto-select-language = { timestamp = "0001-01-01T00:00:00Z", span = "PT0S" } +mitol-django-apigateway = { timestamp = "0001-01-01T00:00:00Z", span = "PT0S" } +ol-openedx-course-structure-api = { timestamp = "0001-01-01T00:00:00Z", span = "PT0S" } +mitol-drf-lint = { timestamp = "0001-01-01T00:00:00Z", span = "PT0S" } +django-aqueduct = { timestamp = "0001-01-01T00:00:00Z", span = "PT0S" } +ol-openedx-otel-monitoring = { timestamp = "0001-01-01T00:00:00Z", span = "PT0S" } +edx-sysadmin = { timestamp = "0001-01-01T00:00:00Z", span = "PT0S" } +ol-openedx-course-export = { timestamp = "0001-01-01T00:00:00Z", span = "PT0S" } +mitol-django-openedx = { timestamp = "0001-01-01T00:00:00Z", span = "PT0S" } +mitol-django-scim = { timestamp = "0001-01-01T00:00:00Z", span = "PT0S" } + [[package]] name = "amqp" version = "5.3.1" From 919ad0c3f84367758d9f3e13e571dfc76265f1b2 Mon Sep 17 00:00:00 2001 From: Dan Subak Date: Thu, 6 Aug 2026 10:20:27 -0400 Subject: [PATCH 3/4] Strip angle brackets (#3825) --- b2b/mail.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/b2b/mail.py b/b2b/mail.py index 6a661e2382..9acd02a651 100644 --- a/b2b/mail.py +++ b/b2b/mail.py @@ -56,7 +56,14 @@ def send_email_helper( # noqa: PLR0913 == "anymail.backends.mailgun.EmailBackend" ): recipient_status = message.anymail_status.recipients.get(email) - message_id = recipient_status.message_id if recipient_status else None + # Message ID is in the following format when pulled from anymail + # '<20260806133209.67c51081a4f1c478@mitxonline-rc-mail.mitxonline.mit.edu>' + # The webhook doesn't have the leading or trailing angle brackets, so we'll remove those + message_id = ( + recipient_status.message_id.strip("<>") + if recipient_status + else None + ) else: message_id = str(uuid.uuid4()) From 294a8eae72c826cf5fee704c6c8b087920360fc8 Mon Sep 17 00:00:00 2001 From: Doof Date: Thu, 6 Aug 2026 16:05:13 +0000 Subject: [PATCH 4/4] Release 1.162.3 --- RELEASE.rst | 7 +++++++ main/settings.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/RELEASE.rst b/RELEASE.rst index d68f530cc4..f8c1e512dd 100644 --- a/RELEASE.rst +++ b/RELEASE.rst @@ -1,6 +1,13 @@ Release Notes ============= +Version 1.162.3 +--------------- + +- Strip angle brackets (#3825) +- Add zizmor pre-commit hook and 7-day uv dependency delay (#3808) +- Use CMS Certificate Title for program verifiable credentials (#3698) + Version 1.162.2 (Released August 06, 2026) --------------- diff --git a/main/settings.py b/main/settings.py index 0e8ffdf9b6..934855d000 100644 --- a/main/settings.py +++ b/main/settings.py @@ -39,7 +39,7 @@ from main.sentry import init_sentry from openapi.settings_spectacular import open_spectacular_settings -VERSION = "1.162.2" +VERSION = "1.162.3" log = logging.getLogger()