feat: publish CERTIFICATE_REVOKED events to the event bus
This PR adds the ability for the LMS to publish `CERTIFICATE_REVOKED` events to the Event Bus. There is also work in progress for Credentials to consume these events.
This commit is contained in:
@@ -28,3 +28,16 @@ AUTO_CERTIFICATE_GENERATION = WaffleSwitch(f"{WAFFLE_NAMESPACE}.auto_certificate
|
||||
# .. toggle_target_removal_date: 2023-07-31
|
||||
# .. toggle_tickets: TODO
|
||||
SEND_CERTIFICATE_CREATED_SIGNAL = SettingToggle('SEND_CERTIFICATE_CREATED_SIGNAL', default=False, module_name=__name__)
|
||||
|
||||
|
||||
# .. toggle_name: SEND_CERTIFICATE_REVOKED_SIGNAL
|
||||
# .. toggle_implementation: SettingToggle
|
||||
# .. toggle_default: False
|
||||
# .. toggle_description: When True, the system will publish `CERTIFICATE_REVOKED` signals to the event bus. The
|
||||
# `CERTIFICATE_REVOKED` signal is emit when a certificate has been awarded to a learner and the creation process has
|
||||
# completed.
|
||||
# .. toggle_use_cases: temporary
|
||||
# .. toggle_creation_date: 2023-09-15
|
||||
# .. toggle_target_removal_date: 2024-01-01
|
||||
# .. toggle_tickets: TODO
|
||||
SEND_CERTIFICATE_REVOKED_SIGNAL = SettingToggle('SEND_CERTIFICATE_REVOKED_SIGNAL', default=False, module_name=__name__)
|
||||
|
||||
@@ -378,6 +378,10 @@ class GeneratedCertificate(models.Model):
|
||||
|
||||
if not grade:
|
||||
grade = ''
|
||||
# the grade can come through revocation as a float, so we must convert it to a string to be compatible with the
|
||||
# `CERTIFICATE_REVOKED` event definition
|
||||
elif isinstance(grade, float):
|
||||
grade = str(grade)
|
||||
|
||||
if not mode:
|
||||
mode = self.mode
|
||||
|
||||
@@ -11,7 +11,7 @@ from openedx_events.event_bus import get_producer
|
||||
from common.djangoapps.course_modes import api as modes_api
|
||||
from common.djangoapps.student.models import CourseEnrollment
|
||||
from common.djangoapps.student.signals import ENROLLMENT_TRACK_UPDATED
|
||||
from lms.djangoapps.certificates.config import SEND_CERTIFICATE_CREATED_SIGNAL
|
||||
from lms.djangoapps.certificates.config import SEND_CERTIFICATE_CREATED_SIGNAL, SEND_CERTIFICATE_REVOKED_SIGNAL
|
||||
from lms.djangoapps.certificates.generation_handler import (
|
||||
CertificateGenerationNotAllowed,
|
||||
generate_allowlist_certificate_task,
|
||||
@@ -32,7 +32,7 @@ from openedx.core.djangoapps.signals.signals import (
|
||||
COURSE_GRADE_NOW_PASSED,
|
||||
LEARNER_NOW_VERIFIED
|
||||
)
|
||||
from openedx_events.learning.signals import CERTIFICATE_CREATED
|
||||
from openedx_events.learning.signals import CERTIFICATE_CREATED, CERTIFICATE_REVOKED
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -162,7 +162,7 @@ def _listen_for_enrollment_mode_change(sender, user, course_key, mode, **kwargs)
|
||||
|
||||
|
||||
@receiver(CERTIFICATE_CREATED)
|
||||
def listen_for_certificate_created_event(sender, signal, **kwargs):
|
||||
def listen_for_certificate_created_event(sender, signal, **kwargs): # pylint: disable=unused-argument
|
||||
"""
|
||||
Publish `CERTIFICATE_CREATED` events to the event bus.
|
||||
"""
|
||||
@@ -174,3 +174,18 @@ def listen_for_certificate_created_event(sender, signal, **kwargs):
|
||||
event_data={'certificate': kwargs['certificate']},
|
||||
event_metadata=kwargs['metadata']
|
||||
)
|
||||
|
||||
|
||||
@receiver(CERTIFICATE_REVOKED)
|
||||
def listen_for_certificate_revoked_event(sender, signal, **kwargs): # pylint: disable=unused-argument
|
||||
"""
|
||||
Publish `CERTIFICATE_REVOKED` events to the event bus.
|
||||
"""
|
||||
if SEND_CERTIFICATE_REVOKED_SIGNAL.is_enabled():
|
||||
get_producer().send(
|
||||
signal=CERTIFICATE_REVOKED,
|
||||
topic='learning-certificate-lifecycle',
|
||||
event_key_field='certificate.course.course_key',
|
||||
event_data={'certificate': kwargs['certificate']},
|
||||
event_metadata=kwargs['metadata']
|
||||
)
|
||||
|
||||
@@ -21,13 +21,16 @@ from lms.djangoapps.certificates.models import (
|
||||
CertificateGenerationConfiguration,
|
||||
GeneratedCertificate
|
||||
)
|
||||
from lms.djangoapps.certificates.signals import listen_for_certificate_created_event
|
||||
from lms.djangoapps.certificates.signals import (
|
||||
listen_for_certificate_created_event,
|
||||
listen_for_certificate_revoked_event
|
||||
)
|
||||
from lms.djangoapps.certificates.tests.factories import CertificateAllowlistFactory, GeneratedCertificateFactory
|
||||
from lms.djangoapps.grades.course_grade_factory import CourseGradeFactory
|
||||
from lms.djangoapps.grades.tests.utils import mock_passing_grade
|
||||
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification
|
||||
from openedx_events.data import EventsMetadata
|
||||
from openedx_events.learning.signals import CERTIFICATE_CREATED
|
||||
from openedx_events.learning.signals import CERTIFICATE_CREATED, CERTIFICATE_REVOKED
|
||||
from openedx_events.learning.data import CourseData, UserData, UserPersonalData, CertificateData
|
||||
|
||||
|
||||
@@ -458,22 +461,9 @@ class CertificateEventBusTests(ModuleStoreTestCase):
|
||||
mode='verified',
|
||||
)
|
||||
|
||||
@override_settings(SEND_CERTIFICATE_CREATED_SIGNAL=False)
|
||||
@mock.patch('lms.djangoapps.certificates.signals.get_producer', autospec=True)
|
||||
def test_event_disabled(self, mock_producer):
|
||||
def _create_event_data(self, event_type, certificate_status):
|
||||
"""
|
||||
Test to verify that we do not push `CERTIFICATE_CREATED` events to the event bus if the
|
||||
`SEND_CERTIFICATE_CREATED_SIGNAL` setting is disabled.
|
||||
"""
|
||||
listen_for_certificate_created_event(None, CERTIFICATE_CREATED)
|
||||
mock_producer.assert_not_called()
|
||||
|
||||
@override_settings(SEND_CERTIFICATE_CREATED_SIGNAL=True)
|
||||
@mock.patch('lms.djangoapps.certificates.signals.get_producer', autospec=True)
|
||||
def test_event_enabled(self, mock_producer):
|
||||
"""
|
||||
Test to verify that we push `CERTIFICATE_CREATED` events to the event bus if the
|
||||
`SEND_CERTIFICATE_CREATED_SIGNAL` setting is enabled.
|
||||
Utility function to create test data for unit tests.
|
||||
"""
|
||||
expected_course_data = CourseData(course_key=self.course.id)
|
||||
expected_user_data = UserData(
|
||||
@@ -490,12 +480,12 @@ class CertificateEventBusTests(ModuleStoreTestCase):
|
||||
course=expected_course_data,
|
||||
mode='verified',
|
||||
grade='',
|
||||
current_status='downloadable',
|
||||
current_status=certificate_status,
|
||||
download_url='',
|
||||
name='',
|
||||
)
|
||||
event_metadata = EventsMetadata(
|
||||
event_type=CERTIFICATE_CREATED.event_type,
|
||||
expected_event_metadata = EventsMetadata(
|
||||
event_type=event_type.event_type,
|
||||
id=uuid4(),
|
||||
minorversion=0,
|
||||
source='openedx/lms/web',
|
||||
@@ -503,15 +493,59 @@ class CertificateEventBusTests(ModuleStoreTestCase):
|
||||
time=datetime.now(timezone.utc)
|
||||
)
|
||||
|
||||
event_kwargs = {
|
||||
return {
|
||||
'certificate': expected_certificate_data,
|
||||
'metadata': event_metadata
|
||||
'metadata': expected_event_metadata,
|
||||
}
|
||||
|
||||
listen_for_certificate_created_event(None, CERTIFICATE_CREATED, **event_kwargs)
|
||||
@override_settings(SEND_CERTIFICATE_CREATED_SIGNAL=False)
|
||||
@mock.patch('lms.djangoapps.certificates.signals.get_producer', autospec=True)
|
||||
def test_certificate_created_event_disabled(self, mock_producer):
|
||||
"""
|
||||
Test to verify that we do not publish `CERTIFICATE_CREATED` events to the event bus if the
|
||||
`SEND_CERTIFICATE_CREATED_SIGNAL` setting is disabled.
|
||||
"""
|
||||
listen_for_certificate_created_event(None, CERTIFICATE_CREATED)
|
||||
mock_producer.assert_not_called()
|
||||
|
||||
@override_settings(SEND_CERTIFICATE_REVOKED_SIGNAL=False)
|
||||
@mock.patch('lms.djangoapps.certificates.signals.get_producer', autospec=True)
|
||||
def test_certificate_revoked_event_disabled(self, mock_producer):
|
||||
"""
|
||||
Test to verify that we do not publish `CERTIFICATE_REVOKED` events to the event bus if the
|
||||
`SEND_CERTIFICATE_REVOKED_SIGNAL` setting is disabled.
|
||||
"""
|
||||
listen_for_certificate_created_event(None, CERTIFICATE_REVOKED)
|
||||
mock_producer.assert_not_called()
|
||||
|
||||
@override_settings(SEND_CERTIFICATE_CREATED_SIGNAL=True)
|
||||
@mock.patch('lms.djangoapps.certificates.signals.get_producer', autospec=True)
|
||||
def test_certificate_created_event_enabled(self, mock_producer):
|
||||
"""
|
||||
Test to verify that we push `CERTIFICATE_CREATED` events to the event bus if the
|
||||
`SEND_CERTIFICATE_CREATED_SIGNAL` setting is enabled.
|
||||
"""
|
||||
event_data = self._create_event_data(CERTIFICATE_CREATED, CertificateStatuses.downloadable)
|
||||
listen_for_certificate_created_event(None, CERTIFICATE_CREATED, **event_data)
|
||||
# verify that the data sent to the event bus matches what we expect
|
||||
data = mock_producer.return_value.send.call_args.kwargs
|
||||
assert data['signal'].event_type == CERTIFICATE_CREATED.event_type
|
||||
assert data['event_data']['certificate'] == expected_certificate_data
|
||||
assert data['event_data']['certificate'] == event_data['certificate']
|
||||
assert data['topic'] == 'learning-certificate-lifecycle'
|
||||
assert data['event_key_field'] == 'certificate.course.course_key'
|
||||
|
||||
@override_settings(SEND_CERTIFICATE_REVOKED_SIGNAL=True)
|
||||
@mock.patch('lms.djangoapps.certificates.signals.get_producer', autospec=True)
|
||||
def test_certificate_revoked_event_enabled(self, mock_producer):
|
||||
"""
|
||||
Test to verify that we push `CERTIFICATE_REVOKED` events to the event bus if the
|
||||
`SEND_CERTIFICATE_REVOKED_SIGNAL` setting is enabled.
|
||||
"""
|
||||
event_data = self._create_event_data(CERTIFICATE_REVOKED, CertificateStatuses.notpassing)
|
||||
listen_for_certificate_revoked_event(None, CERTIFICATE_REVOKED, **event_data)
|
||||
# verify that the data sent to the event bus matches what we expect
|
||||
data = mock_producer.return_value.send.call_args.kwargs
|
||||
assert data['signal'].event_type == CERTIFICATE_REVOKED.event_type
|
||||
assert data['event_data']['certificate'] == event_data['certificate']
|
||||
assert data['topic'] == 'learning-certificate-lifecycle'
|
||||
assert data['event_key_field'] == 'certificate.course.course_key'
|
||||
|
||||
Reference in New Issue
Block a user