feat: add optional-exposed extra field type to registration form
This defines optional extra fields that are not hidden under the toggle on the registration page.
This commit is contained in:
@@ -3437,7 +3437,8 @@ LOGIN_REDIRECT_WHITELIST = []
|
|||||||
# 'terms_of_service': 'hidden', 'city': 'hidden', 'country': 'hidden'}
|
# 'terms_of_service': 'hidden', 'city': 'hidden', 'country': 'hidden'}
|
||||||
# .. setting_description: The signup form may contain extra fields that are presented to every user. For every field, we
|
# .. setting_description: The signup form may contain extra fields that are presented to every user. For every field, we
|
||||||
# can specifiy whether it should be "required": to display the field, and make it mandatory; "optional": to display
|
# can specifiy whether it should be "required": to display the field, and make it mandatory; "optional": to display
|
||||||
# the field, and make it non-mandatory; "hidden": to not display the field.
|
# the optional field as part of a toggled input field list; "optional-exposed": to display the optional fields among
|
||||||
|
# the required fields, and make it non-mandatory; "hidden": to not display the field.
|
||||||
# When the terms of service are not visible and agreement to the honor code is required (the default), the signup page
|
# When the terms of service are not visible and agreement to the honor code is required (the default), the signup page
|
||||||
# includes a paragraph that links to the honor code page (defined my MKTG_URLS["HONOR"]). This page might not be
|
# includes a paragraph that links to the honor code page (defined my MKTG_URLS["HONOR"]). This page might not be
|
||||||
# available for all Open edX platforms. In such cases, the "honor_code" registration field should be "hidden".
|
# available for all Open edX platforms. In such cases, the "honor_code" registration field should be "hidden".
|
||||||
|
|||||||
@@ -88,6 +88,7 @@
|
|||||||
defaultValue: '',
|
defaultValue: '',
|
||||||
type: 'email',
|
type: 'email',
|
||||||
required: true,
|
required: true,
|
||||||
|
exposed: true,
|
||||||
instructions: 'Enter your email.',
|
instructions: 'Enter your email.',
|
||||||
restrictions: {}
|
restrictions: {}
|
||||||
},
|
},
|
||||||
@@ -98,6 +99,7 @@
|
|||||||
defaultValue: '',
|
defaultValue: '',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
required: true,
|
required: true,
|
||||||
|
exposed: true,
|
||||||
instructions: 'Enter your email.',
|
instructions: 'Enter your email.',
|
||||||
restrictions: {}
|
restrictions: {}
|
||||||
},
|
},
|
||||||
@@ -108,6 +110,7 @@
|
|||||||
defaultValue: '',
|
defaultValue: '',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
required: true,
|
required: true,
|
||||||
|
exposed: true,
|
||||||
instructions: 'Enter your username.',
|
instructions: 'Enter your username.',
|
||||||
restrictions: {}
|
restrictions: {}
|
||||||
},
|
},
|
||||||
@@ -118,6 +121,7 @@
|
|||||||
defaultValue: '',
|
defaultValue: '',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
required: true,
|
required: true,
|
||||||
|
exposed: true,
|
||||||
instructions: 'Enter your username.',
|
instructions: 'Enter your username.',
|
||||||
restrictions: {}
|
restrictions: {}
|
||||||
},
|
},
|
||||||
@@ -128,6 +132,7 @@
|
|||||||
defaultValue: '',
|
defaultValue: '',
|
||||||
type: 'password',
|
type: 'password',
|
||||||
required: true,
|
required: true,
|
||||||
|
exposed: true,
|
||||||
instructions: 'Enter your password.',
|
instructions: 'Enter your password.',
|
||||||
restrictions: {}
|
restrictions: {}
|
||||||
},
|
},
|
||||||
@@ -144,6 +149,7 @@
|
|||||||
{value: 'b', name: "Bachelor's degree"}
|
{value: 'b', name: "Bachelor's degree"}
|
||||||
],
|
],
|
||||||
required: false,
|
required: false,
|
||||||
|
exposed: false,
|
||||||
instructions: 'Select your education level.',
|
instructions: 'Select your education level.',
|
||||||
restrictions: {}
|
restrictions: {}
|
||||||
},
|
},
|
||||||
@@ -160,6 +166,7 @@
|
|||||||
{value: 'o', name: 'Other'}
|
{value: 'o', name: 'Other'}
|
||||||
],
|
],
|
||||||
required: false,
|
required: false,
|
||||||
|
exposed: false,
|
||||||
instructions: 'Select your gender.',
|
instructions: 'Select your gender.',
|
||||||
restrictions: {}
|
restrictions: {}
|
||||||
},
|
},
|
||||||
@@ -176,6 +183,7 @@
|
|||||||
{value: 2014, name: '2014'}
|
{value: 2014, name: '2014'}
|
||||||
],
|
],
|
||||||
required: false,
|
required: false,
|
||||||
|
exposed: false,
|
||||||
instructions: 'Select your year of birth.',
|
instructions: 'Select your year of birth.',
|
||||||
restrictions: {}
|
restrictions: {}
|
||||||
},
|
},
|
||||||
@@ -186,6 +194,7 @@
|
|||||||
defaultValue: '',
|
defaultValue: '',
|
||||||
type: 'textarea',
|
type: 'textarea',
|
||||||
required: false,
|
required: false,
|
||||||
|
exposed: false,
|
||||||
instructions: 'Enter your mailing address.',
|
instructions: 'Enter your mailing address.',
|
||||||
restrictions: {}
|
restrictions: {}
|
||||||
},
|
},
|
||||||
@@ -196,6 +205,7 @@
|
|||||||
defaultValue: '',
|
defaultValue: '',
|
||||||
type: 'textarea',
|
type: 'textarea',
|
||||||
required: false,
|
required: false,
|
||||||
|
exposed: false,
|
||||||
instructions: "If you'd like, tell us why you're interested in edX.",
|
instructions: "If you'd like, tell us why you're interested in edX.",
|
||||||
restrictions: {}
|
restrictions: {}
|
||||||
},
|
},
|
||||||
@@ -206,11 +216,12 @@
|
|||||||
defaultValue: '',
|
defaultValue: '',
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
required: true,
|
required: true,
|
||||||
|
exposed: true,
|
||||||
instructions: '',
|
instructions: '',
|
||||||
restrictions: {},
|
restrictions: {},
|
||||||
supplementalLink: '/honor',
|
supplementalLink: '/honor',
|
||||||
supplementalText: 'Review the Terms of Service and Honor Code'
|
supplementalText: 'Review the Terms of Service and Honor Code'
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
var createRegisterView = function(that, formFields) {
|
var createRegisterView = function(that, formFields) {
|
||||||
@@ -502,6 +513,29 @@
|
|||||||
expect(view.$submitButton).toHaveAttr('disabled');
|
expect(view.$submitButton).toHaveAttr('disabled');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('shows optional exposed fields', function() {
|
||||||
|
var formFields = FORM_DESCRIPTION.fields
|
||||||
|
formFields.push({
|
||||||
|
placeholder: '',
|
||||||
|
name: 'exposed_custom_optional_field',
|
||||||
|
label: 'Exposed custom optional field.',
|
||||||
|
defaultValue: '',
|
||||||
|
type: 'checkbox',
|
||||||
|
required: false,
|
||||||
|
exposed: true,
|
||||||
|
instructions: 'Check this field if you would like to.',
|
||||||
|
restrictions: {}
|
||||||
|
})
|
||||||
|
|
||||||
|
createRegisterView(this, formFields);
|
||||||
|
var elementClasses = view.$('.exposed-optional-fields').attr('class');
|
||||||
|
var elementChildren = view.$('.exposed-optional-fields .form-field')
|
||||||
|
// Expect the exposed optional fields container does not have other
|
||||||
|
// classes assigned, like .hidden
|
||||||
|
expect(elementClasses).toEqual('exposed-optional-fields');
|
||||||
|
expect(elementChildren.length).toEqual(1)
|
||||||
|
});
|
||||||
|
|
||||||
it('hides optional fields by default', function() {
|
it('hides optional fields by default', function() {
|
||||||
createRegisterView(this);
|
createRegisterView(this);
|
||||||
expect(view.$('.optional-fields')).toHaveClass('hidden');
|
expect(view.$('.optional-fields')).toHaveClass('hidden');
|
||||||
|
|||||||
@@ -101,7 +101,8 @@
|
|||||||
field,
|
field,
|
||||||
len = data.length,
|
len = data.length,
|
||||||
requiredFields = [],
|
requiredFields = [],
|
||||||
optionalFields = [];
|
optionalFields = [],
|
||||||
|
exposedOptionalFields = [];
|
||||||
|
|
||||||
this.fields = data;
|
this.fields = data;
|
||||||
|
|
||||||
@@ -123,12 +124,18 @@
|
|||||||
// input elements that are visible on the page.
|
// input elements that are visible on the page.
|
||||||
this.hasOptionalFields = true;
|
this.hasOptionalFields = true;
|
||||||
}
|
}
|
||||||
optionalFields.push(field);
|
|
||||||
|
if (field.exposed) {
|
||||||
|
exposedOptionalFields.push(field);
|
||||||
|
} else {
|
||||||
|
optionalFields.push(field);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
html = this.renderFields(requiredFields, 'required-fields');
|
html = this.renderFields(requiredFields, 'required-fields');
|
||||||
|
|
||||||
|
html.push.apply(html, this.renderFields(exposedOptionalFields, 'exposed-optional-fields'));
|
||||||
html.push.apply(html, this.renderFields(
|
html.push.apply(html, this.renderFields(
|
||||||
optionalFields, `optional-fields ${!this.enableCoppaCompliance ? '' : 'full-length-fields'}`
|
optionalFields, `optional-fields ${!this.enableCoppaCompliance ? '' : 'full-length-fields'}`
|
||||||
));
|
));
|
||||||
@@ -251,6 +258,14 @@
|
|||||||
$('.optional-fields').toggleClass('hidden');
|
$('.optional-fields').toggleClass('hidden');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Since the honor TOS text has a composed css selector, it is more future proof
|
||||||
|
// to insert the not toggled optional fields before .honor_tos_combined's parent
|
||||||
|
// that is the container for the honor TOS text and checkbox.
|
||||||
|
// xss-lint: disable=javascript-jquery-insert-into-target
|
||||||
|
$('.exposed-optional-fields').insertBefore(
|
||||||
|
$('.honor_tos_combined').parent()
|
||||||
|
);
|
||||||
|
|
||||||
// We are swapping the order of these elements here because the honor code agreement
|
// We are swapping the order of these elements here because the honor code agreement
|
||||||
// is a required checkbox field and the optional fields toggle is a cosmetic
|
// is a required checkbox field and the optional fields toggle is a cosmetic
|
||||||
// improvement so that we don't have to show all the optional fields.
|
// improvement so that we don't have to show all the optional fields.
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ class FormDescription:
|
|||||||
|
|
||||||
def add_field(
|
def add_field(
|
||||||
self, name, label="", field_type="text", default="",
|
self, name, label="", field_type="text", default="",
|
||||||
placeholder="", instructions="", required=True, restrictions=None,
|
placeholder="", instructions="", exposed=None, required=True, restrictions=None,
|
||||||
options=None, include_default_option=False, error_messages=None,
|
options=None, include_default_option=False, error_messages=None,
|
||||||
supplementalLink="", supplementalText=""
|
supplementalLink="", supplementalText=""
|
||||||
):
|
):
|
||||||
@@ -159,6 +159,9 @@ class FormDescription:
|
|||||||
instructions (unicode): Short instructions for using the field
|
instructions (unicode): Short instructions for using the field
|
||||||
(e.g. "This is the email address you used when you registered.")
|
(e.g. "This is the email address you used when you registered.")
|
||||||
|
|
||||||
|
exposed (boolean): Whether the field is shown if not required.
|
||||||
|
If the field is not set, the field will be visible if it's required.
|
||||||
|
|
||||||
required (boolean): Whether the field is required or optional.
|
required (boolean): Whether the field is required or optional.
|
||||||
|
|
||||||
restrictions (dict): Validation restrictions for the field.
|
restrictions (dict): Validation restrictions for the field.
|
||||||
@@ -195,6 +198,9 @@ class FormDescription:
|
|||||||
)
|
)
|
||||||
raise InvalidFieldError(msg)
|
raise InvalidFieldError(msg)
|
||||||
|
|
||||||
|
if exposed is None:
|
||||||
|
exposed = required
|
||||||
|
|
||||||
field_dict = {
|
field_dict = {
|
||||||
"name": name,
|
"name": name,
|
||||||
"label": label,
|
"label": label,
|
||||||
@@ -202,6 +208,7 @@ class FormDescription:
|
|||||||
"defaultValue": default,
|
"defaultValue": default,
|
||||||
"placeholder": placeholder,
|
"placeholder": placeholder,
|
||||||
"instructions": instructions,
|
"instructions": instructions,
|
||||||
|
"exposed": exposed,
|
||||||
"required": required,
|
"required": required,
|
||||||
"restrictions": {},
|
"restrictions": {},
|
||||||
"errorMessages": {},
|
"errorMessages": {},
|
||||||
@@ -268,6 +275,7 @@ class FormDescription:
|
|||||||
"label": "Cheese or Wine?",
|
"label": "Cheese or Wine?",
|
||||||
"defaultValue": "cheese",
|
"defaultValue": "cheese",
|
||||||
"type": "select",
|
"type": "select",
|
||||||
|
"exposed": True,
|
||||||
"required": True,
|
"required": True,
|
||||||
"placeholder": "",
|
"placeholder": "",
|
||||||
"instructions": "",
|
"instructions": "",
|
||||||
@@ -283,6 +291,7 @@ class FormDescription:
|
|||||||
"label": "comments",
|
"label": "comments",
|
||||||
"defaultValue": "",
|
"defaultValue": "",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
|
"exposed": False,
|
||||||
"required": False,
|
"required": False,
|
||||||
"placeholder": "Any comments?",
|
"placeholder": "Any comments?",
|
||||||
"instructions": "Please enter additional comments here."
|
"instructions": "Please enter additional comments here."
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ class FormDescriptionTest(TestCase):
|
|||||||
placeholder="placeholder",
|
placeholder="placeholder",
|
||||||
instructions="instructions",
|
instructions="instructions",
|
||||||
required=True,
|
required=True,
|
||||||
|
exposed=True,
|
||||||
restrictions={
|
restrictions={
|
||||||
"min_length": 2,
|
"min_length": 2,
|
||||||
"max_length": 10
|
"max_length": 10
|
||||||
@@ -110,8 +111,8 @@ class FormDescriptionTest(TestCase):
|
|||||||
json.dumps({'method': 'post',
|
json.dumps({'method': 'post',
|
||||||
'submit_url': '/submit',
|
'submit_url': '/submit',
|
||||||
'fields': [{'name': 'name', 'label': 'label', 'type': 'text', 'defaultValue': 'default',
|
'fields': [{'name': 'name', 'label': 'label', 'type': 'text', 'defaultValue': 'default',
|
||||||
'placeholder': 'placeholder', 'instructions': 'instructions', 'required': True,
|
'placeholder': 'placeholder', 'instructions': 'instructions', 'exposed': True,
|
||||||
'restrictions': {'min_length': 2, 'max_length': 10},
|
'required': True, 'restrictions': {'min_length': 2, 'max_length': 10},
|
||||||
'errorMessages': {'required': 'You must provide a value!'},
|
'errorMessages': {'required': 'You must provide a value!'},
|
||||||
'supplementalLink': '', 'supplementalText': '',
|
'supplementalLink': '', 'supplementalText': '',
|
||||||
'loginIssueSupportLink': 'https://support.example.com/login-issue-help.html'}]})
|
'loginIssueSupportLink': 'https://support.example.com/login-issue-help.html'}]})
|
||||||
|
|||||||
@@ -328,12 +328,16 @@ class RegistrationFormFactory:
|
|||||||
|
|
||||||
def _is_field_visible(self, field_name):
|
def _is_field_visible(self, field_name):
|
||||||
"""Check whether a field is visible based on Django settings. """
|
"""Check whether a field is visible based on Django settings. """
|
||||||
return self._extra_fields_setting.get(field_name) in ["required", "optional"]
|
return self._extra_fields_setting.get(field_name) in ["required", "optional", "optional-exposed"]
|
||||||
|
|
||||||
def _is_field_required(self, field_name):
|
def _is_field_required(self, field_name):
|
||||||
"""Check whether a field is required based on Django settings. """
|
"""Check whether a field is required based on Django settings. """
|
||||||
return self._extra_fields_setting.get(field_name) == "required"
|
return self._extra_fields_setting.get(field_name) == "required"
|
||||||
|
|
||||||
|
def _is_field_exposed(self, field_name):
|
||||||
|
"""Check whether a field is optional and should be toggled. """
|
||||||
|
return self._extra_fields_setting.get(field_name) in ["required", "optional-exposed"]
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|
||||||
if settings.ENABLE_COPPA_COMPLIANCE and 'year_of_birth' in self.EXTRA_FIELDS:
|
if settings.ENABLE_COPPA_COMPLIANCE and 'year_of_birth' in self.EXTRA_FIELDS:
|
||||||
@@ -441,6 +445,7 @@ class RegistrationFormFactory:
|
|||||||
FormDescription.FIELD_TYPE_MAP.get(field.__class__)),
|
FormDescription.FIELD_TYPE_MAP.get(field.__class__)),
|
||||||
placeholder=field.initial,
|
placeholder=field.initial,
|
||||||
instructions=field.help_text,
|
instructions=field.help_text,
|
||||||
|
exposed=self._is_field_exposed(field_name),
|
||||||
required=(self._is_field_required(field_name) or field.required),
|
required=(self._is_field_required(field_name) or field.required),
|
||||||
restrictions=restrictions,
|
restrictions=restrictions,
|
||||||
options=getattr(field, 'choices', None), error_messages=field.error_messages,
|
options=getattr(field, 'choices', None), error_messages=field.error_messages,
|
||||||
|
|||||||
@@ -1020,8 +1020,8 @@ class LoginSessionViewTest(ApiTestCase, OpenEdxEventsTestMixin):
|
|||||||
form_desc = json.loads(response.content.decode('utf-8'))
|
form_desc = json.loads(response.content.decode('utf-8'))
|
||||||
assert form_desc['method'] == 'post'
|
assert form_desc['method'] == 'post'
|
||||||
assert form_desc['submit_url'] == reverse('user_api_login_session', kwargs={'api_version': 'v1'})
|
assert form_desc['submit_url'] == reverse('user_api_login_session', kwargs={'api_version': 'v1'})
|
||||||
assert form_desc['fields'] == [{'name': 'email', 'defaultValue': '', 'type': 'email', 'required': True,
|
assert form_desc['fields'] == [{'name': 'email', 'defaultValue': '', 'type': 'email', 'exposed': True,
|
||||||
'label': 'Email', 'placeholder': '',
|
'required': True, 'label': 'Email', 'placeholder': '',
|
||||||
'instructions': 'The email address you used to register with {platform_name}'
|
'instructions': 'The email address you used to register with {platform_name}'
|
||||||
.format(platform_name=settings.PLATFORM_NAME),
|
.format(platform_name=settings.PLATFORM_NAME),
|
||||||
'restrictions': {'min_length': EMAIL_MIN_LENGTH,
|
'restrictions': {'min_length': EMAIL_MIN_LENGTH,
|
||||||
@@ -1033,6 +1033,7 @@ class LoginSessionViewTest(ApiTestCase, OpenEdxEventsTestMixin):
|
|||||||
{'name': 'password',
|
{'name': 'password',
|
||||||
'defaultValue': '',
|
'defaultValue': '',
|
||||||
'type': 'password',
|
'type': 'password',
|
||||||
|
'exposed': True,
|
||||||
'required': True,
|
'required': True,
|
||||||
'label': 'Password',
|
'label': 'Password',
|
||||||
'placeholder': '',
|
'placeholder': '',
|
||||||
|
|||||||
@@ -710,8 +710,8 @@ class PasswordResetViewTest(UserAPITestCase):
|
|||||||
assert form_desc['method'] == 'post'
|
assert form_desc['method'] == 'post'
|
||||||
assert form_desc['submit_url'] == reverse('password_change_request')
|
assert form_desc['submit_url'] == reverse('password_change_request')
|
||||||
assert form_desc['fields'] ==\
|
assert form_desc['fields'] ==\
|
||||||
[{'name': 'email', 'defaultValue': '', 'type': 'email', 'required': True,
|
[{'name': 'email', 'defaultValue': '', 'type': 'email', 'exposed': True,
|
||||||
'label': 'Email', 'placeholder': 'username@domain.com',
|
'required': True, 'label': 'Email', 'placeholder': 'username@domain.com',
|
||||||
'instructions': 'The email address you used to register with {platform_name}'
|
'instructions': 'The email address you used to register with {platform_name}'
|
||||||
.format(platform_name=settings.PLATFORM_NAME),
|
.format(platform_name=settings.PLATFORM_NAME),
|
||||||
'restrictions': {'min_length': EMAIL_MIN_LENGTH,
|
'restrictions': {'min_length': EMAIL_MIN_LENGTH,
|
||||||
|
|||||||
Reference in New Issue
Block a user