diff --git a/lms/envs/aws.py b/lms/envs/aws.py
index a88fcd1873..d786a3e87b 100644
--- a/lms/envs/aws.py
+++ b/lms/envs/aws.py
@@ -168,6 +168,7 @@ AWS_SES_REGION_ENDPOINT = ENV_TOKENS.get('AWS_SES_REGION_ENDPOINT', 'email.us-ea
REGISTRATION_EXTRA_FIELDS = ENV_TOKENS.get('REGISTRATION_EXTRA_FIELDS', REGISTRATION_EXTRA_FIELDS)
REGISTRATION_EXTENSION_FORM = ENV_TOKENS.get('REGISTRATION_EXTENSION_FORM', REGISTRATION_EXTENSION_FORM)
REGISTRATION_EMAIL_PATTERNS_ALLOWED = ENV_TOKENS.get('REGISTRATION_EMAIL_PATTERNS_ALLOWED')
+REGISTRATION_FIELD_ORDER = ENV_TOKENS.get('REGISTRATION_FIELD_ORDER', REGISTRATION_FIELD_ORDER)
# Set the names of cookies shared with the marketing site
# These have the same cookie domain as the session, which in production
diff --git a/lms/envs/common.py b/lms/envs/common.py
index b8f3a97e7e..b5215f9864 100644
--- a/lms/envs/common.py
+++ b/lms/envs/common.py
@@ -2419,6 +2419,7 @@ XDOMAIN_PROXY_CACHE_TIMEOUT = 60 * 15
# - 'hidden': to not display the field
REGISTRATION_EXTRA_FIELDS = {
+ 'confirm_email': 'hidden',
'level_of_education': 'optional',
'gender': 'optional',
'year_of_birth': 'optional',
@@ -2430,6 +2431,28 @@ REGISTRATION_EXTRA_FIELDS = {
'country': 'hidden',
}
+REGISTRATION_FIELD_ORDER = [
+ "email",
+ "confirm_email",
+ "name",
+ "username",
+ "password",
+ "first_name",
+ "last_name",
+ "city",
+ "state",
+ "country",
+ "gender",
+ "year_of_birth",
+ "level_of_education",
+ "company",
+ "title",
+ "mailing_address",
+ "goals",
+ "honor_code",
+ "terms_of_service",
+]
+
# Optional setting to restrict registration / account creation to only emails
# that match a regex in this list. Set to None to allow any email (default).
REGISTRATION_EMAIL_PATTERNS_ALLOWED = None
diff --git a/lms/static/js/spec/student_account/register_spec.js b/lms/static/js/spec/student_account/register_spec.js
index 3947aceee8..f32db1b0de 100644
--- a/lms/static/js/spec/student_account/register_spec.js
+++ b/lms/static/js/spec/student_account/register_spec.js
@@ -26,6 +26,7 @@
year_of_birth: 2014,
mailing_address: '141 Portland',
goals: 'To boldly learn what no letter of the alphabet has learned before',
+ confirm_email: 'xsy@edx.org',
honor_code: true
},
THIRD_PARTY_AUTH = {
@@ -61,6 +62,16 @@
instructions: 'Enter your email.',
restrictions: {}
},
+ {
+ placeholder: '',
+ name: 'confirm_email',
+ label: 'Confirm Email',
+ defaultValue: '',
+ type: 'text',
+ required: true,
+ instructions: 'Enter your email.',
+ restrictions: {}
+ },
{
placeholder: 'Jane Doe',
name: 'name',
@@ -203,6 +214,7 @@
// Simulate manual entry of registration form data
$('#register-email').val(USER_DATA.email);
+ $('#register-confirm_email').val(USER_DATA.email);
$('#register-name').val(USER_DATA.name);
$('#register-username').val(USER_DATA.username);
$('#register-password').val(USER_DATA.password);
diff --git a/lms/static/js/student_account/views/RegisterView.js b/lms/static/js/student_account/views/RegisterView.js
index db54ce14f7..3d47fbdefe 100644
--- a/lms/static/js/student_account/views/RegisterView.js
+++ b/lms/static/js/student_account/views/RegisterView.js
@@ -127,6 +127,37 @@
jsHook: this.authWarningJsHook,
message: fullMsg
});
+ },
+
+ getFormData: function() {
+ var obj = FormView.prototype.getFormData.apply(this, arguments),
+ $form = this.$form,
+ $label,
+ $emailElement,
+ $confirmEmailElement,
+ email = '',
+ confirmEmail = '';
+
+ $emailElement = $form.find('input[name=email]');
+ $confirmEmailElement = $form.find('input[name=confirm_email]');
+
+ if ($confirmEmailElement.length) {
+ email = $emailElement.val();
+ confirmEmail = $confirmEmailElement.val();
+ $label = $form.find('label[for=' + $confirmEmailElement.attr('id') + ']');
+
+ if (confirmEmail !== '' && email !== confirmEmail) {
+ this.errors.push('
' + $confirmEmailElement.data('errormsg-required') + '');
+ $confirmEmailElement.addClass('error');
+ $label.addClass('error');
+ } else if (confirmEmail !== '') {
+ obj.confirm_email = confirmEmail;
+ $confirmEmailElement.removeClass('error');
+ $label.removeClass('error');
+ }
+ }
+
+ return obj;
}
});
});
diff --git a/openedx/core/djangoapps/user_api/tests/test_views.py b/openedx/core/djangoapps/user_api/tests/test_views.py
index 1aee8ecd9f..10f0fa2685 100644
--- a/openedx/core/djangoapps/user_api/tests/test_views.py
+++ b/openedx/core/djangoapps/user_api/tests/test_views.py
@@ -1188,6 +1188,20 @@ class RegistrationViewTest(ThirdPartyAuthTestMixin, UserAPITestCase):
}
)
+ def test_registration_form_confirm_email(self):
+ self._assert_reg_field(
+ {"confirm_email": "required"},
+ {
+ "name": "confirm_email",
+ "type": "text",
+ "required": True,
+ "label": "Confirm Email",
+ "errorMessages": {
+ "required": "Please confirm your email.",
+ }
+ }
+ )
+
@override_settings(
MKTG_URLS={"ROOT": "https://www.test.com/", "HONOR": "honor"},
)
@@ -1343,6 +1357,7 @@ class RegistrationViewTest(ThirdPartyAuthTestMixin, UserAPITestCase):
"state": "optional",
"country": "required",
"honor_code": "required",
+ "confirm_email": "required",
},
REGISTRATION_EXTENSION_FORM='openedx.core.djangoapps.user_api.tests.test_helpers.TestCaseForm',
)
@@ -1360,6 +1375,123 @@ class RegistrationViewTest(ThirdPartyAuthTestMixin, UserAPITestCase):
"password",
"favorite_movie",
"favorite_editor",
+ "confirm_email",
+ "city",
+ "state",
+ "country",
+ "gender",
+ "year_of_birth",
+ "level_of_education",
+ "mailing_address",
+ "goals",
+ "honor_code",
+ ])
+
+ @override_settings(
+ REGISTRATION_EXTRA_FIELDS={
+ "level_of_education": "optional",
+ "gender": "optional",
+ "year_of_birth": "optional",
+ "mailing_address": "optional",
+ "goals": "optional",
+ "city": "optional",
+ "state": "optional",
+ "country": "required",
+ "honor_code": "required",
+ "confirm_email": "required",
+ },
+ REGISTRATION_FIELD_ORDER=[
+ "name",
+ "username",
+ "email",
+ "confirm_email",
+ "password",
+ "first_name",
+ "last_name",
+ "city",
+ "state",
+ "country",
+ "gender",
+ "year_of_birth",
+ "level_of_education",
+ "company",
+ "title",
+ "mailing_address",
+ "goals",
+ "honor_code",
+ "terms_of_service",
+ ],
+ )
+ def test_field_order_override(self):
+ response = self.client.get(self.url)
+ self.assertHttpOK(response)
+
+ # Verify that all fields render in the correct order
+ form_desc = json.loads(response.content)
+ field_names = [field["name"] for field in form_desc["fields"]]
+ self.assertEqual(field_names, [
+ "name",
+ "username",
+ "email",
+ "confirm_email",
+ "password",
+ "city",
+ "state",
+ "country",
+ "gender",
+ "year_of_birth",
+ "level_of_education",
+ "mailing_address",
+ "goals",
+ "honor_code",
+ ])
+
+ @override_settings(
+ REGISTRATION_EXTRA_FIELDS={
+ "level_of_education": "optional",
+ "gender": "optional",
+ "year_of_birth": "optional",
+ "mailing_address": "optional",
+ "goals": "optional",
+ "city": "optional",
+ "state": "optional",
+ "country": "required",
+ "honor_code": "required",
+ "confirm_email": "required",
+ },
+ REGISTRATION_EXTENSION_FORM='openedx.core.djangoapps.user_api.tests.test_helpers.TestCaseForm',
+ REGISTRATION_FIELD_ORDER=[
+ "name",
+ "confirm_email",
+ "password",
+ "first_name",
+ "last_name",
+ "gender",
+ "year_of_birth",
+ "level_of_education",
+ "company",
+ "title",
+ "mailing_address",
+ "goals",
+ "honor_code",
+ "terms_of_service",
+ ],
+ )
+ def test_field_order_invalid_override(self):
+ response = self.client.get(self.url)
+ self.assertHttpOK(response)
+
+ # Verify that all fields render in the correct order
+ form_desc = json.loads(response.content)
+ field_names = [field["name"] for field in form_desc["fields"]]
+ self.assertEqual(field_names, [
+ "email",
+ "name",
+ "username",
+ "password",
+ "favorite_movie",
+ "favorite_editor",
+ "confirm_email",
"city",
"state",
"country",
diff --git a/openedx/core/djangoapps/user_api/views.py b/openedx/core/djangoapps/user_api/views.py
index 5da932dad6..0627bf7cc4 100644
--- a/openedx/core/djangoapps/user_api/views.py
+++ b/openedx/core/djangoapps/user_api/views.py
@@ -160,6 +160,7 @@ class RegistrationView(APIView):
DEFAULT_FIELDS = ["email", "name", "username", "password"]
EXTRA_FIELDS = [
+ "confirm_email",
"first_name",
"last_name",
"city",
@@ -206,10 +207,21 @@ class RegistrationView(APIView):
# Map field names to the instance method used to add the field to the form
self.field_handlers = {}
- for field_name in self.DEFAULT_FIELDS + self.EXTRA_FIELDS:
+ valid_fields = self.DEFAULT_FIELDS + self.EXTRA_FIELDS
+ for field_name in valid_fields:
handler = getattr(self, "_add_{field_name}_field".format(field_name=field_name))
self.field_handlers[field_name] = handler
+ field_order = configuration_helpers.get_value('REGISTRATION_FIELD_ORDER')
+ if not field_order:
+ field_order = settings.REGISTRATION_FIELD_ORDER or valid_fields
+
+ # Check that all of the valid_fields are in the field order and vice versa, if not set to the default order
+ if set(valid_fields) != set(field_order):
+ field_order = valid_fields
+
+ self.field_order = field_order
+
@method_decorator(ensure_csrf_cookie)
def get(self, request):
"""Return a description of the registration form.
@@ -235,14 +247,14 @@ class RegistrationView(APIView):
form_desc = FormDescription("post", reverse("user_api_registration"))
self._apply_third_party_auth_overrides(request, form_desc)
- # Default fields are always required
- for field_name in self.DEFAULT_FIELDS:
- self.field_handlers[field_name](form_desc, required=True)
-
# Custom form fields can be added via the form set in settings.REGISTRATION_EXTENSION_FORM
custom_form = get_registration_extension_form()
if custom_form:
+ # Default fields are always required
+ for field_name in self.DEFAULT_FIELDS:
+ self.field_handlers[field_name](form_desc, required=True)
+
for field_name, field in custom_form.fields.items():
restrictions = {}
if getattr(field, 'max_length', None):
@@ -270,14 +282,24 @@ class RegistrationView(APIView):
include_default_option=field_options.get('include_default_option'),
)
- # Extra fields configured in Django settings
- # may be required, optional, or hidden
- for field_name in self.EXTRA_FIELDS:
- if self._is_field_visible(field_name):
- self.field_handlers[field_name](
- form_desc,
- required=self._is_field_required(field_name)
- )
+ # Extra fields configured in Django settings
+ # may be required, optional, or hidden
+ for field_name in self.EXTRA_FIELDS:
+ if self._is_field_visible(field_name):
+ self.field_handlers[field_name](
+ form_desc,
+ required=self._is_field_required(field_name)
+ )
+ else:
+ # Go through the fields in the fields order and add them if they are required or visible
+ for field_name in self.field_order:
+ if field_name in self.DEFAULT_FIELDS:
+ self.field_handlers[field_name](form_desc, required=True)
+ elif self._is_field_visible(field_name):
+ self.field_handlers[field_name](
+ form_desc,
+ required=self._is_field_required(field_name)
+ )
return HttpResponse(form_desc.to_json(), content_type="application/json")
@@ -386,6 +408,30 @@ class RegistrationView(APIView):
required=required
)
+ def _add_confirm_email_field(self, form_desc, required=True):
+ """Add an email confirmation field to a form description.
+
+ Arguments:
+ form_desc: A form description
+
+ Keyword Arguments:
+ required (bool): Whether this field is required; defaults to True
+
+ """
+ # Translators: This label appears above a field on the registration form
+ # meant to confirm the user's email address.
+ email_label = _(u"Confirm Email")
+ error_msg = _(u"Please confirm your email.")
+
+ form_desc.add_field(
+ "confirm_email",
+ label=email_label,
+ required=required,
+ error_messages={
+ "required": error_msg
+ }
+ )
+
def _add_name_field(self, form_desc, required=True):
"""Add a name field to a form description.