Skip to content

Commit

Permalink
feat: add custom field on register form
Browse files Browse the repository at this point in the history
  • Loading branch information
Henrrypg committed Nov 11, 2022
1 parent fdcc29b commit d152b75
Show file tree
Hide file tree
Showing 2 changed files with 224 additions and 1 deletion.
77 changes: 76 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 gettext as _
Expand Down Expand Up @@ -38,6 +39,75 @@

log = logging.getLogger(__name__)

class Pipeline():
""" Pipeline used to add custom options to Account Settings. """

def run_filter(self, context):
""" Run the pipeline filter. """
extended_profile_fields = context.get("extended_profile_fields", [])

custom_options, field_labels_map = self._get_custom_context(extended_profile_fields) # pylint: disable=line-too-long

extended_profile_field_options = configuration_helpers.get_value('EXTRA_FIELD_OPTIONS', custom_options) # pylint: disable=line-too-long
extended_profile_field_option_tuples = {}
for field in extended_profile_field_options.keys():
field_options = extended_profile_field_options[field]
extended_profile_field_option_tuples[field] = [(option.lower(), option) for option in field_options] # pylint: disable=line-too-long

for field in custom_options:
field_dict = {
"field_name": field,
"field_label": field_labels_map.get(field, field),
}

field_options = extended_profile_field_option_tuples.get(field)
if field_options:
field_dict["field_type"] = "ListField"
field_dict["field_options"] = field_options
else:
field_dict["field_type"] = "TextField"

field_index = next(
(index for (index, d) in enumerate(extended_profile_fields) if d["field_name"] == field_dict["field_name"]), # pylint: disable=line-too-long
None
)
if field_index is not None:
context["extended_profile_fields"][field_index] = field_dict

return context

def _get_custom_context(self, extended_profile_fields):
""" Get custom context for the field. """
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 not any(extended_field['field_name'] == \
field_name for extended_field in extended_profile_fields) 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_options, field_labels

class AccountSettingsRenderStarted():
""" Custom class used to create Account settings filters. """

def filter(self, context):
""" Run the pipeline filter. """
return Pipeline().run_filter(context)

@login_required
@require_http_methods(['GET'])
Expand Down Expand Up @@ -72,6 +142,12 @@ def account_settings(request):
return redirect(url)

context = account_settings_context(request)

try:
context = AccountSettingsRenderStarted().filter(context)
except Exception: # pylint: disable=broad-except
log.exception("Error filtering context")

return render_to_response('student_account/account_settings.html', context)


Expand Down Expand Up @@ -214,7 +290,6 @@ def get_user_orders(user):

return user_orders


def _get_extended_profile_fields():
"""Retrieve the extended profile fields from site configuration to be shown on the
Account Settings page
Expand Down
148 changes: 148 additions & 0 deletions openedx/core/djangoapps/user_authn/views/registration_form.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"""

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

Expand Down Expand Up @@ -36,6 +37,147 @@
)


class Pipeline():
""" Pipeline used to add custom fields to the registration form. """
def __init__(self):
self._extra_fields_setting = copy.deepcopy(
configuration_helpers.get_value('REGISTRATION_EXTRA_FIELDS')
)
if not self._extra_fields_setting:
self._extra_fields_setting = copy.deepcopy(settings.REGISTRATION_EXTRA_FIELDS)

def run_filter(self, form_desc):
"""Run the pipeline filter."""
extra_fields = self._get_extra_fields()

for field_name in extra_fields:
extra_field = {"field_name": field_name}
self._add_custom_field(
form_desc,
required=self._is_field_required(field_name),
**extra_field
)
fields = form_desc.fields
fields = [field['name'] for field in fields]

field_order = configuration_helpers.get_value('REGISTRATION_FIELD_ORDER')
if not field_order:
field_order = settings.REGISTRATION_FIELD_ORDER or fields

# Check that all of the fields are in the field order and vice versa,
# if not append missing fields at end of field order
if set(fields) != set(field_order):
difference = set(fields).difference(set(field_order))
field_order.extend(sorted(difference))

ordered_fields = []
for field in field_order:
for form_field in form_desc.fields:
if field == form_field['name']:
ordered_fields.append(form_field)
break

form_desc.fields = ordered_fields

return form_desc

def _get_extra_fields(self):
"""Returns the list of extra fields to include in the registration form.
Returns:
list of strings
"""
extended_profile_fields = [field.lower() for field in getattr(settings, 'extended_profile_fields', [])] # lint-amnesty, pylint: disable=line-too-long

# Removing duplicates while mantaining order, important when running tests.
return list(OrderedDict.fromkeys(extended_profile_fields))

def _is_field_required(self, field_name):
"""Check whether a field is required based on Django settings. """
return self._extra_fields_setting.get(field_name) == "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:
return

# Check to convert options:
field_options = custom_field_dict.get("options")
if isinstance(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")
)


class StudentRegistrationRenderStarted():
""" Pipeline used to add custom fields to the registration form. """

def run_filter(self, form_desc):
"""Run the pipeline filter."""

return Pipeline().run_filter(form_desc)


class TrueCheckbox(widgets.CheckboxInput):
"""
A checkbox widget that only accepts "true" (case-insensitive) as true.
Expand Down Expand Up @@ -472,6 +614,12 @@ def get_registration_form(self, request):
if field['name'] == 'confirm_email':
del form_desc.fields[index]
break

try:
form_desc = StudentRegistrationRenderStarted().run_filter(form_desc)
except Exception as exc: # pylint: disable=broad-except, disable=unused-variable
pass

return form_desc

def _get_registration_submit_url(self, request):
Expand Down

0 comments on commit d152b75

Please sign in to comment.