diff --git a/lms/djangoapps/student_account/test/test_views.py b/lms/djangoapps/student_account/test/test_views.py index f6cde214ff..c2c99faf98 100644 --- a/lms/djangoapps/student_account/test/test_views.py +++ b/lms/djangoapps/student_account/test/test_views.py @@ -246,12 +246,25 @@ class StudentAccountLoginAndRegistrationTest(UrlResetMixin, ModuleStoreTestCase) 'course_id': 'edX/DemoX/Demo_Course' } + # The response should have a "Sign In" button with the URL + # that preserves the querystring params + with mock.patch.dict(settings.FEATURES, {'IS_EDX_DOMAIN': is_edx_domain}): + response = self.client.get(reverse(url_name), params) + self.assertContains(response, "login?course_id=edX%2FDemoX%2FDemo_Course&enrollment_action=enroll") + + # Add an additional "course mode" parameter + params['course_mode'] = 'honor' + + # Verify that this parameter is also preserved with mock.patch.dict(settings.FEATURES, {'IS_EDX_DOMAIN': is_edx_domain}): response = self.client.get(reverse(url_name), params) - # The response should have a "Sign In" button with the URL - # that preserves the querystring params - self.assertContains(response, "login?course_id=edX%2FDemoX%2FDemo_Course&enrollment_action=enroll") + expected_url = ( + "login?course_id=edX%2FDemoX%2FDemo_Course" + "&enrollment_action=enroll" + "&course_mode=honor" + ) + self.assertContains(response, expected_url) @mock.patch.dict(settings.FEATURES, {"ENABLE_THIRD_PARTY_AUTH": False}) @ddt.data("account_login", "account_register") diff --git a/lms/djangoapps/student_account/views.py b/lms/djangoapps/student_account/views.py index 2b110e2b62..47f2a56c47 100644 --- a/lms/djangoapps/student_account/views.py +++ b/lms/djangoapps/student_account/views.py @@ -91,7 +91,8 @@ def login_and_registration_form(request, initial_mode="login"): # We need to pass these parameters so that the header's # "Sign In" button preserves the querystring params. 'enrollment_action': request.GET.get('enrollment_action'), - 'course_id': request.GET.get('course_id') + 'course_id': request.GET.get('course_id'), + 'course_mode': request.GET.get('course_mode'), } return render_to_response('student_account/login_and_register.html', context) diff --git a/lms/static/js/spec/student_account/access_spec.js b/lms/static/js/spec/student_account/access_spec.js index 5a0ac71d1a..cced4d910a 100644 --- a/lms/static/js/spec/student_account/access_spec.js +++ b/lms/static/js/spec/student_account/access_spec.js @@ -166,7 +166,52 @@ define([ view.subview.login.trigger('auth-complete'); // Expect that the view tried to enroll the student - expect( EnrollmentInterface.enroll ).toHaveBeenCalledWith( COURSE_KEY ); + expect( EnrollmentInterface.enroll ).toHaveBeenCalledWith( + COURSE_KEY, + '/course_modes/choose/' + COURSE_KEY + '/' + ); + }); + + it('sends the user to the payment flow when the course mode is not honor', function() { + ajaxSpyAndInitialize(this, 'login'); + + // Simulate providing enrollment query string params + // AND specifying a course mode. + setFakeQueryParams({ + '?enrollment_action': 'enroll', + '?course_id': COURSE_KEY, + '?course_mode': 'verified' + }); + + // Trigger auth complete on the login view + view.subview.login.trigger('auth-complete'); + + // Expect that the view tried to auto-enroll the student + // with a redirect into the payment flow. + expect( EnrollmentInterface.enroll ).toHaveBeenCalledWith( + COURSE_KEY, + '/verify_student/start-flow/' + COURSE_KEY + '/' + ); + }); + + it('sends the user to the student dashboard when the course mode is honor', function() { + ajaxSpyAndInitialize(this, 'login'); + + // Simulate providing enrollment query string params + // AND specifying a course mode. + setFakeQueryParams({ + '?enrollment_action': 'enroll', + '?course_id': COURSE_KEY, + '?course_mode': 'honor' + }); + + // Trigger auth complete on the login view + view.subview.login.trigger('auth-complete'); + + // Expect that the view tried auto-enrolled the student + // and sent the student to the dashboard + // (skipping the payment flow). + expect( EnrollmentInterface.enroll ).toHaveBeenCalledWith(COURSE_KEY, '/dashboard'); }); it('adds a white-label course to the shopping cart on auth complete', function() { diff --git a/lms/static/js/spec/student_account/enrollment_spec.js b/lms/static/js/spec/student_account/enrollment_spec.js index 42f3335d35..cb8ab745af 100644 --- a/lms/static/js/spec/student_account/enrollment_spec.js +++ b/lms/static/js/spec/student_account/enrollment_spec.js @@ -19,7 +19,7 @@ define(['js/common_helpers/ajax_helpers', 'js/student_account/enrollment'], var requests = AjaxHelpers.requests( this ); // Attempt to enroll the user - EnrollmentInterface.enroll( COURSE_KEY ); + EnrollmentInterface.enroll( COURSE_KEY, FORWARD_URL ); // Expect that the correct request was made to the server AjaxHelpers.expectRequest( @@ -41,7 +41,7 @@ define(['js/common_helpers/ajax_helpers', 'js/student_account/enrollment'], var requests = AjaxHelpers.requests( this ); // Attempt to enroll the user - EnrollmentInterface.enroll( COURSE_KEY ); + EnrollmentInterface.enroll( COURSE_KEY, FORWARD_URL ); // Simulate an error response from the server AjaxHelpers.respondWithError(requests); @@ -55,7 +55,7 @@ define(['js/common_helpers/ajax_helpers', 'js/student_account/enrollment'], var requests = AjaxHelpers.requests( this ); // Attempt to enroll the user - EnrollmentInterface.enroll( COURSE_KEY ); + EnrollmentInterface.enroll( COURSE_KEY, FORWARD_URL ); // Simulate an error response (403) from the server // with a "user_message_url" parameter for the redirect. diff --git a/lms/static/js/student_account/enrollment.js b/lms/static/js/student_account/enrollment.js index 5c87b2b10a..dbaaf6a1c3 100644 --- a/lms/static/js/student_account/enrollment.js +++ b/lms/static/js/student_account/enrollment.js @@ -10,7 +10,6 @@ var edx = edx || {}; urls: { orders: '/commerce/orders/', - trackSelection: '/course_modes/choose/' }, headers: { @@ -18,11 +17,11 @@ var edx = edx || {}; }, /** - * Enroll a user in a course, then redirect the user - * to the track selection page. + * Enroll a user in a course, then redirect the user. * @param {string} courseKey Slash-separated course key. + * @param {string} redirectUrl The URL to redirect to once enrollment completes. */ - enroll: function( courseKey ) { + enroll: function( courseKey, redirectUrl ) { var data_obj = {course_id: courseKey}, data = JSON.stringify(data_obj); @@ -42,30 +41,22 @@ var edx = edx || {}; // If so, redirect to a page explaining to the user // why they were blocked. this.redirect( responseData.user_message_url ); - } - else { - // Otherwise, go to the track selection page as usual. - // This can occur, for example, when a course does not - // have a free enrollment mode, so we can't auto-enroll. - this.redirect( this.trackSelectionUrl( courseKey ) ); + } else { + // Otherwise, redirect the user to the next page. + if ( redirectUrl ) { + this.redirect( redirectUrl ); + } } }) .done(function() { - // If we successfully enrolled, go to the track selection - // page to allow the user to choose a paid enrollment mode. - this.redirect( this.trackSelectionUrl( courseKey ) ); + // If we successfully enrolled, redirect the user + // to the next page (usually the student dashboard or payment flow) + if ( redirectUrl ) { + this.redirect( redirectUrl ); + } }); }, - /** - * Construct the URL to the track selection page for a course. - * @param {string} courseKey Slash-separated course key. - * @return {string} The URL to the track selection page. - */ - trackSelectionUrl: function( courseKey ) { - return this.urls.trackSelection + courseKey + '/'; - }, - /** * Redirect to a URL. Mainly useful for mocking out in tests. * @param {string} url The URL to redirect to. diff --git a/lms/static/js/student_account/views/AccessView.js b/lms/static/js/student_account/views/AccessView.js index 79a3f595ed..1d9f0f7208 100644 --- a/lms/static/js/student_account/views/AccessView.js +++ b/lms/static/js/student_account/views/AccessView.js @@ -29,6 +29,12 @@ var edx = edx || {}; passwordHelp: {} }, + urls: { + dashboard: '/dashboard', + payment: '/verify_student/start-flow/', + trackSelection: '/course_modes/choose/' + }, + // The form currently loaded activeForm: '', @@ -243,15 +249,37 @@ var edx = edx || {}; enrollment: function() { var enrollment = edx.student.account.EnrollmentInterface, shoppingcart = edx.student.account.ShoppingCartInterface, - redirectUrl = '/dashboard', + redirectUrl = this.urls.dashboard, queryParams = this.queryParams(); - if ( queryParams.enrollmentAction === 'enroll' && queryParams.courseId) { - /* - If we need to enroll in a course, mark as enrolled. - The enrollment interface will redirect the student once enrollment completes. - */ - enrollment.enroll( decodeURIComponent( queryParams.courseId ) ); + if ( queryParams.enrollmentAction === 'enroll' && queryParams.courseId ) { + var courseId = decodeURIComponent( queryParams.courseId ); + + // Determine where to redirect the user after auto-enrollment. + if ( !queryParams.courseMode ) { + /* Backwards compatibility with the original course details page. + The old implementation did not specify the course mode for enrollment, + so we'd always send the user to the "track selection" page. + The track selection page would allow the user to select the course mode + ("verified", "honor", etc.) -- or, if the only course mode was "honor", + it would redirect the user to the dashboard. */ + redirectUrl = this.urls.trackSelection + courseId + '/'; + } else if ( queryParams.courseMode === 'honor' || queryParams.courseMode === 'audit' ) { + /* The newer version of the course details page allows the user + to specify which course mode to enroll as. If the student has + chosen "honor", we send them immediately to the dashboard + rather than the payment flow. The user may decide to upgrade + from the dashboard later. */ + redirectUrl = this.urls.dashboard; + } else { + /* If the user selected any other kind of course mode, send them + to the payment/verification flow. */ + redirectUrl = this.urls.payment + courseId + '/'; + } + + /* Attempt to auto-enroll the user in a free mode of the course, + then redirect to the next location. */ + enrollment.enroll( courseId, redirectUrl ); } else if ( queryParams.enrollmentAction === 'add_to_cart' && queryParams.courseId) { /* If this is a paid course, add it to the shopping cart and redirect @@ -298,6 +326,7 @@ var edx = edx || {}; next: $.url( '?next' ), enrollmentAction: $.url( '?enrollment_action' ), courseId: $.url( '?course_id' ), + courseMode: $.url( '?course_mode' ), emailOptIn: $.url( '?email_opt_in') }; }, diff --git a/lms/templates/main.html b/lms/templates/main.html index 0c29e080a8..748f1bfd96 100644 --- a/lms/templates/main.html +++ b/lms/templates/main.html @@ -173,9 +173,13 @@ <%def name="login_query()">${ - u"?course_id={0}&enrollment_action={1}{email_opt_in}".format( + u"?course_id={0}&enrollment_action={1}{course_mode}{email_opt_in}".format( urlquote_plus(course_id), urlquote_plus(enrollment_action), + course_mode=( + u"&course_mode=" + urlquote_plus(course_mode) + if course_mode else "" + ), email_opt_in=( u"&email_opt_in=" + urlquote_plus(email_opt_in) if email_opt_in else ""