diff --git a/common/djangoapps/course_modes/tests/test_views.py b/common/djangoapps/course_modes/tests/test_views.py index 25dfad2a2b..16868037f4 100644 --- a/common/djangoapps/course_modes/tests/test_views.py +++ b/common/djangoapps/course_modes/tests/test_views.py @@ -1,97 +1,120 @@ import ddt import unittest -from django.test import TestCase from django.conf import settings +from django.test.utils import override_settings from django.core.urlresolvers import reverse -from mock import patch, Mock +from xmodule.modulestore.tests.django_utils import ( + ModuleStoreTestCase, mixed_store_config +) + +from xmodule.modulestore.tests.factories import CourseFactory from course_modes.tests.factories import CourseModeFactory from student.tests.factories import CourseEnrollmentFactory, UserFactory -from opaque_keys.edx.locations import SlashSeparatedCourseKey + + +# Since we don't need any XML course fixtures, use a modulestore configuration +# that disables the XML modulestore. +MODULESTORE_CONFIG = mixed_store_config(settings.COMMON_TEST_DATA_ROOT, {}, include_xml=False) @ddt.ddt -class CourseModeViewTest(TestCase): +@override_settings(MODULESTORE=MODULESTORE_CONFIG) +@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') +class CourseModeViewTest(ModuleStoreTestCase): def setUp(self): - self.course_id = SlashSeparatedCourseKey('org', 'course', 'run') + super(CourseModeViewTest, self).setUp() + self.course = CourseFactory.create() + self.user = UserFactory.create(username="Bob", email="bob@example.com", password="edx") + self.client.login(username=self.user.username, password="edx") - for mode in ('audit', 'verified', 'honor'): - CourseModeFactory(mode_slug=mode, course_id=self.course_id) - - @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') @ddt.data( - # is_active?, enrollment_mode, upgrade?, redirect? - (True, 'verified', True, True), # User is already verified - (True, 'verified', False, True), # User is already verified - (True, 'honor', True, False), # User isn't trying to upgrade - (True, 'honor', False, True), # User is trying to upgrade - (True, 'audit', True, False), # User isn't trying to upgrade - (True, 'audit', False, True), # User is trying to upgrade - (False, 'verified', True, False), # User isn't active - (False, 'verified', False, False), # User isn't active - (False, 'honor', True, False), # User isn't active - (False, 'honor', False, False), # User isn't active - (False, 'audit', True, False), # User isn't active - (False, 'audit', False, False), # User isn't active + # is_active?, enrollment_mode, upgrade?, redirect? auto_register? + (True, 'verified', True, True, False), # User is already verified + (True, 'verified', False, True, False), # User is already verified + (True, 'honor', True, False, False), # User isn't trying to upgrade + (True, 'honor', False, True, False), # User is trying to upgrade + (True, 'audit', True, False, False), # User isn't trying to upgrade + (True, 'audit', False, True, False), # User is trying to upgrade + (False, 'verified', True, False, False), # User isn't active + (False, 'verified', False, False, False), # User isn't active + (False, 'honor', True, False, False), # User isn't active + (False, 'honor', False, False, False), # User isn't active + (False, 'audit', True, False, False), # User isn't active + (False, 'audit', False, False, False), # User isn't active + + # When auto-registration is enabled, users may already be + # registered when they reach the "choose your track" + # page. In this case, we do NOT want to redirect them + # to the dashboard, because we want to give them the option + # to enter the verification/payment track. + # TODO (ECOM-16): based on the outcome of the auto-registration AB test, + # either keep these tests or remove them. In either case, + # remove the "auto_register" flag from this test case. + (True, 'verified', True, False, True), + (True, 'verified', False, True, True), + (True, 'honor', True, False, True), + (True, 'honor', False, False, True), + (True, 'audit', True, False, True), + (True, 'audit', False, False, True), ) @ddt.unpack - @patch('course_modes.views.modulestore', Mock()) - def test_reregister_redirect(self, is_active, enrollment_mode, upgrade, redirect): - enrollment = CourseEnrollmentFactory( + def test_redirect_to_dashboard(self, is_active, enrollment_mode, upgrade, redirect, auto_register): + + # TODO (ECOM-16): Remove once we complete the auto-reg AB test. + if auto_register: + session = self.client.session + session['auto_register'] = True + session.save() + + # Create the course modes + for mode in ('audit', 'honor', 'verified'): + CourseModeFactory(mode_slug=mode, course_id=self.course.id) + + # Enroll the user in the test course + CourseEnrollmentFactory( is_active=is_active, mode=enrollment_mode, - course_id=self.course_id - ) - - self.client.login( - username=enrollment.user.username, - password='test' + course_id=self.course.id, + user=self.user ) + # Configure whether we're upgrading or not + get_params = {} if upgrade: get_params = {'upgrade': True} - else: - get_params = {} - response = self.client.get( - reverse('course_modes_choose', args=[self.course_id.to_deprecated_string()]), - get_params, - follow=False, - ) + url = reverse('course_modes_choose', args=[unicode(self.course.id)]) + response = self.client.get(url, get_params) + # Check whether we were correctly redirected if redirect: - self.assertEquals(response.status_code, 302) - self.assertTrue(response['Location'].endswith(reverse('dashboard'))) + self.assertRedirects(response, reverse('dashboard')) else: self.assertEquals(response.status_code, 200) - # TODO: Fix it so that response.templates works w/ mako templates, and then assert - # that the right template rendered - @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') @ddt.data( '', '1,,2', '1, ,2', '1, 2, 3' ) - @patch('course_modes.views.modulestore', Mock()) def test_suggested_prices(self, price_list): - course_id = SlashSeparatedCourseKey('org', 'course', 'price_course') - user = UserFactory() + # Create the course modes for mode in ('audit', 'honor'): - CourseModeFactory(mode_slug=mode, course_id=course_id) + CourseModeFactory(mode_slug=mode, course_id=self.course.id) - CourseModeFactory(mode_slug='verified', course_id=course_id, suggested_prices=price_list) - - self.client.login( - username=user.username, - password='test' + CourseModeFactory( + mode_slug='verified', + course_id=self.course.id, + suggested_prices=price_list ) + # Verify that the prices render correctly response = self.client.get( - reverse('course_modes_choose', args=[self.course_id.to_deprecated_string()]), + reverse('course_modes_choose', args=[unicode(self.course.id)]), follow=False, ) @@ -99,44 +122,84 @@ class CourseModeViewTest(TestCase): # TODO: Fix it so that response.templates works w/ mako templates, and then assert # that the right template rendered + # TODO (ECOM-16): Remove the auto-registration flag once the AB test is complete + # and we choose the winner as the default + @ddt.data(True, False) + def test_professional_registration(self, auto_register): -class ProfessionalModeViewTest(TestCase): - """ - Tests for redirects specific to the 'professional' course mode. - Can't really put this in the ddt-style tests in CourseModeViewTest, - since 'professional' mode implies it is the *only* mode for a course - """ - def setUp(self): - self.course_id = SlashSeparatedCourseKey('org', 'course', 'run') - CourseModeFactory(mode_slug='professional', course_id=self.course_id) - self.user = UserFactory() + # TODO (ECOM-16): Remove once we complete the auto-reg AB test. + if auto_register: + self.client.session['auto_register'] = True + self.client.session.save() - @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') - def test_professional_registration(self): - self.client.login( - username=self.user.username, - password='test' - ) + # The only course mode is professional ed + CourseModeFactory(mode_slug='professional', course_id=self.course.id) - response = self.client.get( - reverse('course_modes_choose', args=[self.course_id.to_deprecated_string()]), - follow=False, - ) + # Go to the "choose your track" page + choose_track_url = reverse('course_modes_choose', args=[unicode(self.course.id)]) + response = self.client.get(choose_track_url) - self.assertEquals(response.status_code, 302) - self.assertTrue(response['Location'].endswith(reverse('verify_student_show_requirements', args=[unicode(self.course_id)]))) + # Expect that we're redirected immediately to the "show requirements" page + # (since the only available track is professional ed) + show_reqs_url = reverse('verify_student_show_requirements', args=[unicode(self.course.id)]) + self.assertRedirects(response, show_reqs_url) + # Now enroll in the course CourseEnrollmentFactory( user=self.user, is_active=True, mode="professional", - course_id=unicode(self.course_id), + course_id=unicode(self.course.id), ) - response = self.client.get( - reverse('course_modes_choose', args=[self.course_id.to_deprecated_string()]), - follow=False, - ) + # Expect that this time we're redirected to the dashboard (since we're already registered) + response = self.client.get(choose_track_url) + self.assertRedirects(response, reverse('dashboard')) - self.assertEquals(response.status_code, 302) - self.assertTrue(response['Location'].endswith(reverse('dashboard'))) + + # Mapping of course modes to the POST parameters sent + # when the user chooses that mode. + POST_PARAMS_FOR_COURSE_MODE = { + 'audit': {'audit_mode': True}, + 'honor': {'honor-code': True}, + 'verified': {'certificate_mode': True} + } + + # TODO (ECOM-16): Remove the auto-register flag once the AB-test completes + # and we default it to enabled or disabled. + @ddt.data( + (False, 'honor', 'dashboard'), + (False, 'verified', 'show_requirements'), + (False, 'audit', 'dashboard'), + (True, 'honor', 'dashboard'), + (True, 'verified', 'show_requirements'), + (True, 'audit', 'dashboard'), + ) + @ddt.unpack + def test_choose_mode_redirect(self, auto_register, course_mode, expected_redirect): + + # TODO (ECOM-16): Remove once we complete the auto-reg AB test. + if auto_register: + self.client.session['auto_register'] = True + self.client.session.save() + + # Create the course modes + for mode in ('audit', 'honor', 'verified'): + CourseModeFactory(mode_slug=mode, course_id=self.course.id) + + # Choose the mode (POST request) + choose_track_url = reverse('course_modes_choose', args=[unicode(self.course.id)]) + resp = self.client.post(choose_track_url, self.POST_PARAMS_FOR_COURSE_MODE[course_mode]) + + # Verify the redirect + if expected_redirect == 'dashboard': + redirect_url = reverse('dashboard') + elif expected_redirect == 'show_requirements': + redirect_url = reverse( + 'verify_student_show_requirements', + kwargs={'course_id': unicode(self.course.id)} + ) + "?upgrade=False" + else: + self.fail("Must provide a valid redirect URL name") + + self.assertRedirects(resp, redirect_url) diff --git a/common/djangoapps/course_modes/views.py b/common/djangoapps/course_modes/views.py index 0aa242b4b2..d5b9932842 100644 --- a/common/djangoapps/course_modes/views.py +++ b/common/djangoapps/course_modes/views.py @@ -30,20 +30,52 @@ class ChooseModeView(View): from the selection page, parses the response, and then sends user to the next step in the flow """ + @method_decorator(login_required) def get(self, request, course_id, error=None): - """ Displays the course mode choice page """ + """ Displays the course mode choice page + Args: + request (`Request`): The Django Request object. + course_id (unicode): The slash-separated course key. + + Keyword Args: + error (unicode): If provided, display this error message + on the page. + + Returns: + Response + + """ course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) enrollment_mode, is_active = CourseEnrollment.enrollment_mode_for_user(request.user, course_key) upgrade = request.GET.get('upgrade', False) request.session['attempting_upgrade'] = upgrade + # TODO (ECOM-16): Remove once the AB-test of auto-registration completes + auto_register = request.session.get('auto_register', False) + # Inactive users always need to re-register - # verified and professional users do not need to register or upgrade - # registered users who are not trying to upgrade do not need to re-register - if is_active and (upgrade is False or enrollment_mode == 'verified' or enrollment_mode == 'professional'): + # Verified and professional users do not need to register or upgrade + # Registered users who are not trying to upgrade do not need to re-register + if not auto_register: + go_to_dashboard = ( + is_active and + (not upgrade or enrollment_mode in ['verified', 'professional']) + ) + + # If auto-registration is enabled, then students might already be registered, + # but we should still show them the "choose your track" page so they have + # the option to enter the verification/payment flow. + # TODO (ECOM-16): Based on the results of the AB-test, set the default behavior to + # either enable or disable auto-registration. + else: + go_to_dashboard = ( + not upgrade and enrollment_mode in ['verified', 'professional'] + ) + + if go_to_dashboard: return redirect(reverse('dashboard')) modes = CourseMode.modes_for_course_dict(course_key) @@ -73,6 +105,7 @@ class ChooseModeView(View): "error": error, "upgrade": upgrade, "can_audit": "audit" in modes, + "autoreg": auto_register } if "verified" in modes: context["suggested_prices"] = [ diff --git a/common/djangoapps/external_auth/tests/test_shib.py b/common/djangoapps/external_auth/tests/test_shib.py index acf095ee8d..4397ba7e35 100644 --- a/common/djangoapps/external_auth/tests/test_shib.py +++ b/common/djangoapps/external_auth/tests/test_shib.py @@ -13,6 +13,7 @@ from django.test.client import RequestFactory, Client as DjangoTestClient from django.test.utils import override_settings from django.core.urlresolvers import reverse from django.contrib.auth.models import AnonymousUser, User +from django.contrib.sessions.middleware import SessionMiddleware from django.utils.importlib import import_module from xmodule.modulestore.tests.factories import CourseFactory @@ -513,6 +514,11 @@ class ShibSPTest(ModuleStoreTestCase): for course in [shib_course, open_enroll_course]: for student in [shib_student, other_ext_student, int_student]: request = self.request_factory.post('/change_enrollment') + + # Add a session to the request + SessionMiddleware().process_request(request) + request.session.save() + request.POST.update({'enrollment_action': 'enroll', 'course_id': course.id.to_deprecated_string()}) request.user = student diff --git a/common/djangoapps/student/tests/test_enrollment.py b/common/djangoapps/student/tests/test_enrollment.py new file mode 100644 index 0000000000..83fb38a58b --- /dev/null +++ b/common/djangoapps/student/tests/test_enrollment.py @@ -0,0 +1,175 @@ +""" +Tests for student enrollment. +""" +from datetime import datetime, timedelta +import pytz +import ddt +import unittest + +from django.test.utils import override_settings +from django.conf import settings +from django.core.urlresolvers import reverse +from xmodule.modulestore.tests.django_utils import ( + ModuleStoreTestCase, mixed_store_config +) +from xmodule.modulestore.tests.factories import CourseFactory +from student.tests.factories import UserFactory, CourseModeFactory +from student.models import CourseEnrollment + + +# Since we don't need any XML course fixtures, use a modulestore configuration +# that disables the XML modulestore. +MODULESTORE_CONFIG = mixed_store_config(settings.COMMON_TEST_DATA_ROOT, {}, include_xml=False) + + +@ddt.ddt +@override_settings(MODULESTORE=MODULESTORE_CONFIG) +@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') +class EnrollmentTest(ModuleStoreTestCase): + """ + Test student enrollment, especially with different course modes. + """ + def setUp(self): + """ Create a course and user, then log in. """ + super(EnrollmentTest, self).setUp() + self.course = CourseFactory.create() + self.user = UserFactory.create(username="Bob", email="bob@example.com", password="edx") + self.client.login(username=self.user.username, password="edx") + + self.urls = [ + reverse('course_modes_choose', kwargs={'course_id': unicode(self.course.id)}) + ] + + # TODO (ECOM-16): We need separate test cases for both conditions in the auto-registration + # AB-test. Once we get the results of that test, we should + # remove the losing condition from this test. + @ddt.data( + # Default (no course modes in the database) + # Expect that we're redirected to the dashboard + # and automatically enrolled as "honor" + ([], '', 'honor', False), + ([], '', 'honor', True), + + # Audit / Verified / Honor + # We should always go to the "choose your course" page, + # If auto-registration is enabled, we should also be registered + # as "honor" by default. + (['honor', 'verified', 'audit'], 'course_modes_choose', None, False), + (['honor', 'verified', 'audit'], 'course_modes_choose', 'honor', True), + + # Professional ed + # Expect that we're sent to the "choose your track" page + # (which will, in turn, redirect us to a page where we can verify/pay) + # Even if auto registration is enabled, we should NOT be auto-registered, + # because that would be giving away an expensive course for free :) + (['professional'], 'course_modes_choose', None, False), + (['professional'], 'course_modes_choose', None, True), + + ) + @ddt.unpack + def test_enroll(self, course_modes, next_url, enrollment_mode, auto_reg): + # Create the course modes (if any) required for this test case + for mode_slug in course_modes: + CourseModeFactory.create( + course_id=self.course.id, + mode_slug=mode_slug, + mode_display_name=mode_slug, + expiration_datetime=datetime.now(pytz.UTC) + timedelta(days=1) + ) + + # Reverse the expected next URL, if one is provided + # (otherwise, use an empty string, which the JavaScript client + # interprets as a redirect to the dashboard) + full_url = ( + reverse(next_url, kwargs={'course_id': unicode(self.course.id)}) + if next_url else next_url + ) + + # Enroll in the course and verify the URL we get sent to + resp = self._change_enrollment('enroll', auto_reg=auto_reg) + self.assertEqual(resp.status_code, 200) + self.assertEqual(resp.content, full_url) + + # TODO (ECOM-16): If auto-registration is enabled, check that we're + # storing the auto-reg flag in the user's session + if auto_reg: + self.assertIn('auto_register', self.client.session) + self.assertTrue(self.client.session['auto_register']) + + # If we're not expecting to be enrolled, verify that this is the case + if enrollment_mode is None: + self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course.id)) + + # Otherwise, verify that we're enrolled with the expected course mode + else: + self.assertTrue(CourseEnrollment.is_enrolled(self.user, self.course.id)) + course_mode, is_active = CourseEnrollment.enrollment_mode_for_user(self.user, self.course.id) + self.assertTrue(is_active) + self.assertEqual(course_mode, enrollment_mode) + + def test_unenroll(self): + # Enroll the student in the course + CourseEnrollment.enroll(self.user, self.course.id, mode="honor") + + # Attempt to unenroll the student + resp = self._change_enrollment('unenroll') + self.assertEqual(resp.status_code, 200) + + # Expect that we're no longer enrolled + self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course.id)) + + def test_user_not_authenticated(self): + # Log out, so we're no longer authenticated + self.client.logout() + + # Try to enroll, expecting a forbidden response + resp = self._change_enrollment('enroll') + self.assertEqual(resp.status_code, 403) + + def test_missing_course_id_param(self): + resp = self.client.post( + reverse('change_enrollment'), + {'enrollment_action': 'enroll'} + ) + self.assertEqual(resp.status_code, 400) + + def test_unenroll_not_enrolled_in_course(self): + # Try unenroll without first enrolling in the course + resp = self._change_enrollment('unenroll') + self.assertEqual(resp.status_code, 400) + + def test_invalid_enrollment_action(self): + resp = self._change_enrollment('not_an_action') + self.assertEqual(resp.status_code, 400) + + def _change_enrollment(self, action, course_id=None, auto_reg=False): + """ + Change the student's enrollment status in a course. + + Args: + action (string): The action to perform (either "enroll" or "unenroll") + + Keyword Args: + course_id (unicode): If provided, use this course ID. Otherwise, use the + course ID created in the setup for this test. + + auto_reg (boolean): Whether to use the auto-registration hook. + TODO (ECOM-16): remove this once we complete the AB test for auto-registration. + + Returns: + Response + + """ + if course_id is None: + course_id = unicode(self.course.id) + + url = ( + reverse('change_enrollment') + if not auto_reg + else reverse('change_enrollment_autoreg') + ) + params = { + 'enrollment_action': action, + 'course_id': course_id + } + return self.client.post(url, params) diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index 4abdc0861c..a7a389359e 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -582,7 +582,7 @@ def try_change_enrollment(request): @require_POST -def change_enrollment(request): +def change_enrollment(request, auto_register=False): """ Modify the enrollment status for the logged-in user. @@ -598,6 +598,24 @@ def change_enrollment(request): happens. This function should only be called from an AJAX request or as a post-login/registration helper, so the error messages in the responses should never actually be user-visible. + The original version of the change enrollment handler, + which does NOT perform auto-registration. + + TODO (ECOM-16): We created a second variation of this handler that performs + auto-registration for an AB-test. Depending on the results of that test, + we should make the winning implementation the default. + + Args: + request (`Request`): The Django request object + + Keyword Args: + auto_register (boolean): If True, auto-register the user + for a default course mode when they first enroll + before sending them to the "choose your track" page + + Returns: + Response + """ user = request.user @@ -635,24 +653,79 @@ def change_enrollment(request): _("Student is already enrolled") ) - # If this course is available in multiple modes, redirect them to a page - # where they can choose which mode they want. - available_modes = CourseMode.modes_for_course(course_id) - if len(available_modes) > 1: - return HttpResponse( - reverse("course_modes_choose", kwargs={'course_id': unicode(course_id)}) - ) + # We use this flag to determine which condition of an AB-test + # for auto-registration we're currently in. + # (We have two URLs that both point to this view, but vary the + # value of `auto_register`) + # In the auto-registration case, we automatically register the student + # as "honor" before allowing them to choose a track. + # TODO (ECOM-16): Once the auto-registration AB-test is complete, delete + # one of these two conditions and remove the `auto_register` flag. + if auto_register: + # TODO (ECOM-16): This stores a flag in the session so downstream + # views will recognize that the user is in the "auto-registration" + # experimental condition. We can remove this once the AB test completes. + request.session['auto_register'] = True - current_mode = available_modes[0] - # only automatically enroll people if the only mode is 'honor' - if current_mode.slug != 'honor': - return HttpResponse( - reverse("course_modes_choose", kwargs={'course_id': unicode(course_id)}) - ) + available_modes = CourseMode.modes_for_course_dict(course_id) - CourseEnrollment.enroll(user, course.id, mode=current_mode.slug) + # Handle professional ed as a special case. + # If professional ed is included in the list of available modes, + # then do NOT automatically enroll the student (we want them to pay first!) + # By convention, professional ed should be the *only* available course mode, + # if it's included at all -- anything else is a misconfiguration. But if someone + # messes up and adds an additional course mode, we err on the side of NOT + # accidentally giving away free courses. + if "professional" not in available_modes: + # Enroll the user using the default mode (honor) + # We're assuming that users of the course enrollment table + # will NOT try to look up the course enrollment model + # by its slug. If they do, it's possible (based on the state of the database) + # for no such model to exist, even though we've set the enrollment type + # to "honor". + CourseEnrollment.enroll(user, course.id) + + # If we have more than one course mode or professional ed is enabled, + # then send the user to the choose your track page. + # (In the case of professional ed, this will redirect to a page that + # funnels users directly into the verification / payment flow) + if len(available_modes) > 1 or "professional" in available_modes: + return HttpResponse( + reverse("course_modes_choose", kwargs={'course_id': unicode(course_id)}) + ) + + # Otherwise, there is only one mode available (the default) + return HttpResponse() + + # If auto-registration is disabled, do NOT register the student + # before sending them to the "choose your track" page. + # This is the control for the auto-registration AB-test. + else: + # TODO (ECOM-16): If the user is NOT in the experimental condition, + # make sure their session reflects this. We can remove this + # once the AB test completes. + if 'auto_register' in request.session: + del request.session['auto_register'] + + # If this course is available in multiple modes, redirect them to a page + # where they can choose which mode they want. + available_modes = CourseMode.modes_for_course(course_id) + if len(available_modes) > 1: + return HttpResponse( + reverse("course_modes_choose", kwargs={'course_id': unicode(course_id)}) + ) + + current_mode = available_modes[0] + # only automatically enroll people if the only mode is 'honor' + if current_mode.slug != 'honor': + return HttpResponse( + reverse("course_modes_choose", kwargs={'course_id': unicode(course_id)}) + ) + + CourseEnrollment.enroll(user, course.id, mode=current_mode.slug) + + return HttpResponse() - return HttpResponse() elif action == "add_to_cart": # Pass the request handling to shoppingcart.views diff --git a/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py b/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py index 38ce305592..15bf4282ff 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py @@ -15,36 +15,45 @@ from xmodule.modulestore.tests.sample_courses import default_block_info_tree, TO from xmodule.modulestore.tests.mongo_connection import MONGO_PORT_NUM, MONGO_HOST -def mixed_store_config(data_dir, mappings): +def mixed_store_config(data_dir, mappings, include_xml=True): """ Return a `MixedModuleStore` configuration, which provides access to both Mongo- and XML-backed courses. - `data_dir` is the directory from which to load XML-backed courses. - `mappings` is a dictionary mapping course IDs to modulestores, for example: + Args: + data_dir (string): the directory from which to load XML-backed courses. + mappings (string): a dictionary mapping course IDs to modulestores, for example: - { - 'MITx/2.01x/2013_Spring': 'xml', - 'edx/999/2013_Spring': 'default' - } + { + 'MITx/2.01x/2013_Spring': 'xml', + 'edx/999/2013_Spring': 'default' + } + + where 'xml' and 'default' are the two options provided by this configuration, + mapping (respectively) to XML-backed and Mongo-backed modulestores.. + + Keyword Args: + + include_xml (boolean): If True, include an XML modulestore in the configuration. + Note that this will require importing multiple XML courses from disk, + so unless your tests really needs XML course fixtures or is explicitly + testing mixed modulestore, set this to False. - where 'xml' and 'default' are the two options provided by this configuration, - mapping (respectively) to XML-backed and Mongo-backed modulestores.. """ - draft_mongo_config = draft_mongo_store_config(data_dir) - xml_config = xml_store_config(data_dir) - split_mongo = split_mongo_store_config(data_dir) + stores = [ + draft_mongo_store_config(data_dir)['default'], + split_mongo_store_config(data_dir)['default'] + ] + + if include_xml: + stores.append(xml_store_config(data_dir)['default']) store = { 'default': { 'ENGINE': 'xmodule.modulestore.mixed.MixedModuleStore', 'OPTIONS': { 'mappings': mappings, - 'stores': [ - draft_mongo_config['default'], - split_mongo['default'], - xml_config['default'], - ] + 'stores': stores, } } } diff --git a/common/templates/course_modes/choose.html b/common/templates/course_modes/choose.html index f4f8c6480c..26fe66da77 100644 --- a/common/templates/course_modes/choose.html +++ b/common/templates/course_modes/choose.html @@ -67,6 +67,11 @@ $(document).ready(function() { <%include file="/verify_student/_verification_header.html" args="course_name=course_name" /> +## TODO (ECOM-16): This is part of an AB-test of auto-registration. +## Once the test completes, we can make the winning configuration the default +## and remove this flag. + %if not autoreg: +
@@ -214,6 +219,92 @@ $(document).ready(function() {
+ %else: + +
+
+ + %if not upgrade: +

${_("Now choose your course track:")}

+ %endif + +
+ + % if "verified" in modes: +
+
+ +

${_("Pursue a Verified Certificate")}

+ + %if upgrade: +
+

${_("Plan to use your completed coursework for job applications, career advancement, or school applications? Upgrade to work toward a Verified Certificate of Achievement to document your accomplishment. A minimum fee applies.")}

+
+ %else: +
+

${_("Plan to use your completed coursework for job applications, career advancement, or school applications? Then work toward a Verified Certificate of Achievement to document your accomplishment. A minimum fee applies.")}

+
+ %endif +
+ +
+
${_("Select your contribution for this course (min. $")} ${min_price} ${currency}${_("):")}
+ + %if error: +
+
+

${error}

+
+
+ %endif + + <%include file="_contribution.html" args="suggested_prices=suggested_prices, currency=currency, chosen_price=chosen_price, min_price=min_price"/> + +
    +
  • + %if upgrade: + + %else: + + %endif + +
  • +
+
+
+ % endif + + %if not upgrade: + + % if "audit" in modes: + + ${_("or")} + +
+
+

${_("Audit This Course")}

+
+

${_("You can audit this course and still have complete access to the course material. If you complete and pass the course you will automatically earn an Honor Code Certificate free of charge.")}

+
+
+ +
    +
  • + +
  • +
+
+ + % endif + + %endif + + +
+
+
+ %endif + <%include file="/verify_student/_verification_support.html" /> diff --git a/lms/djangoapps/verify_student/views.py b/lms/djangoapps/verify_student/views.py index 0400f7c269..1d28c2665c 100644 --- a/lms/djangoapps/verify_student/views.py +++ b/lms/djangoapps/verify_student/views.py @@ -111,6 +111,9 @@ class VerifyView(View): "upgrade": upgrade == u'True', "can_audit": CourseMode.mode_for_course(course_id, 'audit') is not None, "modes_dict": CourseMode.modes_for_course_dict(course_id), + + # TODO (ECOM-16): Remove once the AB test completes + "autoreg": request.session.get('auto_register', False), } return render_to_response('verify_student/photo_verification.html', context) @@ -160,6 +163,9 @@ class VerifiedView(View): "upgrade": upgrade == u'True', "can_audit": "audit" in modes_dict, "modes_dict": modes_dict, + + # TODO (ECOM-16): Remove once the AB test completes + "autoreg": request.session.get('auto_register', False), } return render_to_response('verify_student/verified.html', context) @@ -326,6 +332,9 @@ def show_requirements(request, course_id): "is_not_active": not request.user.is_active, "upgrade": upgrade == u'True', "modes_dict": modes_dict, + + # TODO (ECOM-16): Remove once the AB test completes + "autoreg": request.session.get('auto_register', False), } return render_to_response("verify_student/show_requirements.html", context) diff --git a/lms/envs/dev.py b/lms/envs/dev.py index 0f4e91d547..c6aae3dc78 100644 --- a/lms/envs/dev.py +++ b/lms/envs/dev.py @@ -37,6 +37,7 @@ FEATURES['ENABLE_SHOPPING_CART'] = True FEATURES['AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING'] = True FEATURES['ENABLE_S3_GRADE_DOWNLOADS'] = True FEATURES['IS_EDX_DOMAIN'] = True # Is this an edX-owned domain? (used on instructor dashboard) +FEATURES['ENABLE_PAYMENT_FAKE'] = True FEEDBACK_SUBMISSION_EMAIL = "dummy@example.com" @@ -280,7 +281,8 @@ if SEGMENT_IO_LMS_KEY: CC_PROCESSOR['CyberSource']['SHARED_SECRET'] = os.environ.get('CYBERSOURCE_SHARED_SECRET', '') CC_PROCESSOR['CyberSource']['MERCHANT_ID'] = os.environ.get('CYBERSOURCE_MERCHANT_ID', '') CC_PROCESSOR['CyberSource']['SERIAL_NUMBER'] = os.environ.get('CYBERSOURCE_SERIAL_NUMBER', '') -CC_PROCESSOR['CyberSource']['PURCHASE_ENDPOINT'] = os.environ.get('CYBERSOURCE_PURCHASE_ENDPOINT', '') +#CC_PROCESSOR['CyberSource']['PURCHASE_ENDPOINT'] = os.environ.get('CYBERSOURCE_PURCHASE_ENDPOINT', '') +CC_PROCESSOR['CyberSource']['PURCHASE_ENDPOINT'] = '/shoppingcart/payment_fake/' ########################## USER API ########################## EDX_API_KEY = None diff --git a/lms/static/sass/views/_verification.scss b/lms/static/sass/views/_verification.scss index 97faacd271..8de9b48bc9 100644 --- a/lms/static/sass/views/_verification.scss +++ b/lms/static/sass/views/_verification.scss @@ -415,6 +415,57 @@ } } + // CASE: page header - experiment variant A overrides + .page-header.exp-variant-A { + margin-bottom: 0; + border-bottom: none; + + .title { + margin: 0; + } + + + .sts-label { + display: inline-block; + margin: 0; + border: none; + padding: 0; + text-transform: none; + } + + .sts-course-org { + margin-right: 0; + } + + .sts-label, .sts-course-org, .sts-course-number, .sts-course-name { + @extend %t-title5; + @extend %t-weight4; + @include font-size(14); + @include line-height(14); + display: inline-block; + color: $gray; + text-transform: none; + } + + .wrapper-sts { + display: inline-block; + width: flex-grid(9,12); + margin-bottom: ($baseline/4); + } + + .sts-course { + width: initial; + } + + .title .sts-track { + display: inline-block; + + .sts-track-value { + background: $m-blue-l1; + } + } + } + // ==================== // UI : progress @@ -1123,6 +1174,14 @@ } } + // CASE: supplemental content - experiment variant A overrides + .wrapper-content-supplementary.exp-variant-A { + + .help-item-technical { + width: flex-grid(8,12); + } + } + // ==================== // VIEW: select a track @@ -1192,11 +1251,11 @@ border-color: $m-blue-d1; .wrapper-copy { - width: flex-grid(5,8); + width: flex-grid(8,8); } .list-actions { - width: flex-grid(3,8); + width: flex-grid(8,8); } .action-select input { @@ -1221,7 +1280,6 @@ .list-actions { margin: ($baseline/2) 0; border-top: ($baseline/10) solid $m-gray-t1; - padding-top: $baseline; } .action-intro, .action-select { @@ -1336,6 +1394,39 @@ } } + // CASE: select a track - experiment variant A overrides + .wrapper-register-choose.exp-variant-A { + + .register-choice { + width: flex-grid(12,12); + } + + .deco-divider{ + width: flex-grid(12,12); + } + + .contribution-options { + width: flex-grid(8,12); + margin: 0; + + &:after{ + clear: none; + display: none; + } + } + + .register-choice-certificate .list-actions { + border-top: none; + width: flex-grid(4,12); + float: right; + margin: ($baseline/4) 0; + + .action-select { + width: initial; + } + } + } + // VIEW: requirements &.step-requirements { diff --git a/lms/templates/verify_student/_verification_header.html b/lms/templates/verify_student/_verification_header.html index 682652b870..0aedca7242 100644 --- a/lms/templates/verify_student/_verification_header.html +++ b/lms/templates/verify_student/_verification_header.html @@ -1,5 +1,10 @@ <%! from django.utils.translation import ugettext as _ %> +## TODO (ECOM-16): This is part of an AB-test of auto-registration. +## Once the test completes, we can make the winning configuration the default +## and remove this flag. +%if not autoreg: + + +%else: + + +%endif diff --git a/lms/templates/verify_student/_verification_support.html b/lms/templates/verify_student/_verification_support.html index de54e63346..389852bad8 100644 --- a/lms/templates/verify_student/_verification_support.html +++ b/lms/templates/verify_student/_verification_support.html @@ -1,5 +1,10 @@ <%! from django.utils.translation import ugettext as _ %> +## TODO (ECOM-16): This is part of an AB-test of auto-registration. +## Once the test completes, we can make the winning configuration the default +## and remove this flag. +%if not autoreg: +
+ +%else: + +
+ +
+%endif diff --git a/lms/urls.py b/lms/urls.py index 05df6193b4..d3d220c289 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -224,6 +224,17 @@ if settings.COURSEWARE_ENABLED: 'student.views.change_enrollment', name="change_enrollment"), url(r'^change_email_settings$', 'student.views.change_email_settings', name="change_email_settings"), + # Used for an AB-test of auto-registration + # TODO (ECOM-16): Based on the AB-test, update the default behavior and change + # this URL to point to the original view. Eventually, this URL + # should be removed, but not the AB test completes. + url( + r'^change_enrollment_autoreg$', + 'student.views.change_enrollment', + {'auto_register': True}, + name="change_enrollment_autoreg", + ), + #About the course url(r'^courses/{}/about$'.format(settings.COURSE_ID_PATTERN), 'courseware.views.course_about', name="about_course"),