From b65020ac806dba1f94d88076e792da9172630a0e Mon Sep 17 00:00:00 2001 From: mariagrimaldi Date: Thu, 19 Nov 2020 18:25:35 -0400 Subject: [PATCH] Added the ability to add custom fields using extended_profile_fields and EDNX_CUSTOM_REGISTRATION_FIELDS --- .../user_api/accounts/settings_views.py | 36 +++++- .../user_authn/views/registration_form.py | 108 ++++++++++++++++-- 2 files changed, 134 insertions(+), 10 deletions(-) diff --git a/openedx/core/djangoapps/user_api/accounts/settings_views.py b/openedx/core/djangoapps/user_api/accounts/settings_views.py index f40a0da71f4..a27190d6a34 100644 --- a/openedx/core/djangoapps/user_api/accounts/settings_views.py +++ b/openedx/core/djangoapps/user_api/accounts/settings_views.py @@ -8,6 +8,7 @@ from django.conf import settings from django.contrib import messages from django.contrib.auth.decorators import login_required +from django.core.exceptions import ImproperlyConfigured from django.shortcuts import redirect from django.urls import reverse from django.utils.translation import ugettext as _ @@ -214,6 +215,36 @@ def get_user_orders(user): return user_orders +def _get_custom_context(field_labels_map): + """eduNEXT: Used to retrieve labels and options of custom fields defined in + EDNX_CUSTOM_REGISTRATION_FIELDS. + + Returns: + A tuple with custom labels and options. + """ + field_labels = {} + field_options = {} + custom_fields = getattr(settings, "EDNX_CUSTOM_REGISTRATION_FIELDS", []) + + for field in custom_fields: + field_name = field.get("name") + + if not field_name: # Required to identify the field. + msg = "Custom fields must have a `name` defined in their configuration." + raise ImproperlyConfigured(msg) + + field_label = field.get("label") + if field_name not in field_labels_map and field_label: + field_labels[field_name] = _(field_label) # pylint: disable=translation-of-non-string + + options = field.get("options") + + if options: + field_options[field_name] = options + + return field_labels, field_options + + def _get_extended_profile_fields(): """Retrieve the extended profile fields from site configuration to be shown on the Account Settings page @@ -246,12 +277,15 @@ def _get_extended_profile_fields(): "specialty": _(u"Specialty") } + custom_labels, custom_options = _get_custom_context(field_labels_map) + field_labels_map.update(custom_labels) + extended_profile_field_names = configuration_helpers.get_value('extended_profile_fields', []) for field_to_exclude in fields_already_showing: if field_to_exclude in extended_profile_field_names: extended_profile_field_names.remove(field_to_exclude) - extended_profile_field_options = configuration_helpers.get_value('EXTRA_FIELD_OPTIONS', []) + extended_profile_field_options = configuration_helpers.get_value('EXTRA_FIELD_OPTIONS', custom_options) extended_profile_field_option_tuples = {} for field in extended_profile_field_options.keys(): field_options = extended_profile_field_options[field] diff --git a/openedx/core/djangoapps/user_authn/views/registration_form.py b/openedx/core/djangoapps/user_authn/views/registration_form.py index 4ce0a8917c2..dd0de5f8099 100644 --- a/openedx/core/djangoapps/user_authn/views/registration_form.py +++ b/openedx/core/djangoapps/user_authn/views/registration_form.py @@ -5,6 +5,7 @@ import copy from importlib import import_module +import logging import re import six @@ -37,6 +38,8 @@ validate_password, ) +log = logging.getLogger(__name__) + class TrueCheckbox(widgets.CheckboxInput): """ @@ -296,7 +299,7 @@ class RegistrationFormFactory(object): DEFAULT_FIELDS = ["email", "name", "username", "password"] - EXTRA_FIELDS = [ + EXTRA_FIELDS_BASE = [ "confirm_email", "first_name", "last_name", @@ -344,7 +347,7 @@ def __init__(self): self.field_handlers = {} 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._add_custom_field) self.field_handlers[field_name] = handler field_order = configuration_helpers.get_value('REGISTRATION_FIELD_ORDER') @@ -358,6 +361,20 @@ def __init__(self): self.field_order = field_order + @property + def EXTRA_FIELDS(self): + """eduNEXT: Property that returns extra fields list plus extended profile fields. This + property allows us to add custom fields to the registration form using extended_profile_fields. + """ + extended_profile_fields = getattr(settings, 'extended_profile_fields', []) + + # In case there's duplicates + if set(self.EXTRA_FIELDS_BASE) != set(extended_profile_fields): + difference = set(extended_profile_fields).difference(set(self.EXTRA_FIELDS_BASE)) + return self.EXTRA_FIELDS_BASE + list(difference) + + return self.EXTRA_FIELDS_BASE + extended_profile_fields + def get_registration_form(self, request): """Return a description of the registration form. This decouples clients from the API definition: @@ -425,9 +442,12 @@ def get_registration_form(self, request): 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]( + field_handler = self.field_handlers[field_name] + extra_field = {"field_name": field_name} if field_handler.__name__ == "_add_custom_field" else {} + field_handler( form_desc, - required=self._is_field_required(field_name) + required=self._is_field_required(field_name), + **extra_field ) return form_desc @@ -625,6 +645,77 @@ def _add_year_of_birth_field(self, form_desc, required=True): required=required ) + def _get_custom_field_dict(self, field_name): + """Given a field name searches for its definition dictionary. + Arguments: + field_name (str): the name of the field to search for. + """ + custom_fields = getattr(settings, "EDNX_CUSTOM_REGISTRATION_FIELDS", []) + for field in custom_fields: + if field.get("name") == field_name: + return field + return {} + + def _get_custom_html_override(self, text_field, html_piece=None): + """Overrides field with html piece. + Arguments: + text_field: field to override. It must have the following format: + "Here {} goes the HTML piece." In `{}` will be inserted the HTML piece. + + Keyword Arguments: + html_piece: string containing HTML components to be inserted. + """ + if html_piece: + html_piece = HTML(html_piece) if isinstance(html_piece, six.string_types) else "" + return Text(_(text_field)).format(html_piece) # pylint: disable=translation-of-non-string + return text_field + + def _add_custom_field(self, form_desc, required=True, **kwargs): + """Adds custom fields to a form description. + Arguments: + form_desc: A form description + + Keyword Arguments: + required (bool): Whether this field is required; defaults to False + field_name (str): Name used to get field information when creating it. + """ + field_name = kwargs.pop("field_name") + custom_field_dict = self._get_custom_field_dict(field_name) + if not custom_field_dict: + log.error("Field with name {} must have a definition dictionary.".format(field_name)) + return + + # Check to convert options: + field_options = custom_field_dict.get("options") + if field_options: + field_options = [(six.text_type(option.lower()), option) for option in field_options] + + # Set default option if applies: + default_option = custom_field_dict.get("default") + if default_option: + form_desc.override_field_properties( + field_name, + default=default_option + ) + + field_type = custom_field_dict.get("type") + + form_desc.add_field( + field_name, + label=self._get_custom_html_override( + custom_field_dict.get("label"), + custom_field_dict.get("html_override"), + ), + field_type=field_type, + options=field_options, + instructions=custom_field_dict.get("instructions"), + placeholder=custom_field_dict.get("placeholder"), + restrictions=custom_field_dict.get("restrictions"), + include_default_option=bool(default_option) or field_type == "select", + required=required, + error_messages=custom_field_dict.get("errorMessages") + ) + def _add_field_with_configurable_select_options(self, field_name, field_label, form_desc, required=False): """Add a field to a form description. If select options are given for this field, it will be a select type @@ -637,11 +728,10 @@ def _add_field_with_configurable_select_options(self, field_name, field_label, f Keyword Arguments: required (bool): Whether this field is required; defaults to False - """ - - extra_field_options = configuration_helpers.get_value('EXTRA_FIELD_OPTIONS') - if extra_field_options is None or extra_field_options.get(field_name) is None: + custom_options = self._get_custom_field_dict(field_name).get("options") + extra_field_options = configuration_helpers.get_value('EXTRA_FIELD_OPTIONS', {}) + if extra_field_options.get(field_name) is None and custom_options is None: field_type = "text" include_default_option = False options = None @@ -650,7 +740,7 @@ def _add_field_with_configurable_select_options(self, field_name, field_label, f else: field_type = "select" include_default_option = True - field_options = extra_field_options.get(field_name) + field_options = extra_field_options.get(field_name, custom_options) options = [(six.text_type(option.lower()), option) for option in field_options] error_msg = '' error_msg = getattr(accounts, u'REQUIRED_FIELD_{}_SELECT_MSG'.format(field_name.upper()))