Merge pull request #22416 from edx/robrap/ARCH-1253-remove-studio-login

ARCH-1253: remove studio signin and signup pages
This commit is contained in:
Robert Raposa
2019-12-04 03:20:39 -05:00
committed by GitHub
42 changed files with 227 additions and 1069 deletions

View File

@@ -2186,10 +2186,12 @@ class EntryPageTestCase(TestCase):
self._test_page("/howitworks")
def test_signup(self):
self._test_page("/signup")
# deprecated signup url redirects to LMS register.
self._test_page("/signup", 301)
def test_login(self):
self._test_page("/signin")
# deprecated signin url redirects to LMS login.
self._test_page("/signin", 302)
def test_logout(self):
# Logout redirects.
@@ -2202,36 +2204,6 @@ class EntryPageTestCase(TestCase):
self._test_page('/accessibility')
class SigninPageTestCase(TestCase):
"""
Tests that the CSRF token is directly included in the signin form. This is
important to make sure that the script is functional independently of any
other script.
"""
def test_csrf_token_is_present_in_form(self):
# Expected html:
# <form>
# ...
# <fieldset>
# ...
# <input name="csrfmiddlewaretoken" value="...">
# ...
# </fieldset>
# ...
# </form>
response = self.client.get("/signin")
csrf_token = response.cookies.get("csrftoken")
form = lxml.html.fromstring(response.content).get_element_by_id("login_form")
csrf_input_field = form.find(".//input[@name='csrfmiddlewaretoken']")
self.assertIsNotNone(csrf_token)
self.assertIsNotNone(csrf_token.value)
self.assertIsNotNone(csrf_input_field)
self.assertTrue(_compare_salted_tokens(csrf_token.value, csrf_input_field.attrib["value"]))
def _create_course(test, course_key, course_data):
"""
Creates a course via an AJAX request and verifies the URL returned in the response.

View File

@@ -7,19 +7,14 @@ import datetime
import time
import mock
import pytest
from contentstore.tests.test_course_settings import CourseTestCase
from contentstore.tests.utils import AjaxEnabledTestClient, parse_json, registration, user
from ddt import data, ddt, unpack
from django.conf import settings
from django.contrib.auth.models import User
from django.core.cache import cache
from django.test import TestCase
from django.test.utils import override_settings
from django.urls import reverse
from freezegun import freeze_time
from pytz import UTC
from six.moves import range
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
@@ -87,36 +82,7 @@ class ContentStoreTestCase(ModuleStoreTestCase):
self.assertTrue(user(email).is_active)
@pytest.mark.django_db
def test_create_account_email_already_exists(django_db_use_migrations):
"""
This is tricky. Django's user model doesn't have a constraint on
unique email addresses, but we *add* that constraint during the
migration process:
see common/djangoapps/student/migrations/0004_add_email_index.py
The behavior we *want* is for this account creation request
to fail, due to this uniqueness constraint, but the request will
succeed if the migrations have not run.
django_db_use_migration is a pytest fixture that tells us if
migrations have been run. Since pytest fixtures don't play nice
with TestCase objects this is a function and doesn't get to use
assertRaises.
"""
if django_db_use_migrations:
email = 'a@b.com'
pw = 'xyz'
username = 'testuser'
User.objects.create_user(username, email, pw)
# Hack to use the _create_account shortcut
case = ContentStoreTestCase()
resp = case._create_account("abcdef", email, "password") # pylint: disable=protected-access
assert resp.status_code == 400, 'Migrations are run, but creating an account with duplicate email succeeded!'
@ddt
class AuthTestCase(ContentStoreTestCase):
"""Check that various permissions-related things work"""
@@ -138,114 +104,6 @@ class AuthTestCase(ContentStoreTestCase):
self.assertEqual(resp.status_code, expected)
return resp
def test_public_pages_load(self):
"""Make sure pages that don't require login load without error."""
pages = (
reverse('login'),
reverse('signup'),
)
for page in pages:
print(u"Checking '{0}'".format(page))
self.check_page_get(page, 200)
def test_create_account_errors(self):
# No post data -- should fail
registration_url = reverse('user_api_registration')
resp = self.client.post(registration_url, {})
self.assertEqual(resp.status_code, 400)
def test_create_account(self):
self.create_account(self.username, self.email, self.pw)
self.activate_user(self.email)
def test_create_account_username_already_exists(self):
User.objects.create_user(self.username, self.email, self.pw)
resp = self._create_account(self.username, "abc@def.com", "password")
# we have a constraint on unique usernames, so this should fail
self.assertEqual(resp.status_code, 409)
def test_create_account_pw_already_exists(self):
User.objects.create_user(self.username, self.email, self.pw)
resp = self._create_account("abcdef", "abc@def.com", self.pw)
# we can have two users with the same password, so this should succeed
self.assertEqual(resp.status_code, 200)
def test_login(self):
self.create_account(self.username, self.email, self.pw)
# Not activated yet. Login should fail.
self._login(self.email, self.pw)
self.activate_user(self.email)
# Now login should work
self.login(self.email, self.pw)
def test_login_ratelimited(self):
# try logging in 30 times, the default limit in the number of failed
# login attempts in one 5 minute period before the rate gets limited
for i in range(30):
resp = self._login(self.email, 'wrong_password{0}'.format(i))
self.assertEqual(resp.status_code, 403)
resp = self._login(self.email, 'wrong_password')
self.assertContains(resp, 'Too many failed login attempts.', status_code=403)
@override_settings(MAX_FAILED_LOGIN_ATTEMPTS_ALLOWED=3)
@override_settings(MAX_FAILED_LOGIN_ATTEMPTS_LOCKOUT_PERIOD_SECS=2)
def test_excessive_login_failures(self):
# try logging in 3 times, the account should get locked for 3 seconds
# note we want to keep the lockout time short, so we don't slow down the tests
with mock.patch.dict('django.conf.settings.FEATURES', {'ENABLE_MAX_FAILED_LOGIN_ATTEMPTS': True}):
self.create_account(self.username, self.email, self.pw)
self.activate_user(self.email)
for i in range(3):
resp = self._login(self.email, 'wrong_password{0}'.format(i))
self.assertContains(
resp,
'Email or password is incorrect.',
status_code=403,
)
# now the account should be locked
resp = self._login(self.email, 'wrong_password')
self.assertContains(
resp,
'This account has been temporarily locked due to excessive login failures.',
status_code=403,
)
with freeze_time('2100-01-01'):
self.login(self.email, self.pw)
# make sure the failed attempt counter gets reset on successful login
resp = self._login(self.email, 'wrong_password')
self.assertContains(
resp,
'Email or password is incorrect.',
status_code=403,
)
# account should not be locked out after just one attempt
self.login(self.email, self.pw)
# do one more login when there is no bad login counter row at all in the database to
# test the "ObjectNotFound" case
self.login(self.email, self.pw)
def test_login_link_on_activation_age(self):
self.create_account(self.username, self.email, self.pw)
# we want to test the rendering of the activation page when the user isn't logged in
self.client.logout()
resp = self._activate_user(self.email)
# check the the HTML has links to the right login page. Note that this is merely a content
# check and thus could be fragile should the wording change on this page
expected = 'You can now <a href="' + reverse('login') + '">sign in</a>.'
self.assertContains(resp, expected)
def test_private_pages_auth(self):
"""Make sure pages that do require login work."""
auth_pages = (
@@ -259,7 +117,8 @@ class AuthTestCase(ContentStoreTestCase):
)
# need an activated user
self.test_create_account()
self.create_account(self.username, self.email, self.pw)
self.activate_user(self.email)
# Create a new session
self.client = AjaxEnabledTestClient()
@@ -278,14 +137,6 @@ class AuthTestCase(ContentStoreTestCase):
print(u"Checking '{0}'".format(page))
self.check_page_get(page, expected=200)
def test_index_auth(self):
# not logged in. Should return a redirect.
resp = self.client.get_html('/home/')
self.assertEqual(resp.status_code, 302)
# Logged in should work.
@override_settings(SESSION_INACTIVITY_TIMEOUT_IN_SECONDS=1)
def test_inactive_session_timeout(self):
"""
@@ -308,37 +159,30 @@ class AuthTestCase(ContentStoreTestCase):
resp = self.client.get_html(course_url)
# re-request, and we should get a redirect to login page
self.assertRedirects(resp, settings.LOGIN_URL + '?next=/home/')
self.assertRedirects(resp, settings.LOGIN_URL + '?next=/home/', target_status_code=302)
@mock.patch.dict(settings.FEATURES, {"ALLOW_PUBLIC_ACCOUNT_CREATION": False})
def test_signup_button_index_page(self):
@data(
(True, 'assertContains'),
(False, 'assertNotContains'))
@unpack
def test_signin_and_signup_buttons_index_page(self, allow_account_creation, assertion_method_name):
"""
Navigate to the home page and check the Sign Up button is hidden when ALLOW_PUBLIC_ACCOUNT_CREATION flag
is turned off
is turned off, and not when it is turned on. The Sign In button should always appear.
"""
response = self.client.get(reverse('homepage'))
self.assertNotContains(response, '<a class="action action-signup" href="/signup">Sign Up</a>')
@mock.patch.dict(settings.FEATURES, {"ALLOW_PUBLIC_ACCOUNT_CREATION": False})
def test_signup_button_login_page(self):
"""
Navigate to the login page and check the Sign Up button is hidden when ALLOW_PUBLIC_ACCOUNT_CREATION flag
is turned off
"""
response = self.client.get(reverse('login'))
self.assertNotContains(response, '<a class="action action-signup" href="/signup">Sign Up</a>')
@mock.patch.dict(settings.FEATURES, {"ALLOW_PUBLIC_ACCOUNT_CREATION": False})
def test_signup_link_login_page(self):
"""
Navigate to the login page and check the Sign Up link is hidden when ALLOW_PUBLIC_ACCOUNT_CREATION flag
is turned off
"""
response = self.client.get(reverse('login'))
self.assertNotContains(
response,
'<a href="/signup" class="action action-signin">Don&#39;t have a Studio Account? Sign up!</a>'
)
with mock.patch.dict(settings.FEATURES, {"ALLOW_PUBLIC_ACCOUNT_CREATION": allow_account_creation}):
response = self.client.get(reverse('homepage'))
assertion_method = getattr(self, assertion_method_name)
assertion_method(
response,
u'<a class="action action-signup" href="{}/register?next=http%3A%2F%2Ftestserver%2F">Sign Up</a>'.format( # pylint: disable=line-too-long
settings.LMS_ROOT_URL
)
)
self.assertContains(
response,
u'<a class="action action-signin" href="/signin_redirect_to_lms?next=http%3A%2F%2Ftestserver%2F">Sign In</a>' # pylint: disable=line-too-long
)
class ForumTestCase(CourseTestCase):

View File

@@ -5,48 +5,25 @@ from __future__ import absolute_import
from django.conf import settings
from django.shortcuts import redirect
from django.template.context_processors import csrf
from django.utils.http import urlquote_plus
from django.views.decorators.clickjacking import xframe_options_deny
from django.views.decorators.csrf import ensure_csrf_cookie
from waffle.decorators import waffle_switch
from contentstore.config import waffle
from edxmako.shortcuts import render_to_response
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
__all__ = ['signup', 'login_page', 'login_redirect_to_lms', 'howitworks', 'accessibility']
__all__ = ['register_redirect_to_lms', 'login_redirect_to_lms', 'howitworks', 'accessibility']
@ensure_csrf_cookie
@xframe_options_deny
def signup(request):
def register_redirect_to_lms(request):
"""
Display the signup form.
This view redirects to the LMS register view. It is used to temporarily keep the old
Studio signup url alive.
"""
csrf_token = csrf(request)['csrf_token']
if request.user.is_authenticated:
return redirect('/course/')
return render_to_response('register.html', {'csrf': csrf_token})
@ensure_csrf_cookie
@xframe_options_deny
def login_page(request):
"""
Display the login form.
"""
csrf_token = csrf(request)['csrf_token']
return render_to_response(
'login.html',
{
'csrf': csrf_token,
'forgot_password_link': "//{base}/login#forgot-password-modal".format(base=settings.LMS_BASE),
'platform_name': configuration_helpers.get_value('platform_name', settings.PLATFORM_NAME),
}
register_url = '{register_url}{params}'.format(
register_url=settings.FRONTEND_REGISTER_URL,
params=_build_next_param(request),
)
return redirect(register_url, permanent=True)
def login_redirect_to_lms(request):
@@ -54,15 +31,25 @@ def login_redirect_to_lms(request):
This view redirects to the LMS login view. It is used for Django's LOGIN_URL
setting, which is where unauthenticated requests to protected endpoints are redirected.
"""
next_url = request.GET.get('next')
absolute_next_url = request.build_absolute_uri(next_url)
login_url = '{base_url}/login{params}'.format(
base_url=settings.LMS_ROOT_URL,
params='?next=' + urlquote_plus(absolute_next_url) if next_url else '',
login_url = '{login_url}{params}'.format(
login_url=settings.FRONTEND_LOGIN_URL,
params=_build_next_param(request),
)
return redirect(login_url)
def _build_next_param(request):
""" Returns the next param to be used with login or register. """
next_url = request.GET.get('next')
next_url = next_url if next_url else settings.LOGIN_REDIRECT_URL
if next_url:
# Warning: do not use `build_absolute_uri` when `next_url` is empty because `build_absolute_uri` would
# build use the login url for the next url, which would cause a login redirect loop.
absolute_next_url = request.build_absolute_uri(next_url)
return '?next=' + urlquote_plus(absolute_next_url)
return ''
def howitworks(request):
"Proxy view"
if request.user.is_authenticated:

View File

@@ -92,11 +92,12 @@ class MaintenanceViewAccessTests(MaintenanceViewTestCase):
# Expect a redirect to the login page
redirect_url = '{login_url}?next={original_url}'.format(
login_url=reverse('login'),
login_url=settings.LOGIN_URL,
original_url=url,
)
self.assertRedirects(response, redirect_url)
# Studio login redirects to LMS login
self.assertRedirects(response, redirect_url, target_status_code=302)
@ddt.data(*MAINTENANCE_URLS)
def test_global_staff_access(self, url):

View File

@@ -475,8 +475,6 @@ AWS_S3_CUSTOM_DOMAIN = 'SET-ME-PLEASE (ex. bucket-name.s3.amazonaws.com)'
##############################################################################
EDX_ROOT_URL = ''
LOGIN_REDIRECT_URL = EDX_ROOT_URL + '/home/'
LOGIN_URL = reverse_lazy('login_redirect_to_lms')
# use the ratelimit backend to prevent brute force attacks
AUTHENTICATION_BACKENDS = [
@@ -496,13 +494,21 @@ LOGGING_ENV = 'sandbox'
LMS_BASE = 'localhost:18000'
LMS_ROOT_URL = "https://localhost:18000"
LMS_INTERNAL_ROOT_URL = LMS_ROOT_URL
LOGIN_REDIRECT_URL = EDX_ROOT_URL + '/home/'
# TODO: Determine if LOGIN_URL could be set to the FRONTEND_LOGIN_URL value instead.
LOGIN_URL = reverse_lazy('login_redirect_to_lms')
FRONTEND_LOGIN_URL = lambda settings: settings.LMS_ROOT_URL + '/login'
derived('FRONTEND_LOGIN_URL')
FRONTEND_LOGOUT_URL = lambda settings: settings.LMS_ROOT_URL + '/logout'
derived('FRONTEND_LOGOUT_URL')
FRONTEND_REGISTER_URL = lambda settings: settings.LMS_ROOT_URL + '/register'
derived('FRONTEND_REGISTER_URL')
LMS_ENROLLMENT_API_PATH = "/api/enrollment/v1/"
ENTERPRISE_API_URL = LMS_INTERNAL_ROOT_URL + '/enterprise/api/v1/'
ENTERPRISE_CONSENT_API_URL = LMS_INTERNAL_ROOT_URL + '/consent/api/v1/'
ENTERPRISE_MARKETING_FOOTER_QUERY_PARAMS = {}
FRONTEND_LOGIN_URL = LOGIN_URL
FRONTEND_LOGOUT_URL = lambda settings: settings.LMS_ROOT_URL + '/logout'
derived('FRONTEND_LOGOUT_URL')
# Public domain name of Studio (should be resolvable from the end-user's browser)
CMS_BASE = 'localhost:18010'
@@ -2122,3 +2128,44 @@ REGISTRATION_EXTRA_FIELDS = {
'country': 'hidden',
}
EDXAPP_PARSE_KEYS = {}
###################### DEPRECATED URLS ##########################
# .. toggle_name: DISABLE_DEPRECATED_SIGNIN_URL
# .. toggle_implementation: DjangoSetting
# .. toggle_default: False
# .. toggle_description: Toggle for removing the deprecated /signin url.
# .. toggle_category: n/a
# .. toggle_use_cases: incremental_release
# .. toggle_creation_date: 2019-12-02
# .. toggle_expiration_date: 2020-06-01
# .. toggle_warnings: This url can be removed once it no longer has any real traffic.
# .. toggle_tickets: ARCH-1253
# .. toggle_status: supported
DISABLE_DEPRECATED_SIGNIN_URL = False
# .. toggle_name: DISABLE_DEPRECATED_SIGNUP_URL
# .. toggle_implementation: DjangoSetting
# .. toggle_default: False
# .. toggle_description: Toggle for removing the deprecated /signup url.
# .. toggle_category: n/a
# .. toggle_use_cases: incremental_release
# .. toggle_creation_date: 2019-12-02
# .. toggle_expiration_date: 2020-06-01
# .. toggle_warnings: This url can be removed once it no longer has any real traffic.
# .. toggle_tickets: ARCH-1253
# .. toggle_status: supported
DISABLE_DEPRECATED_SIGNUP_URL = False
# .. toggle_name: DISABLE_DEPRECATED_LOGIN_POST
# .. toggle_implementation: DjangoSetting
# .. toggle_default: False
# .. toggle_description: Toggle for removing the deprecated /login_post url.
# .. toggle_category: n/a
# .. toggle_use_cases: incremental_release
# .. toggle_creation_date: 2019-12-02
# .. toggle_expiration_date: 2020-06-01
# .. toggle_warnings: This url can be removed once it no longer has any real traffic. Note: We have permission to remove for traffic from user_agent including `mitx-quantum`.
# .. toggle_tickets: ARCH-1253
# .. toggle_status: supported
DISABLE_DEPRECATED_LOGIN_POST = False

View File

@@ -308,13 +308,6 @@ HEARTBEAT_CHECKS = ENV_TOKENS.get('HEARTBEAT_CHECKS', HEARTBEAT_CHECKS)
HEARTBEAT_EXTENDED_CHECKS = ENV_TOKENS.get('HEARTBEAT_EXTENDED_CHECKS', HEARTBEAT_EXTENDED_CHECKS)
HEARTBEAT_CELERY_TIMEOUT = ENV_TOKENS.get('HEARTBEAT_CELERY_TIMEOUT', HEARTBEAT_CELERY_TIMEOUT)
# Login using the LMS as the identity provider.
# Turning the flag to True means that the LMS will NOT be used as the Identity Provider (idp)
if FEATURES.get('DISABLE_STUDIO_SSO_OVER_LMS', False):
LOGIN_URL = reverse_lazy('login')
FRONTEND_LOGIN_URL = LOGIN_URL
FRONTEND_LOGOUT_URL = reverse_lazy('logout')
LOGIN_REDIRECT_WHITELIST = [reverse_lazy('home')]
# Specific setting for the File Upload Service to store media in a bucket.

View File

@@ -19,7 +19,6 @@ from .common import *
import os
from uuid import uuid4
from django.utils.translation import ugettext_lazy
from path import Path as path
@@ -142,8 +141,6 @@ if os.environ.get('DISABLE_MIGRATIONS'):
LMS_BASE = "localhost:8000"
LMS_ROOT_URL = "http://{}".format(LMS_BASE)
FEATURES['PREVIEW_LMS_BASE'] = "preview.localhost"
LOGIN_URL = EDX_ROOT_URL + '/signin'
CACHES = {
# This is the cache used for most things. Askbot will not work without a

View File

@@ -27,7 +27,6 @@
'js/factories/index',
'js/factories/manage_users',
'js/factories/outline',
'js/factories/register',
'js/factories/settings',
'js/factories/settings_advanced',
'js/factories/settings_graders',

View File

@@ -22,7 +22,6 @@ window.edx.StringUtils = StringUtils;
import './xblock/cms.runtime.v1_spec.js';
import '../../../js/spec/factories/xblock_validation_spec.js';
import '../../../js/spec/views/container_spec.js';
import '../../../js/spec/views/login_studio_spec.js';
import '../../../js/spec/views/modals/edit_xblock_spec.js';
import '../../../js/spec/views/module_edit_spec.js';
import '../../../js/spec/views/move_xblock_spec.js';

View File

@@ -1,63 +0,0 @@
'use strict';
import cookie from 'jquery.cookie';
import utility from 'utility';
import ViewUtils from 'common/js/components/utils/view_utils';
export default function LoginFactory(homepageURL) {
function postJSON(url, data, callback) {
$.ajax({
type: 'POST',
url: url,
dataType: 'json',
data: data,
success: callback
});
}
// Clear the login error message when credentials are edited
$('input#email').on('input', function () {
$('#login_error').removeClass('is-shown');
});
$('input#password').on('input', function () {
$('#login_error').removeClass('is-shown');
});
$('form#login_form').submit(function (event) {
event.preventDefault();
var $submitButton = $('#submit'),
deferred = new $.Deferred(),
promise = deferred.promise();
ViewUtils.disableElementWhileRunning($submitButton, function () { return promise; });
var submit_data = $('#login_form').serialize();
postJSON('/login_post', submit_data, function (json) {
if (json.success) {
var next = /next=([^&]*)/g.exec(decodeURIComponent(window.location.search));
if (next && next.length > 1 && !isExternal(next[1])) {
ViewUtils.redirect(next[1]);
} else {
ViewUtils.redirect(homepageURL);
}
} else if ($('#login_error').length === 0) {
$('#login_form').prepend(
'<div id="login_error" class="message message-status error">' +
json.value +
'</span></div>'
);
$('#login_error').addClass('is-shown');
deferred.resolve();
} else {
$('#login_error')
.stop()
.addClass('is-shown')
.html(json.value);
deferred.resolve();
}
});
});
};
export { LoginFactory }

View File

@@ -1,59 +0,0 @@
define(['jquery', 'jquery.cookie'], function($) {
'use strict';
return function() {
$('form :input')
.focus(function() {
$('label[for="' + this.id + '"]').addClass('is-focused');
})
.blur(function() {
$('label').removeClass('is-focused');
});
$('form#register_form').submit(function(event) {
event.preventDefault();
var submit_data = $('#register_form').serialize();
$.ajax({
url: '/create_account',
type: 'POST',
dataType: 'json',
headers: {'X-CSRFToken': $.cookie('csrftoken')},
notifyOnError: false,
data: submit_data,
success: function(json) {
location.href = '/course/';
},
error: function(jqXHR, textStatus, errorThrown) {
var json = $.parseJSON(jqXHR.responseText);
$('#register_error').html(json.value).stop().addClass('is-shown');
}
});
});
$('input#password').blur(function() {
var $formErrors = $('#password_error'),
data = {
password: $('#password').val()
};
// Uninitialize the errors on blur
$formErrors.empty();
$formErrors.addClass('hidden');
$.ajax({
url: '/api/user/v1/validation/registration',
type: 'POST',
dataType: 'json',
data: data,
success: function(json) {
_.each(json.validation_decisions, function(value, key) {
if (key === 'password' && value) {
$formErrors.html(value);
$formErrors.removeClass('hidden');
}
});
}
});
});
};
});

View File

@@ -1,35 +0,0 @@
'use strict';
import $ from 'jquery';
import LoginFactory from 'js/factories/login';
import AjaxHelpers from 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers';
import ViewUtils from 'common/js/components/utils/view_utils';
describe('Studio Login Page', () => {
var $submitButton;
beforeEach(function() {
loadFixtures('mock/login.underscore');
var login_factory = LoginFactory('/home/');
$submitButton = $('#submit');
});
it('disable the submit button once it is clicked', function() {
spyOn(ViewUtils, 'redirect').and.callFake(function() {});
var requests = AjaxHelpers.requests(this);
expect($submitButton).not.toHaveClass('is-disabled');
$submitButton.click();
AjaxHelpers.respondWithJson(requests, {success: true});
expect($submitButton).toHaveClass('is-disabled');
});
it('It will not disable the submit button if there are errors in ajax request', function() {
var requests = AjaxHelpers.requests(this);
expect($submitButton).not.toHaveClass('is-disabled');
$submitButton.click();
expect($submitButton).toHaveClass('is-disabled');
AjaxHelpers.respondWithError(requests, {});
expect($submitButton).not.toHaveClass('is-disabled');
});
});

View File

@@ -1,37 +0,0 @@
<%page expression_filter="h"/>
<%! from django.utils.translation import ugettext as _ %>
<%inherit file="base.html" />
<%block name="content">
<div class="wrapper-mast wrapper sr">
<header class="mast">
<h1 class="page-header">
${_("{studio_name} Account Activation").format(studio_name=settings.STUDIO_SHORT_NAME)}
</h1>
</header>
</div>
<div class="wrapper-content wrapper">
<section class="content activation is-active">
<article class="content-primary" role="main">
</article>
<div class="notice notice-incontext notice-instruction has-actions">
<div class="msg">
<h2 class="title">${_("Your account is already active")}</h2>
<div class="copy">
<p>${_("This account, set up using {email}, has already been activated. Please sign in to start working within {studio_name}.".format(email=user.email, studio_name=settings.STUDIO_NAME))}</p>
</div>
</div>
<ul class="list-actions">
<li class="action-item">
<a href="/signin" class="action-primary action-signin">
${_("Sign into {studio_name}").format(studio_name=settings.STUDIO_SHORT_NAME)}
</a>
</li>
</ul>
</div>
</section>
</div>
</%block>

View File

@@ -1,48 +0,0 @@
<%!
from openedx.core.djangolib.markup import Text
from django.utils.translation import ugettext as _
%>
<%page expression_filter="h"/>
<%inherit file="base.html" />
<%block name="content">
<div class="wrapper-mast wrapper sr">
<header class="mast">
<h1 class="page-header">
${Text(_("{studio_name} Account Activation")).format(
studio_name=Text(settings.STUDIO_SHORT_NAME),
)}
</h1>
</header>
</div>
<div class="wrapper-content wrapper">
<section class="content activation is-complete">
<article class="content-primary" role="main">
</article>
<div class="notice notice-incontext notice-instruction has-actions">
<div class="msg">
<h1 class="title">${_("Your account activation is complete!")}</h1>
<div class="copy">
<p>
${Text(_("Thank you for activating your account. You may now sign in and start using {studio_name} to author courses.")).format(
studio_name=Text(settings.STUDIO_NAME)
)}
</p>
</div>
</div>
<ul class="list-actions">
<li class="action-item">
<a href="/signin" class="action-primary action-signin">
${Text(_("Sign into {studio_name}")).format(
studio_name=Text(settings.STUDIO_SHORT_NAME)
)}
</a>
</li>
</ul>
</div>
</section>
</div>
</%block>

View File

@@ -1,42 +0,0 @@
<%!
from openedx.core.djangolib.markup import HTML, Text
from django.utils.translation import ugettext as _
%>
<%page expression_filter="h"/>
<%inherit file="base.html" />
<%block name="content">
<div class="wrapper-mast wrapper sr">
<header class="mast">
<h1 class="page-header">
${Text(_("{studio_name} Account Activation")).format(
studio_name=Text(settings.STUDIO_SHORT_NAME)
)}
</h1>
</header>
</div>
<div class="wrapper-content wrapper">
<section class="content activation is-invalid">
<article class="content-primary" role="main">
</article>
<div class="notice notice-incontext notice-instruction has-actions">
<div class="msg">
<h1 class="title">${_("Your account activation is invalid")}</h1>
<div class="copy">
<p>${_("We're sorry. Something went wrong with your activation. Check to make sure the URL you went to was correct, as e-mail programs will sometimes split it into two lines.")}</p>
<p>
${Text(_("If you still have issues, contact {platform_name} Support. In the meantime, you can also return to {link_start}the {studio_name} homepage.{link_end}")).format(
platform_name=Text(settings.PLATFORM_NAME),
studio_name=Text(settings.STUDIO_NAME),
link_start=HTML('<a href="/">'),
link_end=HTML('</a>')
)}
</p>
</div>
</div>
</div>
</section>
</div>
</%block>

View File

@@ -3,7 +3,7 @@
<%def name="online_help_token()"><% return "welcome" %></%def>
<%namespace name='static' file='static_content.html'/>
<%!
from django.urls import reverse
from django.conf import settings
from django.utils.translation import ugettext as _
from openedx.core.djangolib.markup import HTML, Text
%>
@@ -161,10 +161,10 @@
<ul class="list-actions">
<li class="action-item">
<a href="${reverse('signup')}" class="action action-primary">${_("Sign Up & Start Making Your {platform_name} Course").format(platform_name=settings.PLATFORM_NAME)}</a>
<a href="${settings.FRONTEND_REGISTER_URL}?next=${current_url}" class="action action-primary">${_("Sign Up & Start Making Your {platform_name} Course").format(platform_name=settings.PLATFORM_NAME)}</a>
</li>
<li class="action-item">
<a href="${reverse('login')}" class="action action-secondary">${_("Already have a {studio_name} Account? Sign In").format(studio_name=settings.STUDIO_SHORT_NAME)}</a>
<a href="${settings.LOGIN_URL}?next=${current_url}" class="action action-secondary">${_("Already have a {studio_name} Account? Sign In").format(studio_name=settings.STUDIO_SHORT_NAME)}</a>
</li>
</ul>
</section>

View File

@@ -1,12 +0,0 @@
<div class="wrapper-content wrapper">
<form id="login_form" method="post" action="login_post" onsubmit="return false;">
<input type="hidden" name="csrfmiddlewaretoken" value="csrf"/>
<input id="email" type="email" name="email" placeholder="'example: username@domain.com'"/>
<input id="password" type="password" name="password"/>
<div class="form-actions">
<button type="submit" id="submit" name="submit" class="action action-primary">Sign In</button>
</div>
<input name="honor_code" type="checkbox" value="true" checked="true" hidden="true">
</form>
</div>

View File

@@ -1,61 +0,0 @@
<%namespace name='static' file='/static_content.html'/>
<%page expression_filter="h"/>
<%inherit file="base.html" />
<%def name="online_help_token()"><% return "login" %></%def>
<%!
from django.urls import reverse
from django.utils.translation import ugettext as _
from openedx.core.djangolib.js_utils import js_escaped_string
%>
<%block name="title">${_("Sign In")}</%block>
<%block name="bodyclass">not-signedin view-signin</%block>
<%block name="content">
<div class="wrapper-content wrapper">
<section class="content">
<header>
<h1 class="title title-1">${_("Sign In to {studio_name}").format(studio_name=settings.STUDIO_NAME)}</h1>
% if static.get_value('ALLOW_PUBLIC_ACCOUNT_CREATION', settings.FEATURES.get('ALLOW_PUBLIC_ACCOUNT_CREATION')):
<a href="${reverse('signup')}" class="action action-signin">${_("Don't have a {studio_name} Account? Sign up!").format(studio_name=settings.STUDIO_SHORT_NAME)}</a>
% endif
</header>
<article class="content-primary" role="main">
<form id="login_form" method="post" action="login_post" onsubmit="return false;">
<fieldset>
<legend class="sr">${_("Required Information to Sign In to {studio_name}").format(studio_name=settings.STUDIO_NAME)}</legend>
<input type="hidden" name="csrfmiddlewaretoken" value="${ csrf }" />
<ol class="list-input">
<li class="field text required" id="field-email">
<label for="email">${_("E-mail")}</label>
<input id="email" type="email" name="email" placeholder="${_('example: username@domain.com')}"/>
</li>
<li class="field text required" id="field-password">
<label for="password">${_("Password")}</label>
<input id="password" type="password" name="password" />
<a href="${forgot_password_link}" class="action action-forgotpassword">${_("Forgot password?")}</a>
</li>
</ol>
</fieldset>
<div class="form-actions">
<button type="submit" id="submit" name="submit" class="action action-primary">${_("Sign In to {studio_name}").format(studio_name=settings.STUDIO_NAME)}</button>
</div>
<!-- no honor code for CMS, but need it because we're using the lms student object -->
<input name="honor_code" type="checkbox" value="true" checked="true" hidden="true">
</form>
</article>
</section>
</div>
</%block>
<%block name="page_bundle">
<%static:webpack entry="js/factories/login">
LoginFactory("${reverse('homepage') | n, js_escaped_string}");
</%static:webpack>
</%block>

View File

@@ -1,116 +0,0 @@
<%inherit file="base.html" />
<%def name="online_help_token()"><% return "register" %></%def>
<%!
from django.utils.translation import ugettext as _
from django.urls import reverse
%>
<%block name="title">${_("Sign Up")}</%block>
<%block name="bodyclass">not-signedin view-signup</%block>
<%block name="content">
<div class="wrapper-content wrapper">
<section class="content">
<header>
<h1 class="title title-1">${_("Sign Up for {studio_name}").format(studio_name=settings.STUDIO_NAME)}</h1>
<a href="${reverse('login')}" class="action action-signin">${_("Already have a {studio_name} Account? Sign in").format(studio_name=settings.STUDIO_SHORT_NAME)}</a>
</header>
<p class="introduction">${_("Ready to start creating online courses? Sign up below and start creating your first {platform_name} course today.").format(platform_name=settings.PLATFORM_NAME)}</p>
<article class="content-primary" role="main">
<form id="register_form" method="post">
<div id="register_error" name="register_error" class="message message-status message-status error">
</div>
<fieldset>
<legend class="sr">${_("Required Information to Sign Up for {studio_name}").format(studio_name=settings.STUDIO_NAME)}</legend>
<ol class="list-input">
<li class="field text required" id="field-email">
<label for="email">${_("E-mail")}</label>
## Translators: This is the placeholder text for a field that requests an email address.
<input id="email" type="email" name="email" placeholder="${_("example: username@domain.com")}" />
</li>
<li class="field text required" id="field-name">
<label for="name">${_("Full Name")}</label>
## Translators: This is the placeholder text for a field that requests the user's full name.
<input id="name" type="text" name="name" placeholder="${_("example: Jane Doe")}" />
</li>
<li class="field text required" id="field-username">
<label for="username">${_("Public Username")}</label>
## Translators: This is the placeholder text for a field that asks the user to pick a username
<input id="username" type="text" name="username" placeholder="${_("example: JaneDoe")}" />
<span class="tip tip-stacked">${_("This will be used in public discussions with your courses and in our edX101 support forums")}</span>
</li>
<li class="field text required" id="field-password">
<label for="password">${_("Password")}</label>
<input id="password" type="password" name="password" />
<span id="password_error" class="tip tip-error hidden" role="alert"></span>
</li>
<li class="field-group">
<div class="field text" id="field-location">
<label for="location">${_("Your Location")}</label>
<input class="short" id="location" type="text" name="location" />
</div>
<div class="field text" id="field-language">
<label for="language">${_("Preferred Language")}</label>
<input class="short" id="language" type="text" name="language" />
</div>
</li>
<li class="field checkbox required" id="field-tos">
<input id="tos" name="terms_of_service" type="checkbox" value="true" />
<label for="tos">
${_("I agree to the {a_start} Terms of Service {a_end}").format(a_start='<a data-rel="edx.org" href="{}">'.format(marketing_link('TOS')), a_end="</a>")}
</label>
</li>
</ol>
</fieldset>
<div class="form-actions">
<button type="submit" id="submit" name="submit" class="action action-primary">${_("Create My Account &amp; Start Authoring Courses")}</button>
</div>
<!-- no honor code for CMS, but need it because we're using the lms student object -->
<input name="honor_code" type="checkbox" value="true" checked="true" hidden="true">
</form>
</article>
<aside class="content-supplementary" role="complementary">
<h2 class="sr">${_("Common {studio_name} Questions").format(studio_name=settings.STUDIO_SHORT_NAME)}</h2>
<div class="bit">
<h3 class="title-3">${_("Who is {studio_name} for?").format(studio_name=settings.STUDIO_SHORT_NAME)}</h3>
<p>${_("{studio_name} is for anyone that wants to create online courses that leverage the global {platform_name} platform. Our users are often faculty members, teaching assistants and course staff, and members of instructional technology groups.").format(
studio_name=settings.STUDIO_SHORT_NAME, platform_name=settings.PLATFORM_NAME,
)}</p>
</div>
<div class="bit">
<h3 class="title-3">${_("How technically savvy do I need to be to create courses in {studio_name}?").format(studio_name=settings.STUDIO_SHORT_NAME)}</h3>
<p>${_("{studio_name} is designed to be easy to use by almost anyone familiar with common web-based authoring environments (Wordpress, Moodle, etc.). No programming knowledge is required, but for some of the more advanced features, a technical background would be helpful. As always, we are here to help, so don't hesitate to dive right in.").format(
studio_name=settings.STUDIO_SHORT_NAME,
)}</p>
</div>
<div class="bit">
<h3 class="title-3">${_("I've never authored a course online before. Is there help?")}</h3>
<p>${_("Absolutely. We have created an online course, edX101, that describes some best practices: from filming video, creating exercises, to the basics of running an online course. Additionally, we're always here to help, just drop us a note.")}</p>
</div>
</aside>
</section>
</div>
</%block>
<%block name="requirejs">
require(["js/factories/register"], function (RegisterFactory) {
RegisterFactory();
});
</%block>

View File

@@ -1,7 +1,9 @@
<%page expression_filter="h"/>
<%inherit file="../base.html" />
<%!
from django.conf import settings
from django.utils.translation import ugettext as _
from django.urls import reverse
from openedx.core.djangolib.markup import HTML, Text
%>
<%namespace name='static' file='../static_content.html'/>
@@ -23,9 +25,14 @@ from django.urls import reverse
%endif
%if user_logged_in:
${_("Visit your {link_start}dashboard{link_end} to see your courses.").format(link_start='<a href="/">', link_end='</a>')}
${Text(_("Visit your {link_start}dashboard{link_end} to see your courses.")).format(
link_start=HTML('<a href="/">'),
link_end=HTML('</a>')
)}
%else:
${_("You can now {link_start}sign in{link_end}.").format(link_start='<a href="{url}">'.format(url=reverse('login')), link_end='</a>')}
${Text(_("You can now {link_start}sign in{link_end}.")).format(
link_start=HTML('<a href="{url}">').format(url=settings.LOGIN_URL, link_end=HTML('</a>'))
)}
%endif
</p>
</section>

View File

@@ -1,3 +0,0 @@
<%! from django.utils.translation import ugettext as _ %>
<h1>Check your email</h1>
<p>${_("We've sent an email message to {email} with instructions for activating your account.").format(email=email)}</p>

View File

@@ -237,9 +237,6 @@
</nav>
% else:
<%
register_url = settings.LMS_ROOT_URL + '/register'
%>
<nav class="nav-not-signedin nav-pitch" aria-label="${_('Account')}">
<h2 class="sr-only">${_("Account Navigation")}</h2>
<ol>
@@ -248,11 +245,11 @@
</li>
% if static.get_value('ALLOW_PUBLIC_ACCOUNT_CREATION', settings.FEATURES.get('ALLOW_PUBLIC_ACCOUNT_CREATION')):
<li class="nav-item nav-not-signedin-signup">
<a class="action action-signup" href="${register_url}?next=${current_url}">${_("Sign Up")}</a>
<a class="action action-signup" href="${settings.FRONTEND_REGISTER_URL}?next=${current_url}">${_("Sign Up")}</a>
</li>
% endif
<li class="nav-item nav-not-signedin-signin">
<a class="action action-signin" href="${settings.FRONTEND_LOGIN_URL}?next=${current_url}">${_("Sign In")}</a>
<a class="action action-signin" href="${settings.LOGIN_URL}?next=${current_url}">${_("Sign In")}</a>
</li>
</ol>
</nav>

View File

@@ -4,10 +4,16 @@
from django.conf import settings
from django.urls import reverse
from django.utils.translation import ugettext as _
from edx_django_utils.monitoring import set_custom_metric
from student.roles import GlobalStaff
%>
% if uses_pattern_library:
<%!
## TODO: Use metric to see if CMS ever uses pattern library or if this case can be deleted.
## NOTE: When removing, remove all references to `set_custom_metric`.
set_custom_metric('uses_pattern_library', True)
%>
<div class="wrapper-user-menu dropdown-menu-container logged-in js-header-user-menu">
<h3 class="title menu-title">
<span class="sr-only">${_("Currently signed in as:")}</span>
@@ -26,12 +32,15 @@
</li>
</%block>
<li class="dropdown-item item has-block-link">
<a class="action action-signout" href="${reverse('logout')}">${_("Sign Out")}</a>
<a class="action action-signout" href="${settings.FRONTEND_LOGOUT_URL}">${_("Sign Out")}</a>
</li>
</ul>
</div>
% else:
<%!
set_custom_metric('uses_pattern_library', False)
%>
<h3 class="title">
<span class="label">
<span class="label-prefix sr-only">${_("Currently signed in as:")}</span>

View File

@@ -86,8 +86,6 @@ urlpatterns = [
# restful api
url(r'^$', contentstore.views.howitworks, name='homepage'),
url(r'^howitworks$', contentstore.views.howitworks, name='howitworks'),
url(r'^signup$', contentstore.views.signup, name='signup'),
url(r'^signin$', contentstore.views.login_page, name='login'),
url(r'^signin_redirect_to_lms$', contentstore.views.login_redirect_to_lms, name='login_redirect_to_lms'),
url(r'^request_course_creator$', contentstore.views.request_course_creator, name='request_course_creator'),
url(r'^course_team/{}(?:/(?P<email>.+))?$'.format(COURSELIKE_KEY_PATTERN),
@@ -180,6 +178,18 @@ urlpatterns = [
url(r'^accessibility$', contentstore.views.accessibility, name='accessibility'),
]
if not settings.DISABLE_DEPRECATED_SIGNIN_URL:
# TODO: Remove deprecated signin url when traffic proves it is no longer in use
urlpatterns += [
url(r'^signin$', contentstore.views.login_redirect_to_lms),
]
if not settings.DISABLE_DEPRECATED_SIGNUP_URL:
# TODO: Remove deprecated signup url when traffic proves it is no longer in use
urlpatterns += [
url(r'^signup$', contentstore.views.register_redirect_to_lms, name='register_redirect_to_lms'),
]
JS_INFO_DICT = {
'domain': 'djangojs',
# We need to explicitly include external Django apps that are not in LOCALE_PATHS.

View File

@@ -4,11 +4,11 @@ import unittest
import ddt
from django.conf import settings
from django.urls import reverse
from django.http import HttpResponse
from django.test import TestCase
from django.test.client import RequestFactory
from django.test.utils import override_settings
from django.urls import reverse
from edx_django_utils.cache import RequestCache
from mock import Mock, patch
@@ -25,45 +25,52 @@ class ShortcutsTests(UrlResetMixin, TestCase):
Test the edxmako shortcuts file
"""
@override_settings(MKTG_URLS={'ROOT': 'https://dummy-root', 'ABOUT': '/about-us'})
@override_settings(MKTG_URL_LINK_MAP={'ABOUT': 'login'})
def test_marketing_link(self):
# test marketing site on
with patch.dict('django.conf.settings.FEATURES', {'ENABLE_MKTG_SITE': True}):
expected_link = 'https://dummy-root/about-us'
link = marketing_link('ABOUT')
self.assertEquals(link, expected_link)
# test marketing site off
with patch.dict('django.conf.settings.FEATURES', {'ENABLE_MKTG_SITE': False}):
# we are using login because it is common across both cms and lms
expected_link = reverse('login')
link = marketing_link('ABOUT')
self.assertEquals(link, expected_link)
with override_settings(MKTG_URL_LINK_MAP={'ABOUT': self._get_test_url_name()}):
# test marketing site on
with patch.dict('django.conf.settings.FEATURES', {'ENABLE_MKTG_SITE': True}):
expected_link = 'https://dummy-root/about-us'
link = marketing_link('ABOUT')
self.assertEquals(link, expected_link)
# test marketing site off
with patch.dict('django.conf.settings.FEATURES', {'ENABLE_MKTG_SITE': False}):
expected_link = reverse(self._get_test_url_name())
link = marketing_link('ABOUT')
self.assertEquals(link, expected_link)
@override_settings(MKTG_URLS={'ROOT': 'https://dummy-root', 'ABOUT': '/about-us'})
@override_settings(MKTG_URL_LINK_MAP={'ABOUT': 'login'})
def test_is_marketing_link_set(self):
# test marketing site on
with patch.dict('django.conf.settings.FEATURES', {'ENABLE_MKTG_SITE': True}):
self.assertTrue(is_marketing_link_set('ABOUT'))
self.assertFalse(is_marketing_link_set('NOT_CONFIGURED'))
# test marketing site off
with patch.dict('django.conf.settings.FEATURES', {'ENABLE_MKTG_SITE': False}):
self.assertTrue(is_marketing_link_set('ABOUT'))
self.assertFalse(is_marketing_link_set('NOT_CONFIGURED'))
with override_settings(MKTG_URL_LINK_MAP={'ABOUT': self._get_test_url_name()}):
# test marketing site on
with patch.dict('django.conf.settings.FEATURES', {'ENABLE_MKTG_SITE': True}):
self.assertTrue(is_marketing_link_set('ABOUT'))
self.assertFalse(is_marketing_link_set('NOT_CONFIGURED'))
# test marketing site off
with patch.dict('django.conf.settings.FEATURES', {'ENABLE_MKTG_SITE': False}):
self.assertTrue(is_marketing_link_set('ABOUT'))
self.assertFalse(is_marketing_link_set('NOT_CONFIGURED'))
@override_settings(MKTG_URLS={'ROOT': 'https://dummy-root', 'ABOUT': '/about-us'})
@override_settings(MKTG_URL_LINK_MAP={'ABOUT': 'login'})
def test_is_any_marketing_link_set(self):
# test marketing site on
with patch.dict('django.conf.settings.FEATURES', {'ENABLE_MKTG_SITE': True}):
self.assertTrue(is_any_marketing_link_set(['ABOUT']))
self.assertTrue(is_any_marketing_link_set(['ABOUT', 'NOT_CONFIGURED']))
self.assertFalse(is_any_marketing_link_set(['NOT_CONFIGURED']))
# test marketing site off
with patch.dict('django.conf.settings.FEATURES', {'ENABLE_MKTG_SITE': False}):
self.assertTrue(is_any_marketing_link_set(['ABOUT']))
self.assertTrue(is_any_marketing_link_set(['ABOUT', 'NOT_CONFIGURED']))
self.assertFalse(is_any_marketing_link_set(['NOT_CONFIGURED']))
with override_settings(MKTG_URL_LINK_MAP={'ABOUT': self._get_test_url_name()}):
# test marketing site on
with patch.dict('django.conf.settings.FEATURES', {'ENABLE_MKTG_SITE': True}):
self.assertTrue(is_any_marketing_link_set(['ABOUT']))
self.assertTrue(is_any_marketing_link_set(['ABOUT', 'NOT_CONFIGURED']))
self.assertFalse(is_any_marketing_link_set(['NOT_CONFIGURED']))
# test marketing site off
with patch.dict('django.conf.settings.FEATURES', {'ENABLE_MKTG_SITE': False}):
self.assertTrue(is_any_marketing_link_set(['ABOUT']))
self.assertTrue(is_any_marketing_link_set(['ABOUT', 'NOT_CONFIGURED']))
self.assertFalse(is_any_marketing_link_set(['NOT_CONFIGURED']))
def _get_test_url_name(self):
if settings.ROOT_URLCONF == 'lms.urls':
# return any lms url name
return 'dashboard'
else:
# return any cms url name
return 'organizations'
class AddLookupTests(TestCase):

View File

@@ -10,7 +10,6 @@ from django.contrib.sessions.middleware import SessionMiddleware
from django.test import TestCase
from django.test.client import RequestFactory
from django.test.utils import override_settings
from django.urls import reverse
from mock import patch
from testfixtures import LogCapture
@@ -57,7 +56,7 @@ class TestLoginHelper(TestCase):
def test_next_failures(self, log_level, log_name, unsafe_url, http_accept, user_agent, expected_log):
""" Test unsafe next parameter """
with LogCapture(LOGGER_NAME, level=log_level) as logger:
req = self.request.get(reverse("login") + "?next={url}".format(url=unsafe_url))
req = self.request.get(settings.LOGIN_URL + "?next={url}".format(url=unsafe_url))
req.META["HTTP_ACCEPT"] = http_accept # pylint: disable=no-member
req.META["HTTP_USER_AGENT"] = user_agent # pylint: disable=no-member
get_next_url_for_login_page(req)
@@ -75,7 +74,7 @@ class TestLoginHelper(TestCase):
@override_settings(LOGIN_REDIRECT_WHITELIST=['test.edx.org', 'test2.edx.org'])
def test_safe_next(self, next_url, host):
""" Test safe next parameter """
req = self.request.get(reverse("login") + "?next={url}".format(url=next_url), HTTP_HOST=host)
req = self.request.get(settings.LOGIN_URL + "?next={url}".format(url=next_url), HTTP_HOST=host)
req.META["HTTP_ACCEPT"] = "text/html" # pylint: disable=no-member
next_page = get_next_url_for_login_page(req)
self.assertEqual(next_page, next_url)
@@ -103,7 +102,7 @@ class TestLoginHelper(TestCase):
mock_running_pipeline.return_value = running_pipeline
def validate_login():
req = self.request.get(reverse("login") + "?next={url}".format(url=next_url))
req = self.request.get(settings.LOGIN_URL + "?next={url}".format(url=next_url))
req.META["HTTP_ACCEPT"] = "text/html" # pylint: disable=no-member
self._add_session(req)
next_page = get_next_url_for_login_page(req)

View File

@@ -14,10 +14,7 @@ from django.conf import settings
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import AnonymousUser, User
from django.contrib.auth.views import password_reset_confirm
from django.contrib.sites.models import Site
from django.core import mail
from django.core.exceptions import ObjectDoesNotExist
from django.core.validators import ValidationError, validate_email
from django.db import transaction
from django.db.models.signals import post_save
@@ -25,10 +22,7 @@ from django.dispatch import Signal, receiver
from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseForbidden
from django.shortcuts import redirect
from django.template.context_processors import csrf
from django.template.response import TemplateResponse
from django.urls import reverse
from django.utils.encoding import force_bytes, force_text
from django.utils.http import base36_to_int, urlsafe_base64_encode
from django.utils.translation import ugettext as _
from django.views.decorators.csrf import csrf_exempt, ensure_csrf_cookie
from django.views.decorators.http import require_GET, require_http_methods, require_POST
@@ -53,16 +47,11 @@ from openedx.core.djangoapps.ace_common.template_context import get_base_templat
from openedx.core.djangoapps.catalog.utils import get_programs_with_type
from openedx.core.djangoapps.embargo import api as embargo_api
from openedx.core.djangoapps.lang_pref import LANGUAGE_KEY
from openedx.core.djangoapps.oauth_dispatch.api import destroy_oauth_tokens
from openedx.core.djangoapps.programs.models import ProgramsApiConfig
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.core.djangoapps.theming import helpers as theming_helpers
from openedx.core.djangoapps.theming.helpers import get_current_site
from openedx.core.djangoapps.user_api.accounts.utils import is_secondary_email_feature_enabled
from openedx.core.djangoapps.user_api.config.waffle import PREVENT_AUTH_USER_WRITES, SYSTEM_MAINTENANCE_MSG, waffle
from openedx.core.djangoapps.user_api.models import UserRetirementRequest
from openedx.core.djangoapps.user_api.preferences import api as preferences_api
from openedx.core.djangoapps.user_authn.message_types import PasswordReset
from openedx.core.djangolib.markup import HTML, Text
from student.helpers import DISABLE_UNENROLL_CERT_STATES, cert_info, generate_activation_email_context
from student.message_types import AccountActivation, EmailChange, EmailChangeConfirmation, RecoveryEmailCreate
@@ -83,10 +72,8 @@ from student.models import (
from student.signals import REFUND_ORDER
from student.tasks import send_activation_email
from student.text_me_the_app import TextMeTheAppFragmentView
from util.request_rate_limiter import BadRequestRateLimiter, PasswordResetEmailRateLimiter
from util.db import outer_atomic
from util.json_request import JsonResponse
from util.password_policy_validators import normalize_password, validate_password
from xmodule.modulestore.django import modulestore
log = logging.getLogger("edx.student")
@@ -519,8 +506,12 @@ def activate_account(request, key):
"""
# If request is in Studio call the appropriate view
if theming_helpers.get_project_root_name().lower() == u'cms':
monitoring_utils.set_custom_metric('student_activate_account', 'cms')
return activate_account_studio(request, key)
# TODO: Use metric to determine if there are any `activate_account` calls for cms in Production.
# If not, the templates wouldn't be needed for cms, but we still need a way to activate for cms tests.
monitoring_utils.set_custom_metric('student_activate_account', 'lms')
try:
registration = Registration.objects.get(activation_key=key)
except (Registration.DoesNotExist, Registration.MultipleObjectsReturned):

View File

@@ -106,7 +106,7 @@ class GoogleOauth2IntegrationTest(base.Oauth2IntegrationTest):
# Now our custom registration form creates or logs in the user:
email, password = data_parsed['user_details']['email'], 'random_password'
created_user = UserFactory(email=email, password=password)
login_response = self.client.post(reverse('login'), {'email': email, 'password': password})
login_response = self.client.post(reverse('login_api'), {'email': email, 'password': password})
self.assertEqual(login_response.status_code, 200)
# Now our custom login/registration page must resume the pipeline:

View File

@@ -157,8 +157,8 @@ class SignUpAndSignInTest(UniqueCourseTest):
Given I have opened a new course in Studio
And I am not logged in
And I visit the url "/course/slashes:MITx+999+Robot_Super_Course"
And I should see that the path is "/signin?next=/course/slashes%3AMITx%2B999%2BRobot_Super_Course"
When I fill in and submit the signin form
And I should see the path is "/signin_redirect_to_lms?next=/course/slashes%3AMITx%2B999%2BRobot_Super_Course"
When I fill in and submit the LMS login form
Then I should see that the path is "/course/slashes:MITx+999+Robot_Super_Course"
"""
self.install_course_fixture()
@@ -171,65 +171,6 @@ class SignUpAndSignInTest(UniqueCourseTest):
# Verify that correct course is displayed after sign in.
self.assertEqual(self.browser.current_url, course_url)
def test_login_with_invalid_redirect(self):
"""
Scenario: Login with an invalid redirect
Given I have opened a new course in Studio
And I am not logged in
And I visit the url "/signin?next=http://www.google.com/"
When I fill in and submit the signin form
Then I should see that the path is "/home/"
"""
self.install_course_fixture()
# Visit course
self.course_outline_sign_in_redirect_page.visit()
# Change redirect url
self.browser.get(self.browser.current_url.split('=')[0] + '=http://www.google.com')
# Login
self.course_outline_sign_in_redirect_page.login(self.user['email'], self.user['password'])
# Verify that we land in LMS instead of the invalid redirect url
self.assertEqual(self.browser.current_url, LMS_URL + "/dashboard")
def test_login_with_mistyped_credentials(self):
"""
Given I have opened a new course in Studio
And I am not logged in
And I visit the Studio homepage
When I click the link with the text "Sign In"
Then I should see that the path is "/signin"
And I should not see a login error message
And I fill in and submit the signin form incorrectly
Then I should see a login error message
And I edit the password field
Then I should not see a login error message
And I submit the signin form
And I wait for "2" seconds
Then I should see that the path is "/course/slashes:MITx+999+Robot_Super_Course"
"""
self.install_course_fixture()
self.course_outline_sign_in_redirect_page.visit()
# Verify login_error is not present
self.course_outline_sign_in_redirect_page.wait_for_element_absence(
'#login_error',
'Login error not be present'
)
# Login with wrong credentials
self.course_outline_sign_in_redirect_page.login(
self.user['email'],
'wrong_password',
expect_success=False
)
# Verify that login error is shown
self.course_outline_sign_in_redirect_page.wait_for_element_visibility(
".js-form-errors.status.submission-error",
'Login error is visible'
)
# Login with correct credentials
self.course_outline_sign_in_redirect_page.login(self.user['email'], self.user['password'])
self.course_outline_page.wait_for_page()
# Verify that correct course is displayed after sign in.
self.assertEqual(self.browser.current_url, self.course_outline_page.url)
class CoursePagesTest(StudioCourseTest):
"""

View File

@@ -1,59 +0,0 @@
<%namespace name='static' file='/static_content.html'/>
<%page expression_filter="h"/>
<%inherit file="base.html" />
<%def name="online_help_token()"><% return "login" %></%def>
<%!
from django.urls import reverse
from django.utils.translation import ugettext as _
from openedx.core.djangolib.js_utils import js_escaped_string
%>
<%block name="title">${_("Sign In")}</%block>
<%block name="bodyclass">not-signedin view-signin</%block>
<%block name="content">
<div class="wrapper-content wrapper">
<section class="content">
<header>
<h1 class="title title-1">${_("Sign In to {studio_name}").format(studio_name=settings.STUDIO_NAME)}</h1>
<a href="${reverse('signup')}" class="action action-signin">${_("Don't have a {studio_name} Account? Sign up!").format(studio_name=settings.STUDIO_SHORT_NAME)}</a>
</header>
<!-- Login Page override for test-theme. -->
<article class="content-primary" role="main">
<form id="login_form" method="post" action="login_post" onsubmit="return false;">
<fieldset>
<legend class="sr">${_("Required Information to Sign In to {studio_name}").format(studio_name=settings.STUDIO_NAME)}</legend>
<input type="hidden" name="csrfmiddlewaretoken" value="${ csrf }" />
<ol class="list-input">
<li class="field text required" id="field-email">
<label for="email">${_("E-mail")}</label>
<input id="email" type="email" name="email" placeholder="${_('example: username@domain.com')}"/>
</li>
<li class="field text required" id="field-password">
<label for="password">${_("Password")}</label>
<input id="password" type="password" name="password" />
<a href="${forgot_password_link}" class="action action-forgotpassword">${_("Forgot password?")}</a>
</li>
</ol>
</fieldset>
<div class="form-actions">
<button type="submit" id="submit" name="submit" class="action action-primary">${_("Sign In to {studio_name}").format(studio_name=settings.STUDIO_NAME)}</button>
</div>
<!-- no honor code for CMS, but need it because we're using the lms student object -->
<input name="honor_code" type="checkbox" value="true" checked="true" hidden="true">
</form>
</article>
</section>
</div>
</%block>
<%block name="page_bundle">
<%static:webpack entry="js/factories/login">
LoginFactory("${reverse('homepage') | n, js_escaped_string}");
</%static:webpack>
</%block>

View File

@@ -506,7 +506,10 @@ ENABLE_COMPREHENSIVE_THEMING = True
LMS_ROOT_URL = "http://localhost:8000"
FRONTEND_LOGOUT_URL = LMS_ROOT_URL + '/logout'
# Needed for derived settings used by cms only.
FRONTEND_LOGIN_URL = '/login'
FRONTEND_LOGOUT_URL = '/logout'
FRONTEND_REGISTER_URL = '/register'
ECOMMERCE_API_URL = 'https://ecommerce.example.com/api/v2/'
ECOMMERCE_PUBLIC_URL_ROOT = None

View File

@@ -1,6 +1,7 @@
"""Tests for cached authentication middleware."""
from __future__ import absolute_import
from django.conf import settings
from django.contrib.auth.models import User
from django.urls import reverse
from django.test import TestCase
@@ -19,7 +20,7 @@ class CachedAuthMiddlewareTestCase(TestCase):
self.user = UserFactory(password=password)
self.client.login(username=self.user.username, password=password)
def _test_change_session_hash(self, test_url, redirect_url):
def _test_change_session_hash(self, test_url, redirect_url, target_status_code=200):
"""
Verify that if a user's session auth hash and the request's hash
differ, the user is logged out. The URL to test and the
@@ -31,7 +32,7 @@ class CachedAuthMiddlewareTestCase(TestCase):
self.assertEqual(response.status_code, 200)
with patch.object(User, 'get_session_auth_hash', return_value='abc123'):
response = self.client.get(test_url)
self.assertRedirects(response, redirect_url)
self.assertRedirects(response, redirect_url, target_status_code=target_status_code)
@skip_unless_lms
def test_session_change_lms(self):
@@ -43,4 +44,5 @@ class CachedAuthMiddlewareTestCase(TestCase):
def test_session_change_cms(self):
"""Test session verification with CMS-specific URLs."""
home_url = reverse('home')
self._test_change_session_hash(home_url, reverse('login') + '?next=' + home_url)
# Studio login redirects to LMS login
self._test_change_session_hash(home_url, settings.LOGIN_URL + '?next=' + home_url, target_status_code=302)

View File

@@ -153,16 +153,12 @@ class TestHelpersLMS(TestCase):
@skip_unless_cms
class TestHelpersCMS(TestCase):
"""Test comprehensive theming helper functions."""
@with_comprehensive_theme('red-theme')
def test_get_template_path_with_theme_enabled(self):
"""
Tests template paths are returned from enabled theme.
"""
template_path = get_template_path_with_theme('login.html')
self.assertEqual(template_path, 'red-theme/cms/templates/login.html')
"""
Test comprehensive theming helper functions.
Note: There is no `test_get_template_path_with_theme_enabled` because there currently
is no template to be themed.
"""
@with_comprehensive_theme('red-theme')
def test_get_template_path_with_theme_for_missing_template(self):
"""

View File

@@ -142,32 +142,6 @@ class TestComprehensiveThemeLMS(TestCase):
self.assertContains(resp, "This is a custom template.")
@skip_unless_cms
class TestComprehensiveThemeCMS(TestCase):
"""
Test html, sass and static file overrides for comprehensive themes.
"""
def setUp(self):
"""
Clear static file finders cache and register cleanup methods.
"""
super(TestComprehensiveThemeCMS, self).setUp()
# Clear the internal staticfiles caches, to get test isolation.
staticfiles.finders.get_finder.cache_clear()
@with_comprehensive_theme("test-theme")
def test_template_override(self):
"""
Test that theme templates are used instead of default templates.
"""
resp = self.client.get('/signin')
self.assertEqual(resp.status_code, 200)
# This string comes from login.html of test-theme
self.assertContains(resp, "Login Page override for test-theme.")
@skip_unless_lms
class TestComprehensiveThemeDisabledLMS(TestCase):
"""
@@ -191,30 +165,6 @@ class TestComprehensiveThemeDisabledLMS(TestCase):
self.assertEqual(result, settings.REPO_ROOT / 'lms/static/images/logo.png')
@skip_unless_cms
class TestComprehensiveThemeDisabledCMS(TestCase):
"""
Test default html, sass and static file when no theme is applied.
"""
def setUp(self):
"""
Clear static file finders cache and register cleanup methods.
"""
super(TestComprehensiveThemeDisabledCMS, self).setUp()
# Clear the internal staticfiles caches, to get test isolation.
staticfiles.finders.get_finder.cache_clear()
def test_template_override(self):
"""
Test that defaults templates are used when no theme is applied.
"""
resp = self.client.get('/signin')
self.assertEqual(resp.status_code, 200)
self.assertNotContains(resp, "Login Page override for test-theme.")
@skip_unless_lms
class TestStanfordTheme(TestCase):
"""

View File

@@ -46,12 +46,15 @@ class TestThemingViews(TestCase):
"""
# Anonymous users get redirected to the login page
response = self.client.get(THEMING_ADMIN_URL)
# Studio login redirects to LMS login
expected_target_status_code = 200 if settings.ROOT_URLCONF == 'lms.urls' else 302
self.assertRedirects(
response,
'{login_url}?next={url}'.format(
login_url=settings.LOGIN_URL,
url=THEMING_ADMIN_URL,
)
),
target_status_code=expected_target_status_code
)
# Logged in non-global staff get a 404

View File

@@ -11,15 +11,12 @@ import six
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from django.core.validators import ValidationError, validate_email
from django.db import IntegrityError, transaction
from django.http import HttpResponseForbidden
from django.utils.translation import override as override_language
from django.utils.translation import ugettext as _
from edx_django_utils.monitoring import set_custom_metric
from pytz import UTC
from six import text_type # pylint: disable=ungrouped-imports
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.core.djangoapps.theming.helpers import get_current_request
from openedx.core.djangoapps.user_api import accounts, errors, helpers
from openedx.core.djangoapps.user_api.config.waffle import PREVENT_AUTH_USER_WRITES, SYSTEM_MAINTENANCE_MSG, waffle
from openedx.core.djangoapps.user_api.errors import (
@@ -349,6 +346,9 @@ def activate_account(activation_key):
errors.UserAPIInternalError: the operation failed due to an unexpected error.
"""
# TODO: Confirm this `activate_account` is only used for tests. If so, this should not be used for tests, and we
# should instead use the `activate_account` used for /activate.
set_custom_metric('user_api_activate_account', 'True')
if waffle().is_enabled(PREVENT_AUTH_USER_WRITES):
raise errors.UserAPIInternalError(SYSTEM_MAINTENANCE_MSG)
try:

View File

@@ -35,9 +35,7 @@ urlpatterns = [
name='registration_validation'
),
# Login
url(r'^login_post$', login.login_user, name='login_post'),
url(r'^login_ajax$', login.login_user, name="login"),
url(r'^login_ajax$', login.login_user, name="login_api"),
# Moved from user_api/legacy_urls.py
# `user_api` prefix is preserved for backwards compatibility.
@@ -64,6 +62,13 @@ urlpatterns = [
]
if not getattr(settings, 'DISABLE_DEPRECATED_LOGIN_POST', False):
# TODO: Remove login_post once it no longer has real traffic.
# It was only used by old Studio sign-in and some miscellaneous callers, which should no longer be in use.
urlpatterns += [
url(r'^login_post$', login.login_user, name='login_post'),
]
# password reset django views (see above for password reset views)
urlpatterns += [
url(

View File

@@ -73,11 +73,7 @@ class LogoutView(TemplateView):
logout(request)
# If we are using studio logout directly and there is not OIDC logouts we can just redirect the user
if settings.FEATURES.get('DISABLE_STUDIO_SSO_OVER_LMS', False) and not self.oauth_client_ids:
response = redirect(self.target)
else:
response = super(LogoutView, self).dispatch(request, *args, **kwargs)
response = super(LogoutView, self).dispatch(request, *args, **kwargs)
# Clear the cookie used by the edx.org marketing site
delete_logged_in_cookies(response)

View File

@@ -67,10 +67,7 @@ class LoginTest(SiteMixin, CacheIsolationTestCase):
self.client = Client()
cache.clear()
try:
self.url = reverse('login_post')
except NoReverseMatch:
self.url = reverse('login')
self.url = reverse('login_api')
def _create_user(self, username, user_email):
user = UserFactory.build(username=username, email=user_email)

View File

@@ -142,8 +142,8 @@ class UserAccountUpdateTest(CacheIsolationTestCase, UrlResetMixin):
self.client.logout()
# Verify that the new password can be used to log in
login_url = reverse('login_post')
response = self.client.post(login_url, {'email': self.OLD_EMAIL, 'password': self.NEW_PASSWORD})
login_api_url = reverse('login_api')
response = self.client.post(login_api_url, {'email': self.OLD_EMAIL, 'password': self.NEW_PASSWORD})
assert response.status_code == 200
response_dict = json.loads(response.content.decode('utf-8'))
assert response_dict['success']
@@ -161,7 +161,7 @@ class UserAccountUpdateTest(CacheIsolationTestCase, UrlResetMixin):
self.assertFalse(result)
# Verify that the new password continues to be valid
response = self.client.post(login_url, {'email': self.OLD_EMAIL, 'password': self.NEW_PASSWORD})
response = self.client.post(login_api_url, {'email': self.OLD_EMAIL, 'password': self.NEW_PASSWORD})
assert response.status_code == 200
response_dict = json.loads(response.content.decode('utf-8'))
assert response_dict['success']

View File

@@ -1,58 +0,0 @@
<%namespace name='static' file='/static_content.html'/>
<%page expression_filter="h"/>
<%inherit file="base.html" />
<%def name="online_help_token()"><% return "login" %></%def>
<%!
from django.urls import reverse
from django.utils.translation import ugettext as _
%>
<%block name="title">${_("Sign In")}</%block>
<%block name="bodyclass">not-signedin view-signin</%block>
<%block name="content">
<div class="wrapper-content wrapper">
<section class="content">
<header>
<h1 class="title title-1">${_("Sign In to {studio_name}").format(studio_name=settings.STUDIO_NAME)}</h1>
<a href="${reverse('signup')}" class="action action-signin">${_("Don't have a {studio_name} Account? Sign up!").format(studio_name=settings.STUDIO_SHORT_NAME)}</a>
</header>
<!-- Login Page override for red-theme. -->
<article class="content-primary" role="main">
<form id="login_form" method="post" action="login_post" onsubmit="return false;">
<fieldset>
<legend class="sr">${_("Required Information to Sign In to {studio_name}").format(studio_name=settings.STUDIO_NAME)}</legend>
<input type="hidden" name="csrfmiddlewaretoken" value="${ csrf }" />
<ol class="list-input">
<li class="field text required" id="field-email">
<label for="email">${_("E-mail")}</label>
<input id="email" type="email" name="email" placeholder="${_('example: username@domain.com')}"/>
</li>
<li class="field text required" id="field-password">
<label for="password">${_("Password")}</label>
<input id="password" type="password" name="password" />
<a href="${forgot_password_link}" class="action action-forgotpassword">${_("Forgot password?")}</a>
</li>
</ol>
</fieldset>
<div class="form-actions">
<button type="submit" id="submit" name="submit" class="action action-primary">${_("Sign In to {studio_name}").format(studio_name=settings.STUDIO_NAME)}</button>
</div>
<!-- no honor code for CMS, but need it because we're using the lms student object -->
<input name="honor_code" type="checkbox" value="true" checked="true" hidden="true">
</form>
</article>
</section>
</div>
</%block>
<%block name="page_bundle">
<%static:webpack entry="js/factories/login">
LoginFactory("${reverse('homepage') | n, js_escaped_string}");
</%static:webpack>
</%block>

View File

@@ -72,7 +72,6 @@ module.exports = Merge.smart({
// Studio
Import: './cms/static/js/features/import/factories/import.js',
CourseOrLibraryListing: './cms/static/js/features_jsx/studio/CourseOrLibraryListing.jsx',
'js/factories/login': './cms/static/js/factories/login.js',
'js/factories/textbooks': './cms/static/js/factories/textbooks.js',
'js/factories/container': './cms/static/js/factories/container.js',
'js/factories/context_course': './cms/static/js/factories/context_course.js',