Skip to content

Comments

PADV-3091: Add ENABLE_CCX_CERTIFICATES feature#190

Open
sergivalero20 wants to merge 4 commits intopearson-release/olive.stagefrom
vue/PADV-3091
Open

PADV-3091: Add ENABLE_CCX_CERTIFICATES feature#190
sergivalero20 wants to merge 4 commits intopearson-release/olive.stagefrom
vue/PADV-3091

Conversation

@sergivalero20
Copy link

Ticket

Description

Added a feature flag ENABLE_CCX_CERTIFICATES to allow certificate generation for CCX (Custom Courses for edX) courses. By default, edx-platform blocks certificate generation for all CCX courses. This feature flag enables operators to opt-in to CCX certificate support without modifying the blocking logic permanently.

Changes Made

  • Added ENABLE_CCX_CERTIFICATES: False to the FEATURES dictionary, placed directly below the related CUSTOM_COURSES_EDX flag.
  • Update unit tests

How to test

  • Deploy edx-platform with the feature flag changes
  • Create a master course with:
    Certificate template configured at org level
    cert_html_view_enabled = True
    HTML certificates enabled globally (CERTIFICATES_HTML_VIEW = True)
  • Create a CCX from the master course
  • Enroll a test student in the CCX with mode no-id-professional

Copy and parte following lines in the Django Shell:

from django.contrib.auth.models import User
from opaque_keys.edx.locator import CCXLocator
from lms.djangoapps.grades.api import CourseGradeFactory
from lms.djangoapps.grades.course_grade_factory import CourseGradeFactory
from openedx.core.djangoapps.signals.signals import COURSE_GRADE_NOW_PASSED


from unittest.mock import Mock

user = User.objects.get(username='<student_username>')
ccx_key = CCXLocator.from_string('ccx-v1:<org>+<course>+<run>+ccx@<id>')

mock_grade = Mock()
mock_grade.percent = 0.9
mock_grade.passed = True

COURSE_GRADE_NOW_PASSED.send(
    sender=None,
    user=user,
    course_id=ccx_key,
    course_grade=mock_grade,
)

After firing, verify the certificate was created:

from lms.djangoapps.certificates.models import GeneratedCertificate

cert = GeneratedCertificate.objects.filter(user=user, course_id=ccx_key).first()
if cert:
    print(f"Status: {cert.status}, Grade: {cert.grade}, Mode: {cert.mode}")
else:
    print("No certificate generated")

@sergivalero20 sergivalero20 self-assigned this Feb 10, 2026
@sergivalero20 sergivalero20 added the feature New feature label Feb 10, 2026
self.grade)
assert _set_regular_cert_status(self.user, self.course_run_key, self.enrollment_mode, self.grade) is None

@override_settings(FEATURES={**settings.FEATURES, 'ENABLE_CCX_CERTIFICATES': False})

Choose a reason for hiding this comment

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

For readability and organization I think it's better to organize these test cases in their own class and avoid expanding the current one. @sergivalero20

) is None

@override_settings(FEATURES={**settings.FEATURES, 'ENABLE_CCX_CERTIFICATES': False})
def test_ccx_blocked_when_flag_disabled(self):

Choose a reason for hiding this comment

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

I think there is no substantial difference between this test and test_ccx_blocked_by_default. @sergivalero20

Test that CCX certificates are blocked when ENABLE_CCX_CERTIFICATES is not set (default behavior).
"""
with mock.patch(CCX_COURSE_METHOD, return_value=True):
assert not _can_generate_regular_certificate(

Choose a reason for hiding this comment

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

These test classes are using UnitTest from the std lib: https://docs.python.org/3/library/unittest.html so I think it's better to create the assertions using assertions method (e.g. https://docs.python.org/3/library/unittest.html#unittest.TestCase.assertEqual) instead of using Pytest-styled assertions. @sergivalero20

"""
if _is_ccx_course(course_key):
if _is_ccx_course(course_key) and not settings.FEATURES.get('ENABLE_CCX_CERTIFICATES', False):
log.info(f'{course_key} is a CCX course. Certificate cannot be generated for {user.id}.')

Choose a reason for hiding this comment

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

I think the message should change to highlight this change. @sergivalero20

mode=self.enrollment_mode,
)

def test_blocked_by_default(self):

Choose a reason for hiding this comment

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

I guess there is something missing here. You are not testing a CCX course here, you are testing a regular course and a disable ENABLE_CCX_CERTIFICATES, which I think could be misleading in certain ways. So I think you should run these tests with an actual CCX course, not a regular course. @sergivalero20

Copy link
Author

Choose a reason for hiding this comment

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

Since I was mocking the behavior I decided to use master course, however, you are right!
I will use CCX instead.
@Squirrel18

Choose a reason for hiding this comment

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

Thanks!.

I think you can combine test_status_blocked_by_default and test_blocked_by_default. @sergivalero20

assert not result['is_active']


@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')

Choose a reason for hiding this comment

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

Why these tests are only valid in the LMS? @sergivalero20

Copy link
Author

Choose a reason for hiding this comment

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

@Squirrel18 ok, you're right!
The decorator is unnecessary

mode=self.enrollment_mode,
)

# --- Blocked by default ---

Choose a reason for hiding this comment

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

Is this necessary? @sergivalero20

"""
Test that CCX certificate status cannot be set when flag is not set.
"""
self.assertIsNone(_set_regular_cert_status(

Choose a reason for hiding this comment

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

Isn't _can_set_regular_cert_status the one we should be testing it? @sergivalero20 and I guess the assertion is assertIsFalse, which the actual value returned.

Test that CCX certificates are blocked when ENABLE_CCX_CERTIFICATES is not set (default behavior).
"""
self.assertFalse(_can_generate_regular_certificate(
self.user, self.ccx_key, self.enrollment_mode, self.grade

Choose a reason for hiding this comment

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

Trailing comma. @sergivalero20

Test that CCX certificate status cannot be set when flag is not set.
"""
self.assertIsNone(_set_regular_cert_status(
self.user, self.ccx_key, self.enrollment_mode, self.grade

Choose a reason for hiding this comment

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

Trailing comma. @sergivalero20

Test that CCX certificates are blocked when ENABLE_CCX_CERTIFICATES is explicitly False.
"""
self.assertFalse(_can_generate_regular_certificate(
self.user, self.ccx_key, self.enrollment_mode, self.grade

Choose a reason for hiding this comment

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

Trailing comma. @sergivalero20

# --- Allowed when flag enabled ---

@override_settings(FEATURES={**settings.FEATURES, 'ENABLE_CCX_CERTIFICATES': True})
def test_can_generate_when_flag_enabled(self):

Choose a reason for hiding this comment

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

I think you are missing _can_set_regular_cert_status. @sergivalero20

Test that CCX certificates can be generated when ENABLE_CCX_CERTIFICATES is True.
"""
self.assertTrue(_can_generate_regular_certificate(
self.user, self.ccx_key, self.enrollment_mode, self.grade

Choose a reason for hiding this comment

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

Trailing comma. @sergivalero20

"""
Test that certificate status can be set for CCX courses when ENABLE_CCX_CERTIFICATES is True.
"""
GeneratedCertificateFactory(

Choose a reason for hiding this comment

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

Is this part of an integration test? @sergivalero20

self.user, regular_key, self.enrollment_mode, self.grade
))

@override_settings(FEATURES={**settings.FEATURES, 'ENABLE_CCX_CERTIFICATES': True})

Choose a reason for hiding this comment

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

Are these tests necessary? you only change: _can_generate_regular_certificate and _can_set_regular_cert_status, so I don't think these are necessary for a unit test. @sergivalero20

# Default enrollment mode for CCX courses when ENABLE_CCX_CERTIFICATES is True.
# Must be a certificate-eligible mode (anything except 'audit').
# Common choices: 'honor', 'no-id-professional', 'professional', 'verified'.
CCX_DEFAULT_ENROLLMENT_MODE = 'honor'

Choose a reason for hiding this comment

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

Please, use the standard way to document new settings or feature toggles: https://docs.openedx.org/projects/edx-toggles/en/latest/how_to/documenting_new_feature_toggles.html

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants