Merge pull request #5679 from edx/will/logistration-third-party-auth
Integrate third party auth into the combined login/registration page.
This commit is contained in:
@@ -92,6 +92,7 @@ from util.password_policy_validators import (
|
||||
validate_password_dictionary
|
||||
)
|
||||
|
||||
import third_party_auth
|
||||
from third_party_auth import pipeline, provider
|
||||
from xmodule.error_module import ErrorDescriptor
|
||||
from shoppingcart.models import CourseRegistrationCode
|
||||
@@ -406,7 +407,7 @@ def register_user(request, extra_context=None):
|
||||
|
||||
# If third-party auth is enabled, prepopulate the form with data from the
|
||||
# selected provider.
|
||||
if microsite.get_value('ENABLE_THIRD_PARTY_AUTH', settings.FEATURES.get('ENABLE_THIRD_PARTY_AUTH')) and pipeline.running(request):
|
||||
if third_party_auth.is_enabled() and pipeline.running(request):
|
||||
running_pipeline = pipeline.get(request)
|
||||
current_provider = provider.Registry.get_by_backend_name(running_pipeline.get('backend'))
|
||||
overrides = current_provider.get_register_form_data(running_pipeline.get('kwargs'))
|
||||
@@ -619,7 +620,7 @@ def dashboard(request):
|
||||
'provider_states': [],
|
||||
}
|
||||
|
||||
if microsite.get_value('ENABLE_THIRD_PARTY_AUTH', settings.FEATURES.get('ENABLE_THIRD_PARTY_AUTH')):
|
||||
if third_party_auth.is_enabled():
|
||||
context['duplicate_provider'] = pipeline.get_duplicate_provider(messages.get_messages(request))
|
||||
context['provider_user_states'] = pipeline.get_provider_user_states(user)
|
||||
|
||||
@@ -911,7 +912,7 @@ def login_user(request, error=""): # pylint: disable-msg=too-many-statements,un
|
||||
redirect_url = None
|
||||
response = None
|
||||
running_pipeline = None
|
||||
third_party_auth_requested = microsite.get_value('ENABLE_THIRD_PARTY_AUTH', settings.FEATURES.get('ENABLE_THIRD_PARTY_AUTH')) and pipeline.running(request)
|
||||
third_party_auth_requested = third_party_auth.is_enabled() and pipeline.running(request)
|
||||
third_party_auth_successful = False
|
||||
trumped_by_first_party_auth = bool(request.POST.get('email')) or bool(request.POST.get('password'))
|
||||
user = None
|
||||
@@ -933,7 +934,7 @@ def login_user(request, error=""): # pylint: disable-msg=too-many-statements,un
|
||||
AUDIT_LOG.warning(
|
||||
u'Login failed - user with username {username} has no social auth with backend_name {backend_name}'.format(
|
||||
username=username, backend_name=backend_name))
|
||||
return HttpResponseBadRequest(
|
||||
return HttpResponse(
|
||||
_("You've successfully logged into your {provider_name} account, but this account isn't linked with an {platform_name} account yet.").format(
|
||||
platform_name=settings.PLATFORM_NAME, provider_name=requested_provider.NAME
|
||||
)
|
||||
@@ -1344,7 +1345,7 @@ def create_account(request, post_override=None): # pylint: disable-msg=too-many
|
||||
getattr(settings, 'REGISTRATION_EXTRA_FIELDS', {})
|
||||
)
|
||||
|
||||
if microsite.get_value('ENABLE_THIRD_PARTY_AUTH', settings.FEATURES.get('ENABLE_THIRD_PARTY_AUTH')) and pipeline.running(request):
|
||||
if third_party_auth.is_enabled() and pipeline.running(request):
|
||||
post_vars = dict(post_vars.items())
|
||||
post_vars.update({'password': pipeline.make_random_password()})
|
||||
|
||||
@@ -1524,7 +1525,7 @@ def create_account(request, post_override=None): # pylint: disable-msg=too-many
|
||||
|
||||
# If the user is registering via 3rd party auth, track which provider they use
|
||||
provider_name = None
|
||||
if settings.FEATURES.get('ENABLE_THIRD_PARTY_AUTH') and pipeline.running(request):
|
||||
if third_party_auth.is_enabled() and pipeline.running(request):
|
||||
running_pipeline = pipeline.get(request)
|
||||
current_provider = provider.Registry.get_by_backend_name(running_pipeline.get('backend'))
|
||||
provider_name = current_provider.NAME
|
||||
@@ -1615,7 +1616,7 @@ def create_account(request, post_override=None): # pylint: disable-msg=too-many
|
||||
redirect_url = try_change_enrollment(request)
|
||||
|
||||
# Resume the third-party-auth pipeline if necessary.
|
||||
if microsite.get_value('ENABLE_THIRD_PARTY_AUTH', settings.FEATURES.get('ENABLE_THIRD_PARTY_AUTH')) and pipeline.running(request):
|
||||
if third_party_auth.is_enabled() and pipeline.running(request):
|
||||
running_pipeline = pipeline.get(request)
|
||||
redirect_url = pipeline.get_complete_url(running_pipeline['backend'])
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
"""Third party authentication. """
|
||||
|
||||
from microsite_configuration import microsite
|
||||
|
||||
|
||||
def is_enabled():
|
||||
"""Check whether third party authentication has been enabled. """
|
||||
|
||||
# We do this imports internally to avoid initializing settings prematurely
|
||||
from django.conf import settings
|
||||
|
||||
return microsite.get_value(
|
||||
"ENABLE_THIRD_PARTY_AUTH",
|
||||
settings.FEATURES.get("ENABLE_THIRD_PARTY_AUTH")
|
||||
)
|
||||
|
||||
@@ -82,12 +82,27 @@ AUTH_ENTRY_DASHBOARD = 'dashboard'
|
||||
AUTH_ENTRY_LOGIN = 'login'
|
||||
AUTH_ENTRY_PROFILE = 'profile'
|
||||
AUTH_ENTRY_REGISTER = 'register'
|
||||
|
||||
# TODO (ECOM-369): Repace `AUTH_ENTRY_LOGIN` and `AUTH_ENTRY_REGISTER`
|
||||
# with these values once the A/B test completes, then delete
|
||||
# these constants.
|
||||
AUTH_ENTRY_LOGIN_2 = 'account_login'
|
||||
AUTH_ENTRY_REGISTER_2 = 'account_register'
|
||||
|
||||
_AUTH_ENTRY_CHOICES = frozenset([
|
||||
AUTH_ENTRY_DASHBOARD,
|
||||
AUTH_ENTRY_LOGIN,
|
||||
AUTH_ENTRY_PROFILE,
|
||||
AUTH_ENTRY_REGISTER
|
||||
AUTH_ENTRY_REGISTER,
|
||||
|
||||
# TODO (ECOM-369): For the A/B test of the combined
|
||||
# login/registration, we needed to introduce two
|
||||
# additional end-points. Once the test completes,
|
||||
# delete these constants from the choices list.
|
||||
AUTH_ENTRY_LOGIN_2,
|
||||
AUTH_ENTRY_REGISTER_2,
|
||||
])
|
||||
|
||||
_DEFAULT_RANDOM_PASSWORD_LENGTH = 12
|
||||
_PASSWORD_CHARSET = string.letters + string.digits
|
||||
|
||||
@@ -346,11 +361,28 @@ def parse_query_params(strategy, response, *args, **kwargs):
|
||||
'is_register': auth_entry == AUTH_ENTRY_REGISTER,
|
||||
# Whether the auth pipeline entered from /profile.
|
||||
'is_profile': auth_entry == AUTH_ENTRY_PROFILE,
|
||||
|
||||
# TODO (ECOM-369): Delete these once the A/B test
|
||||
# for the combined login/registration form completes.
|
||||
'is_login_2': auth_entry == AUTH_ENTRY_LOGIN_2,
|
||||
'is_register_2': auth_entry == AUTH_ENTRY_REGISTER_2,
|
||||
}
|
||||
|
||||
|
||||
# TODO (ECOM-369): Once the A/B test of the combined login/registration
|
||||
# form completes, we will be able to remove the extra login/registration
|
||||
# end-points. HOWEVER, users who used the new forms during the A/B
|
||||
# test may still have values for "is_login_2" and "is_register_2"
|
||||
# in their sessions. For this reason, we need to continue accepting
|
||||
# these kwargs in `redirect_to_supplementary_form`, but
|
||||
# these should redirect to the same location as "is_login" and "is_register"
|
||||
# (whichever login/registration end-points win in the test).
|
||||
@partial.partial
|
||||
def redirect_to_supplementary_form(strategy, details, response, uid, is_dashboard=None, is_login=None, is_profile=None, is_register=None, user=None, *args, **kwargs):
|
||||
def redirect_to_supplementary_form(
|
||||
strategy, details, response, uid,
|
||||
is_dashboard=None, is_login=None, is_profile=None, is_register=None,
|
||||
is_login_2=None, is_register_2=None,
|
||||
user=None, *args, **kwargs
|
||||
):
|
||||
"""Dispatches user to views outside the pipeline if necessary."""
|
||||
|
||||
# We're deliberately verbose here to make it clear what the intended
|
||||
@@ -364,20 +396,33 @@ def redirect_to_supplementary_form(strategy, details, response, uid, is_dashboar
|
||||
# It is important that we always execute the entire pipeline. Even if
|
||||
# behavior appears correct without executing a step, it means important
|
||||
# invariants have been violated and future misbehavior is likely.
|
||||
|
||||
user_inactive = user and not user.is_active
|
||||
user_unset = user is None
|
||||
dispatch_to_login = is_login and (user_unset or user_inactive)
|
||||
|
||||
# TODO (ECOM-369): Consolidate this with `dispatch_to_login`
|
||||
# once the A/B test completes.
|
||||
dispatch_to_login_2 = is_login_2 and (user_unset or user_inactive)
|
||||
|
||||
if is_dashboard or is_profile:
|
||||
return
|
||||
|
||||
if dispatch_to_login:
|
||||
return redirect('/login', name='signin_user')
|
||||
|
||||
# TODO (ECOM-369): Consolidate this with `dispatch_to_login`
|
||||
# once the A/B test completes.
|
||||
if dispatch_to_login_2:
|
||||
return redirect(reverse(AUTH_ENTRY_LOGIN_2))
|
||||
|
||||
if is_register and user_unset:
|
||||
return redirect('/register', name='register_user')
|
||||
|
||||
# TODO (ECOM-369): Consolidate this with `is_register`
|
||||
# once the A/B test completes.
|
||||
if is_register_2 and user_unset:
|
||||
return redirect(reverse(AUTH_ENTRY_REGISTER_2))
|
||||
|
||||
@partial.partial
|
||||
def login_analytics(*args, **kwargs):
|
||||
""" Sends login info to Segment.io """
|
||||
@@ -387,6 +432,12 @@ def login_analytics(*args, **kwargs):
|
||||
'is_login': 'edx.bi.user.account.authenticated',
|
||||
'is_dashboard': 'edx.bi.user.account.linked',
|
||||
'is_profile': 'edx.bi.user.account.linked',
|
||||
|
||||
# Backwards compatibility: during an A/B test for the combined
|
||||
# login/registration form, we introduced a new login end-point.
|
||||
# Since users may continue to have this in their sessions after
|
||||
# the test concludes, we need to continue accepting this action.
|
||||
'is_login_2': 'edx.bi.user.account.authenticated',
|
||||
}
|
||||
|
||||
# Note: we assume only one of the `action` kwargs (is_dashboard, is_login) to be
|
||||
@@ -408,7 +459,7 @@ def login_analytics(*args, **kwargs):
|
||||
},
|
||||
context={
|
||||
'Google Analytics': {
|
||||
'clientId': tracking_context.get('client_id')
|
||||
'clientId': tracking_context.get('client_id')
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -4,7 +4,9 @@ Utilities for writing third_party_auth tests.
|
||||
Used by Django and non-Django tests; must not have Django deps.
|
||||
"""
|
||||
|
||||
from contextlib import contextmanager
|
||||
import unittest
|
||||
import mock
|
||||
|
||||
from third_party_auth import provider
|
||||
|
||||
@@ -37,3 +39,81 @@ class TestCase(unittest.TestCase):
|
||||
provider.Registry._reset()
|
||||
provider.Registry.configure_once(self._original_providers)
|
||||
super(TestCase, self).tearDown()
|
||||
|
||||
|
||||
@contextmanager
|
||||
def simulate_running_pipeline(pipeline_target, backend, email=None, fullname=None, username=None):
|
||||
"""Simulate that a pipeline is currently running.
|
||||
|
||||
You can use this context manager to test packages that rely on third party auth.
|
||||
|
||||
This uses `mock.patch` to override some calls in `third_party_auth.pipeline`,
|
||||
so you will need to provide the "target" module *as it is imported*
|
||||
in the software under test. For example, if `foo/bar.py` does this:
|
||||
|
||||
>>> from third_party_auth import pipeline
|
||||
|
||||
then you will need to do something like this:
|
||||
|
||||
>>> with simulate_running_pipeline("foo.bar.pipeline", "google-oauth2"):
|
||||
>>> bar.do_something_with_the_pipeline()
|
||||
|
||||
If, on the other hand, `foo/bar.py` had done this:
|
||||
|
||||
>>> import third_party_auth
|
||||
|
||||
then you would use the target "foo.bar.third_party_auth.pipeline" instead.
|
||||
|
||||
Arguments:
|
||||
|
||||
pipeline_target (string): The path to `third_party_auth.pipeline` as it is imported
|
||||
in the software under test.
|
||||
|
||||
backend (string): The name of the backend currently running, for example "google-oauth2".
|
||||
Note that this is NOT the same as the name of the *provider*. See the Python
|
||||
social auth documentation for the names of the backends.
|
||||
|
||||
Keyword Arguments:
|
||||
email (string): If provided, simulate that the current provider has
|
||||
included the user's email address (useful for filling in the registration form).
|
||||
|
||||
fullname (string): If provided, simulate that the current provider has
|
||||
included the user's full name (useful for filling in the registration form).
|
||||
|
||||
username (string): If provided, simulate that the pipeline has provided
|
||||
this suggested username. This is something that the `third_party_auth`
|
||||
app generates itself and should be available by the time the user
|
||||
is authenticating with a third-party provider.
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
"""
|
||||
pipeline_data = {
|
||||
"backend": backend,
|
||||
"kwargs": {
|
||||
"details": {}
|
||||
}
|
||||
}
|
||||
if email is not None:
|
||||
pipeline_data["kwargs"]["details"]["email"] = email
|
||||
if fullname is not None:
|
||||
pipeline_data["kwargs"]["details"]["fullname"] = fullname
|
||||
if username is not None:
|
||||
pipeline_data["kwargs"]["username"] = username
|
||||
|
||||
pipeline_get = mock.patch("{pipeline}.get".format(pipeline=pipeline_target), spec=True)
|
||||
pipeline_running = mock.patch("{pipeline}.running".format(pipeline=pipeline_target), spec=True)
|
||||
|
||||
mock_get = pipeline_get.start()
|
||||
mock_running = pipeline_running.start()
|
||||
|
||||
mock_get.return_value = pipeline_data
|
||||
mock_running.return_value = True
|
||||
|
||||
try:
|
||||
yield
|
||||
|
||||
finally:
|
||||
pipeline_get.stop()
|
||||
pipeline_running.stop()
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
Helper functions for the account/profile Python APIs.
|
||||
This is NOT part of the public API.
|
||||
"""
|
||||
from collections import defaultdict
|
||||
from functools import wraps
|
||||
import logging
|
||||
import json
|
||||
@@ -101,6 +102,12 @@ class FormDescription(object):
|
||||
"password": ["min_length", "max_length"],
|
||||
}
|
||||
|
||||
OVERRIDE_FIELD_PROPERTIES = [
|
||||
"label", "type", "default", "placeholder",
|
||||
"instructions", "required", "restrictions",
|
||||
"options"
|
||||
]
|
||||
|
||||
def __init__(self, method, submit_url):
|
||||
"""Configure how the form should be submitted.
|
||||
|
||||
@@ -112,6 +119,7 @@ class FormDescription(object):
|
||||
self.method = method
|
||||
self.submit_url = submit_url
|
||||
self.fields = []
|
||||
self._field_overrides = defaultdict(dict)
|
||||
|
||||
def add_field(
|
||||
self, name, label=u"", field_type=u"text", default=u"",
|
||||
@@ -161,8 +169,8 @@ class FormDescription(object):
|
||||
raise InvalidFieldError(msg)
|
||||
|
||||
field_dict = {
|
||||
"label": label,
|
||||
"name": name,
|
||||
"label": label,
|
||||
"type": field_type,
|
||||
"default": default,
|
||||
"placeholder": placeholder,
|
||||
@@ -192,6 +200,10 @@ class FormDescription(object):
|
||||
)
|
||||
raise InvalidFieldError(msg)
|
||||
|
||||
# If there are overrides for this field, apply them now.
|
||||
# Any field property can be overwritten (for example, the default value or placeholder)
|
||||
field_dict.update(self._field_overrides.get(name, {}))
|
||||
|
||||
self.fields.append(field_dict)
|
||||
|
||||
def to_json(self):
|
||||
@@ -244,6 +256,31 @@ class FormDescription(object):
|
||||
"fields": self.fields
|
||||
})
|
||||
|
||||
def override_field_properties(self, field_name, **kwargs):
|
||||
"""Override properties of a field.
|
||||
|
||||
The overridden values take precedence over the values provided
|
||||
to `add_field()`.
|
||||
|
||||
Field properties not in `OVERRIDE_FIELD_PROPERTIES` will be ignored.
|
||||
|
||||
Arguments:
|
||||
field_name (string): The name of the field to override.
|
||||
|
||||
Keyword Args:
|
||||
Same as to `add_field()`.
|
||||
|
||||
"""
|
||||
# Transform kwarg "field_type" to "type" (a reserved Python keyword)
|
||||
if "field_type" in kwargs:
|
||||
kwargs["type"] = kwargs["field_type"]
|
||||
|
||||
self._field_overrides[field_name].update({
|
||||
property_name: property_value
|
||||
for property_name, property_value in kwargs.iteritems()
|
||||
if property_name in self.OVERRIDE_FIELD_PROPERTIES
|
||||
})
|
||||
|
||||
|
||||
def shim_student_view(view_func, check_logged_in=False):
|
||||
"""Create a "shim" view for a view function from the student Django app.
|
||||
@@ -320,16 +357,28 @@ def shim_student_view(view_func, check_logged_in=False):
|
||||
success = True
|
||||
redirect_url = None
|
||||
|
||||
# If the user is not authenticated, and we expect them to be
|
||||
# send a status 403.
|
||||
if check_logged_in and not request.user.is_authenticated():
|
||||
response.status_code = 403
|
||||
# If the user is not authenticated when we expect them to be
|
||||
# send the appropriate status code.
|
||||
# We check whether the user attribute is set to make
|
||||
# it easier to test this without necessarily running
|
||||
# the request through authentication middleware.
|
||||
is_authenticated = (
|
||||
getattr(request, 'user', None) is not None
|
||||
and request.user.is_authenticated()
|
||||
)
|
||||
if check_logged_in and not is_authenticated:
|
||||
# Preserve the 401 status code so the client knows
|
||||
# that the user successfully authenticated with third-party auth
|
||||
# but does not have a linked account.
|
||||
# Otherwise, send a 403 to indicate that the login failed.
|
||||
if response.status_code != 401:
|
||||
response.status_code = 403
|
||||
response.content = msg
|
||||
|
||||
# If the view wants to redirect us, send a status 302
|
||||
elif redirect_url is not None:
|
||||
response.status_code = 302
|
||||
response.content = redirect_url
|
||||
response['Location'] = redirect_url
|
||||
|
||||
# If an error condition occurs, send a status 400
|
||||
elif response.status_code != 200 or not success:
|
||||
@@ -343,6 +392,9 @@ def shim_student_view(view_func, check_logged_in=False):
|
||||
# If the response is successful, then return the content
|
||||
# of the response directly rather than including it
|
||||
# in a JSON-serialized dictionary.
|
||||
# This will also preserve error status codes such as a 401
|
||||
# (if the user is trying to log in using a third-party provider
|
||||
# but hasn't yet linked his or her account.)
|
||||
else:
|
||||
response.content = msg
|
||||
|
||||
|
||||
@@ -144,6 +144,15 @@ class StudentViewShimTest(TestCase):
|
||||
self.assertNotIn("enrollment_action", self.captured_request.POST)
|
||||
self.assertNotIn("course_id", self.captured_request.POST)
|
||||
|
||||
@ddt.data(True, False)
|
||||
def test_preserve_401_status(self, check_logged_in):
|
||||
view = self._shimmed_view(
|
||||
HttpResponse(status=401),
|
||||
check_logged_in=check_logged_in
|
||||
)
|
||||
response = view(HttpRequest())
|
||||
self.assertEqual(response.status_code, 401)
|
||||
|
||||
def test_non_json_response(self):
|
||||
view = self._shimmed_view(HttpResponse(content="Not a JSON dict"))
|
||||
response = view(HttpRequest())
|
||||
@@ -160,7 +169,7 @@ class StudentViewShimTest(TestCase):
|
||||
)
|
||||
response = view(HttpRequest())
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.content, "/redirect")
|
||||
self.assertEqual(response['Location'], "/redirect")
|
||||
|
||||
def test_error_from_json(self):
|
||||
view = self._shimmed_view(
|
||||
@@ -180,8 +189,13 @@ class StudentViewShimTest(TestCase):
|
||||
response = view(HttpRequest())
|
||||
self.assertEqual(response["test-header"], "test")
|
||||
|
||||
def _shimmed_view(self, response):
|
||||
def test_check_logged_in(self):
|
||||
view = self._shimmed_view(HttpResponse(), check_logged_in=True)
|
||||
response = view(HttpRequest())
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def _shimmed_view(self, response, check_logged_in=False):
|
||||
def stub_view(request):
|
||||
self.captured_request = request
|
||||
return response
|
||||
return shim_student_view(stub_view)
|
||||
return shim_student_view(stub_view, check_logged_in=check_logged_in)
|
||||
|
||||
@@ -13,7 +13,7 @@ from django.test.utils import override_settings
|
||||
from unittest import SkipTest, skipUnless
|
||||
import ddt
|
||||
from pytz import UTC
|
||||
from mock import patch
|
||||
import mock
|
||||
|
||||
from user_api.api import account as account_api, profile as profile_api
|
||||
|
||||
@@ -21,6 +21,7 @@ from student.tests.factories import UserFactory
|
||||
from user_api.tests.factories import UserPreferenceFactory
|
||||
from django_comment_common import models
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
from third_party_auth.tests.testutil import simulate_running_pipeline
|
||||
|
||||
from user_api.tests.test_constants import SORTED_COUNTRIES
|
||||
|
||||
@@ -808,6 +809,82 @@ class RegistrationViewTest(ApiTestCase):
|
||||
}
|
||||
)
|
||||
|
||||
def test_register_form_third_party_auth_running(self):
|
||||
no_extra_fields_setting = {}
|
||||
|
||||
with simulate_running_pipeline(
|
||||
"user_api.views.third_party_auth.pipeline",
|
||||
"google-oauth2", email="bob@example.com",
|
||||
fullname="Bob", username="Bob123"
|
||||
):
|
||||
# Password field should be hidden
|
||||
self._assert_reg_field(
|
||||
no_extra_fields_setting,
|
||||
{
|
||||
"name": "password",
|
||||
"default": "",
|
||||
"type": "hidden",
|
||||
"required": False,
|
||||
"label": "",
|
||||
"placeholder": "",
|
||||
"instructions": "",
|
||||
"restrictions": {},
|
||||
}
|
||||
)
|
||||
|
||||
# Email should be filled in
|
||||
self._assert_reg_field(
|
||||
no_extra_fields_setting,
|
||||
{
|
||||
u"name": u"email",
|
||||
u"default": u"bob@example.com",
|
||||
u"type": u"text",
|
||||
u"required": True,
|
||||
u"label": u"E-mail",
|
||||
u"placeholder": u"example: username@domain.com",
|
||||
u"instructions": u"This is the e-mail address you used to register with edX",
|
||||
u"restrictions": {
|
||||
u"min_length": 3,
|
||||
u"max_length": 254
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
# Full name should be filled in
|
||||
self._assert_reg_field(
|
||||
no_extra_fields_setting,
|
||||
{
|
||||
u"name": u"name",
|
||||
u"default": u"Bob",
|
||||
u"type": u"text",
|
||||
u"required": True,
|
||||
u"label": u"Full Name",
|
||||
u"placeholder": u"",
|
||||
u"instructions": u"Needed for any certificates you may earn",
|
||||
u"restrictions": {
|
||||
"max_length": 255,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
# Username should be filled in
|
||||
self._assert_reg_field(
|
||||
no_extra_fields_setting,
|
||||
{
|
||||
u"name": u"username",
|
||||
u"default": u"Bob123",
|
||||
u"type": u"text",
|
||||
u"required": True,
|
||||
u"label": u"Public Username",
|
||||
u"placeholder": u"",
|
||||
u"instructions": u"Will be shown in any discussions or forums you participate in (cannot be changed)",
|
||||
u"restrictions": {
|
||||
u"min_length": 2,
|
||||
u"max_length": 30,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
def test_register_form_level_of_education(self):
|
||||
self._assert_reg_field(
|
||||
{"level_of_education": "optional"},
|
||||
@@ -950,7 +1027,7 @@ class RegistrationViewTest(ApiTestCase):
|
||||
@override_settings(
|
||||
MKTG_URLS={"ROOT": "https://www.test.com/", "HONOR": "honor"},
|
||||
)
|
||||
@patch.dict(settings.FEATURES, {"ENABLE_MKTG_SITE": True})
|
||||
@mock.patch.dict(settings.FEATURES, {"ENABLE_MKTG_SITE": True})
|
||||
def test_registration_honor_code_mktg_site_enabled(self):
|
||||
self._assert_reg_field(
|
||||
{"honor_code": "required"},
|
||||
@@ -967,7 +1044,7 @@ class RegistrationViewTest(ApiTestCase):
|
||||
)
|
||||
|
||||
@override_settings(MKTG_URLS_LINK_MAP={"HONOR": "honor"})
|
||||
@patch.dict(settings.FEATURES, {"ENABLE_MKTG_SITE": False})
|
||||
@mock.patch.dict(settings.FEATURES, {"ENABLE_MKTG_SITE": False})
|
||||
def test_registration_honor_code_mktg_site_disabled(self):
|
||||
self._assert_reg_field(
|
||||
{"honor_code": "required"},
|
||||
@@ -988,7 +1065,7 @@ class RegistrationViewTest(ApiTestCase):
|
||||
"HONOR": "honor",
|
||||
"TOS": "tos",
|
||||
})
|
||||
@patch.dict(settings.FEATURES, {"ENABLE_MKTG_SITE": True})
|
||||
@mock.patch.dict(settings.FEATURES, {"ENABLE_MKTG_SITE": True})
|
||||
def test_registration_separate_terms_of_service_mktg_site_enabled(self):
|
||||
# Honor code field should say ONLY honor code,
|
||||
# not "terms of service and honor code"
|
||||
@@ -1022,7 +1099,7 @@ class RegistrationViewTest(ApiTestCase):
|
||||
)
|
||||
|
||||
@override_settings(MKTG_URLS_LINK_MAP={"HONOR": "honor", "TOS": "tos"})
|
||||
@patch.dict(settings.FEATURES, {"ENABLE_MKTG_SITE": False})
|
||||
@mock.patch.dict(settings.FEATURES, {"ENABLE_MKTG_SITE": False})
|
||||
def test_registration_separate_terms_of_service_mktg_site_disabled(self):
|
||||
# Honor code field should say ONLY honor code,
|
||||
# not "terms of service and honor code"
|
||||
|
||||
@@ -24,6 +24,8 @@ from django_comment_common.models import Role
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
from edxmako.shortcuts import marketing_link
|
||||
|
||||
import third_party_auth
|
||||
from microsite_configuration import microsite
|
||||
from user_api.api import account as account_api, profile as profile_api
|
||||
from user_api.helpers import FormDescription, shim_student_view, require_post_params
|
||||
|
||||
@@ -111,6 +113,8 @@ class LoginSessionView(APIView):
|
||||
Returns:
|
||||
HttpResponse: 200 on success
|
||||
HttpResponse: 400 if the request is not valid.
|
||||
HttpResponse: 401 if the user successfully authenticated with a third-party
|
||||
provider but does not have a linked account.
|
||||
HttpResponse: 403 if authentication failed.
|
||||
HttpResponse: 302 if redirecting to another page.
|
||||
|
||||
@@ -189,6 +193,7 @@ class RegistrationView(APIView):
|
||||
|
||||
"""
|
||||
form_desc = FormDescription("post", reverse("user_api_registration"))
|
||||
self._apply_third_party_auth_overrides(request, form_desc)
|
||||
|
||||
# Default fields are always required
|
||||
for field_name in self.DEFAULT_FIELDS:
|
||||
@@ -408,6 +413,53 @@ class RegistrationView(APIView):
|
||||
[("", "--")] + list(options)
|
||||
)
|
||||
|
||||
def _apply_third_party_auth_overrides(self, request, form_desc):
|
||||
"""Modify the registration form if the user has authenticated with a third-party provider.
|
||||
|
||||
If a user has successfully authenticated with a third-party provider,
|
||||
but does not yet have an account with EdX, we want to fill in
|
||||
the registration form with any info that we get from the
|
||||
provider.
|
||||
|
||||
This will also hide the password field, since we assign users a default
|
||||
(random) password on the assumption that they will be using
|
||||
third-party auth to log in.
|
||||
|
||||
Arguments:
|
||||
request (HttpRequest): The request for the registration form, used
|
||||
to determine if the user has successfully authenticated
|
||||
with a third-party provider.
|
||||
|
||||
form_desc (FormDescription): The registration form description
|
||||
|
||||
"""
|
||||
if third_party_auth.is_enabled():
|
||||
running_pipeline = third_party_auth.pipeline.get(request)
|
||||
if running_pipeline:
|
||||
current_provider = third_party_auth.provider.Registry.get_by_backend_name(running_pipeline.get('backend'))
|
||||
|
||||
# Override username / email / full name
|
||||
field_overrides = current_provider.get_register_form_data(
|
||||
running_pipeline.get('kwargs')
|
||||
)
|
||||
|
||||
for field_name in self.DEFAULT_FIELDS:
|
||||
if field_name in field_overrides:
|
||||
form_desc.override_field_properties(
|
||||
field_name, default=field_overrides[field_name]
|
||||
)
|
||||
|
||||
# Hide the password field
|
||||
form_desc.override_field_properties(
|
||||
"password",
|
||||
default="",
|
||||
field_type="hidden",
|
||||
required=False,
|
||||
label="",
|
||||
instructions="",
|
||||
restrictions={}
|
||||
)
|
||||
|
||||
|
||||
class UserViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
authentication_classes = (authentication.SessionAuthentication,)
|
||||
|
||||
@@ -6,7 +6,7 @@ from unittest import skipUnless
|
||||
from urllib import urlencode
|
||||
import json
|
||||
|
||||
from mock import patch
|
||||
import mock
|
||||
import ddt
|
||||
from django.test import TestCase
|
||||
from django.conf import settings
|
||||
@@ -14,14 +14,15 @@ from django.core.urlresolvers import reverse
|
||||
from django.core import mail
|
||||
|
||||
from util.testing import UrlResetMixin
|
||||
from third_party_auth.tests.testutil import simulate_running_pipeline
|
||||
from user_api.api import account as account_api
|
||||
from user_api.api import profile as profile_api
|
||||
from util.bad_request_rate_limiter import BadRequestRateLimiter
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class StudentAccountViewTest(UrlResetMixin, TestCase):
|
||||
""" Tests for the student account views. """
|
||||
class StudentAccountUpdateTest(UrlResetMixin, TestCase):
|
||||
""" Tests for the student account views that update the user's account information. """
|
||||
|
||||
USERNAME = u"heisenberg"
|
||||
ALTERNATE_USERNAME = u"walt"
|
||||
@@ -51,9 +52,9 @@ class StudentAccountViewTest(UrlResetMixin, TestCase):
|
||||
|
||||
INVALID_KEY = u"123abc"
|
||||
|
||||
@patch.dict(settings.FEATURES, {'ENABLE_NEW_DASHBOARD': True})
|
||||
@mock.patch.dict(settings.FEATURES, {'ENABLE_NEW_DASHBOARD': True})
|
||||
def setUp(self):
|
||||
super(StudentAccountViewTest, self).setUp("student_account.urls")
|
||||
super(StudentAccountUpdateTest, self).setUp("student_account.urls")
|
||||
|
||||
# Create/activate a new account
|
||||
activation_key = account_api.create_account(self.USERNAME, self.OLD_PASSWORD, self.OLD_EMAIL)
|
||||
@@ -67,37 +68,6 @@ class StudentAccountViewTest(UrlResetMixin, TestCase):
|
||||
response = self.client.get(reverse('account_index'))
|
||||
self.assertContains(response, "Student Account")
|
||||
|
||||
@ddt.data(
|
||||
("account_login", "login"),
|
||||
("account_register", "register"),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_login_and_registration_form(self, url_name, initial_mode):
|
||||
response = self.client.get(reverse(url_name))
|
||||
expected_data = u"data-initial-mode=\"{mode}\"".format(mode=initial_mode)
|
||||
self.assertContains(response, expected_data)
|
||||
|
||||
@ddt.data("account_login", "account_register")
|
||||
def test_login_and_registration_third_party_auth_urls(self, url_name):
|
||||
response = self.client.get(reverse(url_name))
|
||||
|
||||
# This relies on the THIRD_PARTY_AUTH configuration in the test settings
|
||||
expected_data = u"data-third-party-auth-providers=\"{providers}\"".format(
|
||||
providers=json.dumps([
|
||||
{
|
||||
u'icon_class': u'icon-facebook',
|
||||
u'login_url': u'/auth/login/facebook/?auth_entry=login',
|
||||
u'name': u'Facebook'
|
||||
},
|
||||
{
|
||||
u'icon_class': u'icon-google-plus',
|
||||
u'login_url': u'/auth/login/google-oauth2/?auth_entry=login',
|
||||
u'name': u'Google'
|
||||
}
|
||||
])
|
||||
)
|
||||
self.assertContains(response, expected_data)
|
||||
|
||||
def test_change_email(self):
|
||||
response = self._change_email(self.NEW_EMAIL, self.OLD_PASSWORD)
|
||||
self.assertEquals(response.status_code, 200)
|
||||
@@ -144,7 +114,7 @@ class StudentAccountViewTest(UrlResetMixin, TestCase):
|
||||
|
||||
def test_email_change_request_no_user(self):
|
||||
# Patch account API to raise an internal error when an email change is requested
|
||||
with patch('student_account.views.account_api.request_email_change') as mock_call:
|
||||
with mock.patch('student_account.views.account_api.request_email_change') as mock_call:
|
||||
mock_call.side_effect = account_api.AccountUserNotFound
|
||||
response = self._change_email(self.NEW_EMAIL, self.OLD_PASSWORD)
|
||||
|
||||
@@ -215,7 +185,7 @@ class StudentAccountViewTest(UrlResetMixin, TestCase):
|
||||
activation_key = account_api.request_email_change(self.USERNAME, self.NEW_EMAIL, self.OLD_PASSWORD)
|
||||
|
||||
# Patch account API to return an internal error
|
||||
with patch('student_account.views.account_api.confirm_email_change') as mock_call:
|
||||
with mock.patch('student_account.views.account_api.confirm_email_change') as mock_call:
|
||||
mock_call.side_effect = account_api.AccountInternalError
|
||||
response = self.client.get(reverse('email_change_confirm', kwargs={'key': activation_key}))
|
||||
|
||||
@@ -392,3 +362,88 @@ class StudentAccountViewTest(UrlResetMixin, TestCase):
|
||||
data['email'] = email
|
||||
|
||||
return self.client.post(path=reverse('password_change_request'), data=data)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class StudentAccountLoginAndRegistrationTest(TestCase):
|
||||
""" Tests for the student account views that update the user's account information. """
|
||||
|
||||
USERNAME = "bob"
|
||||
EMAIL = "bob@example.com"
|
||||
PASSWORD = "password"
|
||||
|
||||
@ddt.data(
|
||||
("account_login", "login"),
|
||||
("account_register", "register"),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_login_and_registration_form(self, url_name, initial_mode):
|
||||
response = self.client.get(reverse(url_name))
|
||||
expected_data = u"data-initial-mode=\"{mode}\"".format(mode=initial_mode)
|
||||
self.assertContains(response, expected_data)
|
||||
|
||||
@ddt.data("account_login", "account_register")
|
||||
def test_login_and_registration_form_already_authenticated(self, url_name):
|
||||
# Create/activate a new account and log in
|
||||
activation_key = account_api.create_account(self.USERNAME, self.PASSWORD, self.EMAIL)
|
||||
account_api.activate_account(activation_key)
|
||||
result = self.client.login(username=self.USERNAME, password=self.PASSWORD)
|
||||
self.assertTrue(result)
|
||||
|
||||
# Verify that we're redirected to the dashboard
|
||||
response = self.client.get(reverse(url_name))
|
||||
self.assertRedirects(response, reverse("dashboard"))
|
||||
|
||||
@mock.patch.dict(settings.FEATURES, {"ENABLE_THIRD_PARTY_AUTH": False})
|
||||
@ddt.data("account_login", "account_register")
|
||||
def test_third_party_auth_disabled(self, url_name):
|
||||
response = self.client.get(reverse(url_name))
|
||||
expected_data = "data-third-party-auth='{auth_info}'".format(
|
||||
auth_info=json.dumps({
|
||||
"currentProvider": None,
|
||||
"providers": []
|
||||
})
|
||||
)
|
||||
self.assertContains(response, expected_data)
|
||||
|
||||
@ddt.data(
|
||||
("account_login", None, None),
|
||||
("account_register", None, None),
|
||||
("account_login", "google-oauth2", "Google"),
|
||||
("account_register", "google-oauth2", "Google"),
|
||||
("account_login", "facebook", "Facebook"),
|
||||
("account_register", "facebook", "Facebook"),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_third_party_auth(self, url_name, current_backend, current_provider):
|
||||
# Simulate a running pipeline
|
||||
if current_backend is not None:
|
||||
pipeline_target = "student_account.views.third_party_auth.pipeline"
|
||||
with simulate_running_pipeline(pipeline_target, current_backend):
|
||||
response = self.client.get(reverse(url_name))
|
||||
|
||||
# Do NOT simulate a running pipeline
|
||||
else:
|
||||
response = self.client.get(reverse(url_name))
|
||||
|
||||
# This relies on the THIRD_PARTY_AUTH configuration in the test settings
|
||||
expected_data = u"data-third-party-auth='{auth_info}'".format(
|
||||
auth_info=json.dumps({
|
||||
"currentProvider": current_provider,
|
||||
"providers": [
|
||||
{
|
||||
"name": "Facebook",
|
||||
"iconClass": "icon-facebook",
|
||||
"loginUrl": "/auth/login/facebook/?auth_entry=account_login",
|
||||
"registerUrl": "/auth/login/facebook/?auth_entry=account_register",
|
||||
},
|
||||
{
|
||||
"name": "Google",
|
||||
"iconClass": "icon-google-plus",
|
||||
"loginUrl": "/auth/login/google-oauth2/?auth_entry=account_login",
|
||||
"registerUrl": "/auth/login/google-oauth2/?auth_entry=account_register",
|
||||
}
|
||||
]
|
||||
})
|
||||
)
|
||||
self.assertContains(response, expected_data)
|
||||
|
||||
@@ -6,13 +6,15 @@ from django.conf import settings
|
||||
from django.http import (
|
||||
HttpResponse, HttpResponseBadRequest, HttpResponseForbidden
|
||||
)
|
||||
from django.shortcuts import redirect
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.core.mail import send_mail
|
||||
from django_future.csrf import ensure_csrf_cookie
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.views.decorators.http import require_http_methods
|
||||
from edxmako.shortcuts import render_to_response, render_to_string
|
||||
import third_party_auth
|
||||
from microsite_configuration import microsite
|
||||
import third_party_auth
|
||||
|
||||
from user_api.api import account as account_api
|
||||
from user_api.api import profile as profile_api
|
||||
@@ -58,24 +60,17 @@ def login_and_registration_form(request, initial_mode="login"):
|
||||
initial_mode (string): Either "login" or "registration".
|
||||
|
||||
"""
|
||||
# If we're already logged in, redirect to the dashboard
|
||||
if request.user.is_authenticated():
|
||||
return redirect(reverse('dashboard'))
|
||||
|
||||
# Otherwise, render the combined login/registration page
|
||||
context = {
|
||||
'disable_courseware_js': True,
|
||||
'initial_mode': initial_mode,
|
||||
'third_party_auth_providers': json.dumps([])
|
||||
'third_party_auth': json.dumps(_third_party_auth_context(request)),
|
||||
}
|
||||
|
||||
if microsite.get_value("ENABLE_THIRD_PARTY_AUTH", settings.FEATURES.get("ENABLE_THIRD_PARTY_AUTH")):
|
||||
context["third_party_auth_providers"] = json.dumps([
|
||||
{
|
||||
"name": enabled.NAME,
|
||||
"icon_class": enabled.ICON_CLASS,
|
||||
"login_url": third_party_auth.pipeline.get_login_url(
|
||||
enabled.NAME, third_party_auth.pipeline.AUTH_ENTRY_LOGIN
|
||||
),
|
||||
}
|
||||
for enabled in third_party_auth.provider.Registry.enabled()
|
||||
])
|
||||
|
||||
return render_to_response('student_account/login_and_register.html', context)
|
||||
|
||||
|
||||
@@ -266,3 +261,44 @@ def password_change_request_handler(request):
|
||||
return HttpResponse(status=200)
|
||||
else:
|
||||
return HttpResponseBadRequest("No email address provided.")
|
||||
|
||||
|
||||
def _third_party_auth_context(request):
|
||||
"""Context for third party auth providers and the currently running pipeline.
|
||||
|
||||
Arguments:
|
||||
request (HttpRequest): The request, used to determine if a pipeline
|
||||
is currently running.
|
||||
|
||||
Returns:
|
||||
dict
|
||||
|
||||
"""
|
||||
context = {
|
||||
"currentProvider": None,
|
||||
"providers": []
|
||||
}
|
||||
|
||||
if third_party_auth.is_enabled():
|
||||
context["providers"] = [
|
||||
{
|
||||
"name": enabled.NAME,
|
||||
"iconClass": enabled.ICON_CLASS,
|
||||
"loginUrl": third_party_auth.pipeline.get_login_url(
|
||||
enabled.NAME, third_party_auth.pipeline.AUTH_ENTRY_LOGIN_2
|
||||
),
|
||||
"registerUrl": third_party_auth.pipeline.get_login_url(
|
||||
enabled.NAME, third_party_auth.pipeline.AUTH_ENTRY_REGISTER_2
|
||||
)
|
||||
}
|
||||
for enabled in third_party_auth.provider.Registry.enabled()
|
||||
]
|
||||
|
||||
running_pipeline = third_party_auth.pipeline.get(request)
|
||||
if running_pipeline is not None:
|
||||
current_provider = third_party_auth.provider.Registry.get_by_backend_name(
|
||||
running_pipeline.get('backend')
|
||||
)
|
||||
context["currentProvider"] = current_provider.NAME
|
||||
|
||||
return context
|
||||
|
||||
@@ -13,7 +13,7 @@ from django.contrib.auth.decorators import login_required
|
||||
from edxmako.shortcuts import render_to_response
|
||||
from user_api.api import profile as profile_api
|
||||
from lang_pref import LANGUAGE_KEY, api as language_api
|
||||
from third_party_auth import pipeline
|
||||
import third_party_auth
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -60,8 +60,8 @@ def _get_profile(request):
|
||||
'disable_courseware_js': True
|
||||
}
|
||||
|
||||
if settings.FEATURES.get('ENABLE_THIRD_PARTY_AUTH'):
|
||||
context['provider_user_states'] = pipeline.get_provider_user_states(user)
|
||||
if third_party_auth.is_enabled():
|
||||
context['provider_user_states'] = third_party_auth.pipeline.get_provider_user_states(user)
|
||||
|
||||
return render_to_response('student_profile/index.html', context)
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ var edx = edx || {};
|
||||
edx.student.account = edx.student.account || {};
|
||||
|
||||
return new edx.student.account.AccessView({
|
||||
mode: $('#login-and-registration-container').data('initial-mode') || 'login',
|
||||
thirdPartyAuth: $('#login-and-registration-container').data('third-party-auth-providers') || false
|
||||
mode: $('#login-and-registration-container').data('initial-mode'),
|
||||
thirdPartyAuth: $('#login-and-registration-container').data('third-party-auth')
|
||||
});
|
||||
})(jQuery);
|
||||
})(jQuery);
|
||||
|
||||
@@ -26,7 +26,13 @@ var edx = edx || {};
|
||||
|
||||
initialize: function( obj ) {
|
||||
this.tpl = $(this.tpl).html();
|
||||
this.activeForm = obj.mode;
|
||||
this.activeForm = obj.mode || 'login';
|
||||
this.thirdPartyAuth = obj.thirdPartyAuth || {
|
||||
currentProvider: null,
|
||||
providers: []
|
||||
};
|
||||
console.log(obj);
|
||||
|
||||
this.render();
|
||||
},
|
||||
|
||||
@@ -48,12 +54,12 @@ var edx = edx || {};
|
||||
|
||||
loadForm: function( type ) {
|
||||
if ( type === 'login' ) {
|
||||
this.subview.login = new edx.student.account.LoginView();
|
||||
this.subview.login = new edx.student.account.LoginView( this.thirdPartyAuth );
|
||||
|
||||
// Listen for 'password-help' event to toggle sub-views
|
||||
this.listenTo( this.subview.login, 'password-help', this.resetPassword );
|
||||
} else if ( type === 'register' ) {
|
||||
this.subview.register = new edx.student.account.RegisterView();
|
||||
this.subview.register = new edx.student.account.RegisterView( this.thirdPartyAuth );
|
||||
} else if ( type === 'reset' ) {
|
||||
this.subview.passwordHelp = new edx.student.account.PasswordResetView();
|
||||
}
|
||||
|
||||
@@ -17,15 +17,20 @@ var edx = edx || {};
|
||||
|
||||
events: {
|
||||
'click .js-login': 'submitForm',
|
||||
'click .forgot-password': 'forgotPassword'
|
||||
'click .forgot-password': 'forgotPassword',
|
||||
'click .login-provider': 'thirdPartyAuth'
|
||||
},
|
||||
|
||||
errors: [],
|
||||
|
||||
$form: {},
|
||||
|
||||
initialize: function() {
|
||||
initialize: function( thirdPartyAuthInfo ) {
|
||||
this.tpl = $(this.tpl).html();
|
||||
|
||||
this.providers = thirdPartyAuthInfo.providers || [];
|
||||
this.currentProvider = thirdPartyAuthInfo.currentProvider || "";
|
||||
|
||||
this.getInitialData();
|
||||
},
|
||||
|
||||
@@ -34,7 +39,9 @@ var edx = edx || {};
|
||||
var fields = html || '';
|
||||
|
||||
$(this.el).html( _.template( this.tpl, {
|
||||
fields: fields
|
||||
fields: fields,
|
||||
currentProvider: this.currentProvider,
|
||||
providers: this.providers
|
||||
}));
|
||||
|
||||
this.postRender();
|
||||
@@ -44,9 +51,16 @@ var edx = edx || {};
|
||||
|
||||
postRender: function() {
|
||||
var $container = $(this.el);
|
||||
|
||||
this.$form = $container.find('form');
|
||||
this.$errors = $container.find('.error-msg');
|
||||
this.$alreadyAuthenticatedMsg = $container.find('.already-authenticated-msg');
|
||||
|
||||
// If we're already authenticated with a third-party
|
||||
// provider, try logging in. The easiest way to do this
|
||||
// is to simply submit the form.
|
||||
if (this.currentProvider) {
|
||||
this.model.save();
|
||||
}
|
||||
},
|
||||
|
||||
getInitialData: function() {
|
||||
@@ -58,8 +72,8 @@ var edx = edx || {};
|
||||
url: '/user_api/v1/account/login_session/',
|
||||
success: function( data ) {
|
||||
console.log(data);
|
||||
that.buildForm( data.fields );
|
||||
that.initModel( data.submit_url, data.method );
|
||||
that.buildForm( data.fields );
|
||||
},
|
||||
error: function( jqXHR, textStatus, errorThrown ) {
|
||||
console.log('fail ', errorThrown);
|
||||
@@ -72,9 +86,7 @@ var edx = edx || {};
|
||||
url: url
|
||||
});
|
||||
|
||||
this.listenTo( this.model, 'error', function( error ) {
|
||||
console.log(error.status, ' error: ', error.responseText);
|
||||
});
|
||||
this.listenTo( this.model, 'error', this.saveError );
|
||||
},
|
||||
|
||||
buildForm: function( data ) {
|
||||
@@ -148,6 +160,16 @@ var edx = edx || {};
|
||||
}
|
||||
},
|
||||
|
||||
thirdPartyAuth: function( event ) {
|
||||
var providerUrl = $(event.target).data("provider-url") || "";
|
||||
if (providerUrl) {
|
||||
window.location.href = providerUrl;
|
||||
} else {
|
||||
// TODO -- error handling here
|
||||
console.log("No URL available for third party auth provider");
|
||||
}
|
||||
},
|
||||
|
||||
toggleErrorMsg: function( show ) {
|
||||
if ( show ) {
|
||||
this.$errors.removeClass('hidden');
|
||||
@@ -158,6 +180,23 @@ var edx = edx || {};
|
||||
|
||||
validate: function( $el ) {
|
||||
return edx.utils.validate( $el );
|
||||
},
|
||||
|
||||
saveError: function( error ) {
|
||||
console.log(error.status, ' error: ', error.responseText);
|
||||
|
||||
// If we've gotten a 401 error, it means that we've successfully
|
||||
// authenticated with a third-party provider, but we haven't
|
||||
// linked the account to an EdX account. In this case,
|
||||
// we need to prompt the user to enter a little more information
|
||||
// to complete the registration process.
|
||||
if (error.status === 401 && this.currentProvider) {
|
||||
this.$alreadyAuthenticatedMsg.removeClass("hidden");
|
||||
}
|
||||
else {
|
||||
this.$alreadyAuthenticatedMsg.addClass("hidden");
|
||||
// TODO -- display the error
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -16,15 +16,19 @@ var edx = edx || {};
|
||||
fieldTpl: $('#form_field-tpl').html(),
|
||||
|
||||
events: {
|
||||
'click .js-register': 'submitForm'
|
||||
'click .js-register': 'submitForm',
|
||||
'click .login-provider': 'thirdPartyAuth'
|
||||
},
|
||||
|
||||
errors: [],
|
||||
|
||||
$form: {},
|
||||
|
||||
initialize: function() {
|
||||
initialize: function( thirdPartyAuthInfo ) {
|
||||
this.tpl = $(this.tpl).html();
|
||||
|
||||
this.providers = thirdPartyAuthInfo.providers || [];
|
||||
this.currentProvider = thirdPartyAuthInfo.currentProvider || "";
|
||||
this.getInitialData();
|
||||
},
|
||||
|
||||
@@ -33,7 +37,9 @@ var edx = edx || {};
|
||||
var fields = html || '';
|
||||
|
||||
$(this.el).html( _.template( this.tpl, {
|
||||
fields: fields
|
||||
fields: fields,
|
||||
currentProvider: this.currentProvider,
|
||||
providers: this.providers
|
||||
}));
|
||||
|
||||
this.postRender();
|
||||
@@ -81,8 +87,10 @@ var edx = edx || {};
|
||||
i,
|
||||
len = data.length,
|
||||
fieldTpl = this.fieldTpl;
|
||||
console.log('buildForm ', data);
|
||||
for ( i=0; i<len; i++ ) {
|
||||
// "default" is reserved in JavaScript
|
||||
data[i].value = data[i]["default"];
|
||||
|
||||
html.push( _.template( fieldTpl, $.extend( data[i], {
|
||||
form: 'register'
|
||||
}) ) );
|
||||
@@ -142,6 +150,16 @@ console.log(this.model);
|
||||
}
|
||||
},
|
||||
|
||||
thirdPartyAuth: function( event ) {
|
||||
var providerUrl = $(event.target).data("provider-url") || "";
|
||||
if (providerUrl) {
|
||||
window.location.href = providerUrl;
|
||||
} else {
|
||||
// TODO -- error handling here
|
||||
console.log("No URL available for third party auth provider");
|
||||
}
|
||||
},
|
||||
|
||||
toggleErrorMsg: function( show ) {
|
||||
if ( show ) {
|
||||
this.$errors.removeClass('hidden');
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
<%! from django.template import RequestContext %>
|
||||
<%! import third_party_auth %>
|
||||
<%! from third_party_auth import pipeline %>
|
||||
<%! from microsite_configuration import microsite %>
|
||||
|
||||
@@ -95,7 +96,7 @@
|
||||
<%include file='dashboard/_dashboard_info_language.html' />
|
||||
%endif
|
||||
|
||||
% if microsite.get_value('ENABLE_THIRD_PARTY_AUTH', settings.FEATURES.get('ENABLE_THIRD_PARTY_AUTH')):
|
||||
% if third_party_auth.is_enabled():
|
||||
<li class="controls--account">
|
||||
<span class="title">
|
||||
## Translators: this section lists all the third-party authentication providers (for example, Google and LinkedIn) the user can link with or unlink from their edX account.
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
<%! from django.core.urlresolvers import reverse %>
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
<%! import third_party_auth %>
|
||||
<%! from third_party_auth import provider, pipeline %>
|
||||
|
||||
<%block name="pagetitle">${_("Log into your {platform_name} Account").format(platform_name=platform_name)}</%block>
|
||||
@@ -48,8 +49,16 @@
|
||||
|
||||
$('#login-form').on('ajax:error', function(event, request, status_string) {
|
||||
toggleSubmitButton(true);
|
||||
$('.third-party-signin.message').addClass('is-shown').focus();
|
||||
$('.third-party-signin.message .instructions').html(request.responseText);
|
||||
|
||||
if (request.status === 401) {
|
||||
$('.message.submission-error').removeClass('is-shown');
|
||||
$('.third-party-signin.message').addClass('is-shown').focus();
|
||||
$('.third-party-signin.message .instructions').html(request.responseText);
|
||||
} else {
|
||||
$('.third-party-signin.message').removeClass('is-shown');
|
||||
$('.message.submission-error').addClass('is-shown').focus();
|
||||
$('.message.submission-error').html(gettext("Your request could not be completed. Please try again."));
|
||||
}
|
||||
});
|
||||
|
||||
$('#login-form').on('ajax:success', function(event, json, xhr) {
|
||||
@@ -194,7 +203,7 @@
|
||||
</div>
|
||||
</form>
|
||||
|
||||
% if microsite.get_value('ENABLE_THIRD_PARTY_AUTH', settings.FEATURES.get('ENABLE_THIRD_PARTY_AUTH')):
|
||||
% if third_party_auth.is_enabled():
|
||||
|
||||
<span class="deco-divider">
|
||||
## Developers: this is a sentence fragment, which is usually frowned upon. The design of the pags uses this fragment to provide an "else" clause underneath a number of choices. It's OK to leave it.
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
<%! from student.models import UserProfile %>
|
||||
<%! from datetime import date %>
|
||||
<%! import third_party_auth %>
|
||||
<%! from third_party_auth import pipeline, provider %>
|
||||
<%! import calendar %>
|
||||
|
||||
@@ -116,7 +117,7 @@
|
||||
<ul class="message-copy"> </ul>
|
||||
</div>
|
||||
|
||||
% if microsite.get_value('ENABLE_THIRD_PARTY_AUTH', settings.FEATURES.get('ENABLE_THIRD_PARTY_AUTH')):
|
||||
% if third_party_auth.is_enabled():
|
||||
|
||||
% if not running_pipeline:
|
||||
|
||||
@@ -182,7 +183,7 @@
|
||||
<span class="tip tip-input" id="username-tip">${_('Will be shown in any discussions or forums you participate in')} <strong>(${_('cannot be changed later')})</strong></span>
|
||||
</li>
|
||||
|
||||
% if settings.FEATURES.get('ENABLE_THIRD_PARTY_AUTH') and running_pipeline:
|
||||
% if third_party_auth.is_enabled() and running_pipeline:
|
||||
|
||||
<li class="is-disabled field optional password" id="field-password" hidden>
|
||||
<label for="password">${_('Password')}</label>
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
<% if ( restrictions.min_length ) { %> minlength="<%= restrictions.min_length %>"<% } %>
|
||||
<% if ( restrictions.max_length ) { %> maxlength="<%= restrictions.max_length %>"<% } %>
|
||||
<% if ( required ) { %> required<% } %>
|
||||
value="<%- value %>"
|
||||
/>
|
||||
<% } %>
|
||||
|
||||
|
||||
@@ -5,6 +5,22 @@
|
||||
<p>Email or password is incorrent. <a href="#">Forgot password?</a></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="already-authenticated-msg hidden">
|
||||
<% if (currentProvider) { %>
|
||||
<p class="instructions">
|
||||
You've successfully logged into <%- currentProvider %>, but you need to link
|
||||
your account. Please click "I am a returning user" to create
|
||||
an EdX account.
|
||||
</p>
|
||||
<% } %>
|
||||
</div>
|
||||
<%= fields %>
|
||||
<button class="action action-primary action-update js-login">Log in</button>
|
||||
</form>
|
||||
</form>
|
||||
<% for (var i=0; i < providers.length; i++) {
|
||||
var provider = providers[i];
|
||||
%>
|
||||
<button type="submit"class="button button-primary button-<%- provider.name %> login-provider" data-provider-url="<%- provider.loginUrl %>">
|
||||
<span class="icon <%- provider.iconClass %>"></span>Sign in with <%- provider.name %>
|
||||
</button>
|
||||
<% } %>
|
||||
|
||||
@@ -19,34 +19,10 @@
|
||||
% endfor
|
||||
</%block>
|
||||
|
||||
|
||||
## TODO: Use JavaScript to populate this div with
|
||||
## the actual registration/login forms (loaded asynchronously from the user API)
|
||||
## The URLS for the forms are:
|
||||
## - GET /user_api/v1/registration/
|
||||
## - GET /user_api/v1/login_session/
|
||||
##
|
||||
## You can post back to those URLs with JSON-serialized
|
||||
## data from the form fields in order to complete the registration
|
||||
## or login.
|
||||
##
|
||||
## Also TODO: we need to figure out how to enroll students in
|
||||
## a course if they got here from a course about page.
|
||||
##
|
||||
## third_party_auth_providers is a JSON-serialized list of
|
||||
## dictionaries of the form:
|
||||
## {
|
||||
## "name": "Facebook",
|
||||
## "icon_class": "facebook-icon",
|
||||
## "login_url": "http://api.facebook.com/auth"
|
||||
## }
|
||||
##
|
||||
## Note that this list may be empty.
|
||||
##
|
||||
<div class="section-bkg-wrapper">
|
||||
<div id="login-and-registration-container"
|
||||
class="login-register"
|
||||
data-initial-mode="${initial_mode}"
|
||||
data-third-party-auth-providers="${third_party_auth_providers}"
|
||||
data-third-party-auth='${third_party_auth}'
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
<% if (currentProvider) { %>
|
||||
<p class="instructions">
|
||||
You've successfully signed in with <strong><%- currentProvider %></strong>.<br />
|
||||
We just need a little more information before you start learning with edX.
|
||||
</p>
|
||||
<% } else {
|
||||
for (var i=0; i < providers.length; i++) {
|
||||
var provider = providers[i];
|
||||
%>
|
||||
<button type="submit"class="button button-primary button-<%- provider.name %> login-provider" data-provider-url="<%- provider.registerUrl %>">
|
||||
<span class="icon <%- provider.iconClass %>"></span>Sign up with <%- provider.name %>
|
||||
</button>
|
||||
<% }
|
||||
} %>
|
||||
|
||||
<form id="register" autocomplete="off">
|
||||
<div class="error-msg hidden">
|
||||
<h4>An error occured in your registration.</h4>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
<%! import third_party_auth %>
|
||||
<%namespace name='static' file='/static_content.html'/>
|
||||
|
||||
<%inherit file="../main.html" />
|
||||
@@ -25,6 +26,6 @@
|
||||
|
||||
<div id="profile-container"></div>
|
||||
|
||||
% if settings.FEATURES.get('ENABLE_THIRD_PARTY_AUTH'):
|
||||
% if third_party_auth.is_enabled():
|
||||
<%include file="third_party_auth.html" />
|
||||
% endif
|
||||
|
||||
Reference in New Issue
Block a user