mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-05-30 01:10:04 +02:00
Generalize BaseYesNoForm + centralize logic into helper
This commit is important for two reasons: 1. It keeps things consistent in our code base 2. It moves the ever growing list of wizard base classes into a consistent location
This commit is contained in:
parent
1fa44a61f1
commit
d35fabcbce
2 changed files with 249 additions and 215 deletions
|
@ -1,15 +1,13 @@
|
||||||
from __future__ import annotations # allows forward references in annotations
|
from __future__ import annotations # allows forward references in annotations
|
||||||
from itertools import zip_longest
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Callable
|
|
||||||
from api.views import DOMAIN_API_MESSAGES
|
from api.views import DOMAIN_API_MESSAGES
|
||||||
from phonenumber_field.formfields import PhoneNumberField # type: ignore
|
from phonenumber_field.formfields import PhoneNumberField # type: ignore
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.core.validators import RegexValidator, MaxLengthValidator
|
from django.core.validators import RegexValidator, MaxLengthValidator
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.db.models.fields.related import ForeignObjectRel
|
|
||||||
|
|
||||||
|
from registrar.forms.utility.wizard_form_helper import RegistrarForm, RegistrarFormSet, BaseYesNoForm
|
||||||
from registrar.models import Contact, DomainRequest, DraftDomain, Domain
|
from registrar.models import Contact, DomainRequest, DraftDomain, Domain
|
||||||
from registrar.templatetags.url_helpers import public_site_url
|
from registrar.templatetags.url_helpers import public_site_url
|
||||||
from registrar.utility.enums import ValidationReturnType
|
from registrar.utility.enums import ValidationReturnType
|
||||||
|
@ -17,157 +15,6 @@ from registrar.utility.enums import ValidationReturnType
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class RegistrarForm(forms.Form):
|
|
||||||
"""
|
|
||||||
A common set of methods and configuration.
|
|
||||||
|
|
||||||
The registrar's domain request is several pages of "steps".
|
|
||||||
Each step is an HTML form containing one or more Django "forms".
|
|
||||||
|
|
||||||
Subclass this class to create new forms.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
kwargs.setdefault("label_suffix", "")
|
|
||||||
# save a reference to a domain request object
|
|
||||||
self.domain_request = kwargs.pop("domain_request", None)
|
|
||||||
super(RegistrarForm, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def to_database(self, obj: DomainRequest | Contact):
|
|
||||||
"""
|
|
||||||
Adds this form's cleaned data to `obj` and saves `obj`.
|
|
||||||
|
|
||||||
Does nothing if form is not valid.
|
|
||||||
"""
|
|
||||||
if not self.is_valid():
|
|
||||||
return
|
|
||||||
for name, value in self.cleaned_data.items():
|
|
||||||
setattr(obj, name, value)
|
|
||||||
obj.save()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_database(cls, obj: DomainRequest | Contact | None):
|
|
||||||
"""Returns a dict of form field values gotten from `obj`."""
|
|
||||||
if obj is None:
|
|
||||||
return {}
|
|
||||||
return {name: getattr(obj, name) for name in cls.declared_fields.keys()} # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
class RegistrarFormSet(forms.BaseFormSet):
|
|
||||||
"""
|
|
||||||
As with RegistrarForm, a common set of methods and configuration.
|
|
||||||
|
|
||||||
Subclass this class to create new formsets.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
# save a reference to an domain_request object
|
|
||||||
self.domain_request = kwargs.pop("domain_request", None)
|
|
||||||
super(RegistrarFormSet, self).__init__(*args, **kwargs)
|
|
||||||
# quick workaround to ensure that the HTML `required`
|
|
||||||
# attribute shows up on required fields for any forms
|
|
||||||
# in the formset which have data already (stated another
|
|
||||||
# way: you can leave a form in the formset blank, but
|
|
||||||
# if you opt to fill it out, you must fill it out _right_)
|
|
||||||
for index in range(self.initial_form_count()):
|
|
||||||
self.forms[index].use_required_attribute = True
|
|
||||||
|
|
||||||
def should_delete(self, cleaned):
|
|
||||||
"""Should this entry be deleted from the database?"""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def pre_update(self, db_obj, cleaned):
|
|
||||||
"""Code to run before an item in the formset is saved."""
|
|
||||||
for key, value in cleaned.items():
|
|
||||||
setattr(db_obj, key, value)
|
|
||||||
|
|
||||||
def pre_create(self, db_obj, cleaned):
|
|
||||||
"""Code to run before an item in the formset is created in the database."""
|
|
||||||
return cleaned
|
|
||||||
|
|
||||||
def to_database(self, obj: DomainRequest):
|
|
||||||
"""
|
|
||||||
Adds this form's cleaned data to `obj` and saves `obj`.
|
|
||||||
|
|
||||||
Does nothing if form is not valid.
|
|
||||||
|
|
||||||
Hint: Subclass should call `self._to_database(...)`.
|
|
||||||
"""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def _to_database(
|
|
||||||
self,
|
|
||||||
obj: DomainRequest,
|
|
||||||
join: str,
|
|
||||||
should_delete: Callable,
|
|
||||||
pre_update: Callable,
|
|
||||||
pre_create: Callable,
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Performs the actual work of saving.
|
|
||||||
|
|
||||||
Has hooks such as `should_delete` and `pre_update` by which the
|
|
||||||
subclass can control behavior. Add more hooks whenever needed.
|
|
||||||
"""
|
|
||||||
if not self.is_valid():
|
|
||||||
return
|
|
||||||
obj.save()
|
|
||||||
|
|
||||||
query = getattr(obj, join).order_by("created_at").all() # order matters
|
|
||||||
|
|
||||||
# get the related name for the join defined for the db_obj for this form.
|
|
||||||
# the related name will be the reference on a related object back to db_obj
|
|
||||||
related_name = ""
|
|
||||||
field = obj._meta.get_field(join)
|
|
||||||
if isinstance(field, ForeignObjectRel) and callable(field.related_query_name):
|
|
||||||
related_name = field.related_query_name()
|
|
||||||
elif hasattr(field, "related_query_name") and callable(field.related_query_name):
|
|
||||||
related_name = field.related_query_name()
|
|
||||||
|
|
||||||
# the use of `zip` pairs the forms in the formset with the
|
|
||||||
# related objects gotten from the database -- there should always be
|
|
||||||
# at least as many forms as database entries: extra forms means new
|
|
||||||
# entries, but fewer forms is _not_ the correct way to delete items
|
|
||||||
# (likely a client-side error or an attempt at data tampering)
|
|
||||||
for db_obj, post_data in zip_longest(query, self.forms, fillvalue=None):
|
|
||||||
cleaned = post_data.cleaned_data if post_data is not None else {}
|
|
||||||
|
|
||||||
# matching database object exists, update it
|
|
||||||
if db_obj is not None and cleaned:
|
|
||||||
if should_delete(cleaned):
|
|
||||||
if hasattr(db_obj, "has_more_than_one_join") and db_obj.has_more_than_one_join(related_name):
|
|
||||||
# Remove the specific relationship without deleting the object
|
|
||||||
getattr(db_obj, related_name).remove(self.domain_request)
|
|
||||||
else:
|
|
||||||
# If there are no other relationships, delete the object
|
|
||||||
db_obj.delete()
|
|
||||||
else:
|
|
||||||
if hasattr(db_obj, "has_more_than_one_join") and db_obj.has_more_than_one_join(related_name):
|
|
||||||
# create a new db_obj and disconnect existing one
|
|
||||||
getattr(db_obj, related_name).remove(self.domain_request)
|
|
||||||
kwargs = pre_create(db_obj, cleaned)
|
|
||||||
getattr(obj, join).create(**kwargs)
|
|
||||||
else:
|
|
||||||
pre_update(db_obj, cleaned)
|
|
||||||
db_obj.save()
|
|
||||||
|
|
||||||
# no matching database object, create it
|
|
||||||
# make sure not to create a database object if cleaned has 'delete' attribute
|
|
||||||
elif db_obj is None and cleaned and not cleaned.get("DELETE", False):
|
|
||||||
kwargs = pre_create(db_obj, cleaned)
|
|
||||||
getattr(obj, join).create(**kwargs)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def on_fetch(cls, query):
|
|
||||||
"""Code to run when fetching formset's objects from the database."""
|
|
||||||
return query.values()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_database(cls, obj: DomainRequest, join: str, on_fetch: Callable):
|
|
||||||
"""Returns a dict of form field values gotten from `obj`."""
|
|
||||||
return on_fetch(getattr(obj, join).order_by("created_at")) # order matters
|
|
||||||
|
|
||||||
|
|
||||||
class OrganizationTypeForm(RegistrarForm):
|
class OrganizationTypeForm(RegistrarForm):
|
||||||
generic_org_type = forms.ChoiceField(
|
generic_org_type = forms.ChoiceField(
|
||||||
# use the long names in the domain request form
|
# use the long names in the domain request form
|
||||||
|
@ -588,28 +435,24 @@ class YourContactForm(RegistrarForm):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class OtherContactsYesNoForm(RegistrarForm):
|
class OtherContactsYesNoForm(BaseYesNoForm):
|
||||||
def __init__(self, *args, **kwargs):
|
"""The yes/no field for the OtherContacts form."""
|
||||||
"""Extend the initialization of the form from RegistrarForm __init__"""
|
|
||||||
super().__init__(*args, **kwargs)
|
form_choices = ((True, "Yes, I can name other employees."), (False, "No. (We’ll ask you to explain why.)"))
|
||||||
# set the initial value based on attributes of domain request
|
field_name = "has_other_contacts"
|
||||||
if self.domain_request and self.domain_request.has_other_contacts():
|
|
||||||
initial_value = True
|
@property
|
||||||
elif self.domain_request and self.domain_request.has_rationale():
|
def form_is_checked(self):
|
||||||
initial_value = False
|
"""
|
||||||
|
Determines the initial checked state of the form based on the domain_request's attributes.
|
||||||
|
"""
|
||||||
|
if self.domain_request.has_other_contacts():
|
||||||
|
return True
|
||||||
|
elif self.domain_request.has_rationale():
|
||||||
|
return False
|
||||||
else:
|
else:
|
||||||
# No pre-selection for new domain requests
|
# No pre-selection for new domain requests
|
||||||
initial_value = None
|
return None
|
||||||
|
|
||||||
self.fields["has_other_contacts"] = forms.TypedChoiceField(
|
|
||||||
coerce=lambda x: x.lower() == "true" if x is not None else None, # coerce strings to bool, excepting None
|
|
||||||
choices=((True, "Yes, I can name other employees."), (False, "No. (We’ll ask you to explain why.)")),
|
|
||||||
initial=initial_value,
|
|
||||||
widget=forms.RadioSelect,
|
|
||||||
error_messages={
|
|
||||||
"required": "This question is required.",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class OtherContactsForm(RegistrarForm):
|
class OtherContactsForm(RegistrarForm):
|
||||||
|
@ -864,45 +707,12 @@ class CisaRepresentativeForm(BaseDeletableRegistrarForm):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class BaseYesNoForm(RegistrarForm):
|
|
||||||
"""Used for forms with a yes/no form with a hidden input on toggle"""
|
|
||||||
|
|
||||||
form_is_checked = None
|
|
||||||
typed_choice_field_name = None
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
"""Extend the initialization of the form from RegistrarForm __init__"""
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
# set the initial value based on attributes of domain request
|
|
||||||
if self.domain_request:
|
|
||||||
if self.form_is_checked:
|
|
||||||
initial_value = True
|
|
||||||
else:
|
|
||||||
initial_value = False
|
|
||||||
else:
|
|
||||||
# No pre-selection for new domain requests
|
|
||||||
initial_value = None
|
|
||||||
|
|
||||||
self.fields[self.typed_choice_field_name] = forms.TypedChoiceField(
|
|
||||||
coerce=lambda x: x.lower() == "true" if x is not None else None,
|
|
||||||
choices=((True, "Yes"), (False, "No")),
|
|
||||||
initial=initial_value,
|
|
||||||
widget=forms.RadioSelect,
|
|
||||||
error_messages={
|
|
||||||
"required": "This question is required.",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class CisaRepresentativeYesNoForm(BaseYesNoForm):
|
class CisaRepresentativeYesNoForm(BaseYesNoForm):
|
||||||
"""Yes/no toggle for the CISA regions question on additional details"""
|
"""Yes/no toggle for the CISA regions question on additional details"""
|
||||||
|
|
||||||
# Note that these can be set in __init__ if you need more fine-grained control
|
# Note that these can be set as functions/init if you need more fine-grained control
|
||||||
form_is_checked = property(
|
form_is_checked = property(lambda self: self.domain_request.has_cisa_representative())
|
||||||
lambda self: self.domain_request.has_cisa_representative() if self.domain_request else False
|
field_name = "has_cisa_representative"
|
||||||
)
|
|
||||||
typed_choice_field_name = "has_cisa_representative"
|
|
||||||
|
|
||||||
|
|
||||||
class AdditionalDetailsForm(BaseDeletableRegistrarForm):
|
class AdditionalDetailsForm(BaseDeletableRegistrarForm):
|
||||||
|
@ -922,11 +732,9 @@ class AdditionalDetailsForm(BaseDeletableRegistrarForm):
|
||||||
class AdditionalDetailsYesNoForm(BaseYesNoForm):
|
class AdditionalDetailsYesNoForm(BaseYesNoForm):
|
||||||
"""Yes/no toggle for the anything else question on additional details"""
|
"""Yes/no toggle for the anything else question on additional details"""
|
||||||
|
|
||||||
# Note that these can be set in __init__ if you need more fine-grained control
|
# Note that these can be set as functions/init if you need more fine-grained control
|
||||||
form_is_checked = property(
|
form_is_checked = property(lambda self: self.domain_request.has_anything_else_text())
|
||||||
lambda self: self.domain_request.has_anything_else_text() if self.domain_request else False
|
field_name = "has_anything_else_text"
|
||||||
)
|
|
||||||
typed_choice_field_name = "has_anything_else_text"
|
|
||||||
|
|
||||||
|
|
||||||
class RequirementsForm(RegistrarForm):
|
class RequirementsForm(RegistrarForm):
|
||||||
|
|
226
src/registrar/forms/utility/wizard_form_helper.py
Normal file
226
src/registrar/forms/utility/wizard_form_helper.py
Normal file
|
@ -0,0 +1,226 @@
|
||||||
|
"""Containers helpers and base classes for the domain_request_wizard.py file"""
|
||||||
|
|
||||||
|
from itertools import zip_longest
|
||||||
|
from typing import Callable
|
||||||
|
from django.db.models.fields.related import ForeignObjectRel
|
||||||
|
from django import forms
|
||||||
|
|
||||||
|
from registrar.models import DomainRequest, Contact
|
||||||
|
|
||||||
|
|
||||||
|
class RegistrarForm(forms.Form):
|
||||||
|
"""
|
||||||
|
A common set of methods and configuration.
|
||||||
|
|
||||||
|
The registrar's domain request is several pages of "steps".
|
||||||
|
Each step is an HTML form containing one or more Django "forms".
|
||||||
|
|
||||||
|
Subclass this class to create new forms.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
kwargs.setdefault("label_suffix", "")
|
||||||
|
# save a reference to a domain request object
|
||||||
|
self.domain_request = kwargs.pop("domain_request", None)
|
||||||
|
super(RegistrarForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def to_database(self, obj: DomainRequest | Contact):
|
||||||
|
"""
|
||||||
|
Adds this form's cleaned data to `obj` and saves `obj`.
|
||||||
|
|
||||||
|
Does nothing if form is not valid.
|
||||||
|
"""
|
||||||
|
if not self.is_valid():
|
||||||
|
return
|
||||||
|
for name, value in self.cleaned_data.items():
|
||||||
|
setattr(obj, name, value)
|
||||||
|
obj.save()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_database(cls, obj: DomainRequest | Contact | None):
|
||||||
|
"""Returns a dict of form field values gotten from `obj`."""
|
||||||
|
if obj is None:
|
||||||
|
return {}
|
||||||
|
return {name: getattr(obj, name) for name in cls.declared_fields.keys()} # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
class RegistrarFormSet(forms.BaseFormSet):
|
||||||
|
"""
|
||||||
|
As with RegistrarForm, a common set of methods and configuration.
|
||||||
|
|
||||||
|
Subclass this class to create new formsets.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
# save a reference to an domain_request object
|
||||||
|
self.domain_request = kwargs.pop("domain_request", None)
|
||||||
|
super(RegistrarFormSet, self).__init__(*args, **kwargs)
|
||||||
|
# quick workaround to ensure that the HTML `required`
|
||||||
|
# attribute shows up on required fields for any forms
|
||||||
|
# in the formset which have data already (stated another
|
||||||
|
# way: you can leave a form in the formset blank, but
|
||||||
|
# if you opt to fill it out, you must fill it out _right_)
|
||||||
|
for index in range(self.initial_form_count()):
|
||||||
|
self.forms[index].use_required_attribute = True
|
||||||
|
|
||||||
|
def should_delete(self, cleaned):
|
||||||
|
"""Should this entry be deleted from the database?"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def pre_update(self, db_obj, cleaned):
|
||||||
|
"""Code to run before an item in the formset is saved."""
|
||||||
|
for key, value in cleaned.items():
|
||||||
|
setattr(db_obj, key, value)
|
||||||
|
|
||||||
|
def pre_create(self, db_obj, cleaned):
|
||||||
|
"""Code to run before an item in the formset is created in the database."""
|
||||||
|
return cleaned
|
||||||
|
|
||||||
|
def to_database(self, obj: DomainRequest):
|
||||||
|
"""
|
||||||
|
Adds this form's cleaned data to `obj` and saves `obj`.
|
||||||
|
|
||||||
|
Does nothing if form is not valid.
|
||||||
|
|
||||||
|
Hint: Subclass should call `self._to_database(...)`.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def _to_database(
|
||||||
|
self,
|
||||||
|
obj: DomainRequest,
|
||||||
|
join: str,
|
||||||
|
should_delete: Callable,
|
||||||
|
pre_update: Callable,
|
||||||
|
pre_create: Callable,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Performs the actual work of saving.
|
||||||
|
|
||||||
|
Has hooks such as `should_delete` and `pre_update` by which the
|
||||||
|
subclass can control behavior. Add more hooks whenever needed.
|
||||||
|
"""
|
||||||
|
if not self.is_valid():
|
||||||
|
return
|
||||||
|
obj.save()
|
||||||
|
|
||||||
|
query = getattr(obj, join).order_by("created_at").all() # order matters
|
||||||
|
|
||||||
|
# get the related name for the join defined for the db_obj for this form.
|
||||||
|
# the related name will be the reference on a related object back to db_obj
|
||||||
|
related_name = ""
|
||||||
|
field = obj._meta.get_field(join)
|
||||||
|
if isinstance(field, ForeignObjectRel) and callable(field.related_query_name):
|
||||||
|
related_name = field.related_query_name()
|
||||||
|
elif hasattr(field, "related_query_name") and callable(field.related_query_name):
|
||||||
|
related_name = field.related_query_name()
|
||||||
|
|
||||||
|
# the use of `zip` pairs the forms in the formset with the
|
||||||
|
# related objects gotten from the database -- there should always be
|
||||||
|
# at least as many forms as database entries: extra forms means new
|
||||||
|
# entries, but fewer forms is _not_ the correct way to delete items
|
||||||
|
# (likely a client-side error or an attempt at data tampering)
|
||||||
|
for db_obj, post_data in zip_longest(query, self.forms, fillvalue=None):
|
||||||
|
cleaned = post_data.cleaned_data if post_data is not None else {}
|
||||||
|
|
||||||
|
# matching database object exists, update it
|
||||||
|
if db_obj is not None and cleaned:
|
||||||
|
if should_delete(cleaned):
|
||||||
|
if hasattr(db_obj, "has_more_than_one_join") and db_obj.has_more_than_one_join(related_name):
|
||||||
|
# Remove the specific relationship without deleting the object
|
||||||
|
getattr(db_obj, related_name).remove(self.domain_request)
|
||||||
|
else:
|
||||||
|
# If there are no other relationships, delete the object
|
||||||
|
db_obj.delete()
|
||||||
|
else:
|
||||||
|
if hasattr(db_obj, "has_more_than_one_join") and db_obj.has_more_than_one_join(related_name):
|
||||||
|
# create a new db_obj and disconnect existing one
|
||||||
|
getattr(db_obj, related_name).remove(self.domain_request)
|
||||||
|
kwargs = pre_create(db_obj, cleaned)
|
||||||
|
getattr(obj, join).create(**kwargs)
|
||||||
|
else:
|
||||||
|
pre_update(db_obj, cleaned)
|
||||||
|
db_obj.save()
|
||||||
|
|
||||||
|
# no matching database object, create it
|
||||||
|
# make sure not to create a database object if cleaned has 'delete' attribute
|
||||||
|
elif db_obj is None and cleaned and not cleaned.get("DELETE", False):
|
||||||
|
kwargs = pre_create(db_obj, cleaned)
|
||||||
|
getattr(obj, join).create(**kwargs)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def on_fetch(cls, query):
|
||||||
|
"""Code to run when fetching formset's objects from the database."""
|
||||||
|
return query.values()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_database(cls, obj: DomainRequest, join: str, on_fetch: Callable):
|
||||||
|
"""Returns a dict of form field values gotten from `obj`."""
|
||||||
|
return on_fetch(getattr(obj, join).order_by("created_at")) # order matters
|
||||||
|
|
||||||
|
|
||||||
|
class BaseYesNoForm(RegistrarForm):
|
||||||
|
"""
|
||||||
|
Base class used for forms with a yes/no form with a hidden input on toggle.
|
||||||
|
Use this class when you need something similar to the AdditionalDetailsYesNoForm.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
form_is_checked (bool): Determines the default state (checked or not) of the Yes/No toggle.
|
||||||
|
field_name (str): Specifies the form field name that the Yes/No toggle controls.
|
||||||
|
required_error_message (str): Custom error message displayed when the field is required but not provided.
|
||||||
|
form_choices (tuple): Defines the choice options for the form field, defaulting to Yes/No choices.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
Subclass this form to implement specific Yes/No fields in various parts of the application, customizing
|
||||||
|
`form_is_checked` and `field_name` as necessary for the context.
|
||||||
|
"""
|
||||||
|
|
||||||
|
form_is_checked: bool
|
||||||
|
|
||||||
|
# What field does the yes/no button hook to?
|
||||||
|
# For instance, this could be "has_other_contacts"
|
||||||
|
field_name: str
|
||||||
|
|
||||||
|
required_error_message = "This question is required."
|
||||||
|
|
||||||
|
# Default form choice mapping. Default is suitable for most cases.
|
||||||
|
form_choices = ((True, "Yes"), (False, "No"))
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
"""Extend the initialization of the form from RegistrarForm __init__"""
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
self.fields[self.field_name] = self.get_typed_choice_field()
|
||||||
|
|
||||||
|
def get_typed_choice_field(self):
|
||||||
|
"""
|
||||||
|
Creates a TypedChoiceField for the form with specified initial value and choices.
|
||||||
|
Returns:
|
||||||
|
TypedChoiceField: A Django form field specifically configured for selecting between
|
||||||
|
predefined choices with type coercion and custom error messages.
|
||||||
|
"""
|
||||||
|
choice_field = forms.TypedChoiceField(
|
||||||
|
coerce=lambda x: x.lower() == "true" if x is not None else None,
|
||||||
|
choices=self.form_choices,
|
||||||
|
initial=self.get_initial_value(),
|
||||||
|
widget=forms.RadioSelect,
|
||||||
|
error_messages={
|
||||||
|
"required": self.required_error_message,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return choice_field
|
||||||
|
|
||||||
|
def get_initial_value(self):
|
||||||
|
"""
|
||||||
|
Determines the initial value for TypedChoiceField.
|
||||||
|
More directly, this controls the "initial" field on forms.TypedChoiceField.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool | None: The initial value for the form field. If the domain request is set,
|
||||||
|
this will always return the value of self.form_is_checked.
|
||||||
|
Otherwise, None will be returned as a new domain request can't start out checked.
|
||||||
|
"""
|
||||||
|
# No pre-selection for new domain requests
|
||||||
|
initial_value = self.form_is_checked if self.domain_request else None
|
||||||
|
return initial_value
|
Loading…
Add table
Add a link
Reference in a new issue