Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 51 additions & 1 deletion common/djangoapps/student/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,11 @@
UserData,
UserPersonalData,
)
from openedx_events.learning.signals import COURSE_ENROLLMENT_CREATED
from openedx_events.learning.signals import (
COURSE_ENROLLMENT_CHANGED,
COURSE_ENROLLMENT_CREATED,
COURSE_UNENROLLMENT_COMPLETED,
)
import openedx.core.djangoapps.django_comment_common.comment_client as cc
from common.djangoapps.course_modes.models import CourseMode, get_cosmetic_verified_display_price
from common.djangoapps.student.emails import send_proctoring_requirements_email
Expand Down Expand Up @@ -1417,6 +1421,16 @@ def update_enrollment(self, mode=None, is_active=None, skip_refund=False, enterp
self.mode = mode
mode_changed = True

try:
course_data = CourseData(
course_key=self.course_id,
display_name=self.course.display_name,
)
except CourseOverview.DoesNotExist:
course_data = CourseData(
course_key=self.course_id,
)

if activation_changed or mode_changed:
self.save()
self._update_enrollment_in_request_cache(
Expand All @@ -1425,6 +1439,24 @@ def update_enrollment(self, mode=None, is_active=None, skip_refund=False, enterp
CourseEnrollmentState(self.mode, self.is_active),
)

COURSE_ENROLLMENT_CHANGED.send_event(
enrollment=CourseEnrollmentData(
user=UserData(
pii=UserPersonalData(
username=self.user.username,
email=self.user.email,
name=self.user.profile.name,
),
id=self.user.id,
is_active=self.user.is_active,
),
course=course_data,
mode=self.mode,
is_active=self.is_active,
creation_date=self.created,
)
)

if activation_changed:
if self.is_active:
self.emit_event(EVENT_NAME_ENROLLMENT_ACTIVATED, enterprise_uuid=enterprise_uuid)
Expand All @@ -1433,6 +1465,24 @@ def update_enrollment(self, mode=None, is_active=None, skip_refund=False, enterp
self.emit_event(EVENT_NAME_ENROLLMENT_DEACTIVATED)
self.send_signal(EnrollStatusChange.unenroll)

COURSE_UNENROLLMENT_COMPLETED.send_event(
enrollment=CourseEnrollmentData(
user=UserData(
pii=UserPersonalData(
username=self.user.username,
email=self.user.email,
name=self.user.profile.name,
),
id=self.user.id,
is_active=self.user.is_active,
),
course=course_data,
mode=self.mode,
is_active=self.is_active,
creation_date=self.created,
)
)

if mode_changed:
# If mode changed to one that requires proctoring, send proctoring requirements email
if should_send_proctoring_requirements_email(self.user.username, self.course_id):
Expand Down
100 changes: 98 additions & 2 deletions common/djangoapps/student/tests/test_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@
UserData,
UserPersonalData,
)
from openedx_events.learning.signals import COURSE_ENROLLMENT_CREATED
from openedx_events.learning.signals import (
COURSE_ENROLLMENT_CHANGED,
COURSE_ENROLLMENT_CREATED,
COURSE_UNENROLLMENT_COMPLETED,
)
from openedx_events.tests.utils import OpenEdxEventsTestMixin
from openedx.core.djangolib.testing.utils import skip_unless_lms

Expand Down Expand Up @@ -203,9 +207,15 @@ class EnrollmentEventsTest(SharedModuleStoreTestCase, OpenEdxEventsTestMixin):
the exact Data Attributes as the event definition stated:

- COURSE_ENROLLMENT_CREATED: sent after the user's enrollment.
- COURSE_ENROLLMENT_CHANGED: sent after the enrollment update.
- COURSE_UNENROLLMENT_COMPLETED: sent after the user's unenrollment.
"""

ENABLED_OPENEDX_EVENTS = ["org.openedx.learning.course.enrollment.created.v1"]
ENABLED_OPENEDX_EVENTS = [
"org.openedx.learning.course.enrollment.created.v1",
"org.openedx.learning.course.enrollment.changed.v1",
"org.openedx.learning.course.unenrollment.completed.v1",
]

@classmethod
def setUpClass(cls):
Expand Down Expand Up @@ -276,3 +286,89 @@ def test_enrollment_created_event_emitted(self):
},
event_receiver.call_args.kwargs
)

def test_enrollment_changed_event_emitted(self):
"""
Test whether the student enrollment changed event is sent after the enrollment
update process ends.

Expected result:
- COURSE_ENROLLMENT_CHANGED is sent and received by the mocked receiver.
- The arguments that the receiver gets are the arguments sent by the event
except the metadata generated on the fly.
"""
enrollment = CourseEnrollment.enroll(self.user, self.course.id)
event_receiver = mock.Mock(side_effect=self._event_receiver_side_effect)
COURSE_ENROLLMENT_CHANGED.connect(event_receiver)

enrollment.update_enrollment(mode="verified")

self.assertTrue(self.receiver_called)
self.assertDictContainsSubset(
{
"signal": COURSE_ENROLLMENT_CHANGED,
"sender": None,
"enrollment": CourseEnrollmentData(
user=UserData(
pii=UserPersonalData(
username=self.user.username,
email=self.user.email,
name=self.user.profile.name,
),
id=self.user.id,
is_active=self.user.is_active,
),
course=CourseData(
course_key=self.course.id,
display_name=self.course.display_name,
),
mode=enrollment.mode,
is_active=enrollment.is_active,
creation_date=enrollment.created,
),
},
event_receiver.call_args.kwargs
)

def test_unenrollment_completed_event_emitted(self):
"""
Test whether the student un-enrollment completed event is sent after the
user's unenrollment process.

Expected result:
- COURSE_UNENROLLMENT_COMPLETED is sent and received by the mocked receiver.
- The arguments that the receiver gets are the arguments sent by the event
except the metadata generated on the fly.
"""
enrollment = CourseEnrollment.enroll(self.user, self.course.id)
event_receiver = mock.Mock(side_effect=self._event_receiver_side_effect)
COURSE_UNENROLLMENT_COMPLETED.connect(event_receiver)

CourseEnrollment.unenroll(self.user, self.course.id)

self.assertTrue(self.receiver_called)
self.assertDictContainsSubset(
{
"signal": COURSE_UNENROLLMENT_COMPLETED,
"sender": None,
"enrollment": CourseEnrollmentData(
user=UserData(
pii=UserPersonalData(
username=self.user.username,
email=self.user.email,
name=self.user.profile.name,
),
id=self.user.id,
is_active=self.user.is_active,
),
course=CourseData(
course_key=self.course.id,
display_name=self.course.display_name,
),
mode=enrollment.mode,
is_active=False,
creation_date=enrollment.created,
),
},
event_receiver.call_args.kwargs
)
70 changes: 70 additions & 0 deletions lms/djangoapps/certificates/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@
from openedx.core.djangoapps.signals.signals import COURSE_CERT_AWARDED, COURSE_CERT_CHANGED, COURSE_CERT_REVOKED
from openedx.core.djangoapps.xmodule_django.models import NoneToEmptyManager

from openedx_events.learning.data import CourseData, UserData, UserPersonalData, CertificateData
from openedx_events.learning.signals import CERTIFICATE_CHANGED, CERTIFICATE_CREATED, CERTIFICATE_REVOKED

log = logging.getLogger(__name__)
User = get_user_model()

Expand Down Expand Up @@ -391,6 +394,28 @@ def _revoke_certificate(self, status, mode=None, grade=None, source=None):
status=self.status,
)

CERTIFICATE_REVOKED.send_event(
certificate=CertificateData(
user=UserData(
pii=UserPersonalData(
username=self.user.username,
email=self.user.email,
name=self.user.profile.name,
),
id=self.user.id,
is_active=self.user.is_active,
),
course=CourseData(
course_key=self.course_id,
),
mode=self.mode,
grade=self.grade,
current_status=self.status,
download_url=self.download_url,
name=self.name,
)
)

if previous_certificate_status == CertificateStatuses.downloadable:
# imported here to avoid a circular import issue
from lms.djangoapps.certificates.utils import emit_certificate_event
Expand Down Expand Up @@ -446,6 +471,29 @@ def save(self, *args, **kwargs): # pylint: disable=signature-differs
mode=self.mode,
status=self.status,
)

CERTIFICATE_CHANGED.send_event(
certificate=CertificateData(
user=UserData(
pii=UserPersonalData(
username=self.user.username,
email=self.user.email,
name=self.user.profile.name,
),
id=self.user.id,
is_active=self.user.is_active,
),
course=CourseData(
course_key=self.course_id,
),
mode=self.mode,
grade=self.grade,
current_status=self.status,
download_url=self.download_url,
name=self.name,
)
)

if CertificateStatuses.is_passing_status(self.status):
COURSE_CERT_AWARDED.send_robust(
sender=self.__class__,
Expand All @@ -455,6 +503,28 @@ def save(self, *args, **kwargs): # pylint: disable=signature-differs
status=self.status,
)

CERTIFICATE_CREATED.send_event(
certificate=CertificateData(
user=UserData(
pii=UserPersonalData(
username=self.user.username,
email=self.user.email,
name=self.user.profile.name,
),
id=self.user.id,
is_active=self.user.is_active,
),
course=CourseData(
course_key=self.course_id,
),
mode=self.mode,
grade=self.grade,
current_status=self.status,
download_url=self.download_url,
name=self.name,
)
)


class CertificateGenerationHistory(TimeStampedModel):
"""
Expand Down
Loading