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
33 changes: 33 additions & 0 deletions common/djangoapps/student/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,13 @@
from slumber.exceptions import HttpClientError, HttpServerError
from user_util import user_util

from openedx_events.learning.data import (
CourseData,
CourseEnrollmentData,
UserData,
UserPersonalData,
)
from openedx_events.learning.signals import COURSE_ENROLLMENT_CREATED
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 @@ -1569,9 +1576,16 @@ def enroll(cls, user, course_key, mode=None, check_access=False, can_upgrade=Fal
# All the server-side checks for whether a user is allowed to enroll.
try:
course = CourseOverview.get_from_id(course_key)
course_data = CourseData(
course_key=course.id,
display_name=course.display_name,
)
except CourseOverview.DoesNotExist:
# This is here to preserve legacy behavior which allowed enrollment in courses
# announced before the start of content creation.
course_data = CourseData(
course_key=course_key,
)
if check_access:
log.warning("User %s failed to enroll in non-existent course %s", user.username, str(course_key))
raise NonExistentCourseError # lint-amnesty, pylint: disable=raise-missing-from
Expand Down Expand Up @@ -1607,6 +1621,25 @@ def enroll(cls, user, course_key, mode=None, check_access=False, can_upgrade=Fal
enrollment.update_enrollment(is_active=True, mode=mode, enterprise_uuid=enterprise_uuid)
enrollment.send_signal(EnrollStatusChange.enroll)

# Announce user's enrollment
COURSE_ENROLLMENT_CREATED.send_event(
enrollment=CourseEnrollmentData(
user=UserData(
pii=UserPersonalData(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love the abstraction layers that are being created for these data objects as part of this effort!

username=user.username,
email=user.email,
name=user.profile.name,
),
id=user.id,
is_active=user.is_active,
),
course=course_data,
mode=enrollment.mode,
is_active=enrollment.is_active,
creation_date=enrollment.created,
)
)

return enrollment

@classmethod
Expand Down
12 changes: 11 additions & 1 deletion common/djangoapps/student/tests/test_enrollment.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import pytest
from django.conf import settings
from django.urls import reverse
from openedx_events.tests.utils import OpenEdxEventsTestMixin

from common.djangoapps.course_modes.models import CourseMode
from common.djangoapps.course_modes.tests.factories import CourseModeFactory
Expand All @@ -30,19 +31,28 @@
@ddt.ddt
@patch.dict('django.conf.settings.FEATURES', {'ENABLE_SPECIAL_EXAMS': True})
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
class EnrollmentTest(UrlResetMixin, SharedModuleStoreTestCase):
class EnrollmentTest(UrlResetMixin, SharedModuleStoreTestCase, OpenEdxEventsTestMixin):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the default behavior for the OpenEdxEventsTestMixin is that it will not send out events unless specified in ENABLED_OPENEDX_EVENTS I suggest we make it explicit for the first tests.
By this I mean, inherit from the mixin and also put a ENABLED_OPENEDX_EVENTS = [] statement.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This way anyone looking at this as usage example will learn about the existence of the ENABLED_OPENEDX_EVENTS key setting

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great, thanks for the suggestion!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is the corresponding integration test for when ENABLED_OPENEDX_EVENTS is not an empty list?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, we can make it explicit for the test_enrollment_created_event_emitted test below.

Also any TestCase that does not inherit OpenEdxEventsTestMixin will act as regular code not bound by test restrictions and send the events normally.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the suggestion @nasthagiri! I changed the events tests -e.g. EnrollmentEventsTest-, so they correspond to the integration of each event to the login, registration, and enrollment process.

"""
Test student enrollment, especially with different course modes.
"""

ENABLED_OPENEDX_EVENTS = []

USERNAME = "Bob"
EMAIL = "[email protected]"
PASSWORD = "edx"
URLCONF_MODULES = ['openedx.core.djangoapps.embargo']

@classmethod
def setUpClass(cls):
"""
Set up class method for the Test class.

This method starts manually events isolation. Explanation here:
openedx/core/djangoapps/user_authn/views/tests/test_events.py#L44
"""
super().setUpClass()
cls.start_events_isolation()
cls.course = CourseFactory.create()
cls.course_limited = CourseFactory.create()
cls.proctored_course = CourseFactory(
Expand Down
101 changes: 99 additions & 2 deletions common/djangoapps/student/tests/test_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,23 @@
from django.test import TestCase
from django_countries.fields import Country

from common.djangoapps.student.models import CourseEnrollmentAllowed
from common.djangoapps.student.tests.factories import CourseEnrollmentAllowedFactory, UserFactory
from common.djangoapps.student.models import CourseEnrollmentAllowed, CourseEnrollment
from common.djangoapps.student.tests.factories import CourseEnrollmentAllowedFactory, UserFactory, UserProfileFactory
from common.djangoapps.student.tests.tests import UserSettingsEventTestMixin

from openedx_events.learning.data import (
CourseData,
CourseEnrollmentData,
UserData,
UserPersonalData,
)
from openedx_events.learning.signals import COURSE_ENROLLMENT_CREATED
from openedx_events.tests.utils import OpenEdxEventsTestMixin
from openedx.core.djangolib.testing.utils import skip_unless_lms

from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory


class TestUserProfileEvents(UserSettingsEventTestMixin, TestCase):
"""
Expand Down Expand Up @@ -179,3 +192,87 @@ def test_enrolled_after_email_change(self):
# CEAs shouldn't have been affected
assert CourseEnrollmentAllowed.objects.count() == 1
assert CourseEnrollmentAllowed.objects.filter(email='[email protected]').count() == 1


@skip_unless_lms
class EnrollmentEventsTest(SharedModuleStoreTestCase, OpenEdxEventsTestMixin):
"""
Tests for the Open edX Events associated with the enrollment process through the enroll method.

This class guarantees that the following events are sent during the user's enrollment, with
the exact Data Attributes as the event definition stated:

- COURSE_ENROLLMENT_CREATED: sent after the user's enrollment.
"""

ENABLED_OPENEDX_EVENTS = ["org.openedx.learning.course.enrollment.created.v1"]

@classmethod
def setUpClass(cls):
"""
Set up class method for the Test class.

This method starts manually events isolation. Explanation here:
openedx/core/djangoapps/user_authn/views/tests/test_events.py#L44
"""
super().setUpClass()
cls.start_events_isolation()

def setUp(self): # pylint: disable=arguments-differ
super().setUp()
self.course = CourseFactory.create()
self.user = UserFactory.create(
username="test",
email="[email protected]",
password="password",
)
self.user_profile = UserProfileFactory.create(user=self.user, name="Test Example")
self.receiver_called = False

def _event_receiver_side_effect(self, **kwargs): # pylint: disable=unused-argument
"""
Used show that the Open edX Event was called by the Django signal handler.
"""
self.receiver_called = True

def test_enrollment_created_event_emitted(self):
"""
Test whether the student enrollment event is sent after the user's
enrollment process.

Expected result:
- COURSE_ENROLLMENT_CREATED 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.
"""
event_receiver = mock.Mock(side_effect=self._event_receiver_side_effect)
COURSE_ENROLLMENT_CREATED.connect(event_receiver)

enrollment = CourseEnrollment.enroll(self.user, self.course.id)

self.assertTrue(self.receiver_called)
self.assertDictContainsSubset(
{
"signal": COURSE_ENROLLMENT_CREATED,
"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
)
15 changes: 15 additions & 0 deletions openedx/core/djangoapps/user_authn/views/login.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
from ratelimit.decorators import ratelimit
from rest_framework.views import APIView

from openedx_events.learning.data import UserData, UserPersonalData
from openedx_events.learning.signals import SESSION_LOGIN_COMPLETED
from common.djangoapps.edxmako.shortcuts import render_to_response
from openedx.core.djangoapps.password_policy import compliance as password_policy_compliance
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
Expand Down Expand Up @@ -298,6 +300,19 @@ def _handle_successful_authentication_and_login(user, request):
django_login(request, user)
request.session.set_expiry(604800 * 4)
log.debug("Setting user session expiry to 4 weeks")

# Announce user's login
SESSION_LOGIN_COMPLETED.send_event(
user=UserData(
pii=UserPersonalData(
username=user.username,
email=user.email,
name=user.profile.name,
),
id=user.id,
is_active=user.is_active,
),
)
except Exception as exc:
AUDIT_LOG.critical("Login failed - Could not create session. Is memcached running?")
log.critical("Login failed - Could not create session. Is memcached running?")
Expand Down
14 changes: 14 additions & 0 deletions openedx/core/djangoapps/user_authn/views/register.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
from django.views.decorators.debug import sensitive_post_parameters
from edx_django_utils.monitoring import set_custom_attribute
from edx_toggles.toggles import LegacyWaffleFlag, LegacyWaffleFlagNamespace
from openedx_events.learning.data import UserData, UserPersonalData
from openedx_events.learning.signals import STUDENT_REGISTRATION_COMPLETED
from pytz import UTC
from ratelimit.decorators import ratelimit
from requests import HTTPError
Expand Down Expand Up @@ -252,6 +254,18 @@ def create_account_with_params(request, params):
# Announce registration
REGISTER_USER.send(sender=None, user=user, registration=registration)

STUDENT_REGISTRATION_COMPLETED.send_event(
user=UserData(
pii=UserPersonalData(
username=user.username,
email=user.email,
name=user.profile.name,
),
id=user.id,
is_active=user.is_active,
),
)

create_comments_service_user(user)

try:
Expand Down
Loading