Skip to content

Commit 1c447e6

Browse files
feat: add 2nd batch of Open edX Events
* Add COURSE_ENROLLMENT_CHANGED: sent after the enrollment update * Add COURSE_UNENROLLMENT_COMPLETED: sent after the user's unenrollment * Add CERTIFICATE_CREATED after the user's certificate generation has been completed * Add CERTIFICATE_CHANGED: after the certification update has been completed * Add CERTIFICATE_REVOKED: after the certificate revocation has been completed * Add COHORT_MEMBERSHIP_CHANGED: when a cohort membership update ends
1 parent 15b965c commit 1c447e6

File tree

11 files changed

+694
-14
lines changed

11 files changed

+694
-14
lines changed

common/djangoapps/student/models.py

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,11 @@
6161
UserData,
6262
UserPersonalData,
6363
)
64-
from openedx_events.learning.signals import COURSE_ENROLLMENT_CREATED
64+
from openedx_events.learning.signals import (
65+
COURSE_ENROLLMENT_CHANGED,
66+
COURSE_ENROLLMENT_CREATED,
67+
COURSE_UNENROLLMENT_COMPLETED,
68+
)
6569
import openedx.core.djangoapps.django_comment_common.comment_client as cc
6670
from common.djangoapps.course_modes.models import CourseMode, get_cosmetic_verified_display_price
6771
from common.djangoapps.student.emails import send_proctoring_requirements_email
@@ -1417,6 +1421,16 @@ def update_enrollment(self, mode=None, is_active=None, skip_refund=False, enterp
14171421
self.mode = mode
14181422
mode_changed = True
14191423

1424+
try:
1425+
course_data = CourseData(
1426+
course_key=self.course_id,
1427+
display_name=self.course.display_name,
1428+
)
1429+
except CourseOverview.DoesNotExist:
1430+
course_data = CourseData(
1431+
course_key=self.course_id,
1432+
)
1433+
14201434
if activation_changed or mode_changed:
14211435
self.save()
14221436
self._update_enrollment_in_request_cache(
@@ -1425,6 +1439,24 @@ def update_enrollment(self, mode=None, is_active=None, skip_refund=False, enterp
14251439
CourseEnrollmentState(self.mode, self.is_active),
14261440
)
14271441

1442+
COURSE_ENROLLMENT_CHANGED.send_event(
1443+
enrollment=CourseEnrollmentData(
1444+
user=UserData(
1445+
pii=UserPersonalData(
1446+
username=self.user.username,
1447+
email=self.user.email,
1448+
name=self.user.profile.name,
1449+
),
1450+
id=self.user.id,
1451+
is_active=self.user.is_active,
1452+
),
1453+
course=course_data,
1454+
mode=self.mode,
1455+
is_active=self.is_active,
1456+
creation_date=self.created,
1457+
)
1458+
)
1459+
14281460
if activation_changed:
14291461
if self.is_active:
14301462
self.emit_event(EVENT_NAME_ENROLLMENT_ACTIVATED, enterprise_uuid=enterprise_uuid)
@@ -1433,6 +1465,24 @@ def update_enrollment(self, mode=None, is_active=None, skip_refund=False, enterp
14331465
self.emit_event(EVENT_NAME_ENROLLMENT_DEACTIVATED)
14341466
self.send_signal(EnrollStatusChange.unenroll)
14351467

1468+
COURSE_UNENROLLMENT_COMPLETED.send_event(
1469+
enrollment=CourseEnrollmentData(
1470+
user=UserData(
1471+
pii=UserPersonalData(
1472+
username=self.user.username,
1473+
email=self.user.email,
1474+
name=self.user.profile.name,
1475+
),
1476+
id=self.user.id,
1477+
is_active=self.user.is_active,
1478+
),
1479+
course=course_data,
1480+
mode=self.mode,
1481+
is_active=self.is_active,
1482+
creation_date=self.created,
1483+
)
1484+
)
1485+
14361486
if mode_changed:
14371487
# If mode changed to one that requires proctoring, send proctoring requirements email
14381488
if should_send_proctoring_requirements_email(self.user.username, self.course_id):

common/djangoapps/student/tests/test_events.py

Lines changed: 98 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,11 @@
2020
UserData,
2121
UserPersonalData,
2222
)
23-
from openedx_events.learning.signals import COURSE_ENROLLMENT_CREATED
23+
from openedx_events.learning.signals import (
24+
COURSE_ENROLLMENT_CHANGED,
25+
COURSE_ENROLLMENT_CREATED,
26+
COURSE_UNENROLLMENT_COMPLETED,
27+
)
2428
from openedx_events.tests.utils import OpenEdxEventsTestMixin
2529
from openedx.core.djangolib.testing.utils import skip_unless_lms
2630

@@ -203,9 +207,15 @@ class EnrollmentEventsTest(SharedModuleStoreTestCase, OpenEdxEventsTestMixin):
203207
the exact Data Attributes as the event definition stated:
204208
205209
- COURSE_ENROLLMENT_CREATED: sent after the user's enrollment.
210+
- COURSE_ENROLLMENT_CHANGED: sent after the enrollment update.
211+
- COURSE_UNENROLLMENT_COMPLETED: sent after the user's unenrollment.
206212
"""
207213

208-
ENABLED_OPENEDX_EVENTS = ["org.openedx.learning.course.enrollment.created.v1"]
214+
ENABLED_OPENEDX_EVENTS = [
215+
"org.openedx.learning.course.enrollment.created.v1",
216+
"org.openedx.learning.course.enrollment.changed.v1",
217+
"org.openedx.learning.course.unenrollment.completed.v1",
218+
]
209219

210220
@classmethod
211221
def setUpClass(cls):
@@ -276,3 +286,89 @@ def test_enrollment_created_event_emitted(self):
276286
},
277287
event_receiver.call_args.kwargs
278288
)
289+
290+
def test_enrollment_changed_event_emitted(self):
291+
"""
292+
Test whether the student enrollment changed event is sent after the enrollment
293+
update process ends.
294+
295+
Expected result:
296+
- COURSE_ENROLLMENT_CHANGED is sent and received by the mocked receiver.
297+
- The arguments that the receiver gets are the arguments sent by the event
298+
except the metadata generated on the fly.
299+
"""
300+
enrollment = CourseEnrollment.enroll(self.user, self.course.id)
301+
event_receiver = mock.Mock(side_effect=self._event_receiver_side_effect)
302+
COURSE_ENROLLMENT_CHANGED.connect(event_receiver)
303+
304+
enrollment.update_enrollment(mode="verified")
305+
306+
self.assertTrue(self.receiver_called)
307+
self.assertDictContainsSubset(
308+
{
309+
"signal": COURSE_ENROLLMENT_CHANGED,
310+
"sender": None,
311+
"enrollment": CourseEnrollmentData(
312+
user=UserData(
313+
pii=UserPersonalData(
314+
username=self.user.username,
315+
email=self.user.email,
316+
name=self.user.profile.name,
317+
),
318+
id=self.user.id,
319+
is_active=self.user.is_active,
320+
),
321+
course=CourseData(
322+
course_key=self.course.id,
323+
display_name=self.course.display_name,
324+
),
325+
mode=enrollment.mode,
326+
is_active=enrollment.is_active,
327+
creation_date=enrollment.created,
328+
),
329+
},
330+
event_receiver.call_args.kwargs
331+
)
332+
333+
def test_unenrollment_completed_event_emitted(self):
334+
"""
335+
Test whether the student un-enrollment completed event is sent after the
336+
user's unenrollment process.
337+
338+
Expected result:
339+
- COURSE_UNENROLLMENT_COMPLETED is sent and received by the mocked receiver.
340+
- The arguments that the receiver gets are the arguments sent by the event
341+
except the metadata generated on the fly.
342+
"""
343+
enrollment = CourseEnrollment.enroll(self.user, self.course.id)
344+
event_receiver = mock.Mock(side_effect=self._event_receiver_side_effect)
345+
COURSE_UNENROLLMENT_COMPLETED.connect(event_receiver)
346+
347+
CourseEnrollment.unenroll(self.user, self.course.id)
348+
349+
self.assertTrue(self.receiver_called)
350+
self.assertDictContainsSubset(
351+
{
352+
"signal": COURSE_UNENROLLMENT_COMPLETED,
353+
"sender": None,
354+
"enrollment": CourseEnrollmentData(
355+
user=UserData(
356+
pii=UserPersonalData(
357+
username=self.user.username,
358+
email=self.user.email,
359+
name=self.user.profile.name,
360+
),
361+
id=self.user.id,
362+
is_active=self.user.is_active,
363+
),
364+
course=CourseData(
365+
course_key=self.course.id,
366+
display_name=self.course.display_name,
367+
),
368+
mode=enrollment.mode,
369+
is_active=False,
370+
creation_date=enrollment.created,
371+
),
372+
},
373+
event_receiver.call_args.kwargs
374+
)

lms/djangoapps/certificates/models.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@
3535
from openedx.core.djangoapps.signals.signals import COURSE_CERT_AWARDED, COURSE_CERT_CHANGED, COURSE_CERT_REVOKED
3636
from openedx.core.djangoapps.xmodule_django.models import NoneToEmptyManager
3737

38+
from openedx_events.learning.data import CourseData, UserData, UserPersonalData, CertificateData
39+
from openedx_events.learning.signals import CERTIFICATE_CHANGED, CERTIFICATE_CREATED, CERTIFICATE_REVOKED
40+
3841
log = logging.getLogger(__name__)
3942
User = get_user_model()
4043

@@ -391,6 +394,28 @@ def _revoke_certificate(self, status, mode=None, grade=None, source=None):
391394
status=self.status,
392395
)
393396

397+
CERTIFICATE_REVOKED.send_event(
398+
certificate=CertificateData(
399+
user=UserData(
400+
pii=UserPersonalData(
401+
username=self.user.username,
402+
email=self.user.email,
403+
name=self.user.profile.name,
404+
),
405+
id=self.user.id,
406+
is_active=self.user.is_active,
407+
),
408+
course=CourseData(
409+
course_key=self.course_id,
410+
),
411+
mode=self.mode,
412+
grade=self.grade,
413+
current_status=self.status,
414+
download_url=self.download_url,
415+
name=self.name,
416+
)
417+
)
418+
394419
if previous_certificate_status == CertificateStatuses.downloadable:
395420
# imported here to avoid a circular import issue
396421
from lms.djangoapps.certificates.utils import emit_certificate_event
@@ -446,6 +471,29 @@ def save(self, *args, **kwargs): # pylint: disable=signature-differs
446471
mode=self.mode,
447472
status=self.status,
448473
)
474+
475+
CERTIFICATE_CHANGED.send_event(
476+
certificate=CertificateData(
477+
user=UserData(
478+
pii=UserPersonalData(
479+
username=self.user.username,
480+
email=self.user.email,
481+
name=self.user.profile.name,
482+
),
483+
id=self.user.id,
484+
is_active=self.user.is_active,
485+
),
486+
course=CourseData(
487+
course_key=self.course_id,
488+
),
489+
mode=self.mode,
490+
grade=self.grade,
491+
current_status=self.status,
492+
download_url=self.download_url,
493+
name=self.name,
494+
)
495+
)
496+
449497
if CertificateStatuses.is_passing_status(self.status):
450498
COURSE_CERT_AWARDED.send_robust(
451499
sender=self.__class__,
@@ -455,6 +503,28 @@ def save(self, *args, **kwargs): # pylint: disable=signature-differs
455503
status=self.status,
456504
)
457505

506+
CERTIFICATE_CREATED.send_event(
507+
certificate=CertificateData(
508+
user=UserData(
509+
pii=UserPersonalData(
510+
username=self.user.username,
511+
email=self.user.email,
512+
name=self.user.profile.name,
513+
),
514+
id=self.user.id,
515+
is_active=self.user.is_active,
516+
),
517+
course=CourseData(
518+
course_key=self.course_id,
519+
),
520+
mode=self.mode,
521+
grade=self.grade,
522+
current_status=self.status,
523+
download_url=self.download_url,
524+
name=self.name,
525+
)
526+
)
527+
458528

459529
class CertificateGenerationHistory(TimeStampedModel):
460530
"""

0 commit comments

Comments
 (0)