emit enrollment event
This commit is contained in:
@@ -29,6 +29,12 @@ from django.forms import ModelForm, forms
|
||||
from course_modes.models import CourseMode
|
||||
import comment_client as cc
|
||||
from pytz import UTC
|
||||
import crum
|
||||
|
||||
from track import contexts
|
||||
from track.views import server_track
|
||||
from eventtracking import tracker
|
||||
|
||||
|
||||
unenroll_done = django.dispatch.Signal(providing_args=["course_enrollment"])
|
||||
|
||||
@@ -667,6 +673,11 @@ class PendingEmailChange(models.Model):
|
||||
activation_key = models.CharField(('activation key'), max_length=32, unique=True, db_index=True)
|
||||
|
||||
|
||||
|
||||
EVENT_NAME_ENROLLMENT_ACTIVATED = 'edx.course.enrollment.activated'
|
||||
EVENT_NAME_ENROLLMENT_DEACTIVATED = 'edx.course.enrollment.deactivated'
|
||||
|
||||
|
||||
class CourseEnrollment(models.Model):
|
||||
"""
|
||||
Represents a Student's Enrollment record for a single Course. You should
|
||||
@@ -737,19 +748,55 @@ class CourseEnrollment(models.Model):
|
||||
if user.id is None:
|
||||
user.save()
|
||||
|
||||
enrollment, _ = CourseEnrollment.objects.get_or_create(
|
||||
enrollment, created = CourseEnrollment.objects.get_or_create(
|
||||
user=user,
|
||||
course_id=course_id,
|
||||
)
|
||||
# In case we're reactivating a deactivated enrollment, or changing the
|
||||
# enrollment mode.
|
||||
if enrollment.mode != mode or enrollment.is_active != is_active:
|
||||
enrollment.mode = mode
|
||||
|
||||
activation_changed = False
|
||||
if enrollment.is_active != is_active:
|
||||
enrollment.is_active = is_active
|
||||
activation_changed = True
|
||||
|
||||
mode_changed = False
|
||||
if enrollment.mode != mode:
|
||||
enrollment.mode = mode
|
||||
mode_changed = True
|
||||
|
||||
if activation_changed or mode_changed:
|
||||
enrollment.save()
|
||||
|
||||
if created:
|
||||
if is_active:
|
||||
enrollment.emit_event(EVENT_NAME_ENROLLMENT_ACTIVATED)
|
||||
else:
|
||||
if activation_changed:
|
||||
if is_active:
|
||||
enrollment.emit_event(EVENT_NAME_ENROLLMENT_ACTIVATED)
|
||||
else:
|
||||
enrollment.emit_event(EVENT_NAME_ENROLLMENT_DEACTIVATED)
|
||||
|
||||
return enrollment
|
||||
|
||||
def emit_event(self, event_name):
|
||||
"""
|
||||
Emits an event to explicitly track course enrollment and unenrollment.
|
||||
"""
|
||||
|
||||
try:
|
||||
context = contexts.course_context_from_course_id(self.course_id)
|
||||
data = {
|
||||
'user_id': self.user.id,
|
||||
'course_id': self.course_id,
|
||||
'mode': self.mode,
|
||||
}
|
||||
|
||||
with tracker.get_tracker().context(event_name, context):
|
||||
server_track(crum.get_current_request(), event_name, data)
|
||||
except: # pylint: disable=bare-except
|
||||
if event_name and self.course_id:
|
||||
log.exception('Unable to emit event %s for user %s and course %s', event_name, self.user.username, self.course_id)
|
||||
|
||||
@classmethod
|
||||
def enroll(cls, user, course_id, mode="honor"):
|
||||
"""
|
||||
@@ -825,9 +872,13 @@ class CourseEnrollment(models.Model):
|
||||
"""
|
||||
try:
|
||||
record = CourseEnrollment.objects.get(user=user, course_id=course_id)
|
||||
record.is_active = False
|
||||
record.save()
|
||||
unenroll_done.send(sender=cls, course_enrollment=record)
|
||||
if record.is_active:
|
||||
record.is_active = False
|
||||
record.save()
|
||||
|
||||
record.emit_event(EVENT_NAME_ENROLLMENT_DEACTIVATED)
|
||||
unenroll_done.send(sender=cls, course_enrollment=record)
|
||||
|
||||
except cls.DoesNotExist:
|
||||
err_msg = u"Tried to unenroll student {} from {} but they were not enrolled"
|
||||
log.error(err_msg.format(user, course_id))
|
||||
@@ -918,6 +969,7 @@ class CourseEnrollment(models.Model):
|
||||
if not self.is_active:
|
||||
self.is_active = True
|
||||
self.save()
|
||||
self.emit_event(EVENT_NAME_ENROLLMENT_ACTIVATED)
|
||||
|
||||
def deactivate(self):
|
||||
"""Makes this `CourseEnrollment` record inactive. Saves immediately. An
|
||||
@@ -926,6 +978,7 @@ class CourseEnrollment(models.Model):
|
||||
if self.is_active:
|
||||
self.is_active = False
|
||||
self.save()
|
||||
self.emit_event(EVENT_NAME_ENROLLMENT_DEACTIVATED)
|
||||
|
||||
def refundable(self):
|
||||
"""
|
||||
|
||||
@@ -25,7 +25,7 @@ from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from courseware.tests.tests import TEST_DATA_MIXED_MODULESTORE
|
||||
|
||||
from mock import Mock, patch
|
||||
from mock import Mock, patch, sentinel
|
||||
from textwrap import dedent
|
||||
|
||||
from student.models import unique_id_for_user, CourseEnrollment
|
||||
@@ -276,6 +276,16 @@ class DashboardTest(TestCase):
|
||||
class EnrollInCourseTest(TestCase):
|
||||
"""Tests enrolling and unenrolling in courses."""
|
||||
|
||||
def setUp(self):
|
||||
patcher = patch('student.models.server_track')
|
||||
self.mock_server_track = patcher.start()
|
||||
self.addCleanup(patcher.stop)
|
||||
|
||||
crum_patcher = patch('student.models.crum.get_current_request')
|
||||
self.mock_get_current_request = crum_patcher.start()
|
||||
self.addCleanup(crum_patcher.stop)
|
||||
self.mock_get_current_request.return_value = sentinel.request
|
||||
|
||||
def test_enrollment(self):
|
||||
user = User.objects.create_user("joe", "joe@joe.com", "password")
|
||||
course_id = "edX/Test101/2013"
|
||||
@@ -289,24 +299,28 @@ class EnrollInCourseTest(TestCase):
|
||||
self.assertTrue(CourseEnrollment.is_enrolled(user, course_id))
|
||||
self.assertTrue(CourseEnrollment.is_enrolled_by_partial(user,
|
||||
course_id_partial))
|
||||
self.assert_enrollment_event_was_emitted(user, course_id)
|
||||
|
||||
# Enrolling them again should be harmless
|
||||
CourseEnrollment.enroll(user, course_id)
|
||||
self.assertTrue(CourseEnrollment.is_enrolled(user, course_id))
|
||||
self.assertTrue(CourseEnrollment.is_enrolled_by_partial(user,
|
||||
course_id_partial))
|
||||
self.assert_no_events_were_emitted()
|
||||
|
||||
# Now unenroll the user
|
||||
CourseEnrollment.unenroll(user, course_id)
|
||||
self.assertFalse(CourseEnrollment.is_enrolled(user, course_id))
|
||||
self.assertFalse(CourseEnrollment.is_enrolled_by_partial(user,
|
||||
course_id_partial))
|
||||
self.assert_unenrollment_event_was_emitted(user, course_id)
|
||||
|
||||
# Unenrolling them again should also be harmless
|
||||
CourseEnrollment.unenroll(user, course_id)
|
||||
self.assertFalse(CourseEnrollment.is_enrolled(user, course_id))
|
||||
self.assertFalse(CourseEnrollment.is_enrolled_by_partial(user,
|
||||
course_id_partial))
|
||||
self.assert_no_events_were_emitted()
|
||||
|
||||
# The enrollment record should still exist, just be inactive
|
||||
enrollment_record = CourseEnrollment.objects.get(
|
||||
@@ -315,6 +329,37 @@ class EnrollInCourseTest(TestCase):
|
||||
)
|
||||
self.assertFalse(enrollment_record.is_active)
|
||||
|
||||
def assert_no_events_were_emitted(self):
|
||||
"""Ensures no events were emitted since the last event related assertion"""
|
||||
self.assertFalse(self.mock_server_track.called)
|
||||
self.mock_server_track.reset_mock()
|
||||
|
||||
def assert_enrollment_event_was_emitted(self, user, course_id):
|
||||
"""Ensures an enrollment event was emitted since the last event related assertion"""
|
||||
self.mock_server_track.assert_called_once_with(
|
||||
sentinel.request,
|
||||
'edx.course.enrollment.activated',
|
||||
{
|
||||
'course_id': course_id,
|
||||
'user_id': user.pk,
|
||||
'mode': 'honor'
|
||||
}
|
||||
)
|
||||
self.mock_server_track.reset_mock()
|
||||
|
||||
def assert_unenrollment_event_was_emitted(self, user, course_id):
|
||||
"""Ensures an unenrollment event was emitted since the last event related assertion"""
|
||||
self.mock_server_track.assert_called_once_with(
|
||||
sentinel.request,
|
||||
'edx.course.enrollment.deactivated',
|
||||
{
|
||||
'course_id': course_id,
|
||||
'user_id': user.pk,
|
||||
'mode': 'honor'
|
||||
}
|
||||
)
|
||||
self.mock_server_track.reset_mock()
|
||||
|
||||
def test_enrollment_non_existent_user(self):
|
||||
# Testing enrollment of newly unsaved user (i.e. no database entry)
|
||||
user = User(username="rusty", email="rusty@fake.edx.org")
|
||||
@@ -324,11 +369,13 @@ class EnrollInCourseTest(TestCase):
|
||||
|
||||
# Unenroll does nothing
|
||||
CourseEnrollment.unenroll(user, course_id)
|
||||
self.assert_no_events_were_emitted()
|
||||
|
||||
# Implicit save() happens on new User object when enrolling, so this
|
||||
# should still work
|
||||
CourseEnrollment.enroll(user, course_id)
|
||||
self.assertTrue(CourseEnrollment.is_enrolled(user, course_id))
|
||||
self.assert_enrollment_event_was_emitted(user, course_id)
|
||||
|
||||
def test_enrollment_by_email(self):
|
||||
user = User.objects.create(username="jack", email="jack@fake.edx.org")
|
||||
@@ -336,11 +383,13 @@ class EnrollInCourseTest(TestCase):
|
||||
|
||||
CourseEnrollment.enroll_by_email("jack@fake.edx.org", course_id)
|
||||
self.assertTrue(CourseEnrollment.is_enrolled(user, course_id))
|
||||
self.assert_enrollment_event_was_emitted(user, course_id)
|
||||
|
||||
# This won't throw an exception, even though the user is not found
|
||||
self.assertIsNone(
|
||||
CourseEnrollment.enroll_by_email("not_jack@fake.edx.org", course_id)
|
||||
)
|
||||
self.assert_no_events_were_emitted()
|
||||
|
||||
self.assertRaises(
|
||||
User.DoesNotExist,
|
||||
@@ -349,17 +398,21 @@ class EnrollInCourseTest(TestCase):
|
||||
course_id,
|
||||
ignore_errors=False
|
||||
)
|
||||
self.assert_no_events_were_emitted()
|
||||
|
||||
# Now unenroll them by email
|
||||
CourseEnrollment.unenroll_by_email("jack@fake.edx.org", course_id)
|
||||
self.assertFalse(CourseEnrollment.is_enrolled(user, course_id))
|
||||
self.assert_unenrollment_event_was_emitted(user, course_id)
|
||||
|
||||
# Harmless second unenroll
|
||||
CourseEnrollment.unenroll_by_email("jack@fake.edx.org", course_id)
|
||||
self.assertFalse(CourseEnrollment.is_enrolled(user, course_id))
|
||||
self.assert_no_events_were_emitted()
|
||||
|
||||
# Unenroll on non-existent user shouldn't throw an error
|
||||
CourseEnrollment.unenroll_by_email("not_jack@fake.edx.org", course_id)
|
||||
self.assert_no_events_were_emitted()
|
||||
|
||||
def test_enrollment_multiple_classes(self):
|
||||
user = User(username="rusty", email="rusty@fake.edx.org")
|
||||
@@ -367,15 +420,19 @@ class EnrollInCourseTest(TestCase):
|
||||
course_id2 = "MITx/6.003z/2012"
|
||||
|
||||
CourseEnrollment.enroll(user, course_id1)
|
||||
self.assert_enrollment_event_was_emitted(user, course_id1)
|
||||
CourseEnrollment.enroll(user, course_id2)
|
||||
self.assert_enrollment_event_was_emitted(user, course_id2)
|
||||
self.assertTrue(CourseEnrollment.is_enrolled(user, course_id1))
|
||||
self.assertTrue(CourseEnrollment.is_enrolled(user, course_id2))
|
||||
|
||||
CourseEnrollment.unenroll(user, course_id1)
|
||||
self.assert_unenrollment_event_was_emitted(user, course_id1)
|
||||
self.assertFalse(CourseEnrollment.is_enrolled(user, course_id1))
|
||||
self.assertTrue(CourseEnrollment.is_enrolled(user, course_id2))
|
||||
|
||||
CourseEnrollment.unenroll(user, course_id2)
|
||||
self.assert_unenrollment_event_was_emitted(user, course_id2)
|
||||
self.assertFalse(CourseEnrollment.is_enrolled(user, course_id1))
|
||||
self.assertFalse(CourseEnrollment.is_enrolled(user, course_id2))
|
||||
|
||||
@@ -388,27 +445,33 @@ class EnrollInCourseTest(TestCase):
|
||||
# (calling CourseEnrollment.enroll() would have)
|
||||
enrollment = CourseEnrollment.create_enrollment(user, course_id)
|
||||
self.assertFalse(CourseEnrollment.is_enrolled(user, course_id))
|
||||
self.assert_no_events_were_emitted()
|
||||
|
||||
# Until you explicitly activate it
|
||||
enrollment.activate()
|
||||
self.assertTrue(CourseEnrollment.is_enrolled(user, course_id))
|
||||
self.assert_enrollment_event_was_emitted(user, course_id)
|
||||
|
||||
# Activating something that's already active does nothing
|
||||
enrollment.activate()
|
||||
self.assertTrue(CourseEnrollment.is_enrolled(user, course_id))
|
||||
self.assert_no_events_were_emitted()
|
||||
|
||||
# Now deactive
|
||||
enrollment.deactivate()
|
||||
self.assertFalse(CourseEnrollment.is_enrolled(user, course_id))
|
||||
self.assert_unenrollment_event_was_emitted(user, course_id)
|
||||
|
||||
# Deactivating something that's already inactive does nothing
|
||||
enrollment.deactivate()
|
||||
self.assertFalse(CourseEnrollment.is_enrolled(user, course_id))
|
||||
self.assert_no_events_were_emitted()
|
||||
|
||||
# A deactivated enrollment should be activated if enroll() is called
|
||||
# for that user/course_id combination
|
||||
CourseEnrollment.enroll(user, course_id)
|
||||
self.assertTrue(CourseEnrollment.is_enrolled(user, course_id))
|
||||
self.assert_enrollment_event_was_emitted(user, course_id)
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
|
||||
|
||||
@@ -1,14 +1,33 @@
|
||||
"""Generates common contexts"""
|
||||
|
||||
import re
|
||||
import logging
|
||||
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
|
||||
|
||||
COURSE_REGEX = re.compile(r'^.*?/courses/(?P<course_id>(?P<org_id>[^/]+)/[^/]+/[^/]+)')
|
||||
COURSE_REGEX = re.compile(r'^.*?/courses/(?P<course_id>[^/]+/[^/]+/[^/]+)')
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def course_context_from_url(url):
|
||||
"""
|
||||
Extracts the course_id from the given `url.`
|
||||
Extracts the course_id from the given `url` and passes it on to
|
||||
`course_context_from_course_id()`.
|
||||
"""
|
||||
url = url or ''
|
||||
|
||||
match = COURSE_REGEX.match(url)
|
||||
course_id = ''
|
||||
if match:
|
||||
course_id = match.group('course_id') or ''
|
||||
|
||||
return course_context_from_course_id(course_id)
|
||||
|
||||
|
||||
def course_context_from_course_id(course_id):
|
||||
"""
|
||||
Creates a course context from a `course_id`.
|
||||
|
||||
Example Returned Context::
|
||||
|
||||
@@ -18,14 +37,21 @@ def course_context_from_url(url):
|
||||
}
|
||||
|
||||
"""
|
||||
url = url or ''
|
||||
|
||||
course_id = course_id or ''
|
||||
context = {
|
||||
'course_id': '',
|
||||
'course_id': course_id,
|
||||
'org_id': ''
|
||||
}
|
||||
match = COURSE_REGEX.match(url)
|
||||
if match:
|
||||
context.update(match.groupdict())
|
||||
try:
|
||||
location = CourseDescriptor.id_to_location(course_id)
|
||||
context['org_id'] = location.org
|
||||
except ValueError:
|
||||
log.warning(
|
||||
'Unable to parse course_id "{course_id}"'.format(
|
||||
course_id=course_id
|
||||
),
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
return context
|
||||
|
||||
@@ -8,6 +8,7 @@ Feature: LMS.Verified certificates
|
||||
Given I am logged in
|
||||
When I select the audit track
|
||||
Then I should see the course on my dashboard
|
||||
And a "edx.course.enrollment.activated" server event is emitted
|
||||
|
||||
Scenario: I can submit photos to verify my identity
|
||||
Given I am logged in
|
||||
@@ -36,6 +37,7 @@ Feature: LMS.Verified certificates
|
||||
Then I see the course on my dashboard
|
||||
And I see that I am on the verified track
|
||||
And I do not see the upsell link on my dashboard
|
||||
And a "edx.course.enrollment.activated" server event is emitted
|
||||
|
||||
# Not easily automated
|
||||
# Scenario: I can re-take photos
|
||||
@@ -71,6 +73,7 @@ Feature: LMS.Verified certificates
|
||||
And the course has an honor mode
|
||||
When I give a reason why I cannot pay
|
||||
Then I should see the course on my dashboard
|
||||
And a "edx.course.enrollment.activated" server event is emitted
|
||||
|
||||
Scenario: The upsell offer is on the dashboard if I am auditing.
|
||||
Given I am logged in
|
||||
@@ -91,4 +94,5 @@ Feature: LMS.Verified certificates
|
||||
And I navigate to my dashboard
|
||||
Then I see the course on my dashboard
|
||||
And I see that I am on the verified track
|
||||
And a "edx.course.enrollment.activated" server event is emitted
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ Feature: LMS.Register for a course
|
||||
And I visit the courses page
|
||||
When I register for the course "6.002x"
|
||||
Then I should see the course numbered "6.002x" in my dashboard
|
||||
And a "edx.course.enrollment.activated" server event is emitted
|
||||
|
||||
Scenario: I can unregister for a course
|
||||
Given I am registered for the course "6.002x"
|
||||
@@ -19,3 +20,4 @@ Feature: LMS.Register for a course
|
||||
Then I should be on the dashboard page
|
||||
And I should see an empty dashboard message
|
||||
And I should NOT see the course numbered "6.002x" in my dashboard
|
||||
And a "edx.course.enrollment.deactivated" server event is emitted
|
||||
|
||||
Reference in New Issue
Block a user