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:
Renzo Lucioni
2014-11-26 15:35:29 -05:00
parent e24d4d96ad
commit a8bed5ce98
23 changed files with 341 additions and 794 deletions

View File

@@ -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):

View File

@@ -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)

View File

@@ -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)

View File

@@ -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,