feat: publish CERTIFICATE_CREATED events to the event bus

[APER-2344]

We would like to start consuming Certificate related events in Credentials from the event bus. This PR starts the process by publishing CERTIFICATE_CREATED events to the event bus. It also introduces a new feature flag (`SEND_CERTIFICATE_CREATED_SIGNAL`) to gate the functionality.
This commit is contained in:
Justin Hynes
2023-04-13 19:23:04 +00:00
parent b852344fcf
commit 2e68ca2377
4 changed files with 121 additions and 3 deletions

View File

@@ -3,7 +3,7 @@ This module contains various configuration settings via
waffle switches for the Certificates app.
"""
from edx_toggles.toggles import WaffleSwitch
from edx_toggles.toggles import SettingToggle, WaffleSwitch
# Namespace
WAFFLE_NAMESPACE = 'certificates'
@@ -15,3 +15,16 @@ WAFFLE_NAMESPACE = 'certificates'
# .. toggle_use_cases: open_edx
# .. toggle_creation_date: 2017-09-14
AUTO_CERTIFICATE_GENERATION = WaffleSwitch(f"{WAFFLE_NAMESPACE}.auto_certificate_generation", __name__)
# .. toggle_name: SEND_CERTIFICATE_CREATED_SIGNAL
# .. toggle_implementation: SettingToggle
# .. toggle_default: False
# .. toggle_description: When True, the system will publish `CERTIFICATE_CREATED` signals to the event bus. The
# `CERTIFICATE_CREATED` 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-04-11
# .. toggle_target_removal_date: 2023-07-31
# .. toggle_tickets: TODO
SEND_CERTIFICATE_CREATED_SIGNAL = SettingToggle('SEND_CERTIFICATE_CREATED_SIGNAL', default=False, module_name=__name__)

View File

@@ -2,7 +2,7 @@
Course certificates are created for a student and an offering of a course (a course run).
"""
from datetime import timezone
import json
import logging
import os
@@ -403,6 +403,7 @@ class GeneratedCertificate(models.Model):
# .. event_implemented_name: CERTIFICATE_REVOKED
CERTIFICATE_REVOKED.send_event(
time=self.modified_date.astimezone(timezone.utc),
certificate=CertificateData(
user=UserData(
pii=UserPersonalData(
@@ -473,6 +474,9 @@ class GeneratedCertificate(models.Model):
Credentials IDA.
"""
super().save(*args, **kwargs)
timestamp = self.modified_date.astimezone(timezone.utc)
COURSE_CERT_CHANGED.send_robust(
sender=self.__class__,
user=self.user,
@@ -483,6 +487,7 @@ class GeneratedCertificate(models.Model):
# .. event_implemented_name: CERTIFICATE_CHANGED
CERTIFICATE_CHANGED.send_event(
time=timestamp,
certificate=CertificateData(
user=UserData(
pii=UserPersonalData(
@@ -515,6 +520,7 @@ class GeneratedCertificate(models.Model):
# .. event_implemented_name: CERTIFICATE_CREATED
CERTIFICATE_CREATED.send_event(
time=timestamp,
certificate=CertificateData(
user=UserData(
pii=UserPersonalData(

View File

@@ -6,10 +6,12 @@ import logging
from django.db.models.signals import post_save
from django.dispatch import receiver
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.generation_handler import (
CertificateGenerationNotAllowed,
generate_allowlist_certificate_task,
@@ -30,6 +32,7 @@ from openedx.core.djangoapps.signals.signals import (
COURSE_GRADE_NOW_PASSED,
LEARNER_NOW_VERIFIED
)
from openedx_events.learning.signals import CERTIFICATE_CREATED
log = logging.getLogger(__name__)
@@ -156,3 +159,18 @@ def _listen_for_enrollment_mode_change(sender, user, course_key, mode, **kwargs)
course_key,
)
return False
@receiver(CERTIFICATE_CREATED)
def listen_for_certificate_created_event(sender, signal, **kwargs):
"""
Publish `CERTIFICATE_CREATED` events to the event bus.
"""
if SEND_CERTIFICATE_CREATED_SIGNAL.is_enabled():
get_producer().send(
signal=CERTIFICATE_CREATED,
topic='certificates',
event_key_field='certificate.course.course_key',
event_data={'certificate': kwargs['certificate']},
event_metadata=kwargs['metadata']
)

View File

@@ -3,10 +3,12 @@ Unit tests for enabling self-generated certificates for self-paced courses
and disabling for instructor-paced courses.
"""
from datetime import datetime, timezone
from unittest import mock
from uuid import uuid4
import ddt
from django.test.utils import override_settings
from edx_toggles.toggles.testutils import override_waffle_flag, override_waffle_switch
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
@@ -19,10 +21,14 @@ from lms.djangoapps.certificates.models import (
CertificateGenerationConfiguration,
GeneratedCertificate
)
from lms.djangoapps.certificates.signals import listen_for_certificate_created_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.data import CourseData, UserData, UserPersonalData, CertificateData
class SelfGeneratedCertsSignalTest(ModuleStoreTestCase):
@@ -434,3 +440,78 @@ class EnrollmentModeChangeCertsTest(ModuleStoreTestCase):
) as mock_allowlist_task:
self.verified_enrollment.change_mode('audit')
mock_allowlist_task.assert_not_called()
class CertificateEventBusTests(ModuleStoreTestCase):
"""
Tests for Certificate events that interact with the event bus.
"""
def setUp(self):
super().setUp()
self.user = UserFactory.create()
self.name = f'{self.user.first_name} {self.user.last_name}'
self.course = CourseFactory.create(self_paced=True)
self.enrollment = CourseEnrollmentFactory(
user=self.user,
course_id=self.course.id,
is_active=True,
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):
"""
Test to verify that we push `CERTIFICATE_CREATED` events to the event bus if the
`SEND_CERTIFICATE_CREATED_SIGNAL` setting is enabled.
"""
expected_course_data = CourseData(course_key=self.course.id)
expected_user_data = UserData(
pii=UserPersonalData(
username=self.user.username,
email=self.user.email,
name=self.name,
),
id=self.user.id,
is_active=self.user.is_active
)
expected_certificate_data = CertificateData(
user=expected_user_data,
course=expected_course_data,
mode='verified',
grade='',
current_status='downloadable',
download_url='',
name='',
)
event_metadata = EventsMetadata(
event_type=CERTIFICATE_CREATED.event_type,
id=uuid4(),
minorversion=0,
source='openedx/lms/web',
sourcehost='lms.test',
time=datetime.now(timezone.utc)
)
event_kwargs = {
'certificate': expected_certificate_data,
'metadata': event_metadata
}
listen_for_certificate_created_event(None, CERTIFICATE_CREATED, **event_kwargs)
# 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['topic'] == 'certificates'
assert data['event_key_field'] == 'certificate.course.course_key'