From e78a398f458b3f217c397b509e5ff39b65c84622 Mon Sep 17 00:00:00 2001 From: Justin Helbert Date: Tue, 1 Jul 2014 12:53:48 +0000 Subject: [PATCH] This emits enrollment mode changes events --- common/djangoapps/student/models.py | 5 ++ common/djangoapps/student/tests/tests.py | 29 +++++++++++ .../features/change_enrollment.feature | 23 +++++++++ .../courseware/features/change_enrollment.py | 49 +++++++++++++++++++ lms/djangoapps/courseware/features/common.py | 12 ++--- lms/djangoapps/courseware/features/events.py | 13 +++-- .../courseware/features/registration.py | 9 ++++ .../verify_student/tests/test_views.py | 33 +++++++++++-- 8 files changed, 159 insertions(+), 14 deletions(-) create mode 100644 lms/djangoapps/courseware/features/change_enrollment.feature create mode 100644 lms/djangoapps/courseware/features/change_enrollment.py diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py index fc3460628f..b487088b8c 100644 --- a/common/djangoapps/student/models.py +++ b/common/djangoapps/student/models.py @@ -328,6 +328,7 @@ class PendingEmailChange(models.Model): EVENT_NAME_ENROLLMENT_ACTIVATED = 'edx.course.enrollment.activated' EVENT_NAME_ENROLLMENT_DEACTIVATED = 'edx.course.enrollment.deactivated' +EVENT_NAME_ENROLLMENT_MODE_CHANGED = 'edx.course.enrollment.mode_changed' class PasswordHistory(models.Model): @@ -716,6 +717,10 @@ class CourseEnrollment(models.Model): u"offering:{}".format(self.course_id.offering), u"mode:{}".format(self.mode)] ) + if mode_changed: + # the user's default mode is "honor" and disabled for a course + # mode change events will only be emitted when the user's mode changes from this + self.emit_event(EVENT_NAME_ENROLLMENT_MODE_CHANGED) def emit_event(self, event_name): """ diff --git a/common/djangoapps/student/tests/tests.py b/common/djangoapps/student/tests/tests.py index f8838213b8..0be58d64e5 100644 --- a/common/djangoapps/student/tests/tests.py +++ b/common/djangoapps/student/tests/tests.py @@ -310,6 +310,18 @@ class EnrollInCourseTest(TestCase): self.assertFalse(self.mock_tracker.emit.called) # pylint: disable=maybe-no-member self.mock_tracker.reset_mock() + def assert_enrollment_mode_change_event_was_emitted(self, user, course_key, mode): + """Ensures an enrollment mode change event was emitted""" + self.mock_tracker.emit.assert_called_once_with( # pylint: disable=maybe-no-member + 'edx.course.enrollment.mode_changed', + { + 'course_id': course_key.to_deprecated_string(), + 'user_id': user.pk, + 'mode': mode + } + ) + self.mock_tracker.reset_mock() + def assert_enrollment_event_was_emitted(self, user, course_key): """Ensures an enrollment event was emitted since the last event related assertion""" self.mock_tracker.emit.assert_called_once_with( # pylint: disable=maybe-no-member @@ -447,6 +459,23 @@ class EnrollInCourseTest(TestCase): self.assertTrue(CourseEnrollment.is_enrolled(user, course_id)) self.assert_enrollment_event_was_emitted(user, course_id) + def test_change_enrollment_modes(self): + user = User.objects.create(username="justin", email="jh@fake.edx.org") + course_id = SlashSeparatedCourseKey("edX", "Test101", "2013") + + CourseEnrollment.enroll(user, course_id) + self.assert_enrollment_event_was_emitted(user, course_id) + + CourseEnrollment.enroll(user, course_id, "audit") + self.assert_enrollment_mode_change_event_was_emitted(user, course_id, "audit") + + # same enrollment mode does not emit an event + CourseEnrollment.enroll(user, course_id, "audit") + self.assert_no_events_were_emitted() + + CourseEnrollment.enroll(user, course_id, "honor") + self.assert_enrollment_mode_change_event_was_emitted(user, course_id, "honor") + @override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') diff --git a/lms/djangoapps/courseware/features/change_enrollment.feature b/lms/djangoapps/courseware/features/change_enrollment.feature new file mode 100644 index 0000000000..bfa528e99a --- /dev/null +++ b/lms/djangoapps/courseware/features/change_enrollment.feature @@ -0,0 +1,23 @@ +Feature: Change Enrollment Events +As a registered user +I want to change my enrollment mode + + +Scenario: I can change my enrollment +Given The course "6.002x" exists +And the course "6.002x" has all enrollment modes +And I am logged in +And I visit the courses page +When I register to audit the course +And a "edx.course.enrollment.activated" server event is emitted +And a "edx.course.enrollment.mode_changed" server events is emitted + +And I visit the dashboard +And I click on Challenge Yourself +And I choose an honor code upgrade +Then I should be on the dashboard page +Then 2 "edx.course.enrollment.mode_changed" server event is emitted + +# don't emit another mode_changed event upon unenrollment +When I unregister for the course numbered "6.002x" +Then 2 "edx.course.enrollment.mode_changed" server events is emitted diff --git a/lms/djangoapps/courseware/features/change_enrollment.py b/lms/djangoapps/courseware/features/change_enrollment.py new file mode 100644 index 0000000000..82e56b3a6b --- /dev/null +++ b/lms/djangoapps/courseware/features/change_enrollment.py @@ -0,0 +1,49 @@ +""" Provides lettuce acceptance methods for course enrollment changes """ + +from __future__ import absolute_import +from lettuce import world, step +from opaque_keys.edx.locations import SlashSeparatedCourseKey +from logging import getLogger +logger = getLogger(__name__) + + +@step(u'the course "([^"]*)" has all enrollment modes$') +def add_enrollment_modes_to_course(_step, course): + """ Add honor, audit, and verified modes to the sample course """ + world.CourseModeFactory.create( + course_id=SlashSeparatedCourseKey("edx", course, 'Test_Course'), + mode_slug="verified", + mode_display_name="Verified Course", + min_price=3 + ) + world.CourseModeFactory.create( + course_id=SlashSeparatedCourseKey("edx", course, 'Test_Course'), + mode_slug="honor", + mode_display_name="Honor Course", + ) + + world.CourseModeFactory.create( + course_id=SlashSeparatedCourseKey("edx", course, 'Test_Course'), + mode_slug="audit", + mode_display_name="Audit Course", + ) + + +@step(u'I click on Challenge Yourself$') +def challenge_yourself(_step): + """ Simulates clicking 'Challenge Yourself' button on course """ + challenge_button = world.browser.find_by_css('.wrapper-tip') + challenge_button.click() + verified_button = world.browser.find_by_css('#upgrade-to-verified') + verified_button.click() + + +@step(u'I choose an honor code upgrade$') +def honor_code_upgrade(_step): + """ Simulates choosing the honor code mode on the upgrade page """ + honor_code_link = world.browser.find_by_css('.title-expand') + honor_code_link.click() + honor_code_checkbox = world.browser.find_by_css('#honor-code') + honor_code_checkbox.click() + upgrade_button = world.browser.find_by_name("certificate_mode") + upgrade_button.click() diff --git a/lms/djangoapps/courseware/features/common.py b/lms/djangoapps/courseware/features/common.py index 9811ea6c2f..5dbafa1793 100644 --- a/lms/djangoapps/courseware/features/common.py +++ b/lms/djangoapps/courseware/features/common.py @@ -15,7 +15,6 @@ from opaque_keys.edx.locations import SlashSeparatedCourseKey from xmodule.course_module import CourseDescriptor from courseware.courses import get_course_by_id from xmodule import seq_module, vertical_module - from logging import getLogger logger = getLogger(__name__) @@ -27,7 +26,7 @@ def configure_screenshots_for_all_steps(_step, action): automatic saving of screenshots before and after each step in a scenario. """ - action=action.strip() + action = action.strip() if action == 'enable': world.auto_capture_screenshots = True elif action == 'disable': @@ -35,6 +34,7 @@ def configure_screenshots_for_all_steps(_step, action): else: raise ValueError('Parameter `action` should be one of "enable" or "disable".') + @world.absorb def capture_screenshot_before_after(func): """ @@ -43,12 +43,12 @@ def capture_screenshot_before_after(func): for each step in a scenario, but rather want to debug a single function. """ def inner(*args, **kwargs): - prefix=round(time.time() * 1000) + prefix = round(time.time() * 1000) world.capture_screenshot("{}_{}_{}".format( prefix, func.func_name, 'before' )) - ret_val=func(*args, **kwargs) + ret_val = func(*args, **kwargs) world.capture_screenshot("{}_{}_{}".format( prefix, func.func_name, 'after' )) @@ -94,11 +94,11 @@ def i_am_registered_for_the_course(step, course): # Create the user world.create_user('robot', 'test') - u = User.objects.get(username='robot') + user = User.objects.get(username='robot') # If the user is not already enrolled, enroll the user. # TODO: change to factory - CourseEnrollment.enroll(u, course_id(course)) + CourseEnrollment.enroll(user, course_id(course)) world.log_in(username='robot', password='test') diff --git a/lms/djangoapps/courseware/features/events.py b/lms/djangoapps/courseware/features/events.py index 62638286a4..dc20f7fbe4 100644 --- a/lms/djangoapps/courseware/features/events.py +++ b/lms/djangoapps/courseware/features/events.py @@ -36,8 +36,8 @@ def reset_between_outline_scenarios(_scenario, order, outline, reasons_to_fail): world.event_collection.drop() -@step('[aA]n? "(.*)" (server|browser) event is emitted') -def event_is_emitted(_step, event_type, event_source): +@step(r'([aA]n?|\d+) "(.*)" (server|browser) events? is emitted$') +def n_events_are_emitted(_step, count, event_type, event_source): # Ensure all events are written out to mongo before querying. world.mongo_client.fsync() @@ -54,8 +54,15 @@ def event_is_emitted(_step, event_type, event_source): '$ne': 'python/splinter' } } + cursor = world.event_collection.find(criteria) - assert_equals(cursor.count(), 1) + + try: + number_events = int(count) + except ValueError: + number_events = 1 + + assert_equals(cursor.count(), number_events) event = cursor.next() diff --git a/lms/djangoapps/courseware/features/registration.py b/lms/djangoapps/courseware/features/registration.py index 72db89e3e1..446d897445 100644 --- a/lms/djangoapps/courseware/features/registration.py +++ b/lms/djangoapps/courseware/features/registration.py @@ -10,7 +10,16 @@ def i_register_for_the_course(_step, course): url = django_url('courses/%s/about' % world.scenario_dict['COURSE'].id.to_deprecated_string()) world.browser.visit(url) world.css_click('section.intro a.register') + assert world.is_css_present('section.container.dashboard') + +@step('I register to audit the course$') +def i_register_to_audit_the_course(_step): + url = django_url('courses/%s/about' % world.scenario_dict['COURSE'].id.to_deprecated_string()) + world.browser.visit(url) + world.css_click('section.intro a.register') + audit_button = world.browser.find_by_name("audit_mode") + audit_button.click() assert world.is_css_present('section.container.dashboard') diff --git a/lms/djangoapps/verify_student/tests/test_views.py b/lms/djangoapps/verify_student/tests/test_views.py index 4359d9d943..2a2d112f03 100644 --- a/lms/djangoapps/verify_student/tests/test_views.py +++ b/lms/djangoapps/verify_student/tests/test_views.py @@ -35,7 +35,6 @@ from verify_student.models import SoftwareSecurePhotoVerification from reverification.tests.factories import MidcourseReverificationWindowFactory - def mock_render_to_response(*args, **kwargs): return render_to_response(*args, **kwargs) @@ -386,8 +385,17 @@ class TestMidCourseReverifyView(TestCase): kwargs={"course_id": self.course_key.to_deprecated_string()}) response = self.client.get(url) - # Check that user entering the reverify flow was logged - self.mock_tracker.emit.assert_called_once_with( # pylint: disable=maybe-no-member + self.mock_tracker.emit.assert_any_call( # pylint: disable=maybe-no-member + 'edx.course.enrollment.mode_changed', + { + 'user_id': self.user.id, + 'course_id': self.course_key.to_deprecated_string(), + 'mode': "verified", + } + ) + + # Check that user entering the reverify flow was logged, and that it was the last call + self.mock_tracker.emit.assert_called_with( # pylint: disable=maybe-no-member 'edx.course.enrollment.reverify.started', { 'user_id': self.user.id, @@ -395,6 +403,9 @@ class TestMidCourseReverifyView(TestCase): 'mode': "verified", } ) + + self.assertTrue(self.mock_tracker.emit.call_count, 2) + self.mock_tracker.emit.reset_mock() # pylint: disable=maybe-no-member self.assertEquals(response.status_code, 200) @@ -408,8 +419,17 @@ class TestMidCourseReverifyView(TestCase): response = self.client.post(url, {'face_image': ','}) - # Check that submission event was logged - self.mock_tracker.emit.assert_called_once_with( # pylint: disable=maybe-no-member + self.mock_tracker.emit.assert_any_call( # pylint: disable=maybe-no-member + 'edx.course.enrollment.mode_changed', + { + 'user_id': self.user.id, + 'course_id': self.course_key.to_deprecated_string(), + 'mode': "verified", + } + ) + + # Check that submission event was logged, and that it was the last call + self.mock_tracker.emit.assert_called_with( # pylint: disable=maybe-no-member 'edx.course.enrollment.reverify.submitted', { 'user_id': self.user.id, @@ -417,6 +437,9 @@ class TestMidCourseReverifyView(TestCase): 'mode': "verified", } ) + + self.assertTrue(self.mock_tracker.emit.call_count, 2) + self.mock_tracker.emit.reset_mock() # pylint: disable=maybe-no-member self.assertEquals(response.status_code, 302)