Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 35 additions & 1 deletion openedx/core/djangoapps/user_api/accounts/settings_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 _
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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]
Expand Down
114 changes: 106 additions & 8 deletions openedx/core/djangoapps/user_authn/views/registration_form.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
"""

import copy
from collections import OrderedDict
from importlib import import_module
import logging
import re

from django import forms
Expand Down Expand Up @@ -35,6 +37,8 @@
validate_password,
)

log = logging.getLogger(__name__)


class TrueCheckbox(widgets.CheckboxInput):
"""
Expand Down Expand Up @@ -295,7 +299,7 @@ class RegistrationFormFactory:

DEFAULT_FIELDS = ["email", "name", "username", "password"]

EXTRA_FIELDS = [
EXTRA_FIELDS_BASE = [
"confirm_email",
"first_name",
"last_name",
Expand Down Expand Up @@ -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')
Expand All @@ -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:
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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')
Expand Down