mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-05-18 02:19:23 +02:00
merge
This commit is contained in:
commit
68bc920010
20 changed files with 1018 additions and 312 deletions
|
@ -108,7 +108,7 @@ services:
|
|||
- pa11y
|
||||
|
||||
owasp:
|
||||
image: owasp/zap2docker-stable
|
||||
image: ghcr.io/zaproxy/zaproxy:stable
|
||||
command: zap-baseline.py -t http://app:8080 -c zap.conf -I -r zap_report.html
|
||||
volumes:
|
||||
- .:/zap/wrk/
|
||||
|
|
|
@ -1229,7 +1229,17 @@ class DomainRequestAdmin(ListHeaderAdmin):
|
|||
},
|
||||
),
|
||||
(".gov domain", {"fields": ["requested_domain", "alternative_domains"]}),
|
||||
("Contacts", {"fields": ["authorizing_official", "other_contacts", "no_other_contacts_rationale"]}),
|
||||
(
|
||||
"Contacts",
|
||||
{
|
||||
"fields": [
|
||||
"authorizing_official",
|
||||
"other_contacts",
|
||||
"no_other_contacts_rationale",
|
||||
"cisa_representative_email",
|
||||
]
|
||||
},
|
||||
),
|
||||
("Background info", {"fields": ["purpose", "anything_else", "current_websites"]}),
|
||||
(
|
||||
"Type of organization",
|
||||
|
@ -1302,6 +1312,7 @@ class DomainRequestAdmin(ListHeaderAdmin):
|
|||
"no_other_contacts_rationale",
|
||||
"anything_else",
|
||||
"is_policy_acknowledged",
|
||||
"cisa_representative_email",
|
||||
]
|
||||
autocomplete_fields = [
|
||||
"approved_domain",
|
||||
|
|
|
@ -457,7 +457,7 @@ function enableRelatedWidgetButtons(changeLink, deleteLink, viewLink, elementPk,
|
|||
}
|
||||
|
||||
/** An IIFE for admin in DjangoAdmin to listen to changes on the domain request
|
||||
* status select amd to show/hide the rejection reason
|
||||
* status select and to show/hide the rejection reason
|
||||
*/
|
||||
(function (){
|
||||
let rejectionReasonFormGroup = document.querySelector('.field-rejection_reason')
|
||||
|
|
|
@ -193,6 +193,65 @@ function clearValidators(el) {
|
|||
toggleInputValidity(el, true);
|
||||
}
|
||||
|
||||
/** Hookup listeners for yes/no togglers for form fields
|
||||
* Parameters:
|
||||
* - radioButtonName: The "name=" value for the radio buttons being used as togglers
|
||||
* - elementIdToShowIfYes: The Id of the element (eg. a div) to show if selected value of the given
|
||||
* radio button is true (hides this element if false)
|
||||
* - elementIdToShowIfNo: The Id of the element (eg. a div) to show if selected value of the given
|
||||
* radio button is false (hides this element if true)
|
||||
* **/
|
||||
function HookupYesNoListener(radioButtonName, elementIdToShowIfYes, elementIdToShowIfNo) {
|
||||
// Get the radio buttons
|
||||
let radioButtons = document.querySelectorAll('input[name="'+radioButtonName+'"]');
|
||||
|
||||
function handleRadioButtonChange() {
|
||||
// Check the value of the selected radio button
|
||||
// Attempt to find the radio button element that is checked
|
||||
let radioButtonChecked = document.querySelector('input[name="'+radioButtonName+'"]:checked');
|
||||
|
||||
// Check if the element exists before accessing its value
|
||||
let selectedValue = radioButtonChecked ? radioButtonChecked.value : null;
|
||||
|
||||
switch (selectedValue) {
|
||||
case 'True':
|
||||
toggleTwoDomElements(elementIdToShowIfYes, elementIdToShowIfNo, 1);
|
||||
break;
|
||||
|
||||
case 'False':
|
||||
toggleTwoDomElements(elementIdToShowIfYes, elementIdToShowIfNo, 2);
|
||||
break;
|
||||
|
||||
default:
|
||||
toggleTwoDomElements(elementIdToShowIfYes, elementIdToShowIfNo, 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (radioButtons.length) {
|
||||
// Add event listener to each radio button
|
||||
radioButtons.forEach(function (radioButton) {
|
||||
radioButton.addEventListener('change', handleRadioButtonChange);
|
||||
});
|
||||
|
||||
// initialize
|
||||
handleRadioButtonChange();
|
||||
}
|
||||
}
|
||||
|
||||
// A generic display none/block toggle function that takes an integer param to indicate how the elements toggle
|
||||
function toggleTwoDomElements(ele1, ele2, index) {
|
||||
let element1 = document.getElementById(ele1);
|
||||
let element2 = document.getElementById(ele2);
|
||||
if (element1 || element2) {
|
||||
// Toggle display based on the index
|
||||
if (element1) {element1.style.display = index === 1 ? 'block' : 'none';}
|
||||
if (element2) {element2.style.display = index === 2 ? 'block' : 'none';}
|
||||
}
|
||||
else {
|
||||
console.error('Unable to find elements to toggle');
|
||||
}
|
||||
}
|
||||
|
||||
// <<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>>
|
||||
// Event handlers.
|
||||
|
||||
|
@ -712,57 +771,40 @@ function hideDeletedForms() {
|
|||
}
|
||||
})();
|
||||
|
||||
// A generic display none/block toggle function that takes an integer param to indicate how the elements toggle
|
||||
function toggleTwoDomElements(ele1, ele2, index) {
|
||||
let element1 = document.getElementById(ele1);
|
||||
let element2 = document.getElementById(ele2);
|
||||
if (element1 && element2) {
|
||||
// Toggle display based on the index
|
||||
element1.style.display = index === 1 ? 'block' : 'none';
|
||||
element2.style.display = index === 2 ? 'block' : 'none';
|
||||
} else {
|
||||
console.error('One or both elements not found.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An IIFE that listens to the other contacts radio form on DAs and toggles the contacts/no other contacts forms
|
||||
*
|
||||
*/
|
||||
(function otherContactsFormListener() {
|
||||
// Get the radio buttons
|
||||
let radioButtons = document.querySelectorAll('input[name="other_contacts-has_other_contacts"]');
|
||||
HookupYesNoListener("other_contacts-has_other_contacts",'other-employees', 'no-other-employees')
|
||||
})();
|
||||
|
||||
function handleRadioButtonChange() {
|
||||
// Check the value of the selected radio button
|
||||
// Attempt to find the radio button element that is checked
|
||||
let radioButtonChecked = document.querySelector('input[name="other_contacts-has_other_contacts"]:checked');
|
||||
|
||||
// Check if the element exists before accessing its value
|
||||
let selectedValue = radioButtonChecked ? radioButtonChecked.value : null;
|
||||
/**
|
||||
* An IIFE that listens to the yes/no radio buttons on the anything else form and toggles form field visibility accordingly
|
||||
*
|
||||
*/
|
||||
(function anythingElseFormListener() {
|
||||
HookupYesNoListener("additional_details-has_anything_else_text",'anything-else', null)
|
||||
})();
|
||||
|
||||
switch (selectedValue) {
|
||||
case 'True':
|
||||
toggleTwoDomElements('other-employees', 'no-other-employees', 1);
|
||||
break;
|
||||
|
||||
case 'False':
|
||||
toggleTwoDomElements('other-employees', 'no-other-employees', 2);
|
||||
break;
|
||||
|
||||
default:
|
||||
toggleTwoDomElements('other-employees', 'no-other-employees', 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (radioButtons.length) {
|
||||
// Add event listener to each radio button
|
||||
radioButtons.forEach(function (radioButton) {
|
||||
radioButton.addEventListener('change', handleRadioButtonChange);
|
||||
/**
|
||||
* An IIFE that disables the delete buttons on nameserver forms on page load if < 3 forms
|
||||
*
|
||||
*/
|
||||
(function nameserversFormListener() {
|
||||
let isNameserversForm = document.querySelector(".nameservers-form");
|
||||
if (isNameserversForm) {
|
||||
let forms = document.querySelectorAll(".repeatable-form");
|
||||
if (forms.length < 3) {
|
||||
// Hide the delete buttons on the 2 nameservers
|
||||
forms.forEach((form) => {
|
||||
Array.from(form.querySelectorAll('.delete-record')).forEach((deleteButton) => {
|
||||
deleteButton.setAttribute("disabled", "true");
|
||||
});
|
||||
|
||||
// initialize
|
||||
handleRadioButtonChange();
|
||||
});
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
|
@ -784,3 +826,11 @@ function toggleTwoDomElements(ele1, ele2, index) {
|
|||
}
|
||||
}
|
||||
})();
|
||||
|
||||
/**
|
||||
* An IIFE that listens to the yes/no radio buttons on the CISA representatives form and toggles form field visibility accordingly
|
||||
*
|
||||
*/
|
||||
(function cisaRepresentativesFormListener() {
|
||||
HookupYesNoListener("additional_details-has_cisa_representative",'cisa-representative', null)
|
||||
})();
|
||||
|
|
|
@ -46,7 +46,7 @@ for step, view in [
|
|||
(Step.PURPOSE, views.Purpose),
|
||||
(Step.YOUR_CONTACT, views.YourContact),
|
||||
(Step.OTHER_CONTACTS, views.OtherContacts),
|
||||
(Step.ANYTHING_ELSE, views.AnythingElse),
|
||||
(Step.ADDITIONAL_DETAILS, views.AdditionalDetails),
|
||||
(Step.REQUIREMENTS, views.Requirements),
|
||||
(Step.REVIEW, views.Review),
|
||||
]:
|
||||
|
|
|
@ -93,6 +93,12 @@ class UserFixture:
|
|||
"last_name": "Chin",
|
||||
"email": "szu.chin@associates.cisa.dhs.gov",
|
||||
},
|
||||
{
|
||||
"username": "66bb1a5a-a091-4d7f-a6cf-4d772b4711c7",
|
||||
"first_name": "Christina",
|
||||
"last_name": "Burnett",
|
||||
"email": "christina.burnett@cisa.dhs.gov",
|
||||
},
|
||||
{
|
||||
"username": "012f844d-8a0f-4225-9d82-cbf87bff1d3e",
|
||||
"first_name": "Riley",
|
||||
|
@ -169,6 +175,12 @@ class UserFixture:
|
|||
"last_name": "Chin-Analyst",
|
||||
"email": "szu.chin@ecstech.com",
|
||||
},
|
||||
{
|
||||
"username": "22f88aa5-3b54-4b1f-9c57-201fb02ddba7",
|
||||
"first_name": "Christina-Analyst",
|
||||
"last_name": "Burnett-Analyst",
|
||||
"email": "christina.burnett@gwe.cisa.dhs.gov",
|
||||
},
|
||||
{
|
||||
"username": "d9839768-0c17-4fa2-9c8e-36291eef5c11",
|
||||
"first_name": "Alex-Analyst",
|
||||
|
|
|
@ -1,15 +1,18 @@
|
|||
from __future__ import annotations # allows forward references in annotations
|
||||
from itertools import zip_longest
|
||||
import logging
|
||||
from typing import Callable
|
||||
from api.views import DOMAIN_API_MESSAGES
|
||||
from phonenumber_field.formfields import PhoneNumberField # type: ignore
|
||||
|
||||
from django import forms
|
||||
from django.core.validators import RegexValidator, MaxLengthValidator
|
||||
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,
|
||||
BaseDeletableRegistrarForm,
|
||||
)
|
||||
from registrar.models import Contact, DomainRequest, DraftDomain, Domain
|
||||
from registrar.templatetags.url_helpers import public_site_url
|
||||
from registrar.utility.enums import ValidationReturnType
|
||||
|
@ -17,157 +20,6 @@ from registrar.utility.enums import ValidationReturnType
|
|||
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):
|
||||
generic_org_type = forms.ChoiceField(
|
||||
# use the long names in the domain request form
|
||||
|
@ -588,28 +440,24 @@ class YourContactForm(RegistrarForm):
|
|||
)
|
||||
|
||||
|
||||
class OtherContactsYesNoForm(RegistrarForm):
|
||||
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 and self.domain_request.has_other_contacts():
|
||||
initial_value = True
|
||||
elif self.domain_request and self.domain_request.has_rationale():
|
||||
initial_value = False
|
||||
class OtherContactsYesNoForm(BaseYesNoForm):
|
||||
"""The yes/no field for the OtherContacts form."""
|
||||
|
||||
form_choices = ((True, "Yes, I can name other employees."), (False, "No. (We’ll ask you to explain why.)"))
|
||||
field_name = "has_other_contacts"
|
||||
|
||||
@property
|
||||
def form_is_checked(self):
|
||||
"""
|
||||
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:
|
||||
# No pre-selection for new domain requests
|
||||
initial_value = 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.",
|
||||
},
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
class OtherContactsForm(RegistrarForm):
|
||||
|
@ -779,7 +627,7 @@ OtherContactsFormSet = forms.formset_factory(
|
|||
)
|
||||
|
||||
|
||||
class NoOtherContactsForm(RegistrarForm):
|
||||
class NoOtherContactsForm(BaseDeletableRegistrarForm):
|
||||
no_other_contacts_rationale = forms.CharField(
|
||||
required=True,
|
||||
# label has to end in a space to get the label_suffix to show
|
||||
|
@ -794,59 +642,35 @@ class NoOtherContactsForm(RegistrarForm):
|
|||
error_messages={"required": ("Rationale for no other employees is required.")},
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.form_data_marked_for_deletion = False
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def mark_form_for_deletion(self):
|
||||
"""Marks no_other_contacts form for deletion.
|
||||
This changes behavior of validity checks and to_database
|
||||
methods."""
|
||||
self.form_data_marked_for_deletion = True
|
||||
|
||||
def clean(self):
|
||||
"""
|
||||
This method overrides the default behavior for forms.
|
||||
This cleans the form after field validation has already taken place.
|
||||
In this override, remove errors associated with the form if form data
|
||||
is marked for deletion.
|
||||
"""
|
||||
|
||||
if self.form_data_marked_for_deletion:
|
||||
# clear any errors raised by the form fields
|
||||
# (before this clean() method is run, each field
|
||||
# performs its own clean, which could result in
|
||||
# errors that we wish to ignore at this point)
|
||||
#
|
||||
# NOTE: we cannot just clear() the errors list.
|
||||
# That causes problems.
|
||||
for field in self.fields:
|
||||
if field in self.errors:
|
||||
del self.errors[field]
|
||||
|
||||
return self.cleaned_data
|
||||
|
||||
def to_database(self, obj):
|
||||
"""
|
||||
This method overrides the behavior of RegistrarForm.
|
||||
If form data is marked for deletion, set relevant fields
|
||||
to None before saving.
|
||||
Do nothing if form is not valid.
|
||||
"""
|
||||
if not self.is_valid():
|
||||
return
|
||||
if self.form_data_marked_for_deletion:
|
||||
for field_name, _ in self.fields.items():
|
||||
setattr(obj, field_name, None)
|
||||
else:
|
||||
for name, value in self.cleaned_data.items():
|
||||
setattr(obj, name, value)
|
||||
obj.save()
|
||||
class CisaRepresentativeForm(BaseDeletableRegistrarForm):
|
||||
cisa_representative_email = forms.EmailField(
|
||||
required=True,
|
||||
max_length=None,
|
||||
label="Your representative’s email",
|
||||
validators=[
|
||||
MaxLengthValidator(
|
||||
320,
|
||||
message="Response must be less than 320 characters.",
|
||||
)
|
||||
],
|
||||
error_messages={
|
||||
"invalid": ("Enter your email address in the required format, like name@example.com."),
|
||||
"required": ("Enter the email address of your CISA regional representative."),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class AnythingElseForm(RegistrarForm):
|
||||
class CisaRepresentativeYesNoForm(BaseYesNoForm):
|
||||
"""Yes/no toggle for the CISA regions question on additional details"""
|
||||
|
||||
form_is_checked = property(lambda self: self.domain_request.has_cisa_representative) # type: ignore
|
||||
field_name = "has_cisa_representative"
|
||||
|
||||
|
||||
class AdditionalDetailsForm(BaseDeletableRegistrarForm):
|
||||
anything_else = forms.CharField(
|
||||
required=False,
|
||||
required=True,
|
||||
label="Anything else?",
|
||||
widget=forms.Textarea(),
|
||||
validators=[
|
||||
|
@ -855,7 +679,20 @@ class AnythingElseForm(RegistrarForm):
|
|||
message="Response must be less than 2000 characters.",
|
||||
)
|
||||
],
|
||||
error_messages={
|
||||
"required": (
|
||||
"Provide additional details you’d like us to know. " "If you have nothing to add, select “No.”"
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class AdditionalDetailsYesNoForm(BaseYesNoForm):
|
||||
"""Yes/no toggle for the anything else question on additional details"""
|
||||
|
||||
# Note that these can be set as functions/init if you need more fine-grained control.
|
||||
form_is_checked = property(lambda self: self.domain_request.has_anything_else_text) # type: ignore
|
||||
field_name = "has_anything_else_text"
|
||||
|
||||
|
||||
class RequirementsForm(RegistrarForm):
|
||||
|
|
280
src/registrar/forms/utility/wizard_form_helper.py
Normal file
280
src/registrar/forms/utility/wizard_form_helper.py
Normal file
|
@ -0,0 +1,280 @@
|
|||
"""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 BaseDeletableRegistrarForm(RegistrarForm):
|
||||
"""Adds special validation and delete functionality.
|
||||
Used by forms that are tied to a Yes/No form."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.form_data_marked_for_deletion = False
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def mark_form_for_deletion(self):
|
||||
"""Marks this form for deletion.
|
||||
This changes behavior of validity checks and to_database
|
||||
methods."""
|
||||
self.form_data_marked_for_deletion = True
|
||||
|
||||
def clean(self):
|
||||
"""
|
||||
This method overrides the default behavior for forms.
|
||||
This cleans the form after field validation has already taken place.
|
||||
In this override, remove errors associated with the form if form data
|
||||
is marked for deletion.
|
||||
"""
|
||||
|
||||
if self.form_data_marked_for_deletion:
|
||||
# clear any errors raised by the form fields
|
||||
# (before this clean() method is run, each field
|
||||
# performs its own clean, which could result in
|
||||
# errors that we wish to ignore at this point)
|
||||
#
|
||||
# NOTE: we cannot just clear() the errors list.
|
||||
# That causes problems.
|
||||
for field in self.fields:
|
||||
if field in self.errors:
|
||||
del self.errors[field]
|
||||
|
||||
return self.cleaned_data
|
||||
|
||||
def to_database(self, obj):
|
||||
"""
|
||||
This method overrides the behavior of RegistrarForm.
|
||||
If form data is marked for deletion, set relevant fields
|
||||
to None before saving.
|
||||
Do nothing if form is not valid.
|
||||
"""
|
||||
if not self.is_valid():
|
||||
return
|
||||
if self.form_data_marked_for_deletion:
|
||||
for field_name, _ in self.fields.items():
|
||||
setattr(obj, field_name, None)
|
||||
else:
|
||||
for name, value in self.cleaned_data.items():
|
||||
setattr(obj, name, value)
|
||||
obj.save()
|
||||
|
||||
|
||||
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
|
|
@ -0,0 +1,47 @@
|
|||
# Generated by Django 4.2.10 on 2024-04-25 16:44
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("registrar", "0087_alter_domain_deleted_alter_domain_expiration_date_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="domaininformation",
|
||||
name="cisa_representative_email",
|
||||
field=models.EmailField(blank=True, max_length=320, null=True, verbose_name="CISA regional representative"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="domainrequest",
|
||||
name="cisa_representative_email",
|
||||
field=models.EmailField(blank=True, max_length=320, null=True, verbose_name="CISA regional representative"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="domainrequest",
|
||||
name="has_anything_else_text",
|
||||
field=models.BooleanField(
|
||||
blank=True, help_text="Determines if the user has a anything_else or not", null=True
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="domainrequest",
|
||||
name="has_cisa_representative",
|
||||
field=models.BooleanField(
|
||||
blank=True, help_text="Determines if the user has a representative email or not", null=True
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="domaininformation",
|
||||
name="anything_else",
|
||||
field=models.TextField(blank=True, null=True, verbose_name="Additional details"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="domainrequest",
|
||||
name="anything_else",
|
||||
field=models.TextField(blank=True, null=True, verbose_name="Additional details"),
|
||||
),
|
||||
]
|
|
@ -212,6 +212,14 @@ class DomainInformation(TimeStampedModel):
|
|||
anything_else = models.TextField(
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="Additional details",
|
||||
)
|
||||
|
||||
cisa_representative_email = models.EmailField(
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="CISA regional representative",
|
||||
max_length=320,
|
||||
)
|
||||
|
||||
is_policy_acknowledged = models.BooleanField(
|
||||
|
|
|
@ -651,6 +651,32 @@ class DomainRequest(TimeStampedModel):
|
|||
anything_else = models.TextField(
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="Additional details",
|
||||
)
|
||||
|
||||
# This is a drop-in replacement for a has_anything_else_text() function.
|
||||
# In order to track if the user has clicked the yes/no field (while keeping a none default), we need
|
||||
# a tertiary state. We should not display this in /admin.
|
||||
has_anything_else_text = models.BooleanField(
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Determines if the user has a anything_else or not",
|
||||
)
|
||||
|
||||
cisa_representative_email = models.EmailField(
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="CISA regional representative",
|
||||
max_length=320,
|
||||
)
|
||||
|
||||
# This is a drop-in replacement for an has_cisa_representative() function.
|
||||
# In order to track if the user has clicked the yes/no field (while keeping a none default), we need
|
||||
# a tertiary state. We should not display this in /admin.
|
||||
has_cisa_representative = models.BooleanField(
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Determines if the user has a representative email or not",
|
||||
)
|
||||
|
||||
is_policy_acknowledged = models.BooleanField(
|
||||
|
@ -705,8 +731,33 @@ class DomainRequest(TimeStampedModel):
|
|||
def save(self, *args, **kwargs):
|
||||
"""Save override for custom properties"""
|
||||
self.sync_organization_type()
|
||||
self.sync_yes_no_form_fields()
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def sync_yes_no_form_fields(self):
|
||||
"""Some yes/no forms use a db field to track whether it was checked or not.
|
||||
We handle that here for def save().
|
||||
"""
|
||||
|
||||
# This ensures that if we have prefilled data, the form is prepopulated
|
||||
if self.cisa_representative_email is not None:
|
||||
self.has_cisa_representative = self.cisa_representative_email != ""
|
||||
|
||||
# This check is required to ensure that the form doesn't start out checked
|
||||
if self.has_cisa_representative is not None:
|
||||
self.has_cisa_representative = (
|
||||
self.cisa_representative_email != "" and self.cisa_representative_email is not None
|
||||
)
|
||||
|
||||
# This ensures that if we have prefilled data, the form is prepopulated
|
||||
if self.anything_else is not None:
|
||||
self.has_anything_else_text = self.anything_else != ""
|
||||
|
||||
# This check is required to ensure that the form doesn't start out checked.
|
||||
if self.has_anything_else_text is not None:
|
||||
self.has_anything_else_text = self.anything_else != "" and self.anything_else is not None
|
||||
|
||||
def __str__(self):
|
||||
try:
|
||||
if self.requested_domain and self.requested_domain.name:
|
||||
|
@ -1045,6 +1096,16 @@ class DomainRequest(TimeStampedModel):
|
|||
"""Does this domain request have other contacts listed?"""
|
||||
return self.other_contacts.exists()
|
||||
|
||||
def has_additional_details(self) -> bool:
|
||||
"""Combines the has_anything_else_text and has_cisa_representative fields,
|
||||
then returns if this domain request has either of them."""
|
||||
# Split out for linter
|
||||
has_details = False
|
||||
if self.has_anything_else_text or self.has_cisa_representative:
|
||||
has_details = True
|
||||
|
||||
return has_details
|
||||
|
||||
def is_federal(self) -> Union[bool, None]:
|
||||
"""Is this domain request for a federal agency?
|
||||
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
{% extends 'domain_request_form.html' %}
|
||||
{% load static field_helpers %}
|
||||
|
||||
{% block form_instructions %}
|
||||
<em>These questions are required (<abbr class="usa-hint usa-hint--required" title="required">*</abbr>).</em>
|
||||
{% endblock %}
|
||||
|
||||
{% block form_required_fields_help_text %}
|
||||
{# commented out so it does not appear at this point on this page #}
|
||||
{% endblock %}
|
||||
|
||||
<!-- TODO-NL: (refactor) Breakup into two separate components-->
|
||||
{% block form_fields %}
|
||||
<fieldset class="usa-fieldset margin-top-2">
|
||||
<legend>
|
||||
<h2>Are you working with a CISA regional representative on your domain request?</h2>
|
||||
<p>.gov is managed by the Cybersecurity and Infrastructure Security Agency. CISA has <a href="https://www.cisa.gov/about/regions" target="_blank">10 regions</a> that some organizations choose to work with. Regional representatives use titles like protective security advisors, cyber security advisors, or election security advisors.</p>
|
||||
</legend>
|
||||
|
||||
<!-- Toggle -->
|
||||
{% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %}
|
||||
{% input_with_errors forms.0.has_cisa_representative %}
|
||||
{% endwith %}
|
||||
{# forms.0 is a small yes/no form that toggles the visibility of "cisa representative" formset #}
|
||||
<!-- TODO-NL: Hookup forms.0 to yes/no form for cisa representative (backend def)-->
|
||||
</fieldset>
|
||||
|
||||
<div id="cisa-representative" class="cisa-representative-form">
|
||||
{% input_with_errors forms.1.cisa_representative_email %}
|
||||
{# forms.1 is a form for inputting the e-mail of a cisa representative #}
|
||||
<!-- TODO-NL: Hookup forms.1 to cisa representative form (backend def) -->
|
||||
</div>
|
||||
|
||||
|
||||
<fieldset class="usa-fieldset margin-top-2">
|
||||
<legend>
|
||||
<h2>Is there anything else you’d like us to know about your domain request?</h2>
|
||||
</legend>
|
||||
|
||||
<!-- Toggle -->
|
||||
{% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %}
|
||||
{% input_with_errors forms.2.has_anything_else_text %}
|
||||
{% endwith %}
|
||||
{# forms.2 is a small yes/no form that toggles the visibility of "cisa representative" formset #}
|
||||
<!-- TODO-NL: Hookup forms.2 to yes/no form for anything else form (backend def)-->
|
||||
</fieldset>
|
||||
|
||||
<div id="anything-else">
|
||||
{% with attr_maxlength=2000 add_label_class="usa-sr-only" %}
|
||||
{% input_with_errors forms.3.anything_else %}
|
||||
{% endwith %}
|
||||
{# forms.3 is a form for inputting the e-mail of a cisa representative #}
|
||||
<!-- TODO-NL: Hookup forms.3 to anything else form (backend def) -->
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -1,19 +0,0 @@
|
|||
{% extends 'domain_request_form.html' %}
|
||||
{% load field_helpers %}
|
||||
|
||||
{% block form_instructions %}
|
||||
<h2>Is there anything else you’d like us to know about your domain request?</h2>
|
||||
|
||||
<p>This question is optional.</p>
|
||||
{% endblock %}
|
||||
|
||||
{% block form_required_fields_help_text %}
|
||||
{# commented out so it does not appear on this page #}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block form_fields %}
|
||||
{% with attr_maxlength=2000 add_label_class="usa-sr-only" %}
|
||||
{% input_with_errors forms.0.anything_else %}
|
||||
{% endwith %}
|
||||
{% endblock %}
|
|
@ -155,11 +155,20 @@
|
|||
{% endif %}
|
||||
|
||||
|
||||
{% if step == Step.ANYTHING_ELSE %}
|
||||
{% if step == Step.ADDITIONAL_DETAILS %}
|
||||
{% namespaced_url 'domain-request' step as domain_request_url %}
|
||||
{% with title=form_titles|get_item:step value=domain_request.anything_else|default:"No" %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
|
||||
{% with title=form_titles|get_item:step value=domain_request.requested_domain.name|default:"Incomplete" %}
|
||||
{% include "includes/summary_item.html" with title=title sub_header_text='CISA regional representative' value=domain_request.cisa_representative_email heading_level=heading_level editable=True edit_link=domain_request_url custom_text_for_value_none='No' %}
|
||||
{% endwith %}
|
||||
|
||||
<h3 class="register-form-review-header">Anything else</h3>
|
||||
<ul class="usa-list usa-list--unstyled margin-top-0">
|
||||
{% if domain_request.anything_else %}
|
||||
{{domain_request.anything_else}}
|
||||
{% else %}
|
||||
No
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
|
|
@ -116,7 +116,18 @@
|
|||
{% include "includes/summary_item.html" with title='Other employees from your organization' value=DomainRequest.no_other_contacts_rationale heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% include "includes/summary_item.html" with title='Anything else?' value=DomainRequest.anything_else|default:"No" heading_level=heading_level %}
|
||||
{# We always show this field even if None #}
|
||||
{% if DomainRequest %}
|
||||
{% include "includes/summary_item.html" with title='Additional details' sub_header_text='CISA regional representative' value=DomainRequest.cisa_representative_email custom_text_for_value_none='No' heading_level=heading_level %}
|
||||
<h3 class="register-form-review-header">Anything else</h3>
|
||||
<ul class="usa-list usa-list--unstyled margin-top-0">
|
||||
{% if DomainRequest.anything_else %}
|
||||
{{DomainRequest.anything_else}}
|
||||
{% else %}
|
||||
No
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
{% endwith %}
|
||||
</div>
|
||||
|
|
|
@ -20,6 +20,9 @@
|
|||
</{{ heading_level }}>
|
||||
{% else %}
|
||||
</h2>
|
||||
{% endif %}
|
||||
{% if sub_header_text %}
|
||||
<h3 class="register-form-review-header">{{ sub_header_text }}</h3>
|
||||
{% endif %}
|
||||
{% if address %}
|
||||
{% include "includes/organization_address.html" with organization=value %}
|
||||
|
@ -39,6 +42,10 @@
|
|||
</dd>
|
||||
{% endfor %}
|
||||
</dl>
|
||||
{% elif custom_text_for_value_none %}
|
||||
<p>
|
||||
{{ custom_text_for_value_none }}
|
||||
</p>
|
||||
{% else %}
|
||||
<p>
|
||||
None
|
||||
|
@ -92,6 +99,8 @@
|
|||
<p class="margin-top-0 margin-bottom-0">
|
||||
{% if value %}
|
||||
{{ value }}
|
||||
{% elif custom_text_for_value_none %}
|
||||
{{ custom_text_for_value_none }}
|
||||
{% else %}
|
||||
None
|
||||
{% endif %}
|
||||
|
|
|
@ -2034,6 +2034,9 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
"purpose",
|
||||
"no_other_contacts_rationale",
|
||||
"anything_else",
|
||||
"has_anything_else_text",
|
||||
"cisa_representative_email",
|
||||
"has_cisa_representative",
|
||||
"is_policy_acknowledged",
|
||||
"submission_date",
|
||||
"notes",
|
||||
|
@ -2065,6 +2068,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
"no_other_contacts_rationale",
|
||||
"anything_else",
|
||||
"is_policy_acknowledged",
|
||||
"cisa_representative_email",
|
||||
]
|
||||
|
||||
self.assertEqual(readonly_fields, expected_fields)
|
||||
|
|
|
@ -15,7 +15,7 @@ from registrar.forms.domain_request_wizard import (
|
|||
RequirementsForm,
|
||||
TribalGovernmentForm,
|
||||
PurposeForm,
|
||||
AnythingElseForm,
|
||||
AdditionalDetailsForm,
|
||||
AboutYourOrganizationForm,
|
||||
)
|
||||
from registrar.forms.domain import ContactForm
|
||||
|
@ -274,7 +274,7 @@ class TestFormValidation(MockEppLib):
|
|||
|
||||
def test_anything_else_form_about_your_organization_character_count_invalid(self):
|
||||
"""Response must be less than 2000 characters."""
|
||||
form = AnythingElseForm(
|
||||
form = AdditionalDetailsForm(
|
||||
data={
|
||||
"anything_else": "Bacon ipsum dolor amet fatback strip steak pastrami"
|
||||
"shankle, drumstick doner chicken landjaeger turkey andouille."
|
||||
|
|
|
@ -356,33 +356,39 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
# the post request should return a redirect to the next form in
|
||||
# the domain request page
|
||||
self.assertEqual(other_contacts_result.status_code, 302)
|
||||
self.assertEqual(other_contacts_result["Location"], "/request/anything_else/")
|
||||
self.assertEqual(other_contacts_result["Location"], "/request/additional_details/")
|
||||
num_pages_tested += 1
|
||||
|
||||
# ---- ANYTHING ELSE PAGE ----
|
||||
# ---- ADDITIONAL DETAILS PAGE ----
|
||||
# Follow the redirect to the next form page
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
anything_else_page = other_contacts_result.follow()
|
||||
anything_else_form = anything_else_page.forms[0]
|
||||
additional_details_page = other_contacts_result.follow()
|
||||
additional_details_form = additional_details_page.forms[0]
|
||||
|
||||
anything_else_form["anything_else-anything_else"] = "Nothing else."
|
||||
# load inputs with test data
|
||||
|
||||
additional_details_form["additional_details-has_cisa_representative"] = "True"
|
||||
additional_details_form["additional_details-has_anything_else_text"] = "True"
|
||||
additional_details_form["additional_details-cisa_representative_email"] = "FakeEmail@gmail.com"
|
||||
additional_details_form["additional_details-anything_else"] = "Nothing else."
|
||||
|
||||
# test next button
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
anything_else_result = anything_else_form.submit()
|
||||
additional_details_result = additional_details_form.submit()
|
||||
# validate that data from this step are being saved
|
||||
domain_request = DomainRequest.objects.get() # there's only one
|
||||
self.assertEqual(domain_request.cisa_representative_email, "FakeEmail@gmail.com")
|
||||
self.assertEqual(domain_request.anything_else, "Nothing else.")
|
||||
# the post request should return a redirect to the next form in
|
||||
# the domain request page
|
||||
self.assertEqual(anything_else_result.status_code, 302)
|
||||
self.assertEqual(anything_else_result["Location"], "/request/requirements/")
|
||||
self.assertEqual(additional_details_result.status_code, 302)
|
||||
self.assertEqual(additional_details_result["Location"], "/request/requirements/")
|
||||
num_pages_tested += 1
|
||||
|
||||
# ---- REQUIREMENTS PAGE ----
|
||||
# Follow the redirect to the next form page
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
requirements_page = anything_else_result.follow()
|
||||
requirements_page = additional_details_result.follow()
|
||||
requirements_form = requirements_page.forms[0]
|
||||
|
||||
requirements_form["requirements-is_policy_acknowledged"] = True
|
||||
|
@ -434,6 +440,7 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
self.assertContains(review_page, "Another Tester")
|
||||
self.assertContains(review_page, "testy2@town.com")
|
||||
self.assertContains(review_page, "(201) 555-5557")
|
||||
self.assertContains(review_page, "FakeEmail@gmail.com")
|
||||
self.assertContains(review_page, "Nothing else.")
|
||||
|
||||
# We can't test the modal itself as it relies on JS for init and triggering,
|
||||
|
@ -717,13 +724,25 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
|
||||
self.assertContains(contact_page, self.TITLES[Step.ABOUT_YOUR_ORGANIZATION])
|
||||
|
||||
def test_yes_no_form_inits_blank_for_new_domain_request(self):
|
||||
def test_yes_no_contact_form_inits_blank_for_new_domain_request(self):
|
||||
"""On the Other Contacts page, the yes/no form gets initialized with nothing selected for
|
||||
new domain requests"""
|
||||
other_contacts_page = self.app.get(reverse("domain-request:other_contacts"))
|
||||
other_contacts_form = other_contacts_page.forms[0]
|
||||
self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, None)
|
||||
|
||||
def test_yes_no_additional_form_inits_blank_for_new_domain_request(self):
|
||||
"""On the Additional Details page, the yes/no form gets initialized with nothing selected for
|
||||
new domain requests"""
|
||||
additional_details_page = self.app.get(reverse("domain-request:additional_details"))
|
||||
additional_form = additional_details_page.forms[0]
|
||||
|
||||
# Check the cisa representative yes/no field
|
||||
self.assertEquals(additional_form["additional_details-has_cisa_representative"].value, None)
|
||||
|
||||
# Check the anything else yes/no field
|
||||
self.assertEquals(additional_form["additional_details-has_anything_else_text"].value, None)
|
||||
|
||||
def test_yes_no_form_inits_yes_for_domain_request_with_other_contacts(self):
|
||||
"""On the Other Contacts page, the yes/no form gets initialized with YES selected if the
|
||||
domain request has other contacts"""
|
||||
|
@ -744,6 +763,38 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
other_contacts_form = other_contacts_page.forms[0]
|
||||
self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, "True")
|
||||
|
||||
def test_yes_no_form_inits_yes_for_cisa_representative_and_anything_else(self):
|
||||
"""On the Additional Details page, the yes/no form gets initialized with YES selected
|
||||
for both yes/no radios if the domain request has a value for cisa_representative and
|
||||
anything_else"""
|
||||
|
||||
domain_request = completed_domain_request(user=self.user, has_anything_else=True)
|
||||
domain_request.cisa_representative_email = "test@igorville.gov"
|
||||
domain_request.anything_else = "1234"
|
||||
domain_request.save()
|
||||
|
||||
# prime the form by visiting /edit
|
||||
self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk}))
|
||||
# django-webtest does not handle cookie-based sessions well because it keeps
|
||||
# resetting the session key on each new request, thus destroying the concept
|
||||
# of a "session". We are going to do it manually, saving the session ID here
|
||||
# and then setting the cookie on each request.
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
additional_details_page = self.app.get(reverse("domain-request:additional_details"))
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
additional_details_form = additional_details_page.forms[0]
|
||||
|
||||
# Check the cisa representative yes/no field
|
||||
yes_no_cisa = additional_details_form["additional_details-has_cisa_representative"].value
|
||||
self.assertEquals(yes_no_cisa, "True")
|
||||
|
||||
# Check the anything else yes/no field
|
||||
yes_no_anything_else = additional_details_form["additional_details-has_anything_else_text"].value
|
||||
self.assertEquals(yes_no_anything_else, "True")
|
||||
|
||||
def test_yes_no_form_inits_no_for_domain_request_with_no_other_contacts_rationale(self):
|
||||
"""On the Other Contacts page, the yes/no form gets initialized with NO selected if the
|
||||
domain request has no other contacts"""
|
||||
|
@ -766,6 +817,230 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
other_contacts_form = other_contacts_page.forms[0]
|
||||
self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, "False")
|
||||
|
||||
def test_yes_no_form_for_domain_request_with_no_cisa_representative_and_anything_else(self):
|
||||
"""On the Additional details page, the form preselects "no" when has_cisa_representative
|
||||
and anything_else is no"""
|
||||
|
||||
domain_request = completed_domain_request(user=self.user, has_anything_else=False)
|
||||
|
||||
# Unlike the other contacts form, the no button is tracked with these boolean fields.
|
||||
# This means that we should expect this to correlate with the no button.
|
||||
domain_request.has_anything_else_text = False
|
||||
domain_request.has_cisa_representative = False
|
||||
domain_request.save()
|
||||
|
||||
# prime the form by visiting /edit
|
||||
self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk}))
|
||||
# django-webtest does not handle cookie-based sessions well because it keeps
|
||||
# resetting the session key on each new request, thus destroying the concept
|
||||
# of a "session". We are going to do it manually, saving the session ID here
|
||||
# and then setting the cookie on each request.
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
additional_details_page = self.app.get(reverse("domain-request:additional_details"))
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
additional_details_form = additional_details_page.forms[0]
|
||||
|
||||
# Check the cisa representative yes/no field
|
||||
yes_no_cisa = additional_details_form["additional_details-has_cisa_representative"].value
|
||||
self.assertEquals(yes_no_cisa, "False")
|
||||
|
||||
# Check the anything else yes/no field
|
||||
yes_no_anything_else = additional_details_form["additional_details-has_anything_else_text"].value
|
||||
self.assertEquals(yes_no_anything_else, "False")
|
||||
|
||||
def test_submitting_additional_details_deletes_cisa_representative_and_anything_else(self):
|
||||
"""When a user submits the Additional Details form with no selected for all fields,
|
||||
the domain request's data gets wiped when submitted"""
|
||||
domain_request = completed_domain_request(name="nocisareps.gov", user=self.user)
|
||||
domain_request.cisa_representative_email = "fake@faketown.gov"
|
||||
domain_request.save()
|
||||
|
||||
# Make sure we have the data we need for the test
|
||||
self.assertEqual(domain_request.anything_else, "There is more")
|
||||
self.assertEqual(domain_request.cisa_representative_email, "fake@faketown.gov")
|
||||
|
||||
# prime the form by visiting /edit
|
||||
self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk}))
|
||||
# django-webtest does not handle cookie-based sessions well because it keeps
|
||||
# resetting the session key on each new request, thus destroying the concept
|
||||
# of a "session". We are going to do it manually, saving the session ID here
|
||||
# and then setting the cookie on each request.
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
additional_details_page = self.app.get(reverse("domain-request:additional_details"))
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
additional_details_form = additional_details_page.forms[0]
|
||||
|
||||
# Check the cisa representative yes/no field
|
||||
yes_no_cisa = additional_details_form["additional_details-has_cisa_representative"].value
|
||||
self.assertEquals(yes_no_cisa, "True")
|
||||
|
||||
# Check the anything else yes/no field
|
||||
yes_no_anything_else = additional_details_form["additional_details-has_anything_else_text"].value
|
||||
self.assertEquals(yes_no_anything_else, "True")
|
||||
|
||||
# Set fields to false
|
||||
additional_details_form["additional_details-has_cisa_representative"] = "False"
|
||||
additional_details_form["additional_details-has_anything_else_text"] = "False"
|
||||
|
||||
# Submit the form
|
||||
additional_details_form.submit()
|
||||
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
# Verify that the anything_else and cisa_representative have been deleted from the DB
|
||||
domain_request = DomainRequest.objects.get(requested_domain__name="nocisareps.gov")
|
||||
|
||||
# Check that our data has been cleared
|
||||
self.assertEqual(domain_request.anything_else, None)
|
||||
self.assertEqual(domain_request.cisa_representative_email, None)
|
||||
|
||||
# Double check the yes/no fields
|
||||
self.assertEqual(domain_request.has_anything_else_text, False)
|
||||
self.assertEqual(domain_request.has_cisa_representative, False)
|
||||
|
||||
def test_submitting_additional_details_populates_cisa_representative_and_anything_else(self):
|
||||
"""When a user submits the Additional Details form,
|
||||
the domain request's data gets submitted"""
|
||||
domain_request = completed_domain_request(name="cisareps.gov", user=self.user, has_anything_else=False)
|
||||
|
||||
# Make sure we have the data we need for the test
|
||||
self.assertEqual(domain_request.anything_else, None)
|
||||
self.assertEqual(domain_request.cisa_representative_email, None)
|
||||
|
||||
# These fields should not be selected at all, since we haven't initialized the form yet
|
||||
self.assertEqual(domain_request.has_anything_else_text, None)
|
||||
self.assertEqual(domain_request.has_cisa_representative, None)
|
||||
|
||||
# prime the form by visiting /edit
|
||||
self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk}))
|
||||
# django-webtest does not handle cookie-based sessions well because it keeps
|
||||
# resetting the session key on each new request, thus destroying the concept
|
||||
# of a "session". We are going to do it manually, saving the session ID here
|
||||
# and then setting the cookie on each request.
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
additional_details_page = self.app.get(reverse("domain-request:additional_details"))
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
additional_details_form = additional_details_page.forms[0]
|
||||
|
||||
# Set fields to true, and set data on those fields
|
||||
additional_details_form["additional_details-has_cisa_representative"] = "True"
|
||||
additional_details_form["additional_details-has_anything_else_text"] = "True"
|
||||
additional_details_form["additional_details-cisa_representative_email"] = "test@faketest.gov"
|
||||
additional_details_form["additional_details-anything_else"] = "redandblue"
|
||||
|
||||
# Submit the form
|
||||
additional_details_form.submit()
|
||||
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
# Verify that the anything_else and cisa_representative exist in the db
|
||||
domain_request = DomainRequest.objects.get(requested_domain__name="cisareps.gov")
|
||||
|
||||
self.assertEqual(domain_request.anything_else, "redandblue")
|
||||
self.assertEqual(domain_request.cisa_representative_email, "test@faketest.gov")
|
||||
|
||||
self.assertEqual(domain_request.has_cisa_representative, True)
|
||||
self.assertEqual(domain_request.has_anything_else_text, True)
|
||||
|
||||
def test_if_cisa_representative_yes_no_form_is_yes_then_field_is_required(self):
|
||||
"""Applicants with a cisa representative must provide a value"""
|
||||
domain_request = completed_domain_request(name="cisareps.gov", user=self.user, has_anything_else=False)
|
||||
|
||||
# prime the form by visiting /edit
|
||||
self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk}))
|
||||
# django-webtest does not handle cookie-based sessions well because it keeps
|
||||
# resetting the session key on each new request, thus destroying the concept
|
||||
# of a "session". We are going to do it manually, saving the session ID here
|
||||
# and then setting the cookie on each request.
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
additional_details_page = self.app.get(reverse("domain-request:additional_details"))
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
additional_details_form = additional_details_page.forms[0]
|
||||
|
||||
# Set fields to true, and set data on those fields
|
||||
additional_details_form["additional_details-has_cisa_representative"] = "True"
|
||||
additional_details_form["additional_details-has_anything_else_text"] = "False"
|
||||
|
||||
# Submit the form
|
||||
response = additional_details_form.submit()
|
||||
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
self.assertContains(response, "Enter the email address of your CISA regional representative.")
|
||||
|
||||
def test_if_anything_else_yes_no_form_is_yes_then_field_is_required(self):
|
||||
"""Applicants with a anything else must provide a value"""
|
||||
domain_request = completed_domain_request(name="cisareps.gov", user=self.user, has_anything_else=False)
|
||||
|
||||
# prime the form by visiting /edit
|
||||
self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk}))
|
||||
# django-webtest does not handle cookie-based sessions well because it keeps
|
||||
# resetting the session key on each new request, thus destroying the concept
|
||||
# of a "session". We are going to do it manually, saving the session ID here
|
||||
# and then setting the cookie on each request.
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
additional_details_page = self.app.get(reverse("domain-request:additional_details"))
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
additional_details_form = additional_details_page.forms[0]
|
||||
|
||||
# Set fields to true, and set data on those fields
|
||||
additional_details_form["additional_details-has_cisa_representative"] = "False"
|
||||
additional_details_form["additional_details-has_anything_else_text"] = "True"
|
||||
|
||||
# Submit the form
|
||||
response = additional_details_form.submit()
|
||||
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
expected_message = "Provide additional details you’d like us to know. If you have nothing to add, select “No.”"
|
||||
self.assertContains(response, expected_message)
|
||||
|
||||
def test_additional_details_form_fields_required(self):
|
||||
"""When a user submits the Additional Details form without checking the
|
||||
has_cisa_representative and has_anything_else_text fields, the form should deny this action"""
|
||||
domain_request = completed_domain_request(name="cisareps.gov", user=self.user, has_anything_else=False)
|
||||
|
||||
self.assertEqual(domain_request.has_anything_else_text, None)
|
||||
self.assertEqual(domain_request.has_cisa_representative, None)
|
||||
|
||||
# prime the form by visiting /edit
|
||||
self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk}))
|
||||
# django-webtest does not handle cookie-based sessions well because it keeps
|
||||
# resetting the session key on each new request, thus destroying the concept
|
||||
# of a "session". We are going to do it manually, saving the session ID here
|
||||
# and then setting the cookie on each request.
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
additional_details_page = self.app.get(reverse("domain-request:additional_details"))
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
additional_details_form = additional_details_page.forms[0]
|
||||
|
||||
# Submit the form
|
||||
response = additional_details_form.submit()
|
||||
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
# We expect to see this twice for both fields. This results in a count of 4
|
||||
# due to screen reader information / html.
|
||||
self.assertContains(response, "This question is required.", count=4)
|
||||
|
||||
def test_submitting_other_contacts_deletes_no_other_contacts_rationale(self):
|
||||
"""When a user submits the Other Contacts form with other contacts selected, the domain request's
|
||||
no other contacts rationale gets deleted"""
|
||||
|
|
|
@ -45,7 +45,7 @@ class Step(StrEnum):
|
|||
PURPOSE = "purpose"
|
||||
YOUR_CONTACT = "your_contact"
|
||||
OTHER_CONTACTS = "other_contacts"
|
||||
ANYTHING_ELSE = "anything_else"
|
||||
ADDITIONAL_DETAILS = "additional_details"
|
||||
REQUIREMENTS = "requirements"
|
||||
REVIEW = "review"
|
||||
|
||||
|
@ -91,7 +91,7 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
|||
Step.PURPOSE: _("Purpose of your domain"),
|
||||
Step.YOUR_CONTACT: _("Your contact information"),
|
||||
Step.OTHER_CONTACTS: _("Other employees from your organization"),
|
||||
Step.ANYTHING_ELSE: _("Anything else?"),
|
||||
Step.ADDITIONAL_DETAILS: _("Additional details"),
|
||||
Step.REQUIREMENTS: _("Requirements for operating a .gov domain"),
|
||||
Step.REVIEW: _("Review and submit your domain request"),
|
||||
}
|
||||
|
@ -365,8 +365,9 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
|||
self.domain_request.other_contacts.exists()
|
||||
or self.domain_request.no_other_contacts_rationale is not None
|
||||
),
|
||||
"anything_else": (
|
||||
self.domain_request.anything_else is not None or self.domain_request.is_policy_acknowledged is not None
|
||||
"additional_details": (
|
||||
(self.domain_request.anything_else is not None and self.domain_request.cisa_representative_email)
|
||||
or self.domain_request.is_policy_acknowledged is not None
|
||||
),
|
||||
"requirements": self.domain_request.is_policy_acknowledged is not None,
|
||||
"review": self.domain_request.is_policy_acknowledged is not None,
|
||||
|
@ -581,9 +582,64 @@ class OtherContacts(DomainRequestWizard):
|
|||
return all_forms_valid
|
||||
|
||||
|
||||
class AnythingElse(DomainRequestWizard):
|
||||
template_name = "domain_request_anything_else.html"
|
||||
forms = [forms.AnythingElseForm]
|
||||
class AdditionalDetails(DomainRequestWizard):
|
||||
|
||||
template_name = "domain_request_additional_details.html"
|
||||
|
||||
forms = [
|
||||
forms.CisaRepresentativeYesNoForm,
|
||||
forms.CisaRepresentativeForm,
|
||||
forms.AdditionalDetailsYesNoForm,
|
||||
forms.AdditionalDetailsForm,
|
||||
]
|
||||
|
||||
def is_valid(self, forms: list) -> bool:
|
||||
|
||||
# Validate Cisa Representative
|
||||
"""Overrides default behavior defined in DomainRequestWizard.
|
||||
Depending on value in yes_no forms, marks corresponding data
|
||||
for deletion. Then validates all forms.
|
||||
"""
|
||||
cisa_representative_email_yes_no_form = forms[0]
|
||||
cisa_representative_email_form = forms[1]
|
||||
anything_else_yes_no_form = forms[2]
|
||||
anything_else_form = forms[3]
|
||||
|
||||
# ------- Validate cisa representative -------
|
||||
cisa_rep_portion_is_valid = True
|
||||
# test first for yes_no_form validity
|
||||
if cisa_representative_email_yes_no_form.is_valid():
|
||||
# test for existing data
|
||||
if not cisa_representative_email_yes_no_form.cleaned_data.get("has_cisa_representative"):
|
||||
# mark the cisa_representative_email_form for deletion
|
||||
cisa_representative_email_form.mark_form_for_deletion()
|
||||
else:
|
||||
cisa_rep_portion_is_valid = cisa_representative_email_form.is_valid()
|
||||
else:
|
||||
# if yes no form is invalid, no choice has been made
|
||||
# mark the cisa_representative_email_form for deletion
|
||||
cisa_representative_email_form.mark_form_for_deletion()
|
||||
cisa_rep_portion_is_valid = False
|
||||
|
||||
# ------- Validate anything else -------
|
||||
anything_else_portion_is_valid = True
|
||||
# test first for yes_no_form validity
|
||||
if anything_else_yes_no_form.is_valid():
|
||||
# test for existing data
|
||||
if not anything_else_yes_no_form.cleaned_data.get("has_anything_else_text"):
|
||||
# mark the anything_else_form for deletion
|
||||
anything_else_form.mark_form_for_deletion()
|
||||
else:
|
||||
anything_else_portion_is_valid = anything_else_form.is_valid()
|
||||
else:
|
||||
# if yes no form is invalid, no choice has been made
|
||||
# mark the anything_else_form for deletion
|
||||
anything_else_form.mark_form_for_deletion()
|
||||
anything_else_portion_is_valid = False
|
||||
|
||||
# ------- Return combined validation result -------
|
||||
all_forms_valid = cisa_rep_portion_is_valid and anything_else_portion_is_valid
|
||||
return all_forms_valid
|
||||
|
||||
|
||||
class Requirements(DomainRequestWizard):
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue