Skip to content

Commit

Permalink
Merge pull request #33318 from openedx/jhynes/APER-2649_certificate-r…
Browse files Browse the repository at this point in the history
…evoked-event-bus-support

feat: publish `CERTIFICATE_REVOKED` events to the event bus
  • Loading branch information
justinhynes authored Sep 25, 2023
2 parents bceba32 + f4c7f98 commit 3d33b8c
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 27 deletions.
13 changes: 13 additions & 0 deletions lms/djangoapps/certificates/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,16 @@
# .. 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 revoked from a learner and the revocation 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__)
4 changes: 4 additions & 0 deletions lms/djangoapps/certificates/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,10 @@ def _revoke_certificate(self, status, mode=None, grade=None, source=None):

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
Expand Down
21 changes: 18 additions & 3 deletions lms/djangoapps/certificates/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
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,
Expand All @@ -32,7 +32,7 @@
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__)

Expand Down Expand Up @@ -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.
"""
Expand All @@ -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']
)
82 changes: 58 additions & 24 deletions lms/djangoapps/certificates/tests/test_signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,16 @@
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


Expand Down Expand Up @@ -458,22 +461,9 @@ def setUp(self):
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):
"""
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):
def _create_event_data(self, event_type, certificate_status):
"""
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(
Expand All @@ -490,28 +480,72 @@ def test_event_enabled(self, mock_producer):
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',
sourcehost='lms.test',
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'

0 comments on commit 3d33b8c

Please sign in to comment.