From 32c9230cd1c21ed564d3ad8b19c0a37b23de7f21 Mon Sep 17 00:00:00 2001 From: stephensanchez Date: Wed, 26 Nov 2014 21:26:01 +0000 Subject: [PATCH] Hooking the logistration page into the new HTTP endpoint for email opt in. Update the PR based on Code Review comments, and additional tests. Using underscore js --- .../djangoapps/user_api/tests/test_views.py | 58 +++++++++++++++++++ common/djangoapps/user_api/urls.py | 6 ++ common/djangoapps/user_api/views.py | 31 ++++++++++ lms/envs/common.py | 1 + lms/static/js/spec/main.js | 6 ++ .../js/spec/student_account/access_spec.js | 3 +- .../spec/student_account/emailoptin_spec.js | 28 +++++++++ lms/static/js/student_account/emailoptin.js | 35 +++++++++++ .../js/student_account/views/AccessView.js | 32 +++++++++- 9 files changed, 197 insertions(+), 3 deletions(-) create mode 100644 lms/static/js/spec/student_account/emailoptin_spec.js create mode 100644 lms/static/js/student_account/emailoptin.js diff --git a/common/djangoapps/user_api/tests/test_views.py b/common/djangoapps/user_api/tests/test_views.py index 01886c0093..76f839fb48 100644 --- a/common/djangoapps/user_api/tests/test_views.py +++ b/common/djangoapps/user_api/tests/test_views.py @@ -14,10 +14,12 @@ from unittest import SkipTest, skipUnless import ddt from pytz import UTC import mock +from xmodule.modulestore.tests.factories import CourseFactory from user_api.api import account as account_api, profile as profile_api from student.tests.factories import UserFactory +from user_api.models import UserOrgTag from user_api.tests.factories import UserPreferenceFactory from django_comment_common import models from opaque_keys.edx.locations import SlashSeparatedCourseKey @@ -1468,3 +1470,59 @@ class RegistrationViewTest(ApiTestCase): # Verify that the form description matches what we'd expect form_desc = json.loads(response.content) self.assertIn(expected_field, form_desc["fields"]) + + +@ddt.ddt +class UpdateEmailOptInTestCase(ApiTestCase): + """Tests the UpdateEmailOptInPreference view. """ + + USERNAME = "steve" + EMAIL = "steve@isawesome.com" + PASSWORD = "steveopolis" + + def setUp(self): + """ Create a course and user, then log in. """ + super(UpdateEmailOptInTestCase, self).setUp() + self.course = CourseFactory.create() + self.user = UserFactory.create(username=self.USERNAME, email=self.EMAIL, password=self.PASSWORD) + self.client.login(username=self.USERNAME, password=self.PASSWORD) + self.url = reverse("preferences_email_opt_in") + + @ddt.data( + (u"True", u"True"), + (u"true", u"True"), + (u"TrUe", u"True"), + (u"Banana", u"False"), + (u"strawberries", u"False"), + (u"False", u"False"), + ) + @ddt.unpack + def test_update_email_opt_in(self, opt, result): + """Tests the email opt in preference""" + # Register, which should trigger an activation email + response = self.client.post(self.url, { + "course_id": unicode(self.course.id), + "email_opt_in": opt + }) + self.assertHttpOK(response) + preference = UserOrgTag.objects.get( + user=self.user, org=self.course.id.org, key="email-optin" + ) + self.assertEquals(preference.value, result) + + @ddt.data( + (True, False), + (False, True), + (False, False) + ) + @ddt.unpack + def test_update_email_opt_in_wrong_params(self, use_course_id, use_opt_in): + """Tests the email opt in preference""" + params = {} + if use_course_id: + params["course_id"] = unicode(self.course.id) + if use_opt_in: + params["email_opt_in"] = u"True" + + response = self.client.post(self.url, params) + self.assertHttpBadRequest(response) diff --git a/common/djangoapps/user_api/urls.py b/common/djangoapps/user_api/urls.py index 55a2e077cd..b6b903f350 100644 --- a/common/djangoapps/user_api/urls.py +++ b/common/djangoapps/user_api/urls.py @@ -20,6 +20,12 @@ urlpatterns = patterns( r'^v1/forum_roles/(?P[a-zA-Z]+)/users/$', user_api_views.ForumRoleUsersListView.as_view() ), + + url( + r'^v1/preferences/email_opt_in/$', + user_api_views.UpdateEmailOptInPreference.as_view(), + name="preferences_email_opt_in" + ), ) if settings.FEATURES.get('ENABLE_COMBINED_LOGIN_REGISTRATION'): diff --git a/common/djangoapps/user_api/views.py b/common/djangoapps/user_api/views.py index ac3511b341..39ce384eed 100644 --- a/common/djangoapps/user_api/views.py +++ b/common/djangoapps/user_api/views.py @@ -9,10 +9,12 @@ from django.core.exceptions import ImproperlyConfigured from django.utils.translation import ugettext as _ from django.utils.decorators import method_decorator from django.views.decorators.csrf import ensure_csrf_cookie, csrf_protect +from opaque_keys.edx import locator from rest_framework import authentication from rest_framework import filters from rest_framework import generics from rest_framework import permissions +from rest_framework import status from rest_framework import viewsets from rest_framework.views import APIView from rest_framework.exceptions import ParseError @@ -836,3 +838,32 @@ class PreferenceUsersListView(generics.ListAPIView): def get_queryset(self): return User.objects.filter(preferences__key=self.kwargs["pref_key"]).prefetch_related("preferences") + + +class UpdateEmailOptInPreference(APIView): + """View for updating the email opt in preference. """ + authentication_classes = (authentication.SessionAuthentication,) + + @method_decorator(require_post_params(["course_id", "email_opt_in"])) + @method_decorator(ensure_csrf_cookie) + def post(self, request): + """ Post function for updating the email opt in preference. + + Allows the modification or creation of the email opt in preference at an + organizational level. + + Args: + request (Request): The request should contain the following POST parameters: + * course_id: The slash separated course ID. Used to determine the organization + for this preference setting. + * email_opt_in: "True" or "False" to determine if the user is opting in for emails from + this organization. If the string does not match "True" (case insensitive) it will + assume False. + + """ + course_id = request.DATA['course_id'] + org = locator.CourseLocator.from_string(course_id).org + # Only check for true. All other values are False. + email_opt_in = request.DATA['email_opt_in'].lower() == 'true' + profile_api.update_email_opt_in(request.user, org, email_opt_in) + return HttpResponse(status=status.HTTP_200_OK) diff --git a/lms/envs/common.py b/lms/envs/common.py index 8e167c0074..ff0b44aea3 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -1046,6 +1046,7 @@ student_account_js = [ 'js/utils/edx.utils.validate.js', 'js/src/utility.js', 'js/student_account/enrollment.js', + 'js/student_account/emailoptin.js', 'js/student_account/shoppingcart.js', 'js/student_account/models/LoginModel.js', 'js/student_account/models/RegisterModel.js', diff --git a/lms/static/js/spec/main.js b/lms/static/js/spec/main.js index bbb137be6f..dad276d970 100644 --- a/lms/static/js/spec/main.js +++ b/lms/static/js/spec/main.js @@ -293,6 +293,10 @@ exports: 'edx.student.account.EnrollmentInterface', deps: ['jquery', 'jquery.cookie'] }, + 'js/student_account/emailoptin': { + exports: 'edx.student.account.EmailOptInInterface', + deps: ['jquery', 'jquery.cookie'] + }, 'js/student_account/shoppingcart': { exports: 'edx.student.account.ShoppingCartInterface', deps: ['jquery', 'jquery.cookie', 'underscore'] @@ -362,6 +366,7 @@ 'js/student_account/models/PasswordResetModel', 'js/student_account/models/RegisterModel', 'js/student_account/views/FormView', + 'js/student_account/emailoptin', 'js/student_account/enrollment', 'js/student_account/shoppingcart', ] @@ -383,6 +388,7 @@ 'lms/include/js/spec/student_account/register_spec.js', 'lms/include/js/spec/student_account/password_reset_spec.js', 'lms/include/js/spec/student_account/enrollment_spec.js', + 'lms/include/js/spec/student_account/emailoptin_spec.js', 'lms/include/js/spec/student_account/shoppingcart_spec.js', 'lms/include/js/spec/student_profile/profile_spec.js' ]); diff --git a/lms/static/js/spec/student_account/access_spec.js b/lms/static/js/spec/student_account/access_spec.js index 896ef75cd8..2df49b5d58 100644 --- a/lms/static/js/spec/student_account/access_spec.js +++ b/lms/static/js/spec/student_account/access_spec.js @@ -5,7 +5,8 @@ define([ 'js/student_account/views/AccessView', 'js/student_account/views/FormView', 'js/student_account/enrollment', - 'js/student_account/shoppingcart' + 'js/student_account/shoppingcart', + 'js/student_account/emailoptin' ], function($, TemplateHelpers, AjaxHelpers, AccessView, FormView, EnrollmentInterface, ShoppingCartInterface) { describe('edx.student.account.AccessView', function() { 'use strict'; diff --git a/lms/static/js/spec/student_account/emailoptin_spec.js b/lms/static/js/spec/student_account/emailoptin_spec.js new file mode 100644 index 0000000000..8fc9250de7 --- /dev/null +++ b/lms/static/js/spec/student_account/emailoptin_spec.js @@ -0,0 +1,28 @@ +define(['js/common_helpers/ajax_helpers', 'js/student_account/emailoptin'], + function( AjaxHelpers, EmailOptInInterface ) { + 'use strict'; + + describe( 'edx.student.account.EmailOptInInterface', function() { + + var COURSE_KEY = 'edX/DemoX/Fall', + EMAIL_OPT_IN = 'True', + EMAIL_OPT_IN_URL = '/user_api/v1/preferences/email_opt_in/'; + + it('Opts in for organization emails', function() { + // Spy on Ajax requests + var requests = AjaxHelpers.requests( this ); + + // Attempt to enroll the user + EmailOptInInterface.setPreference( COURSE_KEY, EMAIL_OPT_IN ); + + // Expect that the correct request was made to the server + AjaxHelpers.expectRequest( + requests, 'POST', EMAIL_OPT_IN_URL, 'course_id=edX%2FDemoX%2FFall&email_opt_in=True' + ); + + // Simulate a successful response from the server + AjaxHelpers.respondWithJson(requests, {}); + }); + }); + } +); diff --git a/lms/static/js/student_account/emailoptin.js b/lms/static/js/student_account/emailoptin.js new file mode 100644 index 0000000000..cd41d43a3d --- /dev/null +++ b/lms/static/js/student_account/emailoptin.js @@ -0,0 +1,35 @@ +var edx = edx || {}; + +(function($) { + 'use strict'; + + edx.student = edx.student || {}; + edx.student.account = edx.student.account || {}; + + edx.student.account.EmailOptInInterface = { + + urls: { + emailOptInUrl: '/user_api/v1/preferences/email_opt_in/' + }, + + headers: { + 'X-CSRFToken': $.cookie('csrftoken') + }, + + /** + * Set the email opt in setting for the organization associated + * with this course. + * @param {string} courseKey Slash-separated course key. + * @param {string} emailOptIn The preference to opt in or out of organization emails. + */ + setPreference: function( courseKey, emailOptIn, context ) { + return $.ajax({ + url: this.urls.emailOptInUrl, + type: 'POST', + data: {course_id: courseKey, email_opt_in: emailOptIn}, + headers: this.headers, + context: context + }); + } + }; +})(jQuery); diff --git a/lms/static/js/student_account/views/AccessView.js b/lms/static/js/student_account/views/AccessView.js index 0d79171b59..52902d173f 100644 --- a/lms/static/js/student_account/views/AccessView.js +++ b/lms/static/js/student_account/views/AccessView.js @@ -170,6 +170,33 @@ var edx = edx || {}; * Once authentication has completed successfully, a user may need to: * * - Enroll in a course. + * - Update email opt-in preferences + * + * These actions are delegated from the authComplete function to additional + * functions requiring authentication. + * + */ + authComplete: function() { + var emailOptIn = edx.student.account.EmailOptInInterface, + queryParams = this.queryParams(); + + // Set the email opt in preference. + if (!_.isUndefined(queryParams.emailOptIn) && queryParams.enrollmentAction) { + emailOptIn.setPreference( + decodeURIComponent(queryParams.courseId), + queryParams.emailOptIn, + this + ).always(this.enrollment); + } else { + this.enrollment(); + } + }, + + /** + * Designed to be invoked after authentication has completed. This function enrolls + * the student as requested. + * + * - Enroll in a course. * - Add a course to the shopping cart. * - Be redirected to the dashboard / track selection page / shopping cart. * @@ -191,7 +218,7 @@ var edx = edx || {}; * ?course_id: The slash-separated course ID to enroll in or add to the cart. * */ - authComplete: function() { + enrollment: function() { var enrollment = edx.student.account.EnrollmentInterface, shoppingcart = edx.student.account.ShoppingCartInterface, redirectUrl = '/dashboard', @@ -248,7 +275,8 @@ var edx = edx || {}; return { next: $.url( '?next' ), enrollmentAction: $.url( '?enrollment_action' ), - courseId: $.url( '?course_id' ) + courseId: $.url( '?course_id' ), + emailOptIn: $.url( '?email_opt_in') }; },