Commit 2b48a5e7 by Brittney Exline Committed by GitHub

Merge pull request #14920 from edx/bexline/registration_email

ENT-304 Add confirm email field to registration
parents c626f1f7 c2df64c3
...@@ -168,6 +168,7 @@ AWS_SES_REGION_ENDPOINT = ENV_TOKENS.get('AWS_SES_REGION_ENDPOINT', 'email.us-ea ...@@ -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_EXTRA_FIELDS = ENV_TOKENS.get('REGISTRATION_EXTRA_FIELDS', REGISTRATION_EXTRA_FIELDS)
REGISTRATION_EXTENSION_FORM = ENV_TOKENS.get('REGISTRATION_EXTENSION_FORM', REGISTRATION_EXTENSION_FORM) REGISTRATION_EXTENSION_FORM = ENV_TOKENS.get('REGISTRATION_EXTENSION_FORM', REGISTRATION_EXTENSION_FORM)
REGISTRATION_EMAIL_PATTERNS_ALLOWED = ENV_TOKENS.get('REGISTRATION_EMAIL_PATTERNS_ALLOWED') 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 # Set the names of cookies shared with the marketing site
# These have the same cookie domain as the session, which in production # These have the same cookie domain as the session, which in production
......
...@@ -2419,6 +2419,7 @@ XDOMAIN_PROXY_CACHE_TIMEOUT = 60 * 15 ...@@ -2419,6 +2419,7 @@ XDOMAIN_PROXY_CACHE_TIMEOUT = 60 * 15
# - 'hidden': to not display the field # - 'hidden': to not display the field
REGISTRATION_EXTRA_FIELDS = { REGISTRATION_EXTRA_FIELDS = {
'confirm_email': 'hidden',
'level_of_education': 'optional', 'level_of_education': 'optional',
'gender': 'optional', 'gender': 'optional',
'year_of_birth': 'optional', 'year_of_birth': 'optional',
...@@ -2430,6 +2431,28 @@ REGISTRATION_EXTRA_FIELDS = { ...@@ -2430,6 +2431,28 @@ REGISTRATION_EXTRA_FIELDS = {
'country': 'hidden', '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 # 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). # that match a regex in this list. Set to None to allow any email (default).
REGISTRATION_EMAIL_PATTERNS_ALLOWED = None REGISTRATION_EMAIL_PATTERNS_ALLOWED = None
......
...@@ -26,6 +26,7 @@ ...@@ -26,6 +26,7 @@
year_of_birth: 2014, year_of_birth: 2014,
mailing_address: '141 Portland', mailing_address: '141 Portland',
goals: 'To boldly learn what no letter of the alphabet has learned before', goals: 'To boldly learn what no letter of the alphabet has learned before',
confirm_email: 'xsy@edx.org',
honor_code: true honor_code: true
}, },
THIRD_PARTY_AUTH = { THIRD_PARTY_AUTH = {
...@@ -62,6 +63,16 @@ ...@@ -62,6 +63,16 @@
restrictions: {} restrictions: {}
}, },
{ {
placeholder: '',
name: 'confirm_email',
label: 'Confirm Email',
defaultValue: '',
type: 'text',
required: true,
instructions: 'Enter your email.',
restrictions: {}
},
{
placeholder: 'Jane Doe', placeholder: 'Jane Doe',
name: 'name', name: 'name',
label: 'Full Name', label: 'Full Name',
...@@ -203,6 +214,7 @@ ...@@ -203,6 +214,7 @@
// Simulate manual entry of registration form data // Simulate manual entry of registration form data
$('#register-email').val(USER_DATA.email); $('#register-email').val(USER_DATA.email);
$('#register-confirm_email').val(USER_DATA.email);
$('#register-name').val(USER_DATA.name); $('#register-name').val(USER_DATA.name);
$('#register-username').val(USER_DATA.username); $('#register-username').val(USER_DATA.username);
$('#register-password').val(USER_DATA.password); $('#register-password').val(USER_DATA.password);
......
...@@ -127,6 +127,37 @@ ...@@ -127,6 +127,37 @@
jsHook: this.authWarningJsHook, jsHook: this.authWarningJsHook,
message: fullMsg 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('<li>' + $confirmEmailElement.data('errormsg-required') + '</li>');
$confirmEmailElement.addClass('error');
$label.addClass('error');
} else if (confirmEmail !== '') {
obj.confirm_email = confirmEmail;
$confirmEmailElement.removeClass('error');
$label.removeClass('error');
}
}
return obj;
} }
}); });
}); });
......
...@@ -1188,6 +1188,20 @@ class RegistrationViewTest(ThirdPartyAuthTestMixin, UserAPITestCase): ...@@ -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( @override_settings(
MKTG_URLS={"ROOT": "https://www.test.com/", "HONOR": "honor"}, MKTG_URLS={"ROOT": "https://www.test.com/", "HONOR": "honor"},
) )
...@@ -1343,6 +1357,7 @@ class RegistrationViewTest(ThirdPartyAuthTestMixin, UserAPITestCase): ...@@ -1343,6 +1357,7 @@ class RegistrationViewTest(ThirdPartyAuthTestMixin, UserAPITestCase):
"state": "optional", "state": "optional",
"country": "required", "country": "required",
"honor_code": "required", "honor_code": "required",
"confirm_email": "required",
}, },
REGISTRATION_EXTENSION_FORM='openedx.core.djangoapps.user_api.tests.test_helpers.TestCaseForm', REGISTRATION_EXTENSION_FORM='openedx.core.djangoapps.user_api.tests.test_helpers.TestCaseForm',
) )
...@@ -1360,6 +1375,123 @@ class RegistrationViewTest(ThirdPartyAuthTestMixin, UserAPITestCase): ...@@ -1360,6 +1375,123 @@ class RegistrationViewTest(ThirdPartyAuthTestMixin, UserAPITestCase):
"password", "password",
"favorite_movie", "favorite_movie",
"favorite_editor", "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", "city",
"state", "state",
"country", "country",
......
...@@ -160,6 +160,7 @@ class RegistrationView(APIView): ...@@ -160,6 +160,7 @@ class RegistrationView(APIView):
DEFAULT_FIELDS = ["email", "name", "username", "password"] DEFAULT_FIELDS = ["email", "name", "username", "password"]
EXTRA_FIELDS = [ EXTRA_FIELDS = [
"confirm_email",
"first_name", "first_name",
"last_name", "last_name",
"city", "city",
...@@ -206,10 +207,21 @@ class RegistrationView(APIView): ...@@ -206,10 +207,21 @@ class RegistrationView(APIView):
# Map field names to the instance method used to add the field to the form # Map field names to the instance method used to add the field to the form
self.field_handlers = {} 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)) handler = getattr(self, "_add_{field_name}_field".format(field_name=field_name))
self.field_handlers[field_name] = handler 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) @method_decorator(ensure_csrf_cookie)
def get(self, request): def get(self, request):
"""Return a description of the registration form. """Return a description of the registration form.
...@@ -235,14 +247,14 @@ class RegistrationView(APIView): ...@@ -235,14 +247,14 @@ class RegistrationView(APIView):
form_desc = FormDescription("post", reverse("user_api_registration")) form_desc = FormDescription("post", reverse("user_api_registration"))
self._apply_third_party_auth_overrides(request, form_desc) 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 fields can be added via the form set in settings.REGISTRATION_EXTENSION_FORM
custom_form = get_registration_extension_form() custom_form = get_registration_extension_form()
if custom_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(): for field_name, field in custom_form.fields.items():
restrictions = {} restrictions = {}
if getattr(field, 'max_length', None): if getattr(field, 'max_length', None):
...@@ -278,6 +290,16 @@ class RegistrationView(APIView): ...@@ -278,6 +290,16 @@ class RegistrationView(APIView):
form_desc, form_desc,
required=self._is_field_required(field_name) 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") return HttpResponse(form_desc.to_json(), content_type="application/json")
...@@ -386,6 +408,30 @@ class RegistrationView(APIView): ...@@ -386,6 +408,30 @@ class RegistrationView(APIView):
required=required 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): def _add_name_field(self, form_desc, required=True):
"""Add a name field to a form description. """Add a name field to a form description.
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment