diff --git a/cms/djangoapps/contentstore/tests/tests.py b/cms/djangoapps/contentstore/tests/tests.py
index 5c18e04d5b..69400a008a 100644
--- a/cms/djangoapps/contentstore/tests/tests.py
+++ b/cms/djangoapps/contentstore/tests/tests.py
@@ -14,9 +14,9 @@ from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from contentstore.models import PushNotificationConfig
+from contentstore.tests.test_course_settings import CourseTestCase
from contentstore.tests.utils import parse_json, user, registration, AjaxEnabledTestClient
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
-from contentstore.tests.test_course_settings import CourseTestCase
from xmodule.modulestore.tests.factories import CourseFactory
import datetime
from pytz import UTC
@@ -302,6 +302,34 @@ class AuthTestCase(ContentStoreTestCase):
# re-request, and we should get a redirect to login page
self.assertRedirects(resp, settings.LOGIN_REDIRECT_URL + '?next=/home/')
+ @mock.patch.dict(settings.FEATURES, {"ALLOW_PUBLIC_ACCOUNT_CREATION": False})
+ def test_signup_button_index_page(self):
+ """
+ Navigate to the home page and check the Sign Up button is hidden when ALLOW_PUBLIC_ACCOUNT_CREATION flag
+ is turned off
+ """
+ response = self.client.get(reverse('homepage'))
+ self.assertNotIn('Sign Up', response.content)
+
+ @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.assertNotIn('Sign Up', response.content)
+
+ @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.assertNotIn('Don't have a Studio Account? Sign up!',
+ response.content)
+
class ForumTestCase(CourseTestCase):
def setUp(self):
diff --git a/cms/envs/common.py b/cms/envs/common.py
index 66c5c2cf31..94145d2568 100644
--- a/cms/envs/common.py
+++ b/cms/envs/common.py
@@ -222,6 +222,9 @@ FEATURES = {
# Set this to False to facilitate cleaning up invalid xml from your modulestore.
'ENABLE_XBLOCK_XML_VALIDATION': True,
+
+ # Allow public account creation
+ 'ALLOW_PUBLIC_ACCOUNT_CREATION': True,
}
ENABLE_JASMINE = False
diff --git a/cms/templates/login.html b/cms/templates/login.html
index 7d51e6d9dd..205e64f3cd 100644
--- a/cms/templates/login.html
+++ b/cms/templates/login.html
@@ -1,3 +1,4 @@
+<%namespace name='static' file='/static_content.html'/>
<%page expression_filter="h"/>
<%inherit file="base.html" />
<%def name="online_help_token()"><% return "login" %>%def>
@@ -15,7 +16,9 @@ from openedx.core.djangolib.js_utils import js_escaped_string
${_("Sign In to {studio_name}").format(studio_name=settings.STUDIO_NAME)}
diff --git a/common/djangoapps/student/tests/test_auto_auth.py b/common/djangoapps/student/tests/test_auto_auth.py
index 484ddab13c..79c847e7e6 100644
--- a/common/djangoapps/student/tests/test_auto_auth.py
+++ b/common/djangoapps/student/tests/test_auto_auth.py
@@ -9,7 +9,7 @@ from student.models import anonymous_id_for_user, CourseEnrollment, UserProfile
from util.testing import UrlResetMixin
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from opaque_keys.edx.locator import CourseLocator
-from mock import patch
+from mock import patch, Mock
import ddt
import json
@@ -261,6 +261,14 @@ class AutoAuthEnabledTestCase(AutoAuthTestCase):
return response
+ @patch("openedx.core.djangoapps.site_configuration.helpers.get_value", Mock(return_value=False))
+ def test_create_account_not_allowed(self):
+ """
+ Test case to check user creation is forbidden when ALLOW_PUBLIC_ACCOUNT_CREATION feature flag is turned off
+ """
+ response = self.client.get(self.url)
+ self.assertEqual(response.status_code, 403)
+
class AutoAuthDisabledTestCase(AutoAuthTestCase):
"""
diff --git a/common/djangoapps/student/tests/test_create_account.py b/common/djangoapps/student/tests/test_create_account.py
index 86f812ab58..a3a354e1b9 100644
--- a/common/djangoapps/student/tests/test_create_account.py
+++ b/common/djangoapps/student/tests/test_create_account.py
@@ -4,6 +4,7 @@ import json
import unittest
import ddt
+from mock import patch
from django.conf import settings
from django.contrib.auth.models import User, AnonymousUser
from django.core.urlresolvers import reverse
@@ -404,6 +405,14 @@ class TestCreateAccount(TestCase):
UserAttribute.get_user_attribute(user, REGISTRATION_UTM_CREATED_AT)
)
+ @patch("openedx.core.djangoapps.site_configuration.helpers.get_value", mock.Mock(return_value=False))
+ def test_create_account_not_allowed(self):
+ """
+ Test case to check user creation is forbidden when ALLOW_PUBLIC_ACCOUNT_CREATION feature flag is turned off
+ """
+ response = self.client.get(self.url)
+ self.assertEqual(response.status_code, 403)
+
@ddt.ddt
class TestCreateAccountValidation(TestCase):
diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py
index ca60f6fe7e..1711368197 100644
--- a/common/djangoapps/student/views.py
+++ b/common/djangoapps/student/views.py
@@ -23,6 +23,7 @@ from django.contrib.auth.views import password_reset_confirm
from django.contrib import messages
from django.core.context_processors import csrf
from django.core import mail
+from django.core.exceptions import PermissionDenied
from django.core.urlresolvers import reverse, NoReverseMatch, reverse_lazy
from django.core.validators import validate_email, ValidationError
from django.db import IntegrityError, transaction
@@ -1549,6 +1550,13 @@ def _do_create_account(form, custom_form=None):
Note: this function is also used for creating test users.
"""
+ # Check if ALLOW_PUBLIC_ACCOUNT_CREATION flag turned off to restrict user account creation
+ if not configuration_helpers.get_value(
+ 'ALLOW_PUBLIC_ACCOUNT_CREATION',
+ settings.FEATURES.get('ALLOW_PUBLIC_ACCOUNT_CREATION', True)
+ ):
+ raise PermissionDenied()
+
errors = {}
errors.update(form.errors)
if custom_form:
@@ -1970,6 +1978,13 @@ def create_account(request, post_override=None):
JSON call to create new edX account.
Used by form in signup_modal.html, which is included into navigation.html
"""
+ # Check if ALLOW_PUBLIC_ACCOUNT_CREATION flag turned off to restrict user account creation
+ if not configuration_helpers.get_value(
+ 'ALLOW_PUBLIC_ACCOUNT_CREATION',
+ settings.FEATURES.get('ALLOW_PUBLIC_ACCOUNT_CREATION', True)
+ ):
+ return HttpResponseForbidden(_("Account creation not allowed."))
+
warnings.warn("Please use RegistrationView instead.", DeprecationWarning)
try:
@@ -2074,6 +2089,8 @@ def auto_auth(request):
user.save()
profile = UserProfile.objects.get(user=user)
reg = Registration.objects.get(user=user)
+ except PermissionDenied:
+ return HttpResponseForbidden(_("Account creation not allowed."))
# Set the user's global staff bit
if is_staff is not None:
diff --git a/lms/djangoapps/student_account/test/test_views.py b/lms/djangoapps/student_account/test/test_views.py
index 7b83c16398..5ae3d7de6c 100644
--- a/lms/djangoapps/student_account/test/test_views.py
+++ b/lms/djangoapps/student_account/test/test_views.py
@@ -40,6 +40,7 @@ from openedx.core.djangoapps.programs.tests.mixins import ProgramsApiConfigMixin
from openedx.core.djangoapps.user_api.accounts.api import activate_account, create_account
from openedx.core.djangoapps.user_api.accounts import EMAIL_MAX_LENGTH
from openedx.core.djangolib.js_utils import dump_js_escaped_json
+from openedx.core.djangoapps.site_configuration.tests.mixins import SiteMixin
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase
from student.tests.factories import UserFactory
from student_account.views import account_settings_context, get_user_orders
@@ -735,3 +736,30 @@ class MicrositeLogistrationTests(TestCase):
self.assertEqual(resp.status_code, 200)
self.assertNotIn('
Register',
+ response.content)
diff --git a/lms/djangoapps/student_account/views.py b/lms/djangoapps/student_account/views.py
index 06aafdfa2e..e19c20fa12 100644
--- a/lms/djangoapps/student_account/views.py
+++ b/lms/djangoapps/student_account/views.py
@@ -124,6 +124,8 @@ def login_and_registration_form(request, initial_mode="login"):
'login_form_desc': json.loads(form_descriptions['login']),
'registration_form_desc': json.loads(form_descriptions['registration']),
'password_reset_form_desc': json.loads(form_descriptions['password_reset']),
+ 'account_creation_allowed': configuration_helpers.get_value(
+ 'ALLOW_PUBLIC_ACCOUNT_CREATION', settings.FEATURES.get('ALLOW_PUBLIC_ACCOUNT_CREATION', True))
},
'login_redirect_url': redirect_to, # This gets added to the query string of the "Sign In" button in header
'responsive': True,
diff --git a/lms/envs/common.py b/lms/envs/common.py
index d220de7e2d..4b224e106d 100644
--- a/lms/envs/common.py
+++ b/lms/envs/common.py
@@ -368,6 +368,9 @@ FEATURES = {
# Set this to False to facilitate cleaning up invalid xml from your modulestore.
'ENABLE_XBLOCK_XML_VALIDATION': True,
+
+ # Allow public account creation
+ 'ALLOW_PUBLIC_ACCOUNT_CREATION': True,
}
# Ignore static asset files on import which match this pattern
diff --git a/lms/static/js/spec/student_account/access_spec.js b/lms/static/js/spec/student_account/access_spec.js
index 66ddf2705a..afa139ac03 100644
--- a/lms/static/js/spec/student_account/access_spec.js
+++ b/lms/static/js/spec/student_account/access_spec.js
@@ -53,7 +53,7 @@
),
THIRD_PARTY_COMPLETE_URL = '/auth/complete/provider/';
- var ajaxSpyAndInitialize = function(that, mode, nextUrl, finishAuthUrl) {
+ var ajaxSpyAndInitialize = function(that, mode, nextUrl, finishAuthUrl, createAccountOption) {
var options = {
initial_mode: mode,
third_party_auth: {
@@ -66,7 +66,8 @@
platform_name: 'edX',
login_form_desc: FORM_DESCRIPTION,
registration_form_desc: FORM_DESCRIPTION,
- password_reset_form_desc: FORM_DESCRIPTION
+ password_reset_form_desc: FORM_DESCRIPTION,
+ account_creation_allowed: createAccountOption
},
$logistrationElement = $('#login-and-registration-container');
@@ -225,6 +226,20 @@
// Expect that we ignore the external URL and redirect to the dashboard
expect(view.redirect).toHaveBeenCalledWith('/dashboard');
});
+
+ it('hides create an account section', function() {
+ ajaxSpyAndInitialize(this, 'login', '', '', false);
+
+ // Expect the Create an account section is hidden
+ expect((view.$el.find('.toggle-form')).length).toEqual(0);
+ });
+
+ it('shows create an account section', function() {
+ ajaxSpyAndInitialize(this, 'login', '', '', true);
+
+ // Expect the Create an account section is visible
+ expect((view.$el.find('.toggle-form')).length).toEqual(1);
+ });
});
});
}).call(this, define || RequireJS.define);
diff --git a/lms/static/js/student_account/views/AccessView.js b/lms/static/js/student_account/views/AccessView.js
index 40ef26aaf7..424b74a35a 100644
--- a/lms/static/js/student_account/views/AccessView.js
+++ b/lms/static/js/student_account/views/AccessView.js
@@ -69,6 +69,7 @@
this.platformName = options.platform_name;
this.supportURL = options.support_link;
+ this.createAccountOption = options.account_creation_allowed;
// The login view listens for 'sync' events from the reset model
this.resetModel = new PasswordResetModel({}, {
@@ -119,7 +120,8 @@
resetModel: this.resetModel,
thirdPartyAuth: this.thirdPartyAuth,
platformName: this.platformName,
- supportURL: this.supportURL
+ supportURL: this.supportURL,
+ createAccountOption: this.createAccountOption
});
// Listen for 'password-help' event to toggle sub-views
diff --git a/lms/static/js/student_account/views/LoginView.js b/lms/static/js/student_account/views/LoginView.js
index 274b579868..3a90c91dbf 100644
--- a/lms/static/js/student_account/views/LoginView.js
+++ b/lms/static/js/student_account/views/LoginView.js
@@ -37,6 +37,7 @@
this.platformName = data.platformName;
this.resetModel = data.resetModel;
this.supportURL = data.supportURL;
+ this.createAccountOption = data.createAccountOption;
this.listenTo(this.model, 'sync', this.saveSuccess);
this.listenTo(this.resetModel, 'sync', this.resetEmail);
@@ -53,7 +54,8 @@
currentProvider: this.currentProvider,
providers: this.providers,
hasSecondaryProviders: this.hasSecondaryProviders,
- platformName: this.platformName
+ platformName: this.platformName,
+ createAccountOption: this.createAccountOption
}
}));
diff --git a/lms/templates/navigation.html b/lms/templates/navigation.html
index 27caee7d2d..44af1f7308 100644
--- a/lms/templates/navigation.html
+++ b/lms/templates/navigation.html
@@ -150,7 +150,7 @@ site_status_msg = get_site_status_msg(course_id)
- <%- _.sprintf( gettext("New to %(platformName)s?"), context ) %>
-
+<% if ( context.createAccountOption !== false ) { %>
+
+
+
+ <%- _.sprintf( gettext("New to %(platformName)s?"), context ) %>
+
+
+
-
-
+<% } %>
diff --git a/openedx/core/djangoapps/user_api/accounts/api.py b/openedx/core/djangoapps/user_api/accounts/api.py
index 890e803c3d..06c37d27ff 100644
--- a/openedx/core/djangoapps/user_api/accounts/api.py
+++ b/openedx/core/djangoapps/user_api/accounts/api.py
@@ -8,6 +8,7 @@ from pytz import UTC
from django.core.exceptions import ObjectDoesNotExist
from django.conf import settings
from django.core.validators import validate_email, validate_slug, ValidationError
+from django.http import HttpResponseForbidden
from openedx.core.djangoapps.user_api.preferences.api import update_user_preferences
from openedx.core.djangoapps.user_api.errors import PreferenceValidationError
@@ -292,6 +293,13 @@ def create_account(username, password, email):
AccountPasswordInvalid
UserAPIInternalError: the operation failed due to an unexpected error.
"""
+ # Check if ALLOW_PUBLIC_ACCOUNT_CREATION flag turned off to restrict user account creation
+ if not configuration_helpers.get_value(
+ 'ALLOW_PUBLIC_ACCOUNT_CREATION',
+ settings.FEATURES.get('ALLOW_PUBLIC_ACCOUNT_CREATION', True)
+ ):
+ return HttpResponseForbidden(_("Account creation not allowed."))
+
# Validate the username, password, and email
# This will raise an exception if any of these are not in a valid format.
_validate_username(username)
diff --git a/openedx/core/djangoapps/user_api/accounts/tests/test_api.py b/openedx/core/djangoapps/user_api/accounts/tests/test_api.py
index 514d322739..21dc1dc92b 100644
--- a/openedx/core/djangoapps/user_api/accounts/tests/test_api.py
+++ b/openedx/core/djangoapps/user_api/accounts/tests/test_api.py
@@ -442,3 +442,11 @@ class AccountCreationActivationAndPasswordChangeTest(TestCase):
return False
else:
return True
+
+ @patch("openedx.core.djangoapps.site_configuration.helpers.get_value", Mock(return_value=False))
+ def test_create_account_not_allowed(self):
+ """
+ Test case to check user creation is forbidden when ALLOW_PUBLIC_ACCOUNT_CREATION feature flag is turned off
+ """
+ response = create_account(self.USERNAME, self.PASSWORD, self.EMAIL)
+ self.assertEqual(response.status_code, 403)
diff --git a/openedx/core/djangoapps/user_api/tests/test_views.py b/openedx/core/djangoapps/user_api/tests/test_views.py
index e9496e440c..e731e110fc 100644
--- a/openedx/core/djangoapps/user_api/tests/test_views.py
+++ b/openedx/core/djangoapps/user_api/tests/test_views.py
@@ -19,6 +19,7 @@ from pytz import common_timezones_set, UTC
from social.apps.django_app.default.models import UserSocialAuth
from django_comment_common import models
+from openedx.core.djangoapps.site_configuration.helpers import get_value
from openedx.core.lib.api.test_utils import ApiTestCase, TEST_API_KEY
from openedx.core.lib.time_zone_utils import get_display_time_zone
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase, skip_unless_lms
@@ -1774,6 +1775,24 @@ class RegistrationViewTest(ThirdPartyAuthTestMixin, UserAPITestCase):
self.assertContains(response, 'Kosovo')
+ def test_create_account_not_allowed(self):
+ """
+ Test case to check user creation is forbidden when ALLOW_PUBLIC_ACCOUNT_CREATION feature flag is turned off
+ """
+ def _side_effect_for_get_value(value, default=None):
+ """
+ returns a side_effect with given return value for a given value
+ """
+ if value == 'ALLOW_PUBLIC_ACCOUNT_CREATION':
+ return False
+ else:
+ return get_value(value, default)
+
+ with mock.patch('openedx.core.djangoapps.site_configuration.helpers.get_value') as mock_get_value:
+ mock_get_value.side_effect = _side_effect_for_get_value
+ response = self.client.post(self.url, {"email": self.EMAIL, "username": self.USERNAME})
+ self.assertEqual(response.status_code, 403)
+
@httpretty.activate
@ddt.ddt
diff --git a/openedx/core/djangoapps/user_api/views.py b/openedx/core/djangoapps/user_api/views.py
index ba5a412fad..a987f5cb85 100644
--- a/openedx/core/djangoapps/user_api/views.py
+++ b/openedx/core/djangoapps/user_api/views.py
@@ -4,9 +4,9 @@ import copy
from opaque_keys import InvalidKeyError
from django.conf import settings
from django.contrib.auth.models import User
-from django.http import HttpResponse
+from django.http import HttpResponse, HttpResponseForbidden
from django.core.urlresolvers import reverse
-from django.core.exceptions import ImproperlyConfigured, NON_FIELD_ERRORS, ValidationError
+from django.core.exceptions import ImproperlyConfigured, NON_FIELD_ERRORS, ValidationError, PermissionDenied
from django.utils.translation import ugettext as _
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import ensure_csrf_cookie, csrf_protect, csrf_exempt
@@ -302,6 +302,7 @@ class RegistrationView(APIView):
HttpResponse: 400 if the request is not valid.
HttpResponse: 409 if an account with the given username or email
address already exists
+ HttpResponse: 403 operation not allowed
"""
data = request.POST.copy()
@@ -352,6 +353,8 @@ class RegistrationView(APIView):
for field, error_list in err.message_dict.items()
}
return JsonResponse(errors, status=400)
+ except PermissionDenied:
+ return HttpResponseForbidden(_("Account creation not allowed."))
response = JsonResponse({"success": True})
set_logged_in_cookies(request, response, user)
diff --git a/themes/edx.org/lms/templates/header.html b/themes/edx.org/lms/templates/header.html
index 11fdfab496..41e2be6d26 100644
--- a/themes/edx.org/lms/templates/header.html
+++ b/themes/edx.org/lms/templates/header.html
@@ -135,7 +135,7 @@ site_status_msg = get_site_status_msg(course_id)