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
|
- pa11y
|
||||||
|
|
||||||
owasp:
|
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
|
command: zap-baseline.py -t http://app:8080 -c zap.conf -I -r zap_report.html
|
||||||
volumes:
|
volumes:
|
||||||
- .:/zap/wrk/
|
- .:/zap/wrk/
|
||||||
|
|
|
@ -1229,7 +1229,17 @@ class DomainRequestAdmin(ListHeaderAdmin):
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
(".gov domain", {"fields": ["requested_domain", "alternative_domains"]}),
|
(".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"]}),
|
("Background info", {"fields": ["purpose", "anything_else", "current_websites"]}),
|
||||||
(
|
(
|
||||||
"Type of organization",
|
"Type of organization",
|
||||||
|
@ -1302,6 +1312,7 @@ class DomainRequestAdmin(ListHeaderAdmin):
|
||||||
"no_other_contacts_rationale",
|
"no_other_contacts_rationale",
|
||||||
"anything_else",
|
"anything_else",
|
||||||
"is_policy_acknowledged",
|
"is_policy_acknowledged",
|
||||||
|
"cisa_representative_email",
|
||||||
]
|
]
|
||||||
autocomplete_fields = [
|
autocomplete_fields = [
|
||||||
"approved_domain",
|
"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
|
/** 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 (){
|
(function (){
|
||||||
let rejectionReasonFormGroup = document.querySelector('.field-rejection_reason')
|
let rejectionReasonFormGroup = document.querySelector('.field-rejection_reason')
|
||||||
|
|
|
@ -193,6 +193,65 @@ function clearValidators(el) {
|
||||||
toggleInputValidity(el, true);
|
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.
|
// 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
|
* An IIFE that listens to the other contacts radio form on DAs and toggles the contacts/no other contacts forms
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
(function otherContactsFormListener() {
|
(function otherContactsFormListener() {
|
||||||
// Get the radio buttons
|
HookupYesNoListener("other_contacts-has_other_contacts",'other-employees', 'no-other-employees')
|
||||||
let radioButtons = document.querySelectorAll('input[name="other_contacts-has_other_contacts"]');
|
})();
|
||||||
|
|
||||||
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':
|
* An IIFE that disables the delete buttons on nameserver forms on page load if < 3 forms
|
||||||
toggleTwoDomElements('other-employees', 'no-other-employees', 1);
|
*
|
||||||
break;
|
*/
|
||||||
|
(function nameserversFormListener() {
|
||||||
case 'False':
|
let isNameserversForm = document.querySelector(".nameservers-form");
|
||||||
toggleTwoDomElements('other-employees', 'no-other-employees', 2);
|
if (isNameserversForm) {
|
||||||
break;
|
let forms = document.querySelectorAll(".repeatable-form");
|
||||||
|
if (forms.length < 3) {
|
||||||
default:
|
// Hide the delete buttons on the 2 nameservers
|
||||||
toggleTwoDomElements('other-employees', 'no-other-employees', 0);
|
forms.forEach((form) => {
|
||||||
}
|
Array.from(form.querySelectorAll('.delete-record')).forEach((deleteButton) => {
|
||||||
}
|
deleteButton.setAttribute("disabled", "true");
|
||||||
|
|
||||||
if (radioButtons.length) {
|
|
||||||
// Add event listener to each radio button
|
|
||||||
radioButtons.forEach(function (radioButton) {
|
|
||||||
radioButton.addEventListener('change', handleRadioButtonChange);
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
// 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.PURPOSE, views.Purpose),
|
||||||
(Step.YOUR_CONTACT, views.YourContact),
|
(Step.YOUR_CONTACT, views.YourContact),
|
||||||
(Step.OTHER_CONTACTS, views.OtherContacts),
|
(Step.OTHER_CONTACTS, views.OtherContacts),
|
||||||
(Step.ANYTHING_ELSE, views.AnythingElse),
|
(Step.ADDITIONAL_DETAILS, views.AdditionalDetails),
|
||||||
(Step.REQUIREMENTS, views.Requirements),
|
(Step.REQUIREMENTS, views.Requirements),
|
||||||
(Step.REVIEW, views.Review),
|
(Step.REVIEW, views.Review),
|
||||||
]:
|
]:
|
||||||
|
|
|
@ -93,6 +93,12 @@ class UserFixture:
|
||||||
"last_name": "Chin",
|
"last_name": "Chin",
|
||||||
"email": "szu.chin@associates.cisa.dhs.gov",
|
"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",
|
"username": "012f844d-8a0f-4225-9d82-cbf87bff1d3e",
|
||||||
"first_name": "Riley",
|
"first_name": "Riley",
|
||||||
|
@ -169,6 +175,12 @@ class UserFixture:
|
||||||
"last_name": "Chin-Analyst",
|
"last_name": "Chin-Analyst",
|
||||||
"email": "szu.chin@ecstech.com",
|
"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",
|
"username": "d9839768-0c17-4fa2-9c8e-36291eef5c11",
|
||||||
"first_name": "Alex-Analyst",
|
"first_name": "Alex-Analyst",
|
||||||
|
|
|
@ -1,15 +1,18 @@
|
||||||
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,
|
||||||
|
BaseDeletableRegistrarForm,
|
||||||
|
)
|
||||||
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 +20,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 +440,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):
|
||||||
|
@ -779,7 +627,7 @@ OtherContactsFormSet = forms.formset_factory(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class NoOtherContactsForm(RegistrarForm):
|
class NoOtherContactsForm(BaseDeletableRegistrarForm):
|
||||||
no_other_contacts_rationale = forms.CharField(
|
no_other_contacts_rationale = forms.CharField(
|
||||||
required=True,
|
required=True,
|
||||||
# label has to end in a space to get the label_suffix to show
|
# 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.")},
|
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):
|
class CisaRepresentativeForm(BaseDeletableRegistrarForm):
|
||||||
"""Marks no_other_contacts form for deletion.
|
cisa_representative_email = forms.EmailField(
|
||||||
This changes behavior of validity checks and to_database
|
required=True,
|
||||||
methods."""
|
max_length=None,
|
||||||
self.form_data_marked_for_deletion = True
|
label="Your representative’s email",
|
||||||
|
validators=[
|
||||||
def clean(self):
|
MaxLengthValidator(
|
||||||
"""
|
320,
|
||||||
This method overrides the default behavior for forms.
|
message="Response must be less than 320 characters.",
|
||||||
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.
|
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."),
|
||||||
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 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(
|
anything_else = forms.CharField(
|
||||||
required=False,
|
required=True,
|
||||||
label="Anything else?",
|
label="Anything else?",
|
||||||
widget=forms.Textarea(),
|
widget=forms.Textarea(),
|
||||||
validators=[
|
validators=[
|
||||||
|
@ -855,7 +679,20 @@ class AnythingElseForm(RegistrarForm):
|
||||||
message="Response must be less than 2000 characters.",
|
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):
|
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(
|
anything_else = models.TextField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=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(
|
is_policy_acknowledged = models.BooleanField(
|
||||||
|
|
|
@ -651,6 +651,32 @@ class DomainRequest(TimeStampedModel):
|
||||||
anything_else = models.TextField(
|
anything_else = models.TextField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=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(
|
is_policy_acknowledged = models.BooleanField(
|
||||||
|
@ -705,8 +731,33 @@ class DomainRequest(TimeStampedModel):
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
"""Save override for custom properties"""
|
"""Save override for custom properties"""
|
||||||
self.sync_organization_type()
|
self.sync_organization_type()
|
||||||
|
self.sync_yes_no_form_fields()
|
||||||
|
|
||||||
super().save(*args, **kwargs)
|
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):
|
def __str__(self):
|
||||||
try:
|
try:
|
||||||
if self.requested_domain and self.requested_domain.name:
|
if self.requested_domain and self.requested_domain.name:
|
||||||
|
@ -1045,6 +1096,16 @@ class DomainRequest(TimeStampedModel):
|
||||||
"""Does this domain request have other contacts listed?"""
|
"""Does this domain request have other contacts listed?"""
|
||||||
return self.other_contacts.exists()
|
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]:
|
def is_federal(self) -> Union[bool, None]:
|
||||||
"""Is this domain request for a federal agency?
|
"""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 %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
{% if step == Step.ANYTHING_ELSE %}
|
{% if step == Step.ADDITIONAL_DETAILS %}
|
||||||
{% namespaced_url 'domain-request' step as domain_request_url %}
|
{% namespaced_url 'domain-request' step as domain_request_url %}
|
||||||
{% with title=form_titles|get_item:step value=domain_request.anything_else|default:"No" %}
|
{% with title=form_titles|get_item:step value=domain_request.requested_domain.name|default:"Incomplete" %}
|
||||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
|
{% 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 %}
|
{% 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 %}
|
{% 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 %}
|
{% include "includes/summary_item.html" with title='Other employees from your organization' value=DomainRequest.no_other_contacts_rationale heading_level=heading_level %}
|
||||||
{% endif %}
|
{% 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 %}
|
{% endwith %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -20,6 +20,9 @@
|
||||||
</{{ heading_level }}>
|
</{{ heading_level }}>
|
||||||
{% else %}
|
{% else %}
|
||||||
</h2>
|
</h2>
|
||||||
|
{% endif %}
|
||||||
|
{% if sub_header_text %}
|
||||||
|
<h3 class="register-form-review-header">{{ sub_header_text }}</h3>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if address %}
|
{% if address %}
|
||||||
{% include "includes/organization_address.html" with organization=value %}
|
{% include "includes/organization_address.html" with organization=value %}
|
||||||
|
@ -39,6 +42,10 @@
|
||||||
</dd>
|
</dd>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</dl>
|
</dl>
|
||||||
|
{% elif custom_text_for_value_none %}
|
||||||
|
<p>
|
||||||
|
{{ custom_text_for_value_none }}
|
||||||
|
</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p>
|
<p>
|
||||||
None
|
None
|
||||||
|
@ -92,6 +99,8 @@
|
||||||
<p class="margin-top-0 margin-bottom-0">
|
<p class="margin-top-0 margin-bottom-0">
|
||||||
{% if value %}
|
{% if value %}
|
||||||
{{ value }}
|
{{ value }}
|
||||||
|
{% elif custom_text_for_value_none %}
|
||||||
|
{{ custom_text_for_value_none }}
|
||||||
{% else %}
|
{% else %}
|
||||||
None
|
None
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -2034,6 +2034,9 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
"purpose",
|
"purpose",
|
||||||
"no_other_contacts_rationale",
|
"no_other_contacts_rationale",
|
||||||
"anything_else",
|
"anything_else",
|
||||||
|
"has_anything_else_text",
|
||||||
|
"cisa_representative_email",
|
||||||
|
"has_cisa_representative",
|
||||||
"is_policy_acknowledged",
|
"is_policy_acknowledged",
|
||||||
"submission_date",
|
"submission_date",
|
||||||
"notes",
|
"notes",
|
||||||
|
@ -2065,6 +2068,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
"no_other_contacts_rationale",
|
"no_other_contacts_rationale",
|
||||||
"anything_else",
|
"anything_else",
|
||||||
"is_policy_acknowledged",
|
"is_policy_acknowledged",
|
||||||
|
"cisa_representative_email",
|
||||||
]
|
]
|
||||||
|
|
||||||
self.assertEqual(readonly_fields, expected_fields)
|
self.assertEqual(readonly_fields, expected_fields)
|
||||||
|
|
|
@ -15,7 +15,7 @@ from registrar.forms.domain_request_wizard import (
|
||||||
RequirementsForm,
|
RequirementsForm,
|
||||||
TribalGovernmentForm,
|
TribalGovernmentForm,
|
||||||
PurposeForm,
|
PurposeForm,
|
||||||
AnythingElseForm,
|
AdditionalDetailsForm,
|
||||||
AboutYourOrganizationForm,
|
AboutYourOrganizationForm,
|
||||||
)
|
)
|
||||||
from registrar.forms.domain import ContactForm
|
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):
|
def test_anything_else_form_about_your_organization_character_count_invalid(self):
|
||||||
"""Response must be less than 2000 characters."""
|
"""Response must be less than 2000 characters."""
|
||||||
form = AnythingElseForm(
|
form = AdditionalDetailsForm(
|
||||||
data={
|
data={
|
||||||
"anything_else": "Bacon ipsum dolor amet fatback strip steak pastrami"
|
"anything_else": "Bacon ipsum dolor amet fatback strip steak pastrami"
|
||||||
"shankle, drumstick doner chicken landjaeger turkey andouille."
|
"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 post request should return a redirect to the next form in
|
||||||
# the domain request page
|
# the domain request page
|
||||||
self.assertEqual(other_contacts_result.status_code, 302)
|
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
|
num_pages_tested += 1
|
||||||
|
|
||||||
# ---- ANYTHING ELSE PAGE ----
|
# ---- ADDITIONAL DETAILS PAGE ----
|
||||||
# Follow the redirect to the next form page
|
# Follow the redirect to the next form page
|
||||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
anything_else_page = other_contacts_result.follow()
|
additional_details_page = other_contacts_result.follow()
|
||||||
anything_else_form = anything_else_page.forms[0]
|
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
|
# test next button
|
||||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
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
|
# validate that data from this step are being saved
|
||||||
domain_request = DomainRequest.objects.get() # there's only one
|
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.")
|
self.assertEqual(domain_request.anything_else, "Nothing else.")
|
||||||
# the post request should return a redirect to the next form in
|
# the post request should return a redirect to the next form in
|
||||||
# the domain request page
|
# the domain request page
|
||||||
self.assertEqual(anything_else_result.status_code, 302)
|
self.assertEqual(additional_details_result.status_code, 302)
|
||||||
self.assertEqual(anything_else_result["Location"], "/request/requirements/")
|
self.assertEqual(additional_details_result["Location"], "/request/requirements/")
|
||||||
num_pages_tested += 1
|
num_pages_tested += 1
|
||||||
|
|
||||||
# ---- REQUIREMENTS PAGE ----
|
# ---- REQUIREMENTS PAGE ----
|
||||||
# Follow the redirect to the next form page
|
# Follow the redirect to the next form page
|
||||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
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_page.forms[0]
|
||||||
|
|
||||||
requirements_form["requirements-is_policy_acknowledged"] = True
|
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, "Another Tester")
|
||||||
self.assertContains(review_page, "testy2@town.com")
|
self.assertContains(review_page, "testy2@town.com")
|
||||||
self.assertContains(review_page, "(201) 555-5557")
|
self.assertContains(review_page, "(201) 555-5557")
|
||||||
|
self.assertContains(review_page, "FakeEmail@gmail.com")
|
||||||
self.assertContains(review_page, "Nothing else.")
|
self.assertContains(review_page, "Nothing else.")
|
||||||
|
|
||||||
# We can't test the modal itself as it relies on JS for init and triggering,
|
# 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])
|
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
|
"""On the Other Contacts page, the yes/no form gets initialized with nothing selected for
|
||||||
new domain requests"""
|
new domain requests"""
|
||||||
other_contacts_page = self.app.get(reverse("domain-request:other_contacts"))
|
other_contacts_page = self.app.get(reverse("domain-request:other_contacts"))
|
||||||
other_contacts_form = other_contacts_page.forms[0]
|
other_contacts_form = other_contacts_page.forms[0]
|
||||||
self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, None)
|
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):
|
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
|
"""On the Other Contacts page, the yes/no form gets initialized with YES selected if the
|
||||||
domain request has other contacts"""
|
domain request has other contacts"""
|
||||||
|
@ -744,6 +763,38 @@ class DomainRequestTests(TestWithUser, WebTest):
|
||||||
other_contacts_form = other_contacts_page.forms[0]
|
other_contacts_form = other_contacts_page.forms[0]
|
||||||
self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, "True")
|
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):
|
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
|
"""On the Other Contacts page, the yes/no form gets initialized with NO selected if the
|
||||||
domain request has no other contacts"""
|
domain request has no other contacts"""
|
||||||
|
@ -766,6 +817,230 @@ class DomainRequestTests(TestWithUser, WebTest):
|
||||||
other_contacts_form = other_contacts_page.forms[0]
|
other_contacts_form = other_contacts_page.forms[0]
|
||||||
self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, "False")
|
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):
|
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
|
"""When a user submits the Other Contacts form with other contacts selected, the domain request's
|
||||||
no other contacts rationale gets deleted"""
|
no other contacts rationale gets deleted"""
|
||||||
|
|
|
@ -45,7 +45,7 @@ class Step(StrEnum):
|
||||||
PURPOSE = "purpose"
|
PURPOSE = "purpose"
|
||||||
YOUR_CONTACT = "your_contact"
|
YOUR_CONTACT = "your_contact"
|
||||||
OTHER_CONTACTS = "other_contacts"
|
OTHER_CONTACTS = "other_contacts"
|
||||||
ANYTHING_ELSE = "anything_else"
|
ADDITIONAL_DETAILS = "additional_details"
|
||||||
REQUIREMENTS = "requirements"
|
REQUIREMENTS = "requirements"
|
||||||
REVIEW = "review"
|
REVIEW = "review"
|
||||||
|
|
||||||
|
@ -91,7 +91,7 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
||||||
Step.PURPOSE: _("Purpose of your domain"),
|
Step.PURPOSE: _("Purpose of your domain"),
|
||||||
Step.YOUR_CONTACT: _("Your contact information"),
|
Step.YOUR_CONTACT: _("Your contact information"),
|
||||||
Step.OTHER_CONTACTS: _("Other employees from your organization"),
|
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.REQUIREMENTS: _("Requirements for operating a .gov domain"),
|
||||||
Step.REVIEW: _("Review and submit your domain request"),
|
Step.REVIEW: _("Review and submit your domain request"),
|
||||||
}
|
}
|
||||||
|
@ -365,8 +365,9 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
||||||
self.domain_request.other_contacts.exists()
|
self.domain_request.other_contacts.exists()
|
||||||
or self.domain_request.no_other_contacts_rationale is not None
|
or self.domain_request.no_other_contacts_rationale is not None
|
||||||
),
|
),
|
||||||
"anything_else": (
|
"additional_details": (
|
||||||
self.domain_request.anything_else is not None or self.domain_request.is_policy_acknowledged is not None
|
(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,
|
"requirements": self.domain_request.is_policy_acknowledged is not None,
|
||||||
"review": 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
|
return all_forms_valid
|
||||||
|
|
||||||
|
|
||||||
class AnythingElse(DomainRequestWizard):
|
class AdditionalDetails(DomainRequestWizard):
|
||||||
template_name = "domain_request_anything_else.html"
|
|
||||||
forms = [forms.AnythingElseForm]
|
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):
|
class Requirements(DomainRequestWizard):
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue