Commit a8bed5ce by Renzo Lucioni

Make logistration generally available if feature flag is active

Makes logistration available at /login and /register as well as /accounts/login/ and /accounts/register/. In addition:

- Adds support for redirect URLs in third party auth for combined login/registration page
- Adds support for external auth on the combined login/registration page
- Removes old login and registration acceptance tests
- Adds deprecation warnings to old login and register views
- Moves third party auth util to student_account
- Adds exception for microsites (theming)
parent e24d4d96
"""Intercept login and registration requests.
This module contains legacy code originally from `student.views`.
"""
import re
from django.conf import settings
from django.shortcuts import redirect
from django.core.urlresolvers import reverse
import external_auth.views
from xmodule.modulestore.django import modulestore
from opaque_keys.edx.keys import CourseKey
# pylint: disable=fixme
# TODO: This function is kind of gnarly/hackish/etc and is only used in one location.
# It'd be awesome if we could get rid of it; manually parsing course_id strings form larger strings
# seems Probably Incorrect
def _parse_course_id_from_string(input_str):
"""
Helper function to determine if input_str (typically the queryparam 'next') contains a course_id.
@param input_str:
@return: the course_id if found, None if not
"""
m_obj = re.match(r'^/courses/{}'.format(settings.COURSE_ID_PATTERN), input_str)
if m_obj:
return CourseKey.from_string(m_obj.group('course_id'))
return None
def _get_course_enrollment_domain(course_id):
"""
Helper function to get the enrollment domain set for a course with id course_id
@param course_id:
@return:
"""
course = modulestore().get_course(course_id)
if course is None:
return None
return course.enrollment_domain
def login(request):
"""Allow external auth to intercept and handle a login request.
Arguments:
request (Request): A request for the login page.
Returns:
Response or None
"""
# Default to a `None` response, indicating that external auth
# is not handling the request.
response = None
if settings.FEATURES['AUTH_USE_CERTIFICATES'] and external_auth.views.ssl_get_cert_from_request(request):
# SSL login doesn't require a view, so redirect
# branding and allow that to process the login if it
# is enabled and the header is in the request.
response = external_auth.views.redirect_with_get('root', request.GET)
elif settings.FEATURES.get('AUTH_USE_CAS'):
# If CAS is enabled, redirect auth handling to there
response = redirect(reverse('cas-login'))
elif settings.FEATURES.get('AUTH_USE_SHIB'):
redirect_to = request.GET.get('next')
if redirect_to:
course_id = _parse_course_id_from_string(redirect_to)
if course_id and _get_course_enrollment_domain(course_id):
response = external_auth.views.course_specific_login(request, course_id.to_deprecated_string())
return response
def register(request):
"""Allow external auth to intercept and handle a registration request.
Arguments:
request (Request): A request for the registration page.
Returns:
Response or None
"""
response = None
if settings.FEATURES.get('AUTH_USE_CERTIFICATES_IMMEDIATE_SIGNUP'):
# Redirect to branding to process their certificate if SSL is enabled
# and registration is disabled.
response = external_auth.views.redirect_with_get('root', request.GET)
return response
...@@ -220,8 +220,7 @@ class SSLClientTest(ModuleStoreTestCase): ...@@ -220,8 +220,7 @@ class SSLClientTest(ModuleStoreTestCase):
# Test that they do signin if they don't have a cert # Test that they do signin if they don't have a cert
response = self.client.get(reverse('signin_user')) response = self.client.get(reverse('signin_user'))
self.assertEqual(200, response.status_code) self.assertEqual(200, response.status_code)
self.assertTrue('login_form' in response.content self.assertTrue('login-and-registration-container' in response.content)
or 'login-form' in response.content)
# And get directly logged in otherwise # And get directly logged in otherwise
response = self.client.get( response = self.client.get(
......
...@@ -4,83 +4,9 @@ from datetime import datetime ...@@ -4,83 +4,9 @@ from datetime import datetime
from pytz import UTC from pytz import UTC
from django.utils.http import cookie_date from django.utils.http import cookie_date
from django.conf import settings from django.conf import settings
from django.core.urlresolvers import reverse
from opaque_keys.edx.keys import CourseKey
from course_modes.models import CourseMode
from third_party_auth import ( # pylint: disable=unused-import
pipeline, provider,
is_enabled as third_party_auth_enabled
)
from verify_student.models import SoftwareSecurePhotoVerification # pylint: disable=F0401 from verify_student.models import SoftwareSecurePhotoVerification # pylint: disable=F0401
from course_modes.models import CourseMode
from student_account.helpers import auth_pipeline_urls # pylint: disable=unused-import,import-error
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
on the login/registration forms to allow users to begin
authentication with a third-party provider.
Optionally, we can redirect the user to an arbitrary
url after auth completes successfully. We use this
to redirect the user to a page that required login,
or to send users to the payment flow when enrolling
in a course.
Args:
auth_entry (string): Either `pipeline.AUTH_ENTRY_LOGIN` or `pipeline.AUTH_ENTRY_REGISTER`
Keyword Args:
redirect_url (unicode): If provided, send users to this URL
after they successfully authenticate.
course_id (unicode): The ID of the course the user is enrolling in.
We use this to send users to the track selection page
if the course has a payment option.
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
"""
if not third_party_auth_enabled():
return {}
if redirect_url is not None:
pipeline_redirect = redirect_url
elif course_id is not None:
# If the course is white-label (paid), then we send users
# to the shopping cart. (There is a third party auth pipeline
# step that will add the course to the cart.)
if CourseMode.is_white_label(CourseKey.from_string(course_id)):
pipeline_redirect = reverse("shoppingcart.views.show_cart")
# Otherwise, send the user to the track selection page.
# The track selection page may redirect the user to the dashboard
# (if the only available mode is honor), or directly to verification
# (for professional ed).
else:
pipeline_redirect = reverse(
"course_modes_choose",
kwargs={'course_id': unicode(course_id)}
)
else:
pipeline_redirect = None
return {
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()
}
def set_logged_in_cookie(request, response): def set_logged_in_cookie(request, response):
......
...@@ -14,18 +14,13 @@ from django.http import HttpResponseBadRequest, HttpResponse ...@@ -14,18 +14,13 @@ from django.http import HttpResponseBadRequest, HttpResponse
from external_auth.models import ExternalAuthMap from external_auth.models import ExternalAuthMap
import httpretty import httpretty
from mock import patch from mock import patch
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from social.apps.django_app.default.models import UserSocialAuth from social.apps.django_app.default.models import UserSocialAuth
from xmodule.modulestore.tests.django_utils import TEST_DATA_MOCK_MODULESTORE
from student.tests.factories import UserFactory, RegistrationFactory, UserProfileFactory from student.tests.factories import UserFactory, RegistrationFactory, UserProfileFactory
from student.views import ( from student.views import login_oauth_token
_parse_course_id_from_string,
_get_course_enrollment_domain,
login_oauth_token,
)
from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, TEST_DATA_MOCK_MODULESTORE
class LoginTest(TestCase): class LoginTest(TestCase):
...@@ -324,24 +319,6 @@ class LoginTest(TestCase): ...@@ -324,24 +319,6 @@ class LoginTest(TestCase):
self.assertNotIn(log_string, format_string) self.assertNotIn(log_string, format_string)
class UtilFnTest(TestCase):
"""
Tests for utility functions in student.views
"""
def test__parse_course_id_from_string(self):
"""
Tests the _parse_course_id_from_string util function
"""
COURSE_ID = u'org/num/run' # pylint: disable=invalid-name
COURSE_URL = u'/courses/{}/otherstuff'.format(COURSE_ID) # pylint: disable=invalid-name
NON_COURSE_URL = u'/blahblah' # pylint: disable=invalid-name
self.assertEqual(
_parse_course_id_from_string(COURSE_URL),
SlashSeparatedCourseKey.from_deprecated_string(COURSE_ID)
)
self.assertIsNone(_parse_course_id_from_string(NON_COURSE_URL))
@override_settings(MODULESTORE=TEST_DATA_MOCK_MODULESTORE) @override_settings(MODULESTORE=TEST_DATA_MOCK_MODULESTORE)
class ExternalAuthShibTest(ModuleStoreTestCase): class ExternalAuthShibTest(ModuleStoreTestCase):
""" """
...@@ -388,15 +365,6 @@ class ExternalAuthShibTest(ModuleStoreTestCase): ...@@ -388,15 +365,6 @@ class ExternalAuthShibTest(ModuleStoreTestCase):
}) })
@unittest.skipUnless(settings.FEATURES.get('AUTH_USE_SHIB'), "AUTH_USE_SHIB not set") @unittest.skipUnless(settings.FEATURES.get('AUTH_USE_SHIB'), "AUTH_USE_SHIB not set")
def test__get_course_enrollment_domain(self):
"""
Tests the _get_course_enrollment_domain utility function
"""
self.assertIsNone(_get_course_enrollment_domain(SlashSeparatedCourseKey("I", "DONT", "EXIST")))
self.assertIsNone(_get_course_enrollment_domain(self.course.id))
self.assertEqual(self.shib_course.enrollment_domain, _get_course_enrollment_domain(self.shib_course.id))
@unittest.skipUnless(settings.FEATURES.get('AUTH_USE_SHIB'), "AUTH_USE_SHIB not set")
def test_login_required_dashboard(self): def test_login_required_dashboard(self):
""" """
Tests redirects to when @login_required to dashboard, which should always be the normal login, Tests redirects to when @login_required to dashboard, which should always be the normal login,
...@@ -416,7 +384,7 @@ class ExternalAuthShibTest(ModuleStoreTestCase): ...@@ -416,7 +384,7 @@ class ExternalAuthShibTest(ModuleStoreTestCase):
noshib_response = self.client.get(TARGET_URL, follow=True) noshib_response = self.client.get(TARGET_URL, follow=True)
self.assertEqual(noshib_response.redirect_chain[-1], self.assertEqual(noshib_response.redirect_chain[-1],
('http://testserver/accounts/login?next={url}'.format(url=TARGET_URL), 302)) ('http://testserver/accounts/login?next={url}'.format(url=TARGET_URL), 302))
self.assertContains(noshib_response, ("Log into your {platform_name} Account | {platform_name}" self.assertContains(noshib_response, ("Sign in or Register | {platform_name}"
.format(platform_name=settings.PLATFORM_NAME))) .format(platform_name=settings.PLATFORM_NAME)))
self.assertEqual(noshib_response.status_code, 200) self.assertEqual(noshib_response.status_code, 200)
......
...@@ -2,11 +2,14 @@ ...@@ -2,11 +2,14 @@
import urllib import urllib
import unittest import unittest
from collections import OrderedDict from collections import OrderedDict
import ddt
from mock import patch from mock import patch
from django.conf import settings from django.conf import settings
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
import ddt
from django.test.utils import override_settings from django.test.utils import override_settings
from util.testing import UrlResetMixin
from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.factories import CourseFactory
from student.tests.factories import CourseModeFactory from student.tests.factories import CourseModeFactory
from xmodule.modulestore.tests.django_utils import ( from xmodule.modulestore.tests.django_utils import (
...@@ -42,10 +45,11 @@ def _third_party_login_url(backend_name, auth_entry, course_id=None, redirect_ur ...@@ -42,10 +45,11 @@ def _third_party_login_url(backend_name, auth_entry, course_id=None, redirect_ur
@ddt.ddt @ddt.ddt
@override_settings(MODULESTORE=MODULESTORE_CONFIG) @override_settings(MODULESTORE=MODULESTORE_CONFIG)
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
class LoginFormTest(ModuleStoreTestCase): class LoginFormTest(UrlResetMixin, ModuleStoreTestCase):
"""Test rendering of the login form. """ """Test rendering of the login form. """
@patch.dict(settings.FEATURES, {"ENABLE_COMBINED_LOGIN_REGISTRATION": False})
def setUp(self): def setUp(self):
super(LoginFormTest, self).setUp('lms.urls')
self.url = reverse("signin_user") self.url = reverse("signin_user")
self.course = CourseFactory.create() self.course = CourseFactory.create()
self.course_id = unicode(self.course.id) self.course_id = unicode(self.course.id)
...@@ -153,10 +157,11 @@ class LoginFormTest(ModuleStoreTestCase): ...@@ -153,10 +157,11 @@ class LoginFormTest(ModuleStoreTestCase):
@ddt.ddt @ddt.ddt
@override_settings(MODULESTORE=MODULESTORE_CONFIG) @override_settings(MODULESTORE=MODULESTORE_CONFIG)
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
class RegisterFormTest(ModuleStoreTestCase): class RegisterFormTest(UrlResetMixin, ModuleStoreTestCase):
"""Test rendering of the registration form. """ """Test rendering of the registration form. """
@patch.dict(settings.FEATURES, {"ENABLE_COMBINED_LOGIN_REGISTRATION": False})
def setUp(self): def setUp(self):
super(RegisterFormTest, self).setUp('lms.urls')
self.url = reverse("register_user") self.url = reverse("register_user")
self.course = CourseFactory.create() self.course = CourseFactory.create()
self.course_id = unicode(self.course.id) self.course_id = unicode(self.course.id)
......
...@@ -76,6 +76,10 @@ from django_comment_common.models import Role ...@@ -76,6 +76,10 @@ from django_comment_common.models import Role
from external_auth.models import ExternalAuthMap from external_auth.models import ExternalAuthMap
import external_auth.views import external_auth.views
from external_auth.login_and_register import (
login as external_auth_login,
register as external_auth_register
)
from bulk_email.models import Optout, CourseAuthorization from bulk_email.models import Optout, CourseAuthorization
import shoppingcart import shoppingcart
...@@ -354,18 +358,10 @@ def _cert_info(user, course, cert_status): ...@@ -354,18 +358,10 @@ def _cert_info(user, course, cert_status):
@ensure_csrf_cookie @ensure_csrf_cookie
def signin_user(request): def signin_user(request):
""" """Deprecated. To be replaced by :class:`student_account.views.login_and_registration_form`."""
This view will display the non-modal login form external_auth_response = external_auth_login(request)
""" if external_auth_response is not None:
if (settings.FEATURES['AUTH_USE_CERTIFICATES'] and return external_auth_response
external_auth.views.ssl_get_cert_from_request(request)):
# SSL login doesn't require a view, so redirect
# branding and allow that to process the login if it
# is enabled and the header is in the request.
return external_auth.views.redirect_with_get('root', request.GET)
if settings.FEATURES.get('AUTH_USE_CAS'):
# If CAS is enabled, redirect auth handling to there
return redirect(reverse('cas-login'))
if request.user.is_authenticated(): if request.user.is_authenticated():
return redirect(reverse('dashboard')) return redirect(reverse('dashboard'))
...@@ -391,15 +387,13 @@ def signin_user(request): ...@@ -391,15 +387,13 @@ def signin_user(request):
@ensure_csrf_cookie @ensure_csrf_cookie
def register_user(request, extra_context=None): def register_user(request, extra_context=None):
""" """Deprecated. To be replaced by :class:`student_account.views.login_and_registration_form`."""
This view will display the non-modal registration form
"""
if request.user.is_authenticated(): if request.user.is_authenticated():
return redirect(reverse('dashboard')) return redirect(reverse('dashboard'))
if settings.FEATURES.get('AUTH_USE_CERTIFICATES_IMMEDIATE_SIGNUP'):
# Redirect to branding to process their certificate if SSL is enabled external_auth_response = external_auth_register(request)
# and registration is disabled. if external_auth_response is not None:
return external_auth.views.redirect_with_get('root', request.GET) return external_auth_response
course_id = request.GET.get('course_id') course_id = request.GET.get('course_id')
email_opt_in = request.GET.get('email_opt_in') email_opt_in = request.GET.get('email_opt_in')
...@@ -918,56 +912,15 @@ def change_enrollment(request, check_access=True): ...@@ -918,56 +912,15 @@ def change_enrollment(request, check_access=True):
return HttpResponseBadRequest(_("Enrollment action is invalid")) return HttpResponseBadRequest(_("Enrollment action is invalid"))
# pylint: disable=fixme
# TODO: This function is kind of gnarly/hackish/etc and is only used in one location.
# It'd be awesome if we could get rid of it; manually parsing course_id strings form larger strings
# seems Probably Incorrect
def _parse_course_id_from_string(input_str):
"""
Helper function to determine if input_str (typically the queryparam 'next') contains a course_id.
@param input_str:
@return: the course_id if found, None if not
"""
m_obj = re.match(r'^/courses/{}'.format(settings.COURSE_ID_PATTERN), input_str)
if m_obj:
return SlashSeparatedCourseKey.from_deprecated_string(m_obj.group('course_id'))
return None
def _get_course_enrollment_domain(course_id):
"""
Helper function to get the enrollment domain set for a course with id course_id
@param course_id:
@return:
"""
course = modulestore().get_course(course_id)
if course is None:
return None
return course.enrollment_domain
@never_cache @never_cache
@ensure_csrf_cookie @ensure_csrf_cookie
def accounts_login(request): def accounts_login(request):
""" """Deprecated. To be replaced by :class:`student_account.views.login_and_registration_form`."""
This view is mainly used as the redirect from the @login_required decorator. I don't believe that external_auth_response = external_auth_login(request)
the login path linked from the homepage uses it. if external_auth_response is not None:
""" return external_auth_response
if settings.FEATURES.get('AUTH_USE_CAS'):
return redirect(reverse('cas-login'))
if settings.FEATURES['AUTH_USE_CERTIFICATES']:
# SSL login doesn't require a view, so login
# directly here
return external_auth.views.ssl_login(request)
# see if the "next" parameter has been set, whether it has a course context, and if so, whether
# there is a course-specific place to redirect
redirect_to = request.GET.get('next')
if redirect_to:
course_id = _parse_course_id_from_string(redirect_to)
if course_id and _get_course_enrollment_domain(course_id):
return external_auth.views.course_specific_login(request, course_id.to_deprecated_string())
redirect_to = request.GET.get('next')
context = { context = {
'pipeline_running': 'false', 'pipeline_running': 'false',
'pipeline_url': auth_pipeline_urls(pipeline.AUTH_ENTRY_LOGIN, redirect_url=redirect_to), 'pipeline_url': auth_pipeline_urls(pipeline.AUTH_ENTRY_LOGIN, redirect_url=redirect_to),
...@@ -1173,7 +1126,7 @@ def login_user(request, error=""): # pylint: disable-msg=too-many-statements,un ...@@ -1173,7 +1126,7 @@ def login_user(request, error=""): # pylint: disable-msg=too-many-statements,un
AUDIT_LOG.warning(u"Login failed - Account not active for user {0}, resending activation".format(username)) AUDIT_LOG.warning(u"Login failed - Account not active for user {0}, resending activation".format(username))
reactivation_email_for_user(user) reactivation_email_for_user(user)
not_activated_msg = _("This account has not been activated. We have sent another activation message. Please check your e-mail for the activation instructions.") not_activated_msg = _("This account has not been activated. We have sent another activation message. Please check your email for the activation instructions.")
return JsonResponse({ return JsonResponse({
"success": False, "success": False,
"value": not_activated_msg, "value": not_activated_msg,
......
...@@ -113,10 +113,10 @@ AUTH_ENTRY_LOGIN = 'login' ...@@ -113,10 +113,10 @@ AUTH_ENTRY_LOGIN = 'login'
AUTH_ENTRY_PROFILE = 'profile' AUTH_ENTRY_PROFILE = 'profile'
AUTH_ENTRY_REGISTER = 'register' AUTH_ENTRY_REGISTER = 'register'
# pylint: disable=fixme # This is left-over from an A/B test
# TODO (ECOM-369): Replace `AUTH_ENTRY_LOGIN` and `AUTH_ENTRY_REGISTER` # of the new combined login/registration page (ECOM-369)
# with these values once the A/B test completes, then delete # We need to keep both the old and new entry points
# these constants. # until every session from before the test ended has expired.
AUTH_ENTRY_LOGIN_2 = 'account_login' AUTH_ENTRY_LOGIN_2 = 'account_login'
AUTH_ENTRY_REGISTER_2 = 'account_register' AUTH_ENTRY_REGISTER_2 = 'account_register'
...@@ -134,9 +134,10 @@ AUTH_DISPATCH_URLS = { ...@@ -134,9 +134,10 @@ AUTH_DISPATCH_URLS = {
AUTH_ENTRY_LOGIN: '/login', AUTH_ENTRY_LOGIN: '/login',
AUTH_ENTRY_REGISTER: '/register', AUTH_ENTRY_REGISTER: '/register',
# TODO (ECOM-369): Replace the dispatch URLs # This is left-over from an A/B test
# for `AUTH_ENTRY_LOGIN` and `AUTH_ENTRY_REGISTER` # of the new combined login/registration page (ECOM-369)
# with these values, but DO NOT DELETE THESE KEYS. # We need to keep both the old and new entry points
# until every session from before the test ended has expired.
AUTH_ENTRY_LOGIN_2: '/account/login/', AUTH_ENTRY_LOGIN_2: '/account/login/',
AUTH_ENTRY_REGISTER_2: '/account/register/', AUTH_ENTRY_REGISTER_2: '/account/register/',
...@@ -152,11 +153,10 @@ _AUTH_ENTRY_CHOICES = frozenset([ ...@@ -152,11 +153,10 @@ _AUTH_ENTRY_CHOICES = frozenset([
AUTH_ENTRY_PROFILE, AUTH_ENTRY_PROFILE,
AUTH_ENTRY_REGISTER, AUTH_ENTRY_REGISTER,
# TODO (ECOM-369): For the A/B test of the combined # This is left-over from an A/B test
# login/registration, we needed to introduce two # of the new combined login/registration page (ECOM-369)
# additional end-points. Once the test completes, # We need to keep both the old and new entry points
# delete these constants from the choices list. # until every session from before the test ended has expired.
# pylint: disable=fixme
AUTH_ENTRY_LOGIN_2, AUTH_ENTRY_LOGIN_2,
AUTH_ENTRY_REGISTER_2, AUTH_ENTRY_REGISTER_2,
...@@ -447,31 +447,16 @@ def parse_query_params(strategy, response, *args, **kwargs): ...@@ -447,31 +447,16 @@ def parse_query_params(strategy, response, *args, **kwargs):
# Whether the auth pipeline entered from /dashboard. # Whether the auth pipeline entered from /dashboard.
'is_dashboard': auth_entry == AUTH_ENTRY_DASHBOARD, 'is_dashboard': auth_entry == AUTH_ENTRY_DASHBOARD,
# Whether the auth pipeline entered from /login. # Whether the auth pipeline entered from /login.
'is_login': auth_entry == AUTH_ENTRY_LOGIN, 'is_login': auth_entry in [AUTH_ENTRY_LOGIN, AUTH_ENTRY_LOGIN_2],
# Whether the auth pipeline entered from /register. # Whether the auth pipeline entered from /register.
'is_register': auth_entry == AUTH_ENTRY_REGISTER, 'is_register': auth_entry in [AUTH_ENTRY_REGISTER, AUTH_ENTRY_REGISTER_2],
# Whether the auth pipeline entered from /profile. # Whether the auth pipeline entered from /profile.
'is_profile': auth_entry == AUTH_ENTRY_PROFILE, 'is_profile': auth_entry == AUTH_ENTRY_PROFILE,
# Whether the auth pipeline entered from an API # Whether the auth pipeline entered from an API
'is_api': auth_entry == AUTH_ENTRY_API, 'is_api': auth_entry == AUTH_ENTRY_API,
# TODO (ECOM-369): Delete these once the A/B test
# for the combined login/registration form completes.
# pylint: disable=fixme
'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).
# pylint: disable=fixme
@partial.partial @partial.partial
def ensure_user_information( def ensure_user_information(
strategy, strategy,
...@@ -507,43 +492,30 @@ def ensure_user_information( ...@@ -507,43 +492,30 @@ def ensure_user_information(
# invariants have been violated and future misbehavior is likely. # invariants have been violated and future misbehavior is likely.
user_inactive = user and not user.is_active user_inactive = user and not user.is_active
user_unset = user is None user_unset = user is None
dispatch_to_login = is_login and (user_unset or user_inactive)
dispatch_to_login = (
((is_login or is_login_2) and (user_unset or user_inactive))
or
((is_register or is_register_2) and user_inactive)
)
dispatch_to_register = (is_register or is_register_2) and user_unset
reject_api_request = is_api and (user_unset or user_inactive) reject_api_request = is_api and (user_unset or user_inactive)
if reject_api_request: if reject_api_request:
# Content doesn't matter; we just want to exit the pipeline # Content doesn't matter; we just want to exit the pipeline
return HttpResponseBadRequest() return HttpResponseBadRequest()
# TODO (ECOM-369): Consolidate this with `dispatch_to_login`
# once the A/B test completes. # pylint: disable=fixme
dispatch_to_login_2 = is_login_2 and (user_unset or user_inactive)
if is_dashboard or is_profile: if is_dashboard or is_profile:
return return
if dispatch_to_login:
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(_create_redirect_url(AUTH_DISPATCH_URLS[AUTH_ENTRY_LOGIN_2], strategy))
if is_register and user_unset:
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(_create_redirect_url(AUTH_DISPATCH_URLS[AUTH_ENTRY_REGISTER_2], strategy))
# If the user has a linked account, but has not yet activated # If the user has a linked account, but has not yet activated
# we should send them to the login page. The login page # we should send them to the login page. The login page
# will tell them that they need to activate their account. # will tell them that they need to activate their account.
if is_register and user_inactive: if dispatch_to_login:
return redirect(_create_redirect_url(AUTH_DISPATCH_URLS[AUTH_ENTRY_LOGIN], strategy)) return redirect(_create_redirect_url(AUTH_DISPATCH_URLS[AUTH_ENTRY_LOGIN], strategy))
if is_register_2 and user_inactive: if dispatch_to_register:
return redirect(_create_redirect_url(AUTH_DISPATCH_URLS[AUTH_ENTRY_LOGIN_2], strategy)) return redirect(_create_redirect_url(AUTH_DISPATCH_URLS[AUTH_ENTRY_REGISTER], strategy))
def _create_redirect_url(url, strategy): def _create_redirect_url(url, strategy):
......
...@@ -202,13 +202,6 @@ class IntegrationTest(testutil.TestCase, test.TestCase): ...@@ -202,13 +202,6 @@ class IntegrationTest(testutil.TestCase, test.TestCase):
self.assertFalse(payload.get('success')) self.assertFalse(payload.get('success'))
self.assertIn('incorrect', payload.get('value')) self.assertIn('incorrect', payload.get('value'))
def assert_javascript_would_submit_login_form(self, boolean, response):
"""Asserts we pass form submit JS the right boolean string."""
argument_string = re.search(
r'function\ post_form_if_pipeline_running.*\(([a-z]+)\)', response.content, re.DOTALL).groups()[0]
self.assertIn(argument_string, ['true', 'false'])
self.assertEqual(boolean, True if argument_string == 'true' else False)
def assert_json_failure_response_is_inactive_account(self, response): def assert_json_failure_response_is_inactive_account(self, response):
"""Asserts failure on /login for inactive account looks right.""" """Asserts failure on /login for inactive account looks right."""
self.assertEqual(200, response.status_code) # Yes, it's a 200 even though it's a failure. self.assertEqual(200, response.status_code) # Yes, it's a 200 even though it's a failure.
...@@ -238,15 +231,14 @@ class IntegrationTest(testutil.TestCase, test.TestCase): ...@@ -238,15 +231,14 @@ class IntegrationTest(testutil.TestCase, test.TestCase):
def assert_login_response_before_pipeline_looks_correct(self, response): def assert_login_response_before_pipeline_looks_correct(self, response):
"""Asserts a GET of /login not in the pipeline looks correct.""" """Asserts a GET of /login not in the pipeline looks correct."""
self.assertEqual(200, response.status_code) self.assertEqual(200, response.status_code)
self.assertIn('Sign in with ' + self.PROVIDER_CLASS.NAME, response.content) # The combined login/registration page dynamically generates the login button,
self.assert_javascript_would_submit_login_form(False, response) # but we can still check that the provider name is passed in the data attribute
self.assert_signin_button_looks_functional(response.content, pipeline.AUTH_ENTRY_LOGIN) # for the container element.
self.assertIn(self.PROVIDER_CLASS.NAME, response.content)
def assert_login_response_in_pipeline_looks_correct(self, response): def assert_login_response_in_pipeline_looks_correct(self, response):
"""Asserts a GET of /login in the pipeline looks correct.""" """Asserts a GET of /login in the pipeline looks correct."""
self.assertEqual(200, response.status_code) self.assertEqual(200, response.status_code)
# Make sure the form submit JS is told to submit the form:
self.assert_javascript_would_submit_login_form(True, response)
def assert_password_overridden_by_pipeline(self, username, password): def assert_password_overridden_by_pipeline(self, username, password):
"""Verifies that the given password is not correct. """Verifies that the given password is not correct.
...@@ -268,25 +260,20 @@ class IntegrationTest(testutil.TestCase, test.TestCase): ...@@ -268,25 +260,20 @@ class IntegrationTest(testutil.TestCase, test.TestCase):
def assert_redirect_to_login_looks_correct(self, response): def assert_redirect_to_login_looks_correct(self, response):
"""Asserts a response would redirect to /login.""" """Asserts a response would redirect to /login."""
self.assertEqual(302, response.status_code) self.assertEqual(302, response.status_code)
self.assertEqual('/' + pipeline.AUTH_ENTRY_LOGIN, response.get('Location')) self.assertEqual('/login', response.get('Location'))
def assert_redirect_to_register_looks_correct(self, response): def assert_redirect_to_register_looks_correct(self, response):
"""Asserts a response would redirect to /register.""" """Asserts a response would redirect to /register."""
self.assertEqual(302, response.status_code) self.assertEqual(302, response.status_code)
self.assertEqual('/' + pipeline.AUTH_ENTRY_REGISTER, response.get('Location')) self.assertEqual('/register', response.get('Location'))
def assert_register_response_before_pipeline_looks_correct(self, response): def assert_register_response_before_pipeline_looks_correct(self, response):
"""Asserts a GET of /register not in the pipeline looks correct.""" """Asserts a GET of /register not in the pipeline looks correct."""
self.assertEqual(200, response.status_code) self.assertEqual(200, response.status_code)
self.assertIn('Sign up with ' + self.PROVIDER_CLASS.NAME, response.content) # The combined login/registration page dynamically generates the register button,
self.assert_signin_button_looks_functional(response.content, pipeline.AUTH_ENTRY_REGISTER) # but we can still check that the provider name is passed in the data attribute
# for the container element.
def assert_signin_button_looks_functional(self, content, auth_entry): self.assertIn(self.PROVIDER_CLASS.NAME, response.content)
"""Asserts JS is available to signin buttons and has the right args."""
self.assertTrue(re.search(r'function thirdPartySignin', content))
self.assertEqual(
pipeline.get_login_url(self.PROVIDER_CLASS.NAME, auth_entry),
re.search(r"thirdPartySignin\(event, '([^']+)", content).groups()[0])
def assert_social_auth_does_not_exist_for_user(self, user, strategy): def assert_social_auth_does_not_exist_for_user(self, user, strategy):
"""Asserts a user does not have an auth with the expected provider.""" """Asserts a user does not have an auth with the expected provider."""
......
...@@ -70,7 +70,9 @@ class CombinedLoginAndRegisterPage(PageObject): ...@@ -70,7 +70,9 @@ class CombinedLoginAndRegisterPage(PageObject):
in the bok choy settings. in the bok choy settings.
When enabled, the new page is available from either When enabled, the new page is available from either
`/account/login` or `/account/register`. `/login` or `/register`; the new page is also served at
`/account/login/` or `/account/register/`, where it was
available for a time during an A/B test.
Users can reach this page while attempting to enroll Users can reach this page while attempting to enroll
in a course, in which case users will be auto-enrolled in a course, in which case users will be auto-enrolled
......
...@@ -19,8 +19,6 @@ from ..helpers import ( ...@@ -19,8 +19,6 @@ from ..helpers import (
from ...pages.lms.auto_auth import AutoAuthPage from ...pages.lms.auto_auth import AutoAuthPage
from ...pages.lms.create_mode import ModeCreationPage from ...pages.lms.create_mode import ModeCreationPage
from ...pages.common.logout import LogoutPage from ...pages.common.logout import LogoutPage
from ...pages.lms.find_courses import FindCoursesPage
from ...pages.lms.course_about import CourseAboutPage
from ...pages.lms.course_info import CourseInfoPage from ...pages.lms.course_info import CourseInfoPage
from ...pages.lms.tab_nav import TabNavPage from ...pages.lms.tab_nav import TabNavPage
from ...pages.lms.course_nav import CourseNavPage from ...pages.lms.course_nav import CourseNavPage
...@@ -36,48 +34,6 @@ from ...pages.lms.pay_and_verify import PaymentAndVerificationFlow, FakePaymentP ...@@ -36,48 +34,6 @@ from ...pages.lms.pay_and_verify import PaymentAndVerificationFlow, FakePaymentP
from ...fixtures.course import CourseFixture, XBlockFixtureDesc, CourseUpdateDesc from ...fixtures.course import CourseFixture, XBlockFixtureDesc, CourseUpdateDesc
class RegistrationTest(UniqueCourseTest):
"""
Test the registration process.
"""
def setUp(self):
"""
Initialize pages and install a course fixture.
"""
super(RegistrationTest, self).setUp()
self.find_courses_page = FindCoursesPage(self.browser)
self.course_about_page = CourseAboutPage(self.browser, self.course_id)
# Create a course to register for
CourseFixture(
self.course_info['org'], self.course_info['number'],
self.course_info['run'], self.course_info['display_name']
).install()
def test_register(self):
# Visit the main page with the list of courses
self.find_courses_page.visit()
# Go to the course about page and click the register button
self.course_about_page.visit()
register_page = self.course_about_page.register()
# Fill in registration info and submit
username = "test_" + self.unique_id[0:6]
register_page.provide_info(
username + "@example.com", "test", username, "Test User"
)
dashboard = register_page.submit()
# We should end up at the dashboard
# Check that we're registered for the course
course_names = dashboard.available_courses
self.assertIn(self.course_info['display_name'], course_names)
@attr('shard_1') @attr('shard_1')
class LoginFromCombinedPageTest(UniqueCourseTest): class LoginFromCombinedPageTest(UniqueCourseTest):
"""Test that we can log in using the combined login/registration page. """Test that we can log in using the combined login/registration page.
......
@shard_1
Feature: LMS.Login in as a registered user
As a registered user
In order to access my content
I want to be able to login in to edX
Scenario: Login to an unactivated account
Given I am an edX user
And I am an unactivated user
And I visit the homepage
When I click the link with the text "Sign in"
And I submit my credentials on the login form
Then I should see the login error message "This account has not been activated"
# firefox will not redirect properly when the whole suite is run
@skip_firefox
Scenario: Login to an activated account
Given I am an edX user
And I am an activated user
And I visit the homepage
When I click the link with the text "Sign in"
And I submit my credentials on the login form
Then I should be on the dashboard page
Scenario: Logout of a signed in account
Given I am logged in
When I click the dropdown arrow
And I click the link with the text "Sign out"
Then I should see a link with the text "Sign in"
And I should see that the path is "/"
Scenario: Login with valid redirect
Given I am an edX user
And The course "6.002x" exists
And I am registered for the course "6.002x"
And I am not logged in
And I visit the url "/courses/{}/courseware"
And I should see that the path is "/accounts/login?next=/courses/{}/courseware"
When I submit my credentials on the login form
And I wait for "2" seconds
Then the page title should contain "6.002x Courseware"
Scenario: Login with an invalid redirect
Given I am an edX user
And I am not logged in
And I visit the url "/login?next=http://www.google.com/"
When I submit my credentials on the login form
Then I should be on the dashboard page
Scenario: Login with a redirect with parameters
Given I am an edX user
And I am not logged in
And I visit the url "/debug/show_parameters?foo=hello&bar=world"
And I should see that the path is "/accounts/login?next=/debug/show_parameters%3Ffoo%3Dhello%26bar%3Dworld"
When I submit my credentials on the login form
And I wait for "2" seconds
Then I should see "foo: u'hello'" somewhere on the page
And I should see "bar: u'world'" somewhere on the page
# pylint: disable=missing-docstring
# pylint: disable=redefined-outer-name
from lettuce import step, world
from django.contrib.auth.models import User
@step('I am an unactivated user$')
def i_am_an_unactivated_user(step):
user_is_an_unactivated_user('robot')
@step('I am an activated user$')
def i_am_an_activated_user(step):
user_is_an_activated_user('robot')
@step('I submit my credentials on the login form')
def i_submit_my_credentials_on_the_login_form(step):
fill_in_the_login_form('email', 'robot@edx.org')
fill_in_the_login_form('password', 'test')
def submit_login_form():
login_form = world.browser.find_by_css('form#login-form')
login_form.find_by_name('submit').click()
world.retry_on_exception(submit_login_form)
@step(u'I should see the login error message "([^"]*)"$')
def i_should_see_the_login_error_message(step, msg):
login_error_div = world.browser.find_by_css('.submission-error.is-shown')
assert (msg in login_error_div.text)
@step(u'click the dropdown arrow$')
def click_the_dropdown(step):
world.css_click('.dropdown')
#### helper functions
def user_is_an_unactivated_user(uname):
u = User.objects.get(username=uname)
u.is_active = False
u.save()
def user_is_an_activated_user(uname):
u = User.objects.get(username=uname)
u.is_active = True
u.save()
def fill_in_the_login_form(field, value):
def fill_login_form():
login_form = world.browser.find_by_css('form#login-form')
form_field = login_form.find_by_name(field)
form_field.fill(value)
world.retry_on_exception(fill_login_form)
@shard_2
Feature: LMS.Sign in
In order to use the edX content
As a new user
I want to signup for a student account
# firefox will not redirect properly
@skip_firefox
Scenario: Sign up from the homepage
Given I visit the homepage
When I click the link with the text "Register Now"
And I fill in "email" on the registration form with "robot2@edx.org"
And I fill in "password" on the registration form with "test"
And I fill in "username" on the registration form with "robot2"
And I fill in "name" on the registration form with "Robot Two"
And I check the checkbox named "terms_of_service"
And I check the checkbox named "honor_code"
And I submit the registration form
Then I should see "Thanks for Registering!" in the dashboard banner
# pylint: disable=missing-docstring
# pylint: disable=redefined-outer-name
from lettuce import world, step
@step('I fill in "([^"]*)" on the registration form with "([^"]*)"$')
def when_i_fill_in_field_on_the_registration_form_with_value(step, field, value):
def fill_in_registration():
register_form = world.browser.find_by_css('form#register-form')
form_field = register_form.find_by_name(field)
form_field.fill(value)
world.retry_on_exception(fill_in_registration)
@step('I submit the registration form$')
def i_press_the_button_on_the_registration_form(step):
def submit_registration():
register_form = world.browser.find_by_css('form#register-form')
register_form.find_by_name('submit').click()
world.retry_on_exception(submit_registration)
@step('I check the checkbox named "([^"]*)"$')
def i_check_checkbox(step, checkbox):
css_selector = 'input[name={}]'.format(checkbox)
world.css_check(css_selector)
@step('I should see "([^"]*)" in the dashboard banner$')
def i_should_see_text_in_the_dashboard_banner_section(step, text):
css_selector = "section.dashboard-banner h2"
assert (text in world.css_text(css_selector))
...@@ -260,7 +260,7 @@ def get_email_params(course, auto_enroll, secure=True): ...@@ -260,7 +260,7 @@ def get_email_params(course, auto_enroll, secure=True):
registration_url = u'{proto}://{site}{path}'.format( registration_url = u'{proto}://{site}{path}'.format(
proto=protocol, proto=protocol,
site=stripped_site_name, site=stripped_site_name,
path=reverse('student.views.register_user') path=reverse('register_user')
) )
course_url = u'{proto}://{site}{path}'.format( course_url = u'{proto}://{site}{path}'.format(
proto=protocol, proto=protocol,
......
...@@ -825,7 +825,7 @@ def _do_enroll_students(course, course_key, students, secure=False, overload=Fal ...@@ -825,7 +825,7 @@ def _do_enroll_students(course, course_key, students, secure=False, overload=Fal
registration_url = '{proto}://{site}{path}'.format( registration_url = '{proto}://{site}{path}'.format(
proto=protocol, proto=protocol,
site=stripped_site_name, site=stripped_site_name,
path=reverse('student.views.register_user') path=reverse('register_user')
) )
course_url = '{proto}://{site}{path}'.format( course_url = '{proto}://{site}{path}'.format(
proto=protocol, proto=protocol,
......
"""Helper functions for the student account app. """ """Helper functions for the student account app. """
from django.core.urlresolvers import reverse
from opaque_keys.edx.keys import CourseKey
from course_modes.models import CourseMode
from third_party_auth import ( # pylint: disable=W0611
pipeline, provider,
is_enabled as third_party_auth_enabled
)
# TODO: move this function here instead of importing it from student # pylint: disable=fixme
from student.helpers import auth_pipeline_urls # pylint: disable=unused-import 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
on the login/registration forms to allow users to begin
authentication with a third-party provider.
Optionally, we can redirect the user to an arbitrary
url after auth completes successfully. We use this
to redirect the user to a page that required login,
or to send users to the payment flow when enrolling
in a course.
Args:
auth_entry (string): Either `pipeline.AUTH_ENTRY_LOGIN` or `pipeline.AUTH_ENTRY_REGISTER`
Keyword Args:
redirect_url (unicode): If provided, send users to this URL
after they successfully authenticate.
course_id (unicode): The ID of the course the user is enrolling in.
We use this to send users to the track selection page
if the course has a payment option.
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
"""
if not third_party_auth_enabled():
return {}
if redirect_url is not None:
pipeline_redirect = redirect_url
elif course_id is not None:
# If the course is white-label (paid), then we send users
# to the shopping cart. (There is a third party auth pipeline
# step that will add the course to the cart.)
if CourseMode.is_white_label(CourseKey.from_string(course_id)):
pipeline_redirect = reverse("shoppingcart.views.show_cart")
# Otherwise, send the user to the track selection page.
# The track selection page may redirect the user to the dashboard
# (if the only available mode is honor), or directly to verification
# (for professional ed).
else:
pipeline_redirect = reverse(
"course_modes_choose",
kwargs={'course_id': unicode(course_id)}
)
else:
pipeline_redirect = None
return {
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()
}
...@@ -438,14 +438,14 @@ class StudentAccountLoginAndRegistrationTest(ModuleStoreTestCase): ...@@ -438,14 +438,14 @@ class StudentAccountLoginAndRegistrationTest(ModuleStoreTestCase):
{ {
"name": "Facebook", "name": "Facebook",
"iconClass": "fa-facebook", "iconClass": "fa-facebook",
"loginUrl": self._third_party_login_url("facebook", "account_login"), "loginUrl": self._third_party_login_url("facebook", "login"),
"registerUrl": self._third_party_login_url("facebook", "account_register") "registerUrl": self._third_party_login_url("facebook", "register")
}, },
{ {
"name": "Google", "name": "Google",
"iconClass": "fa-google-plus", "iconClass": "fa-google-plus",
"loginUrl": self._third_party_login_url("google-oauth2", "account_login"), "loginUrl": self._third_party_login_url("google-oauth2", "login"),
"registerUrl": self._third_party_login_url("google-oauth2", "account_register") "registerUrl": self._third_party_login_url("google-oauth2", "register")
} }
] ]
self._assert_third_party_auth_data(response, current_provider, expected_providers) self._assert_third_party_auth_data(response, current_provider, expected_providers)
...@@ -472,12 +472,12 @@ class StudentAccountLoginAndRegistrationTest(ModuleStoreTestCase): ...@@ -472,12 +472,12 @@ class StudentAccountLoginAndRegistrationTest(ModuleStoreTestCase):
"name": "Facebook", "name": "Facebook",
"iconClass": "fa-facebook", "iconClass": "fa-facebook",
"loginUrl": self._third_party_login_url( "loginUrl": self._third_party_login_url(
"facebook", "account_login", "facebook", "login",
course_id=unicode(course.id), course_id=unicode(course.id),
redirect_url=course_modes_choose_url redirect_url=course_modes_choose_url
), ),
"registerUrl": self._third_party_login_url( "registerUrl": self._third_party_login_url(
"facebook", "account_register", "facebook", "register",
course_id=unicode(course.id), course_id=unicode(course.id),
redirect_url=course_modes_choose_url redirect_url=course_modes_choose_url
) )
...@@ -486,12 +486,12 @@ class StudentAccountLoginAndRegistrationTest(ModuleStoreTestCase): ...@@ -486,12 +486,12 @@ class StudentAccountLoginAndRegistrationTest(ModuleStoreTestCase):
"name": "Google", "name": "Google",
"iconClass": "fa-google-plus", "iconClass": "fa-google-plus",
"loginUrl": self._third_party_login_url( "loginUrl": self._third_party_login_url(
"google-oauth2", "account_login", "google-oauth2", "login",
course_id=unicode(course.id), course_id=unicode(course.id),
redirect_url=course_modes_choose_url redirect_url=course_modes_choose_url
), ),
"registerUrl": self._third_party_login_url( "registerUrl": self._third_party_login_url(
"google-oauth2", "account_register", "google-oauth2", "register",
course_id=unicode(course.id), course_id=unicode(course.id),
redirect_url=course_modes_choose_url redirect_url=course_modes_choose_url
) )
...@@ -520,12 +520,12 @@ class StudentAccountLoginAndRegistrationTest(ModuleStoreTestCase): ...@@ -520,12 +520,12 @@ class StudentAccountLoginAndRegistrationTest(ModuleStoreTestCase):
"name": "Facebook", "name": "Facebook",
"iconClass": "fa-facebook", "iconClass": "fa-facebook",
"loginUrl": self._third_party_login_url( "loginUrl": self._third_party_login_url(
"facebook", "account_login", "facebook", "login",
course_id=unicode(course.id), course_id=unicode(course.id),
redirect_url=shoppingcart_url redirect_url=shoppingcart_url
), ),
"registerUrl": self._third_party_login_url( "registerUrl": self._third_party_login_url(
"facebook", "account_register", "facebook", "register",
course_id=unicode(course.id), course_id=unicode(course.id),
redirect_url=shoppingcart_url redirect_url=shoppingcart_url
) )
...@@ -534,12 +534,12 @@ class StudentAccountLoginAndRegistrationTest(ModuleStoreTestCase): ...@@ -534,12 +534,12 @@ class StudentAccountLoginAndRegistrationTest(ModuleStoreTestCase):
"name": "Google", "name": "Google",
"iconClass": "fa-google-plus", "iconClass": "fa-google-plus",
"loginUrl": self._third_party_login_url( "loginUrl": self._third_party_login_url(
"google-oauth2", "account_login", "google-oauth2", "login",
course_id=unicode(course.id), course_id=unicode(course.id),
redirect_url=shoppingcart_url redirect_url=shoppingcart_url
), ),
"registerUrl": self._third_party_login_url( "registerUrl": self._third_party_login_url(
"google-oauth2", "account_register", "google-oauth2", "register",
course_id=unicode(course.id), course_id=unicode(course.id),
redirect_url=shoppingcart_url redirect_url=shoppingcart_url
) )
...@@ -550,6 +550,27 @@ class StudentAccountLoginAndRegistrationTest(ModuleStoreTestCase): ...@@ -550,6 +550,27 @@ class StudentAccountLoginAndRegistrationTest(ModuleStoreTestCase):
response = self.client.get(reverse("account_login"), {"course_id": unicode(course.id)}) response = self.client.get(reverse("account_login"), {"course_id": unicode(course.id)})
self._assert_third_party_auth_data(response, None, expected_providers) self._assert_third_party_auth_data(response, None, expected_providers)
@override_settings(SITE_NAME=settings.MICROSITE_TEST_HOSTNAME)
def test_microsite_uses_old_login_page(self):
# Retrieve the login page from a microsite domain
# and verify that we're served the old page.
resp = self.client.get(
reverse("account_login"),
HTTP_HOST=settings.MICROSITE_TEST_HOSTNAME
)
self.assertContains(resp, "Log into your Test Microsite Account")
self.assertContains(resp, "login-form")
def test_microsite_uses_old_register_page(self):
# Retrieve the register page from a microsite domain
# and verify that we're served the old page.
resp = self.client.get(
reverse("account_register"),
HTTP_HOST=settings.MICROSITE_TEST_HOSTNAME
)
self.assertContains(resp, "Register for Test Microsite")
self.assertContains(resp, "register-form")
def _assert_third_party_auth_data(self, response, current_provider, providers): def _assert_third_party_auth_data(self, response, current_provider, providers):
"""Verify that third party auth info is rendered correctly in a DOM data attribute. """ """Verify that third party auth info is rendered correctly in a DOM data attribute. """
auth_info = markupsafe.escape( auth_info = markupsafe.escape(
......
...@@ -17,6 +17,14 @@ from django.views.decorators.http import require_http_methods ...@@ -17,6 +17,14 @@ from django.views.decorators.http import require_http_methods
from edxmako.shortcuts import render_to_response, render_to_string from edxmako.shortcuts import render_to_response, render_to_string
from microsite_configuration import microsite from microsite_configuration import microsite
import third_party_auth import third_party_auth
from external_auth.login_and_register import (
login as external_auth_login,
register as external_auth_register
)
from student.views import (
signin_user as old_login_view,
register_user as old_register_view
)
from openedx.core.djangoapps.user_api.api import account as account_api from openedx.core.djangoapps.user_api.api import account as account_api
from openedx.core.djangoapps.user_api.api import profile as profile_api from openedx.core.djangoapps.user_api.api import profile as profile_api
...@@ -62,7 +70,7 @@ def login_and_registration_form(request, initial_mode="login"): ...@@ -62,7 +70,7 @@ def login_and_registration_form(request, initial_mode="login"):
the user_api. the user_api.
Keyword Args: Keyword Args:
initial_mode (string): Either "login" or "registration". initial_mode (string): Either "login" or "register".
""" """
# If we're already logged in, redirect to the dashboard # If we're already logged in, redirect to the dashboard
...@@ -72,6 +80,19 @@ def login_and_registration_form(request, initial_mode="login"): ...@@ -72,6 +80,19 @@ def login_and_registration_form(request, initial_mode="login"):
# Retrieve the form descriptions from the user API # Retrieve the form descriptions from the user API
form_descriptions = _get_form_descriptions(request) form_descriptions = _get_form_descriptions(request)
# If this is a microsite, revert to the old login/registration pages.
# We need to do this for now to support existing themes.
if microsite.is_request_in_microsite():
if initial_mode == "login":
return old_login_view(request)
elif initial_mode == "register":
return old_register_view(request)
# Allow external auth to intercept and handle the request
ext_auth_response = _external_auth_intercept(request, initial_mode)
if ext_auth_response is not None:
return ext_auth_response
# Otherwise, render the combined login/registration page # Otherwise, render the combined login/registration page
context = { context = {
'disable_courseware_js': True, 'disable_courseware_js': True,
...@@ -299,13 +320,15 @@ def _third_party_auth_context(request): ...@@ -299,13 +320,15 @@ def _third_party_auth_context(request):
course_id = request.GET.get("course_id") course_id = request.GET.get("course_id")
email_opt_in = request.GET.get('email_opt_in') email_opt_in = request.GET.get('email_opt_in')
redirect_to = request.GET.get("next")
login_urls = auth_pipeline_urls( login_urls = auth_pipeline_urls(
third_party_auth.pipeline.AUTH_ENTRY_LOGIN_2, third_party_auth.pipeline.AUTH_ENTRY_LOGIN,
course_id=course_id, course_id=course_id,
email_opt_in=email_opt_in email_opt_in=email_opt_in,
redirect_url=redirect_to
) )
register_urls = auth_pipeline_urls( register_urls = auth_pipeline_urls(
third_party_auth.pipeline.AUTH_ENTRY_REGISTER_2, third_party_auth.pipeline.AUTH_ENTRY_REGISTER,
course_id=course_id, course_id=course_id,
email_opt_in=email_opt_in email_opt_in=email_opt_in
) )
...@@ -377,3 +400,20 @@ def _local_server_get(url, session): ...@@ -377,3 +400,20 @@ def _local_server_get(url, session):
# Return the content of the response # Return the content of the response
return response.content return response.content
def _external_auth_intercept(request, mode):
"""Allow external auth to intercept a login/registration request.
Arguments:
request (Request): The original request.
mode (str): Either "login" or "register"
Returns:
Response or None
"""
if mode == "login":
return external_auth_login(request)
elif mode == "register":
return external_auth_register(request)
...@@ -178,7 +178,7 @@ var edx = edx || {}; ...@@ -178,7 +178,7 @@ var edx = edx || {};
this.element.scrollTop( $anchor ); this.element.scrollTop( $anchor );
// Update url without reloading page // Update url without reloading page
History.pushState( null, document.title, '/account/' + type + '/' + queryStr ); History.pushState( null, document.title, '/' + type + queryStr );
analytics.page( 'login_and_registration', type ); analytics.page( 'login_and_registration', type );
}, },
......
...@@ -83,7 +83,7 @@ ...@@ -83,7 +83,7 @@
%elif allow_registration: %elif allow_registration:
<a class="action action-register register ${'has-option-verified' if len(course_modes) > 1 else ''}" <a class="action action-register register ${'has-option-verified' if len(course_modes) > 1 else ''}"
%if not user.is_authenticated(): %if not user.is_authenticated():
href="${reverse('register_user')}?course_id=${course.id | u}&enrollment_action=enroll" href="${reverse('register_user')}?course_id=${course.id | u}&enrollment_action=enroll"
%endif %endif
>${_("Enroll in")} <strong>${course.display_number_with_default | h}</strong> >${_("Enroll in")} <strong>${course.display_number_with_default | h}</strong>
%if len(course_modes) > 1: %if len(course_modes) > 1:
......
...@@ -20,8 +20,6 @@ urlpatterns = ('', # nopep8 ...@@ -20,8 +20,6 @@ urlpatterns = ('', # nopep8
url(r'^dashboard$', 'student.views.dashboard', name="dashboard"), url(r'^dashboard$', 'student.views.dashboard', name="dashboard"),
url(r'^login_ajax$', 'student.views.login_user', name="login"), url(r'^login_ajax$', 'student.views.login_user', name="login"),
url(r'^login_ajax/(?P<error>[^/]*)$', 'student.views.login_user'), url(r'^login_ajax/(?P<error>[^/]*)$', 'student.views.login_user'),
url(r'^login$', 'student.views.signin_user', name="signin_user"),
url(r'^register$', 'student.views.register_user', name="register_user"),
url(r'^admin_dashboard$', 'dashboard.views.dashboard'), url(r'^admin_dashboard$', 'dashboard.views.dashboard'),
...@@ -35,7 +33,6 @@ urlpatterns = ('', # nopep8 ...@@ -35,7 +33,6 @@ urlpatterns = ('', # nopep8
url(r'^segmentio/event$', 'track.views.segmentio.segmentio_event'), url(r'^segmentio/event$', 'track.views.segmentio.segmentio_event'),
url(r'^t/(?P<template>[^/]*)$', 'static_template_view.views.index'), # TODO: Is this used anymore? What is STATIC_GRAB? url(r'^t/(?P<template>[^/]*)$', 'static_template_view.views.index'), # TODO: Is this used anymore? What is STATIC_GRAB?
url(r'^accounts/login$', 'student.views.accounts_login', name="accounts_login"),
url(r'^accounts/manage_user_standing', 'student.views.manage_user_standing', url(r'^accounts/manage_user_standing', 'student.views.manage_user_standing',
name='manage_user_standing'), name='manage_user_standing'),
url(r'^accounts/disable_account_ajax$', 'student.views.disable_account_ajax', url(r'^accounts/disable_account_ajax$', 'student.views.disable_account_ajax',
...@@ -86,6 +83,24 @@ urlpatterns = ('', # nopep8 ...@@ -86,6 +83,24 @@ urlpatterns = ('', # nopep8
) )
if settings.FEATURES["ENABLE_COMBINED_LOGIN_REGISTRATION"]:
# Backwards compatibility with old URL structure, but serve the new views
urlpatterns += (
url(r'^login$', 'student_account.views.login_and_registration_form',
{'initial_mode': 'login'}, name="signin_user"),
url(r'^register$', 'student_account.views.login_and_registration_form',
{'initial_mode': 'register'}, name="register_user"),
url(r'^accounts/login$', 'student_account.views.login_and_registration_form',
{'initial_mode': 'login'}, name="accounts_login"),
)
else:
# Serve the old views
urlpatterns += (
url(r'^login$', 'student.views.signin_user', name="signin_user"),
url(r'^register$', 'student.views.register_user', name="register_user"),
url(r'^accounts/login$', 'student.views.accounts_login', name="accounts_login"),
)
if settings.FEATURES["ENABLE_MOBILE_REST_API"]: if settings.FEATURES["ENABLE_MOBILE_REST_API"]:
urlpatterns += ( urlpatterns += (
url(r'^api/mobile/v0.5/', include('mobile_api.urls')), url(r'^api/mobile/v0.5/', include('mobile_api.urls')),
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment