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)
This commit is contained in:
@@ -4,83 +4,9 @@ from datetime import datetime
|
||||
from pytz import UTC
|
||||
from django.utils.http import cookie_date
|
||||
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
|
||||
|
||||
|
||||
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()
|
||||
}
|
||||
from course_modes.models import CourseMode
|
||||
from student_account.helpers import auth_pipeline_urls # pylint: disable=unused-import,import-error
|
||||
|
||||
|
||||
def set_logged_in_cookie(request, response):
|
||||
|
||||
@@ -14,18 +14,13 @@ from django.http import HttpResponseBadRequest, HttpResponse
|
||||
from external_auth.models import ExternalAuthMap
|
||||
import httpretty
|
||||
from mock import patch
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
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.views import (
|
||||
_parse_course_id_from_string,
|
||||
_get_course_enrollment_domain,
|
||||
login_oauth_token,
|
||||
)
|
||||
from student.views import login_oauth_token
|
||||
|
||||
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):
|
||||
@@ -324,24 +319,6 @@ class LoginTest(TestCase):
|
||||
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)
|
||||
class ExternalAuthShibTest(ModuleStoreTestCase):
|
||||
"""
|
||||
@@ -387,15 +364,6 @@ class ExternalAuthShibTest(ModuleStoreTestCase):
|
||||
'redirect': reverse('shib-login'),
|
||||
})
|
||||
|
||||
@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):
|
||||
"""
|
||||
@@ -416,7 +384,7 @@ class ExternalAuthShibTest(ModuleStoreTestCase):
|
||||
noshib_response = self.client.get(TARGET_URL, follow=True)
|
||||
self.assertEqual(noshib_response.redirect_chain[-1],
|
||||
('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)))
|
||||
self.assertEqual(noshib_response.status_code, 200)
|
||||
|
||||
|
||||
@@ -2,11 +2,14 @@
|
||||
import urllib
|
||||
import unittest
|
||||
from collections import OrderedDict
|
||||
|
||||
import ddt
|
||||
from mock import patch
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
import ddt
|
||||
from django.test.utils import override_settings
|
||||
|
||||
from util.testing import UrlResetMixin
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from student.tests.factories import CourseModeFactory
|
||||
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
|
||||
@ddt.ddt
|
||||
@override_settings(MODULESTORE=MODULESTORE_CONFIG)
|
||||
@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. """
|
||||
|
||||
@patch.dict(settings.FEATURES, {"ENABLE_COMBINED_LOGIN_REGISTRATION": False})
|
||||
def setUp(self):
|
||||
super(LoginFormTest, self).setUp('lms.urls')
|
||||
self.url = reverse("signin_user")
|
||||
self.course = CourseFactory.create()
|
||||
self.course_id = unicode(self.course.id)
|
||||
@@ -153,10 +157,11 @@ class LoginFormTest(ModuleStoreTestCase):
|
||||
@ddt.ddt
|
||||
@override_settings(MODULESTORE=MODULESTORE_CONFIG)
|
||||
@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. """
|
||||
|
||||
@patch.dict(settings.FEATURES, {"ENABLE_COMBINED_LOGIN_REGISTRATION": False})
|
||||
def setUp(self):
|
||||
super(RegisterFormTest, self).setUp('lms.urls')
|
||||
self.url = reverse("register_user")
|
||||
self.course = CourseFactory.create()
|
||||
self.course_id = unicode(self.course.id)
|
||||
|
||||
@@ -76,6 +76,10 @@ from django_comment_common.models import Role
|
||||
|
||||
from external_auth.models import ExternalAuthMap
|
||||
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
|
||||
import shoppingcart
|
||||
@@ -354,18 +358,10 @@ def _cert_info(user, course, cert_status):
|
||||
|
||||
@ensure_csrf_cookie
|
||||
def signin_user(request):
|
||||
"""
|
||||
This view will display the non-modal login form
|
||||
"""
|
||||
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.
|
||||
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'))
|
||||
"""Deprecated. To be replaced by :class:`student_account.views.login_and_registration_form`."""
|
||||
external_auth_response = external_auth_login(request)
|
||||
if external_auth_response is not None:
|
||||
return external_auth_response
|
||||
if request.user.is_authenticated():
|
||||
return redirect(reverse('dashboard'))
|
||||
|
||||
@@ -391,15 +387,13 @@ def signin_user(request):
|
||||
|
||||
@ensure_csrf_cookie
|
||||
def register_user(request, extra_context=None):
|
||||
"""
|
||||
This view will display the non-modal registration form
|
||||
"""
|
||||
"""Deprecated. To be replaced by :class:`student_account.views.login_and_registration_form`."""
|
||||
if request.user.is_authenticated():
|
||||
return redirect(reverse('dashboard'))
|
||||
if settings.FEATURES.get('AUTH_USE_CERTIFICATES_IMMEDIATE_SIGNUP'):
|
||||
# Redirect to branding to process their certificate if SSL is enabled
|
||||
# and registration is disabled.
|
||||
return external_auth.views.redirect_with_get('root', request.GET)
|
||||
|
||||
external_auth_response = external_auth_register(request)
|
||||
if external_auth_response is not None:
|
||||
return external_auth_response
|
||||
|
||||
course_id = request.GET.get('course_id')
|
||||
email_opt_in = request.GET.get('email_opt_in')
|
||||
@@ -918,56 +912,15 @@ def change_enrollment(request, check_access=True):
|
||||
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
|
||||
@ensure_csrf_cookie
|
||||
def accounts_login(request):
|
||||
"""
|
||||
This view is mainly used as the redirect from the @login_required decorator. I don't believe that
|
||||
the login path linked from the homepage uses it.
|
||||
"""
|
||||
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())
|
||||
"""Deprecated. To be replaced by :class:`student_account.views.login_and_registration_form`."""
|
||||
external_auth_response = external_auth_login(request)
|
||||
if external_auth_response is not None:
|
||||
return external_auth_response
|
||||
|
||||
redirect_to = request.GET.get('next')
|
||||
context = {
|
||||
'pipeline_running': 'false',
|
||||
'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
|
||||
AUDIT_LOG.warning(u"Login failed - Account not active for user {0}, resending activation".format(username))
|
||||
|
||||
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({
|
||||
"success": False,
|
||||
"value": not_activated_msg,
|
||||
|
||||
Reference in New Issue
Block a user