Merge pull request #6179 from edx/sanchez/email_optin_third_party
3rd-party pipeline supports updating email optin
This commit is contained in:
@@ -14,7 +14,7 @@ from third_party_auth import ( # pylint: disable=unused-import
|
||||
from verify_student.models import SoftwareSecurePhotoVerification # pylint: disable=F0401
|
||||
|
||||
|
||||
def auth_pipeline_urls(auth_entry, redirect_url=None, course_id=None):
|
||||
def auth_pipeline_urls(auth_entry, redirect_url=None, course_id=None, email_opt_in=None):
|
||||
"""Retrieve URLs for each enabled third-party auth provider.
|
||||
|
||||
These URLs are used on the "sign up" and "sign in" buttons
|
||||
@@ -40,6 +40,10 @@ def auth_pipeline_urls(auth_entry, redirect_url=None, course_id=None):
|
||||
Note that `redirect_url` takes precedence over the redirect
|
||||
to the track selection page.
|
||||
|
||||
email_opt_in (unicode): The user choice to opt in for organization wide emails. If set to 'true'
|
||||
(case insensitive), user will be opted into organization-wide email. All other values will
|
||||
be treated as False, and the user will be opted out of organization-wide email.
|
||||
|
||||
Returns:
|
||||
dict mapping provider names to URLs
|
||||
|
||||
@@ -72,6 +76,7 @@ def auth_pipeline_urls(auth_entry, redirect_url=None, course_id=None):
|
||||
provider.NAME: pipeline.get_login_url(
|
||||
provider.NAME, auth_entry,
|
||||
enroll_course_id=course_id,
|
||||
email_opt_in=email_opt_in,
|
||||
redirect_url=pipeline_redirect
|
||||
)
|
||||
for provider in provider.Registry.enabled()
|
||||
|
||||
@@ -373,7 +373,7 @@ def signin_user(request):
|
||||
# party auth pipeline; distinct from the actual instance of the running
|
||||
# pipeline, if any.
|
||||
'pipeline_running': 'true' if pipeline.running(request) else 'false',
|
||||
'pipeline_url': auth_pipeline_urls(pipeline.AUTH_ENTRY_LOGIN, course_id=course_id),
|
||||
'pipeline_url': auth_pipeline_urls(pipeline.AUTH_ENTRY_LOGIN, course_id=course_id, email_opt_in=email_opt_in),
|
||||
'platform_name': microsite.get_value(
|
||||
'platform_name',
|
||||
settings.PLATFORM_NAME
|
||||
@@ -405,7 +405,7 @@ def register_user(request, extra_context=None):
|
||||
'enrollment_action': request.GET.get('enrollment_action'),
|
||||
'name': '',
|
||||
'running_pipeline': None,
|
||||
'pipeline_urls': auth_pipeline_urls(pipeline.AUTH_ENTRY_REGISTER, course_id=course_id),
|
||||
'pipeline_urls': auth_pipeline_urls(pipeline.AUTH_ENTRY_REGISTER, course_id=course_id, email_opt_in=email_opt_in),
|
||||
'platform_name': microsite.get_value(
|
||||
'platform_name',
|
||||
settings.PLATFORM_NAME
|
||||
|
||||
@@ -101,10 +101,12 @@ from . import provider
|
||||
# `AUTH_ENROLL_COURSE_ID_KEY` provides the course ID that a student
|
||||
# is trying to enroll in, used to generate analytics events
|
||||
# and auto-enroll students.
|
||||
from user_api.api import profile
|
||||
|
||||
AUTH_ENTRY_KEY = 'auth_entry'
|
||||
AUTH_REDIRECT_KEY = 'next'
|
||||
AUTH_ENROLL_COURSE_ID_KEY = 'enroll_course_id'
|
||||
AUTH_EMAIL_OPT_IN_KEY = 'email_opt_in'
|
||||
|
||||
AUTH_ENTRY_DASHBOARD = 'dashboard'
|
||||
AUTH_ENTRY_LOGIN = 'login'
|
||||
@@ -250,7 +252,7 @@ def _get_enabled_provider_by_name(provider_name):
|
||||
return enabled_provider
|
||||
|
||||
|
||||
def _get_url(view_name, backend_name, auth_entry=None, redirect_url=None, enroll_course_id=None):
|
||||
def _get_url(view_name, backend_name, auth_entry=None, redirect_url=None, enroll_course_id=None, email_opt_in=None):
|
||||
"""Creates a URL to hook into social auth endpoints."""
|
||||
kwargs = {'backend': backend_name}
|
||||
url = reverse(view_name, kwargs=kwargs)
|
||||
@@ -265,6 +267,9 @@ def _get_url(view_name, backend_name, auth_entry=None, redirect_url=None, enroll
|
||||
if enroll_course_id:
|
||||
query_params[AUTH_ENROLL_COURSE_ID_KEY] = enroll_course_id
|
||||
|
||||
if email_opt_in:
|
||||
query_params[AUTH_EMAIL_OPT_IN_KEY] = email_opt_in
|
||||
|
||||
return u"{url}?{params}".format(
|
||||
url=url,
|
||||
params=urllib.urlencode(query_params)
|
||||
@@ -309,7 +314,7 @@ def get_disconnect_url(provider_name):
|
||||
return _get_url('social:disconnect', enabled_provider.BACKEND_CLASS.name)
|
||||
|
||||
|
||||
def get_login_url(provider_name, auth_entry, redirect_url=None, enroll_course_id=None):
|
||||
def get_login_url(provider_name, auth_entry, redirect_url=None, enroll_course_id=None, email_opt_in=None):
|
||||
"""Gets the login URL for the endpoint that kicks off auth with a provider.
|
||||
|
||||
Args:
|
||||
@@ -326,6 +331,11 @@ def get_login_url(provider_name, auth_entry, redirect_url=None, enroll_course_id
|
||||
enroll_course_id (string): If provided, auto-enroll the user in this
|
||||
course upon successful authentication.
|
||||
|
||||
email_opt_in (string): If set to 'true' (case insensitive), user will
|
||||
be opted into organization-wide email. Any other string will
|
||||
equate to False, and the user will be opted out of organization-wide
|
||||
email.
|
||||
|
||||
Returns:
|
||||
String. URL that starts the auth pipeline for a provider.
|
||||
|
||||
@@ -339,7 +349,8 @@ def get_login_url(provider_name, auth_entry, redirect_url=None, enroll_course_id
|
||||
enabled_provider.BACKEND_CLASS.name,
|
||||
auth_entry=auth_entry,
|
||||
redirect_url=redirect_url,
|
||||
enroll_course_id=enroll_course_id
|
||||
enroll_course_id=enroll_course_id,
|
||||
email_opt_in=email_opt_in
|
||||
)
|
||||
|
||||
|
||||
@@ -425,7 +436,6 @@ def running(request):
|
||||
def parse_query_params(strategy, response, *args, **kwargs):
|
||||
"""Reads whitelisted query params, transforms them into pipeline args."""
|
||||
auth_entry = strategy.session.get(AUTH_ENTRY_KEY)
|
||||
|
||||
if not (auth_entry and auth_entry in _AUTH_ENTRY_CHOICES):
|
||||
raise AuthEntryError(strategy.backend, 'auth_entry missing or invalid')
|
||||
|
||||
@@ -499,7 +509,6 @@ def ensure_user_information(
|
||||
user_unset = user is None
|
||||
dispatch_to_login = is_login and (user_unset or user_inactive)
|
||||
reject_api_request = is_api and (user_unset or user_inactive)
|
||||
|
||||
if reject_api_request:
|
||||
# Content doesn't matter; we just want to exit the pipeline
|
||||
return HttpResponseBadRequest()
|
||||
@@ -512,20 +521,49 @@ def ensure_user_information(
|
||||
return
|
||||
|
||||
if dispatch_to_login:
|
||||
return redirect(AUTH_DISPATCH_URLS[AUTH_ENTRY_LOGIN], name='signin_user')
|
||||
return redirect(_create_redirect_url(AUTH_DISPATCH_URLS[AUTH_ENTRY_LOGIN], strategy))
|
||||
|
||||
# TODO (ECOM-369): Consolidate this with `dispatch_to_login`
|
||||
# once the A/B test completes. # pylint: disable=fixme
|
||||
if dispatch_to_login_2:
|
||||
return redirect(AUTH_DISPATCH_URLS[AUTH_ENTRY_LOGIN_2])
|
||||
return redirect(_create_redirect_url(AUTH_DISPATCH_URLS[AUTH_ENTRY_LOGIN_2], strategy))
|
||||
|
||||
if is_register and user_unset:
|
||||
return redirect(AUTH_DISPATCH_URLS[AUTH_ENTRY_REGISTER], name='register_user')
|
||||
return redirect(_create_redirect_url(AUTH_DISPATCH_URLS[AUTH_ENTRY_REGISTER], strategy))
|
||||
|
||||
# TODO (ECOM-369): Consolidate this with `is_register`
|
||||
# once the A/B test completes. # pylint: disable=fixme
|
||||
if is_register_2 and user_unset:
|
||||
return redirect(AUTH_DISPATCH_URLS[AUTH_ENTRY_REGISTER_2])
|
||||
return redirect(_create_redirect_url(AUTH_DISPATCH_URLS[AUTH_ENTRY_REGISTER_2], strategy))
|
||||
|
||||
|
||||
def _create_redirect_url(url, strategy):
|
||||
""" Given a URL and a Strategy, construct the appropriate redirect URL.
|
||||
|
||||
Construct a redirect URL and append the URL parameters that should be preserved.
|
||||
|
||||
Args:
|
||||
url (string): The base URL to use for the redirect.
|
||||
strategy (Strategy): Used to determine which URL parameters to append to the redirect.
|
||||
|
||||
Returns:
|
||||
A string representation of the URL, with parameters, for redirect.
|
||||
"""
|
||||
url_params = {}
|
||||
enroll_course_id = strategy.session_get(AUTH_ENROLL_COURSE_ID_KEY)
|
||||
if enroll_course_id:
|
||||
url_params['course_id'] = enroll_course_id
|
||||
url_params['enrollment_action'] = 'enroll'
|
||||
email_opt_in = strategy.session_get(AUTH_EMAIL_OPT_IN_KEY)
|
||||
if email_opt_in:
|
||||
url_params[AUTH_EMAIL_OPT_IN_KEY] = email_opt_in
|
||||
if url_params:
|
||||
return u'{url}?{params}'.format(
|
||||
url=url,
|
||||
params=urllib.urlencode(url_params)
|
||||
)
|
||||
else:
|
||||
return url
|
||||
|
||||
|
||||
@partial.partial
|
||||
@@ -639,6 +677,11 @@ def change_enrollment(strategy, user=None, *args, **kwargs):
|
||||
if enroll_course_id:
|
||||
course_id = CourseKey.from_string(enroll_course_id)
|
||||
modes = CourseMode.modes_for_course_dict(course_id)
|
||||
# If the email opt in parameter is found, set the preference.
|
||||
email_opt_in = strategy.session_get(AUTH_EMAIL_OPT_IN_KEY)
|
||||
if email_opt_in:
|
||||
opt_in = email_opt_in.lower() == 'true'
|
||||
profile.update_email_opt_in(user.username, course_id.org, opt_in)
|
||||
if CourseMode.can_auto_enroll(course_id, modes_dict=modes):
|
||||
try:
|
||||
CourseEnrollment.enroll(user, course_id, check_access=True)
|
||||
|
||||
@@ -46,7 +46,7 @@ If true, it:
|
||||
from . import provider
|
||||
|
||||
|
||||
_FIELDS_STORED_IN_SESSION = ['auth_entry', 'next', 'enroll_course_id']
|
||||
_FIELDS_STORED_IN_SESSION = ['auth_entry', 'next', 'enroll_course_id', 'email_opt_in']
|
||||
_MIDDLEWARE_CLASSES = (
|
||||
'third_party_auth.middleware.ExceptionMiddleware',
|
||||
)
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Tests for the change enrollment step of the pipeline. """
|
||||
from collections import namedtuple
|
||||
|
||||
import datetime
|
||||
import unittest
|
||||
@@ -17,6 +19,7 @@ from student.models import CourseEnrollment
|
||||
from xmodule.modulestore.tests.django_utils import (
|
||||
ModuleStoreTestCase, mixed_store_config
|
||||
)
|
||||
from user_api.models import UserOrgTag
|
||||
|
||||
|
||||
MODULESTORE_CONFIG = mixed_store_config(settings.COMMON_TEST_DATA_ROOT, {}, include_xml=False)
|
||||
@@ -42,12 +45,12 @@ class PipelineEnrollmentTest(ModuleStoreTestCase):
|
||||
self.user = UserFactory.create()
|
||||
|
||||
@ddt.data(
|
||||
([], "honor"),
|
||||
(["honor", "verified", "audit"], "honor"),
|
||||
(["professional"], None)
|
||||
([], "honor", u"False", u"False"),
|
||||
(["honor", "verified", "audit"], "honor", u"True", u"True"),
|
||||
(["professional"], None, u"Fålsœ", u"False")
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_auto_enroll_step(self, course_modes, enrollment_mode):
|
||||
def test_auto_enroll_step(self, course_modes, enrollment_mode, email_opt_in, email_opt_in_result):
|
||||
# Create the course modes for the test case
|
||||
for mode_slug in course_modes:
|
||||
CourseModeFactory.create(
|
||||
@@ -61,6 +64,7 @@ class PipelineEnrollmentTest(ModuleStoreTestCase):
|
||||
# when they started the auth process.
|
||||
strategy = self._fake_strategy()
|
||||
strategy.session_set('enroll_course_id', unicode(self.course.id))
|
||||
strategy.session_set('email_opt_in', email_opt_in)
|
||||
|
||||
result = pipeline.change_enrollment(strategy, 1, user=self.user) # pylint: disable=assignment-from-no-return,redundant-keyword-arg
|
||||
self.assertEqual(result, {})
|
||||
@@ -74,6 +78,11 @@ class PipelineEnrollmentTest(ModuleStoreTestCase):
|
||||
else:
|
||||
self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course.id))
|
||||
|
||||
# Check that the Email Opt In option was set
|
||||
tag = UserOrgTag.objects.get(user=self.user)
|
||||
self.assertIsNotNone(tag)
|
||||
self.assertEquals(tag.value, email_opt_in_result)
|
||||
|
||||
def test_add_white_label_to_cart(self):
|
||||
# Create a white label course (honor with a minimum price)
|
||||
CourseModeFactory.create(
|
||||
@@ -121,6 +130,29 @@ class PipelineEnrollmentTest(ModuleStoreTestCase):
|
||||
self.assertEqual(result, {})
|
||||
self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course.id))
|
||||
|
||||
def test_url_creation(self):
|
||||
strategy = self._fake_strategy()
|
||||
strategy.session_set('enroll_course_id', unicode(self.course.id))
|
||||
strategy.session_set('email_opt_in', u"False")
|
||||
backend = namedtuple('backend', 'name')
|
||||
backend.name = self.BACKEND_NAME
|
||||
response = pipeline.ensure_user_information(
|
||||
strategy=strategy,
|
||||
pipeline_index=1,
|
||||
details=None,
|
||||
response=None,
|
||||
uid=None,
|
||||
is_register=True,
|
||||
backend=backend
|
||||
)
|
||||
self.assertIsNotNone(response)
|
||||
self.assertEquals(response.status_code, 302)
|
||||
|
||||
# Get the location
|
||||
_, url = response._headers['location'] # pylint: disable=W0212
|
||||
self.assertIn("email_opt_in=False", url)
|
||||
self.assertIn("course_id=".format(id=unicode(self.course.id)), url)
|
||||
|
||||
def _fake_strategy(self):
|
||||
"""Simulate the strategy passed to the pipeline step. """
|
||||
request = RequestFactory().get(pipeline.get_complete_url(self.BACKEND_NAME))
|
||||
|
||||
@@ -285,13 +285,16 @@ def _third_party_auth_context(request):
|
||||
}
|
||||
|
||||
course_id = request.GET.get("course_id")
|
||||
email_opt_in = request.GET.get('email_opt_in')
|
||||
login_urls = auth_pipeline_urls(
|
||||
third_party_auth.pipeline.AUTH_ENTRY_LOGIN_2,
|
||||
course_id=course_id
|
||||
course_id=course_id,
|
||||
email_opt_in=email_opt_in
|
||||
)
|
||||
register_urls = auth_pipeline_urls(
|
||||
third_party_auth.pipeline.AUTH_ENTRY_REGISTER_2,
|
||||
course_id=course_id
|
||||
course_id=course_id,
|
||||
email_opt_in=email_opt_in
|
||||
)
|
||||
|
||||
if third_party_auth.is_enabled():
|
||||
|
||||
Reference in New Issue
Block a user