Skip to content

Commit c061859

Browse files
feat: add 1st batch of Open edX events
* Add STUDENT_REGISTRATION_COMPLETED event after the user's registration * Add SESSION_LOGIN_COMPLETED event after the user's login session * Add COURSE_ENROLLMENT_CREATED event after the user's enrollment creation
1 parent 32053c6 commit c061859

File tree

12 files changed

+504
-11
lines changed

12 files changed

+504
-11
lines changed

common/djangoapps/student/models.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,13 @@
5555
from slumber.exceptions import HttpClientError, HttpServerError
5656
from user_util import user_util
5757

58+
from openedx_events.learning.data import (
59+
CourseData,
60+
CourseEnrollmentData,
61+
UserData,
62+
UserPersonalData,
63+
)
64+
from openedx_events.learning.signals import COURSE_ENROLLMENT_CREATED
5865
import openedx.core.djangoapps.django_comment_common.comment_client as cc
5966
from common.djangoapps.course_modes.models import CourseMode, get_cosmetic_verified_display_price
6067
from common.djangoapps.student.emails import send_proctoring_requirements_email
@@ -1569,9 +1576,16 @@ def enroll(cls, user, course_key, mode=None, check_access=False, can_upgrade=Fal
15691576
# All the server-side checks for whether a user is allowed to enroll.
15701577
try:
15711578
course = CourseOverview.get_from_id(course_key)
1579+
course_data = CourseData(
1580+
course_key=course.id,
1581+
display_name=course.display_name,
1582+
)
15721583
except CourseOverview.DoesNotExist:
15731584
# This is here to preserve legacy behavior which allowed enrollment in courses
15741585
# announced before the start of content creation.
1586+
course_data = CourseData(
1587+
course_key=course_key,
1588+
)
15751589
if check_access:
15761590
log.warning("User %s failed to enroll in non-existent course %s", user.username, str(course_key))
15771591
raise NonExistentCourseError # lint-amnesty, pylint: disable=raise-missing-from
@@ -1607,6 +1621,25 @@ def enroll(cls, user, course_key, mode=None, check_access=False, can_upgrade=Fal
16071621
enrollment.update_enrollment(is_active=True, mode=mode, enterprise_uuid=enterprise_uuid)
16081622
enrollment.send_signal(EnrollStatusChange.enroll)
16091623

1624+
# Announce user's enrollment
1625+
COURSE_ENROLLMENT_CREATED.send_event(
1626+
enrollment=CourseEnrollmentData(
1627+
user=UserData(
1628+
pii=UserPersonalData(
1629+
username=user.username,
1630+
email=user.email,
1631+
name=user.profile.name,
1632+
),
1633+
id=user.id,
1634+
is_active=user.is_active,
1635+
),
1636+
course=course_data,
1637+
mode=enrollment.mode,
1638+
is_active=enrollment.is_active,
1639+
creation_date=enrollment.created,
1640+
)
1641+
)
1642+
16101643
return enrollment
16111644

16121645
@classmethod

common/djangoapps/student/tests/test_enrollment.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import pytest
1111
from django.conf import settings
1212
from django.urls import reverse
13+
from openedx_events.tests.utils import OpenEdxEventsTestMixin
1314

1415
from common.djangoapps.course_modes.models import CourseMode
1516
from common.djangoapps.course_modes.tests.factories import CourseModeFactory
@@ -30,19 +31,28 @@
3031
@ddt.ddt
3132
@patch.dict('django.conf.settings.FEATURES', {'ENABLE_SPECIAL_EXAMS': True})
3233
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
33-
class EnrollmentTest(UrlResetMixin, SharedModuleStoreTestCase):
34+
class EnrollmentTest(UrlResetMixin, SharedModuleStoreTestCase, OpenEdxEventsTestMixin):
3435
"""
3536
Test student enrollment, especially with different course modes.
3637
"""
3738

39+
ENABLED_OPENEDX_EVENTS = []
40+
3841
USERNAME = "Bob"
3942
4043
PASSWORD = "edx"
4144
URLCONF_MODULES = ['openedx.core.djangoapps.embargo']
4245

4346
@classmethod
4447
def setUpClass(cls):
48+
"""
49+
Set up class method for the Test class.
50+
51+
This method starts manually events isolation. Explanation here:
52+
openedx/core/djangoapps/user_authn/views/tests/test_events.py#L44
53+
"""
4554
super().setUpClass()
55+
cls.start_events_isolation()
4656
cls.course = CourseFactory.create()
4757
cls.course_limited = CourseFactory.create()
4858
cls.proctored_course = CourseFactory(

common/djangoapps/student/tests/test_events.py

Lines changed: 99 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,23 @@
1010
from django.test import TestCase
1111
from django_countries.fields import Country
1212

13-
from common.djangoapps.student.models import CourseEnrollmentAllowed
14-
from common.djangoapps.student.tests.factories import CourseEnrollmentAllowedFactory, UserFactory
13+
from common.djangoapps.student.models import CourseEnrollmentAllowed, CourseEnrollment
14+
from common.djangoapps.student.tests.factories import CourseEnrollmentAllowedFactory, UserFactory, UserProfileFactory
1515
from common.djangoapps.student.tests.tests import UserSettingsEventTestMixin
1616

17+
from openedx_events.learning.data import (
18+
CourseData,
19+
CourseEnrollmentData,
20+
UserData,
21+
UserPersonalData,
22+
)
23+
from openedx_events.learning.signals import COURSE_ENROLLMENT_CREATED
24+
from openedx_events.tests.utils import OpenEdxEventsTestMixin
25+
from openedx.core.djangolib.testing.utils import skip_unless_lms
26+
27+
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
28+
from xmodule.modulestore.tests.factories import CourseFactory
29+
1730

1831
class TestUserProfileEvents(UserSettingsEventTestMixin, TestCase):
1932
"""
@@ -179,3 +192,87 @@ def test_enrolled_after_email_change(self):
179192
# CEAs shouldn't have been affected
180193
assert CourseEnrollmentAllowed.objects.count() == 1
181194
assert CourseEnrollmentAllowed.objects.filter(email='[email protected]').count() == 1
195+
196+
197+
@skip_unless_lms
198+
class EnrollmentEventsTest(SharedModuleStoreTestCase, OpenEdxEventsTestMixin):
199+
"""
200+
Tests for the Open edX Events associated with the enrollment process through the enroll method.
201+
202+
This class guarantees that the following events are sent during the user's enrollment, with
203+
the exact Data Attributes as the event definition stated:
204+
205+
- COURSE_ENROLLMENT_CREATED: sent after the user's enrollment.
206+
"""
207+
208+
ENABLED_OPENEDX_EVENTS = ["org.openedx.learning.course.enrollment.created.v1"]
209+
210+
@classmethod
211+
def setUpClass(cls):
212+
"""
213+
Set up class method for the Test class.
214+
215+
This method starts manually events isolation. Explanation here:
216+
openedx/core/djangoapps/user_authn/views/tests/test_events.py#L44
217+
"""
218+
super().setUpClass()
219+
cls.start_events_isolation()
220+
221+
def setUp(self): # pylint: disable=arguments-differ
222+
super().setUp()
223+
self.course = CourseFactory.create()
224+
self.user = UserFactory.create(
225+
username="test",
226+
227+
password="password",
228+
)
229+
self.user_profile = UserProfileFactory.create(user=self.user, name="Test Example")
230+
self.receiver_called = False
231+
232+
def _event_receiver_side_effect(self, **kwargs): # pylint: disable=unused-argument
233+
"""
234+
Used show that the Open edX Event was called by the Django signal handler.
235+
"""
236+
self.receiver_called = True
237+
238+
def test_enrollment_created_event_emitted(self):
239+
"""
240+
Test whether the student enrollment event is sent after the user's
241+
enrollment process.
242+
243+
Expected result:
244+
- COURSE_ENROLLMENT_CREATED is sent and received by the mocked receiver.
245+
- The arguments that the receiver gets are the arguments sent by the event
246+
except the metadata generated on the fly.
247+
"""
248+
event_receiver = mock.Mock(side_effect=self._event_receiver_side_effect)
249+
COURSE_ENROLLMENT_CREATED.connect(event_receiver)
250+
251+
enrollment = CourseEnrollment.enroll(self.user, self.course.id)
252+
253+
self.assertTrue(self.receiver_called)
254+
self.assertDictContainsSubset(
255+
{
256+
"signal": COURSE_ENROLLMENT_CREATED,
257+
"sender": None,
258+
"enrollment": CourseEnrollmentData(
259+
user=UserData(
260+
pii=UserPersonalData(
261+
username=self.user.username,
262+
email=self.user.email,
263+
name=self.user.profile.name,
264+
),
265+
id=self.user.id,
266+
is_active=self.user.is_active,
267+
),
268+
course=CourseData(
269+
course_key=self.course.id,
270+
display_name=self.course.display_name,
271+
),
272+
mode=enrollment.mode,
273+
is_active=enrollment.is_active,
274+
creation_date=enrollment.created,
275+
),
276+
},
277+
event_receiver.call_args.kwargs
278+
)

openedx/core/djangoapps/user_authn/views/login.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
from ratelimit.decorators import ratelimit
3030
from rest_framework.views import APIView
3131

32+
from openedx_events.learning.data import UserData, UserPersonalData
33+
from openedx_events.learning.signals import SESSION_LOGIN_COMPLETED
3234
from common.djangoapps.edxmako.shortcuts import render_to_response
3335
from openedx.core.djangoapps.password_policy import compliance as password_policy_compliance
3436
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
@@ -298,6 +300,19 @@ def _handle_successful_authentication_and_login(user, request):
298300
django_login(request, user)
299301
request.session.set_expiry(604800 * 4)
300302
log.debug("Setting user session expiry to 4 weeks")
303+
304+
# Announce user's login
305+
SESSION_LOGIN_COMPLETED.send_event(
306+
user=UserData(
307+
pii=UserPersonalData(
308+
username=user.username,
309+
email=user.email,
310+
name=user.profile.name,
311+
),
312+
id=user.id,
313+
is_active=user.is_active,
314+
),
315+
)
301316
except Exception as exc:
302317
AUDIT_LOG.critical("Login failed - Could not create session. Is memcached running?")
303318
log.critical("Login failed - Could not create session. Is memcached running?")

openedx/core/djangoapps/user_authn/views/register.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
from django.views.decorators.debug import sensitive_post_parameters
2424
from edx_django_utils.monitoring import set_custom_attribute
2525
from edx_toggles.toggles import LegacyWaffleFlag, LegacyWaffleFlagNamespace
26+
from openedx_events.learning.data import UserData, UserPersonalData
27+
from openedx_events.learning.signals import STUDENT_REGISTRATION_COMPLETED
2628
from pytz import UTC
2729
from ratelimit.decorators import ratelimit
2830
from requests import HTTPError
@@ -252,6 +254,18 @@ def create_account_with_params(request, params):
252254
# Announce registration
253255
REGISTER_USER.send(sender=None, user=user, registration=registration)
254256

257+
STUDENT_REGISTRATION_COMPLETED.send_event(
258+
user=UserData(
259+
pii=UserPersonalData(
260+
username=user.username,
261+
email=user.email,
262+
name=user.profile.name,
263+
),
264+
id=user.id,
265+
is_active=user.is_active,
266+
),
267+
)
268+
255269
create_comments_service_user(user)
256270

257271
try:

0 commit comments

Comments
 (0)