From b8a2599d81d78880c8a474f1e5155e47a6f1e93f 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 | 114 ++++++++++++++++-- 2 files changed, 141 insertions(+), 9 deletions(-) diff --git a/openedx/core/djangoapps/user_api/accounts/settings_views.py b/openedx/core/djangoapps/user_api/accounts/settings_views.py index 1478313cf6f..9ce904360ba 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": _("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 04407171f10..035480fdffe 100644 --- a/openedx/core/djangoapps/user_authn/views/registration_form.py +++ b/openedx/core/djangoapps/user_authn/views/registration_form.py @@ -3,7 +3,9 @@ """ import copy +from collections import OrderedDict from importlib import import_module +import logging import re from django import forms @@ -35,6 +37,8 @@ validate_password, ) +log = logging.getLogger(__name__) + class TrueCheckbox(widgets.CheckboxInput): """ @@ -295,7 +299,7 @@ class RegistrationFormFactory: DEFAULT_FIELDS = ["email", "name", "username", "password"] - EXTRA_FIELDS = [ + EXTRA_FIELDS_BASE = [ "confirm_email", "first_name", "last_name", @@ -343,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, f"_add_{field_name}_field") + handler = getattr(self, f"_add_{field_name}_field", self._add_custom_field) self.field_handlers[field_name] = handler field_order = configuration_helpers.get_value('REGISTRATION_FIELD_ORDER') @@ -357,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 and + REGISTRATION_EXTRA_FIELDS. + """ + extended_profile_fields = [field.lower() for field in getattr(settings, 'extended_profile_fields', [])] + extra_fields = [ + field.lower() for field in configuration_helpers.get_value('REGISTRATION_EXTRA_FIELDS', {}).keys() + ] + + # Removing duplicates while mantaining order, important when running tests. + return list(OrderedDict.fromkeys(self.EXTRA_FIELDS_BASE + extended_profile_fields + extra_fields)) + def get_registration_form(self, request): """Return a description of the registration form. This decouples clients from the API definition: @@ -424,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 ) # remove confirm_email form v1 registration form if is_api_v1(request): @@ -633,6 +654,83 @@ 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").lower() == 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, str) 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") + if field_name in getattr(settings, "EDNX_IGNORE_REGISTER_FIELDS", []): + return + + 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: + if instance(field_options, dict): + field_options = [(str(value.lower()), name) for value, name in field_options.items()] + elif isinstance(field_options, list): + field_options = [(str(value.lower()), value) for value 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 @@ -647,9 +745,9 @@ def _add_field_with_configurable_select_options(self, field_name, field_label, f 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 @@ -658,7 +756,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 = [(str(option.lower()), option) for option in field_options] error_msg = '' error_msg = getattr(accounts, f'REQUIRED_FIELD_{field_name.upper()}_SELECT_MSG')