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 15, 2022
1 parent fdcc29b commit 1df9cf6
Show file tree
Hide file tree
Showing 2 changed files with 282 additions and 0 deletions.
104 changes: 104 additions & 0 deletions openedx/core/djangoapps/user_api/accounts/settings_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,16 @@
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 _
from django.views.decorators.http import require_http_methods
from django_countries import countries

from openedx_filters import PipelineStep
from openedx_filters.tooling import OpenEdxPublicFilter
from openedx_filters.exceptions import OpenEdxFilterException
from common.djangoapps import third_party_auth
from common.djangoapps.edxmako.shortcuts import render_to_response
from lms.djangoapps.commerce.models import CommerceConfiguration
Expand All @@ -39,6 +43,100 @@
log = logging.getLogger(__name__)


class AddCustomOptionsOnAccountSettings(PipelineStep):
""" Pipeline used to add custom option fields in account settings.
Example usage:
Add the following configurations to your configuration file:
"OPEN_EDX_FILTERS_CONFIG": {
"org.openedx.learning.student.settings.render.started.v1": {
"fail_silently": false,
"pipeline": [
"eox_tenant.samples.pipeline.AddCustomOptionsOnAccountSettings"
]
}
}
"""

def run_filter(self, context): # pylint: disable=arguments-differ
""" 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"]), None) # pylint: disable=line-too-long
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: # pylint: disable=line-too-long
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(OpenEdxPublicFilter):
""" Custom class used to create Account settings filters. """

filter_type = "org.openedx.learning.student.settings.render.started.v1"

class ErrorFilteringContext(OpenEdxFilterException):
"""
Custom class used catch exceptions when filtering the context.
"""

@classmethod
def run_filter(cls, context):
"""
Execute a filter with the signature specified.
Arguments:
context (dict): context for the account settings page.
"""
data = super().run_pipeline(context=context)
context = data.get("context")
return context


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

context = account_settings_context(request)

try:
context = AccountSettingsRenderStarted().run_filter(context=context)
except AccountSettingsRenderStarted.ErrorFilteringContext as exc:
raise ImproperlyConfigured(f'Pipeline configuration error: {exc}') from exc

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


Expand Down
178 changes: 178 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 All @@ -16,6 +17,9 @@
from django.utils.translation import gettext as _
from django_countries import countries

from openedx_filters import PipelineStep
from openedx_filters.tooling import OpenEdxPublicFilter
from openedx_filters.exceptions import OpenEdxFilterException
from common.djangoapps import third_party_auth
from common.djangoapps.edxmako.shortcuts import marketing_link
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
Expand All @@ -36,6 +40,174 @@
)


class AddCustomFieldsBeforeRegistration(PipelineStep):
""" Pipeline used to add custom fields to the registration form.
Example usage:
Add the following configurations to your configuration file:
"OPEN_EDX_FILTERS_CONFIG": {
"org.openedx.learning.student.registration.render.started.v1": {
"fail_silently": false,
"pipeline": [
"eox_tenant.samples.pipeline.AddCustomFieldsBeforeRegistration"
]
}
}
"""

def run_filter(self, form_desc): # pylint: disable=arguments-differ
"""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

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

return list(OrderedDict.fromkeys(extended_profile_fields))

def _is_field_required(self, field_name):
"""Check whether a field is required based on Django settings. """
_extra_fields_setting = copy.deepcopy(
configuration_helpers.get_value('REGISTRATION_EXTRA_FIELDS')
)
if not _extra_fields_setting:
_extra_fields_setting = copy.deepcopy(settings.REGISTRATION_EXTRA_FIELDS)

return _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(OpenEdxPublicFilter):
"""
Custom class used to add custom fields to the registration form.
"""

filter_type = "org.openedx.learning.student.registration.render.started.v1"

class ErrorFilteringFormDescription(OpenEdxFilterException):
"""
Custom class used catch exceptions when filtering form description.
"""

@classmethod
def run_filter(cls, form_desc):
"""
Execute a filter with the signature specified.
Arguments:
form_desc (dict): description form specifying the fields to be rendered
in the registration form.
"""
data = super().run_pipeline(form_desc=form_desc)
form_data = data.get("form_desc")
return form_data


class TrueCheckbox(widgets.CheckboxInput):
"""
A checkbox widget that only accepts "true" (case-insensitive) as true.
Expand Down Expand Up @@ -472,6 +644,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 StudentRegistrationRenderStarted.ErrorFilteringFormDescription as exc:
raise ImproperlyConfigured(f'Pipeline configuration error: {exc}') from exc

return form_desc

def _get_registration_submit_url(self, request):
Expand Down

0 comments on commit 1df9cf6

Please sign in to comment.