mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-04 02:03:32 +02:00
Merge pull request #1613 from cisagov/dk/903-delete-other-contacts
Issue 903: Delete other contacts (DK sandbox)
This commit is contained in:
commit
66cf92e0dd
9 changed files with 503 additions and 151 deletions
|
@ -229,24 +229,12 @@ function handleValidationClick(e) {
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prepare the namerservers and DS data forms delete buttons
|
* Delete method for formsets that diff in the view and delete in the model (Nameservers, DS Data)
|
||||||
* We will call this on the forms init, and also every time we add a form
|
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
function prepareDeleteButtons(formLabel) {
|
function removeForm(e, formLabel, isNameserversForm, addButton, formIdentifier){
|
||||||
let deleteButtons = document.querySelectorAll(".delete-record");
|
let totalForms = document.querySelector(`#id_${formIdentifier}-TOTAL_FORMS`);
|
||||||
let totalForms = document.querySelector("#id_form-TOTAL_FORMS");
|
|
||||||
let isNameserversForm = document.title.includes("DNS name servers |");
|
|
||||||
let addButton = document.querySelector("#add-form");
|
|
||||||
|
|
||||||
// Loop through each delete button and attach the click event listener
|
|
||||||
deleteButtons.forEach((deleteButton) => {
|
|
||||||
deleteButton.addEventListener('click', removeForm);
|
|
||||||
});
|
|
||||||
|
|
||||||
function removeForm(e){
|
|
||||||
let formToRemove = e.target.closest(".repeatable-form");
|
let formToRemove = e.target.closest(".repeatable-form");
|
||||||
formToRemove.remove();
|
formToRemove.remove();
|
||||||
let forms = document.querySelectorAll(".repeatable-form");
|
let forms = document.querySelectorAll(".repeatable-form");
|
||||||
|
@ -309,7 +297,6 @@ function prepareDeleteButtons(formLabel) {
|
||||||
|
|
||||||
// Display the add more button if we have less than 13 forms
|
// Display the add more button if we have less than 13 forms
|
||||||
if (isNameserversForm && forms.length <= 13) {
|
if (isNameserversForm && forms.length <= 13) {
|
||||||
console.log('remove disabled');
|
|
||||||
addButton.removeAttribute("disabled");
|
addButton.removeAttribute("disabled");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -322,6 +309,123 @@ function prepareDeleteButtons(formLabel) {
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete method for formsets using the DJANGO DELETE widget (Other Contacts)
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
function markForm(e, formLabel){
|
||||||
|
// Unlike removeForm, we only work with the visible forms when using DJANGO's DELETE widget
|
||||||
|
let totalShownForms = document.querySelectorAll(`.repeatable-form:not([style*="display: none"])`).length;
|
||||||
|
|
||||||
|
if (totalShownForms == 1) {
|
||||||
|
// toggle the radio buttons
|
||||||
|
let radioButton = document.querySelector('input[name="other_contacts-has_other_contacts"][value="False"]');
|
||||||
|
radioButton.checked = true;
|
||||||
|
// Trigger the change event
|
||||||
|
let event = new Event('change');
|
||||||
|
radioButton.dispatchEvent(event);
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// Grab the hidden delete input and assign a value DJANGO will look for
|
||||||
|
let formToRemove = e.target.closest(".repeatable-form");
|
||||||
|
if (formToRemove) {
|
||||||
|
let deleteInput = formToRemove.querySelector('input[class="deletion"]');
|
||||||
|
if (deleteInput) {
|
||||||
|
deleteInput.value = 'on';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set display to 'none'
|
||||||
|
formToRemove.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update h2s on the visible forms only. We won't worry about the forms' identifiers
|
||||||
|
let shownForms = document.querySelectorAll(`.repeatable-form:not([style*="display: none"])`);
|
||||||
|
let formLabelRegex = RegExp(`${formLabel} (\\d+){1}`, 'g');
|
||||||
|
shownForms.forEach((form, index) => {
|
||||||
|
// Iterate over child nodes of the current element
|
||||||
|
Array.from(form.querySelectorAll('h2')).forEach((node) => {
|
||||||
|
node.textContent = node.textContent.replace(formLabelRegex, `${formLabel} ${index + 1}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare the namerservers, DS data and Other Contacts formsets' delete button
|
||||||
|
* for the last added form. We call this from the Add function
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
function prepareNewDeleteButton(btn, formLabel) {
|
||||||
|
let formIdentifier = "form"
|
||||||
|
let isNameserversForm = document.querySelector(".nameservers-form");
|
||||||
|
let isOtherContactsForm = document.querySelector(".other-contacts-form");
|
||||||
|
let addButton = document.querySelector("#add-form");
|
||||||
|
|
||||||
|
if (isOtherContactsForm) {
|
||||||
|
formIdentifier = "other_contacts";
|
||||||
|
// We will mark the forms for deletion
|
||||||
|
btn.addEventListener('click', function(e) {
|
||||||
|
markForm(e, formLabel);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// We will remove the forms and re-order the formset
|
||||||
|
btn.addEventListener('click', function(e) {
|
||||||
|
removeForm(e, formLabel, isNameserversForm, addButton, formIdentifier);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare the namerservers, DS data and Other Contacts formsets' delete buttons
|
||||||
|
* We will call this on the forms init
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
function prepareDeleteButtons(formLabel) {
|
||||||
|
let formIdentifier = "form"
|
||||||
|
let deleteButtons = document.querySelectorAll(".delete-record");
|
||||||
|
let isNameserversForm = document.querySelector(".nameservers-form");
|
||||||
|
let isOtherContactsForm = document.querySelector(".other-contacts-form");
|
||||||
|
let addButton = document.querySelector("#add-form");
|
||||||
|
if (isOtherContactsForm) {
|
||||||
|
formIdentifier = "other_contacts";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loop through each delete button and attach the click event listener
|
||||||
|
deleteButtons.forEach((deleteButton) => {
|
||||||
|
if (isOtherContactsForm) {
|
||||||
|
// We will mark the forms for deletion
|
||||||
|
deleteButton.addEventListener('click', function(e) {
|
||||||
|
markForm(e, formLabel);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// We will remove the forms and re-order the formset
|
||||||
|
deleteButton.addEventListener('click', function(e) {
|
||||||
|
removeForm(e, formLabel, isNameserversForm, addButton, formIdentifier);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DJANGO formset's DELETE widget
|
||||||
|
* On form load, hide deleted forms, ie. those forms with hidden input of class 'deletion'
|
||||||
|
* with value='on'
|
||||||
|
*/
|
||||||
|
function hideDeletedForms() {
|
||||||
|
let hiddenDeleteButtonsWithValueOn = document.querySelectorAll('input[type="hidden"].deletion[value="on"]');
|
||||||
|
|
||||||
|
// Iterating over the NodeList of hidden inputs
|
||||||
|
hiddenDeleteButtonsWithValueOn.forEach(function(hiddenInput) {
|
||||||
|
// Finding the closest parent element with class "repeatable-form" for each hidden input
|
||||||
|
var repeatableFormToHide = hiddenInput.closest('.repeatable-form');
|
||||||
|
|
||||||
|
// Checking if a matching parent element is found for each hidden input
|
||||||
|
if (repeatableFormToHide) {
|
||||||
|
// Setting the display property to "none" for each matching parent element
|
||||||
|
repeatableFormToHide.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -331,25 +435,38 @@ function prepareDeleteButtons(formLabel) {
|
||||||
* it everywhere.
|
* it everywhere.
|
||||||
*/
|
*/
|
||||||
(function prepareFormsetsForms() {
|
(function prepareFormsetsForms() {
|
||||||
|
let formIdentifier = "form"
|
||||||
let repeatableForm = document.querySelectorAll(".repeatable-form");
|
let repeatableForm = document.querySelectorAll(".repeatable-form");
|
||||||
let container = document.querySelector("#form-container");
|
let container = document.querySelector("#form-container");
|
||||||
let addButton = document.querySelector("#add-form");
|
let addButton = document.querySelector("#add-form");
|
||||||
let totalForms = document.querySelector("#id_form-TOTAL_FORMS");
|
|
||||||
let cloneIndex = 0;
|
let cloneIndex = 0;
|
||||||
let formLabel = '';
|
let formLabel = '';
|
||||||
let isNameserversForm = document.title.includes("DNS name servers |");
|
let isNameserversForm = document.querySelector(".nameservers-form");
|
||||||
|
let isOtherContactsForm = document.querySelector(".other-contacts-form");
|
||||||
|
let isDsDataForm = document.querySelector(".ds-data-form");
|
||||||
|
// The Nameservers formset features 2 required and 11 optionals
|
||||||
if (isNameserversForm) {
|
if (isNameserversForm) {
|
||||||
cloneIndex = 2;
|
cloneIndex = 2;
|
||||||
formLabel = "Name server";
|
formLabel = "Name server";
|
||||||
} else if ((document.title.includes("DS Data |")) || (document.title.includes("Key Data |"))) {
|
// DNSSEC: DS Data
|
||||||
formLabel = "DS Data record";
|
} else if (isDsDataForm) {
|
||||||
|
formLabel = "DS data record";
|
||||||
|
// The Other Contacts form
|
||||||
|
} else if (isOtherContactsForm) {
|
||||||
|
formLabel = "Organization contact";
|
||||||
|
container = document.querySelector("#other-employees");
|
||||||
|
formIdentifier = "other_contacts"
|
||||||
}
|
}
|
||||||
|
let totalForms = document.querySelector(`#id_${formIdentifier}-TOTAL_FORMS`);
|
||||||
|
|
||||||
// On load: Disable the add more button if we have 13 forms
|
// On load: Disable the add more button if we have 13 forms
|
||||||
if (isNameserversForm && document.querySelectorAll(".repeatable-form").length == 13) {
|
if (isNameserversForm && document.querySelectorAll(".repeatable-form").length == 13) {
|
||||||
addButton.setAttribute("disabled", "true");
|
addButton.setAttribute("disabled", "true");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hide forms which have previously been deleted
|
||||||
|
hideDeletedForms()
|
||||||
|
|
||||||
// Attach click event listener on the delete buttons of the existing forms
|
// Attach click event listener on the delete buttons of the existing forms
|
||||||
prepareDeleteButtons(formLabel);
|
prepareDeleteButtons(formLabel);
|
||||||
|
|
||||||
|
@ -360,7 +477,7 @@ function prepareDeleteButtons(formLabel) {
|
||||||
let forms = document.querySelectorAll(".repeatable-form");
|
let forms = document.querySelectorAll(".repeatable-form");
|
||||||
let formNum = forms.length;
|
let formNum = forms.length;
|
||||||
let newForm = repeatableForm[cloneIndex].cloneNode(true);
|
let newForm = repeatableForm[cloneIndex].cloneNode(true);
|
||||||
let formNumberRegex = RegExp(`form-(\\d){1}-`,'g');
|
let formNumberRegex = RegExp(`${formIdentifier}-(\\d){1}-`,'g');
|
||||||
let formLabelRegex = RegExp(`${formLabel} (\\d){1}`, 'g');
|
let formLabelRegex = RegExp(`${formLabel} (\\d){1}`, 'g');
|
||||||
// For the eample on Nameservers
|
// For the eample on Nameservers
|
||||||
let formExampleRegex = RegExp(`ns(\\d){1}`, 'g');
|
let formExampleRegex = RegExp(`ns(\\d){1}`, 'g');
|
||||||
|
@ -393,16 +510,27 @@ function prepareDeleteButtons(formLabel) {
|
||||||
}
|
}
|
||||||
|
|
||||||
formNum++;
|
formNum++;
|
||||||
newForm.innerHTML = newForm.innerHTML.replace(formNumberRegex, `form-${formNum-1}-`);
|
|
||||||
|
newForm.innerHTML = newForm.innerHTML.replace(formNumberRegex, `${formIdentifier}-${formNum-1}-`);
|
||||||
|
// For the other contacts form, we need to update the fieldset headers based on what's visible vs hidden,
|
||||||
|
// since the form on the backend employs Django's DELETE widget. For the other formsets, we delete the form
|
||||||
|
// in JS (completely remove from teh DOM) so we update the headers/labels based on total number of forms.
|
||||||
|
if (isOtherContactsForm) {
|
||||||
|
let totalShownForms = document.querySelectorAll(`.repeatable-form:not([style*="display: none"])`).length;
|
||||||
|
newForm.innerHTML = newForm.innerHTML.replace(formLabelRegex, `${formLabel} ${totalShownForms + 1}`);
|
||||||
|
} else {
|
||||||
newForm.innerHTML = newForm.innerHTML.replace(formLabelRegex, `${formLabel} ${formNum}`);
|
newForm.innerHTML = newForm.innerHTML.replace(formLabelRegex, `${formLabel} ${formNum}`);
|
||||||
|
}
|
||||||
newForm.innerHTML = newForm.innerHTML.replace(formExampleRegex, `ns${formNum}`);
|
newForm.innerHTML = newForm.innerHTML.replace(formExampleRegex, `ns${formNum}`);
|
||||||
container.insertBefore(newForm, addButton);
|
container.insertBefore(newForm, addButton);
|
||||||
|
|
||||||
|
newForm.style.display = 'block';
|
||||||
|
|
||||||
let inputs = newForm.querySelectorAll("input");
|
let inputs = newForm.querySelectorAll("input");
|
||||||
// Reset the values of each input to blank
|
// Reset the values of each input to blank
|
||||||
inputs.forEach((input) => {
|
inputs.forEach((input) => {
|
||||||
input.classList.remove("usa-input--error");
|
input.classList.remove("usa-input--error");
|
||||||
if (input.type === "text" || input.type === "number" || input.type === "password") {
|
if (input.type === "text" || input.type === "number" || input.type === "password" || input.type === "email" || input.type === "tel") {
|
||||||
input.value = ""; // Set the value to an empty string
|
input.value = ""; // Set the value to an empty string
|
||||||
|
|
||||||
} else if (input.type === "checkbox" || input.type === "radio") {
|
} else if (input.type === "checkbox" || input.type === "radio") {
|
||||||
|
@ -439,7 +567,8 @@ function prepareDeleteButtons(formLabel) {
|
||||||
totalForms.setAttribute('value', `${formNum}`);
|
totalForms.setAttribute('value', `${formNum}`);
|
||||||
|
|
||||||
// Attach click event listener on the delete buttons of the new form
|
// Attach click event listener on the delete buttons of the new form
|
||||||
prepareDeleteButtons(formLabel);
|
let newDeleteButton = newForm.querySelector(".delete-record");
|
||||||
|
prepareNewDeleteButton(newDeleteButton, formLabel);
|
||||||
|
|
||||||
// Disable the add more button if we have 13 forms
|
// Disable the add more button if we have 13 forms
|
||||||
if (isNameserversForm && formNum == 13) {
|
if (isNameserversForm && formNum == 13) {
|
||||||
|
@ -484,6 +613,7 @@ function prepareDeleteButtons(formLabel) {
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
// A generic display none/block toggle function that takes an integer param to indicate how the elements toggle
|
||||||
function toggleTwoDomElements(ele1, ele2, index) {
|
function toggleTwoDomElements(ele1, ele2, index) {
|
||||||
let element1 = document.getElementById(ele1);
|
let element1 = document.getElementById(ele1);
|
||||||
let element2 = document.getElementById(ele2);
|
let element2 = document.getElementById(ele2);
|
||||||
|
|
|
@ -5,6 +5,10 @@
|
||||||
@include sr-only;
|
@include sr-only;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.clear-both {
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
|
|
@ -31,3 +31,10 @@
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
border-left: none;
|
border-left: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
legend.float-left-tablet + button.float-right-tablet {
|
||||||
|
margin-top: .5rem;
|
||||||
|
@include at-media('tablet') {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -177,7 +177,7 @@ class RegistrarFormSet(forms.BaseFormSet):
|
||||||
|
|
||||||
# no matching database object, create it
|
# no matching database object, create it
|
||||||
# make sure not to create a database object if cleaned has 'delete' attribute
|
# 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):
|
elif db_obj is None and cleaned and not cleaned.get("DELETE", False):
|
||||||
kwargs = pre_create(db_obj, cleaned)
|
kwargs = pre_create(db_obj, cleaned)
|
||||||
getattr(obj, join).create(**kwargs)
|
getattr(obj, join).create(**kwargs)
|
||||||
|
|
||||||
|
@ -609,9 +609,12 @@ class OtherContactsYesNoForm(RegistrarForm):
|
||||||
|
|
||||||
self.fields["has_other_contacts"] = forms.TypedChoiceField(
|
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
|
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).")),
|
choices=((True, "Yes, I can name other employees."), (False, "No. (We’ll ask you to explain why.)")),
|
||||||
initial=initial_value,
|
initial=initial_value,
|
||||||
widget=forms.RadioSelect,
|
widget=forms.RadioSelect,
|
||||||
|
error_messages={
|
||||||
|
"required": "This question is required.",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -638,7 +641,10 @@ class OtherContactsForm(RegistrarForm):
|
||||||
)
|
)
|
||||||
email = forms.EmailField(
|
email = forms.EmailField(
|
||||||
label="Email",
|
label="Email",
|
||||||
error_messages={"invalid": ("Enter an email address in the required format, like name@example.com.")},
|
error_messages={
|
||||||
|
"required": ("Enter an email address in the required format, like name@example.com."),
|
||||||
|
"invalid": ("Enter an email address in the required format, like name@example.com."),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
phone = PhoneNumberField(
|
phone = PhoneNumberField(
|
||||||
label="Phone",
|
label="Phone",
|
||||||
|
@ -649,8 +655,17 @@ class OtherContactsForm(RegistrarForm):
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Override the __init__ method for RegistrarForm.
|
||||||
|
Set form_data_marked_for_deletion to false.
|
||||||
|
Empty_permitted set to False, as this is overridden in certain circumstances by
|
||||||
|
Django's BaseFormSet, and results in empty forms being allowed and field level
|
||||||
|
errors not appropriately raised. This works with code in the view which appropriately
|
||||||
|
displays required attributes on fields.
|
||||||
|
"""
|
||||||
self.form_data_marked_for_deletion = False
|
self.form_data_marked_for_deletion = False
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
self.empty_permitted = False
|
||||||
|
|
||||||
def mark_form_for_deletion(self):
|
def mark_form_for_deletion(self):
|
||||||
self.form_data_marked_for_deletion = True
|
self.form_data_marked_for_deletion = True
|
||||||
|
@ -659,12 +674,11 @@ class OtherContactsForm(RegistrarForm):
|
||||||
"""
|
"""
|
||||||
This method overrides the default behavior for forms.
|
This method overrides the default behavior for forms.
|
||||||
This cleans the form after field validation has already taken place.
|
This cleans the form after field validation has already taken place.
|
||||||
In this override, allow for a form which is empty to be considered
|
In this override, allow for a form which is deleted by user or marked for
|
||||||
valid even though certain required fields have not passed field
|
deletion by formset to be considered valid even though certain required fields have
|
||||||
validation
|
not passed field validation
|
||||||
"""
|
"""
|
||||||
|
if self.form_data_marked_for_deletion or self.cleaned_data.get("DELETE"):
|
||||||
if self.form_data_marked_for_deletion:
|
|
||||||
# clear any errors raised by the form fields
|
# clear any errors raised by the form fields
|
||||||
# (before this clean() method is run, each field
|
# (before this clean() method is run, each field
|
||||||
# performs its own clean, which could result in
|
# performs its own clean, which could result in
|
||||||
|
@ -678,12 +692,26 @@ class OtherContactsForm(RegistrarForm):
|
||||||
# return empty object with only 'delete' attribute defined.
|
# return empty object with only 'delete' attribute defined.
|
||||||
# this will prevent _to_database from creating an empty
|
# this will prevent _to_database from creating an empty
|
||||||
# database object
|
# database object
|
||||||
return {"delete": True}
|
return {"DELETE": True}
|
||||||
|
|
||||||
return self.cleaned_data
|
return self.cleaned_data
|
||||||
|
|
||||||
|
|
||||||
class BaseOtherContactsFormSet(RegistrarFormSet):
|
class BaseOtherContactsFormSet(RegistrarFormSet):
|
||||||
|
"""
|
||||||
|
FormSet for Other Contacts
|
||||||
|
|
||||||
|
There are two conditions by which a form in the formset can be marked for deletion.
|
||||||
|
One is if the user clicks 'DELETE' button, and this is submitted in the form. The
|
||||||
|
other is if the YesNo form, which is submitted with this formset, is set to No; in
|
||||||
|
this case, all forms in formset are marked for deletion. Both of these conditions
|
||||||
|
must co-exist.
|
||||||
|
Also, other_contacts have db relationships to multiple db objects. When attempting
|
||||||
|
to delete an other_contact from an application, those db relationships must be
|
||||||
|
tested and handled; this is configured with REVERSE_JOINS, which is an array of
|
||||||
|
strings representing the relationships between contact model and other models.
|
||||||
|
"""
|
||||||
|
|
||||||
JOIN = "other_contacts"
|
JOIN = "other_contacts"
|
||||||
REVERSE_JOINS = [
|
REVERSE_JOINS = [
|
||||||
"user",
|
"user",
|
||||||
|
@ -695,7 +723,13 @@ class BaseOtherContactsFormSet(RegistrarFormSet):
|
||||||
"contact_applications_information",
|
"contact_applications_information",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def get_deletion_widget(self):
|
||||||
|
return forms.HiddenInput(attrs={"class": "deletion"})
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Override __init__ for RegistrarFormSet.
|
||||||
|
"""
|
||||||
self.formset_data_marked_for_deletion = False
|
self.formset_data_marked_for_deletion = False
|
||||||
self.application = kwargs.pop("application", None)
|
self.application = kwargs.pop("application", None)
|
||||||
super(RegistrarFormSet, self).__init__(*args, **kwargs)
|
super(RegistrarFormSet, self).__init__(*args, **kwargs)
|
||||||
|
@ -706,8 +740,17 @@ class BaseOtherContactsFormSet(RegistrarFormSet):
|
||||||
self.forms[index].use_required_attribute = True
|
self.forms[index].use_required_attribute = True
|
||||||
|
|
||||||
def should_delete(self, cleaned):
|
def should_delete(self, cleaned):
|
||||||
empty = (isinstance(v, str) and (v.strip() == "" or v is None) for v in cleaned.values())
|
"""
|
||||||
return all(empty) or self.formset_data_marked_for_deletion
|
Implements should_delete method from BaseFormSet.
|
||||||
|
"""
|
||||||
|
return self.formset_data_marked_for_deletion or cleaned.get("DELETE", False)
|
||||||
|
|
||||||
|
def pre_create(self, db_obj, cleaned):
|
||||||
|
"""Code to run before an item in the formset is created in the database."""
|
||||||
|
# remove DELETE from cleaned
|
||||||
|
if "DELETE" in cleaned:
|
||||||
|
cleaned.pop("DELETE")
|
||||||
|
return cleaned
|
||||||
|
|
||||||
def to_database(self, obj: DomainApplication):
|
def to_database(self, obj: DomainApplication):
|
||||||
self._to_database(obj, self.JOIN, self.REVERSE_JOINS, self.should_delete, self.pre_update, self.pre_create)
|
self._to_database(obj, self.JOIN, self.REVERSE_JOINS, self.should_delete, self.pre_update, self.pre_create)
|
||||||
|
@ -736,9 +779,10 @@ class BaseOtherContactsFormSet(RegistrarFormSet):
|
||||||
|
|
||||||
OtherContactsFormSet = forms.formset_factory(
|
OtherContactsFormSet = forms.formset_factory(
|
||||||
OtherContactsForm,
|
OtherContactsForm,
|
||||||
extra=1,
|
extra=0,
|
||||||
absolute_max=1500, # django default; use `max_num` to limit entries
|
absolute_max=1500, # django default; use `max_num` to limit entries
|
||||||
min_num=1,
|
min_num=1,
|
||||||
|
can_delete=True,
|
||||||
validate_min=True,
|
validate_min=True,
|
||||||
formset=BaseOtherContactsFormSet,
|
formset=BaseOtherContactsFormSet,
|
||||||
)
|
)
|
||||||
|
@ -748,11 +792,7 @@ class NoOtherContactsForm(RegistrarForm):
|
||||||
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
|
||||||
label=(
|
label=("No other employees rationale"),
|
||||||
"You don’t need to provide names of other employees now, but it may "
|
|
||||||
"slow down our assessment of your eligibility. Describe why there are "
|
|
||||||
"no other employees who can help verify your request."
|
|
||||||
),
|
|
||||||
widget=forms.Textarea(),
|
widget=forms.Textarea(),
|
||||||
validators=[
|
validators=[
|
||||||
MaxLengthValidator(
|
MaxLengthValidator(
|
||||||
|
|
|
@ -29,17 +29,31 @@
|
||||||
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<div id="other-employees">
|
<div id="other-employees" class="other-contacts-form">
|
||||||
{% include "includes/required_fields.html" %}
|
{% include "includes/required_fields.html" %}
|
||||||
{{ forms.1.management_form }}
|
{{ forms.1.management_form }}
|
||||||
{# forms.1 is a formset and this iterates over its forms #}
|
{# forms.1 is a formset and this iterates over its forms #}
|
||||||
{% for form in forms.1.forms %}
|
{% for form in forms.1.forms %}
|
||||||
<fieldset class="usa-fieldset">
|
<fieldset class="usa-fieldset repeatable-form padding-y-1">
|
||||||
<legend>
|
|
||||||
<h2>Organization contact {{ forloop.counter }} (optional)</h2>
|
<legend class="float-left-tablet">
|
||||||
|
<h2 class="margin-top-1">Organization contact {{ forloop.counter }}</h2>
|
||||||
</legend>
|
</legend>
|
||||||
|
|
||||||
|
<button type="button" class="usa-button usa-button--unstyled display-block float-right-tablet delete-record margin-bottom-2">
|
||||||
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
|
<use xlink:href="{%static 'img/sprite.svg'%}#delete"></use>
|
||||||
|
</svg><span class="margin-left-05">Delete</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
|
||||||
|
{% if forms.1.can_delete %}
|
||||||
|
{{ form.DELETE }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="clear-both">
|
||||||
{% input_with_errors form.first_name %}
|
{% input_with_errors form.first_name %}
|
||||||
|
</div>
|
||||||
|
|
||||||
{% input_with_errors form.middle_name %}
|
{% input_with_errors form.middle_name %}
|
||||||
|
|
||||||
|
@ -62,7 +76,7 @@
|
||||||
</fieldset>
|
</fieldset>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
<button type="submit" name="submit_button" value="save" class="usa-button usa-button--unstyled">
|
<button type="button" class="usa-button usa-button--unstyled" id="add-form">
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
<use xlink:href="{%static 'img/sprite.svg'%}#add_circle"></use>
|
<use xlink:href="{%static 'img/sprite.svg'%}#add_circle"></use>
|
||||||
</svg><span class="margin-left-05">Add another contact</span>
|
</svg><span class="margin-left-05">Add another contact</span>
|
||||||
|
@ -70,10 +84,13 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="no-other-employees">
|
<div id="no-other-employees">
|
||||||
<fieldset class="usa-fieldset margin-top-2">
|
<fieldset class="usa-fieldset margin-top-4">
|
||||||
<legend>
|
<legend>
|
||||||
<h2>No other employees from your organization?</h2>
|
<h2 class="margin-bottom-0">No other employees from your organization?</h2>
|
||||||
</legend>
|
</legend>
|
||||||
|
<p>You don't need to provide names of other employees now, but it may
|
||||||
|
slow down our assessment of your eligibility. Describe why there are
|
||||||
|
no other employees who can help verify your request.</p>
|
||||||
{% with attr_maxlength=1000 add_label_class="usa-sr-only" %}
|
{% with attr_maxlength=1000 add_label_class="usa-sr-only" %}
|
||||||
{% input_with_errors forms.2.no_other_contacts_rationale %}
|
{% input_with_errors forms.2.no_other_contacts_rationale %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
|
|
||||||
{% include "includes/required_fields.html" %}
|
{% include "includes/required_fields.html" %}
|
||||||
|
|
||||||
<form class="usa-form usa-form--extra-large" method="post" novalidate id="form-container">
|
<form class="usa-form usa-form--extra-large ds-data-form" method="post" novalidate id="form-container">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ formset.management_form }}
|
{{ formset.management_form }}
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
|
|
||||||
{% include "includes/required_fields.html" %}
|
{% include "includes/required_fields.html" %}
|
||||||
|
|
||||||
<form class="usa-form usa-form--extra-large" method="post" novalidate id="form-container">
|
<form class="usa-form usa-form--extra-large nameservers-form" method="post" novalidate id="form-container">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ formset.management_form }}
|
{{ formset.management_form }}
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ from django.conf import settings
|
||||||
from django.test import Client, TestCase
|
from django.test import Client, TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
from .common import MockEppLib, MockSESClient, completed_application, create_user # type: ignore
|
from .common import MockEppLib, MockSESClient, completed_application, create_user # type: ignore
|
||||||
from django_webtest import WebTest # type: ignore
|
from django_webtest import WebTest # type: ignore
|
||||||
import boto3_mocking # type: ignore
|
import boto3_mocking # type: ignore
|
||||||
|
@ -950,7 +951,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
||||||
def test_submitting_no_other_contacts_rationale_removes_reference_other_contacts_when_joined(self):
|
def test_submitting_no_other_contacts_rationale_removes_reference_other_contacts_when_joined(self):
|
||||||
"""When a user submits the Other Contacts form with no other contacts selected, the application's
|
"""When a user submits the Other Contacts form with no other contacts selected, the application's
|
||||||
other contacts references get removed for other contacts that exist and are joined to other objects"""
|
other contacts references get removed for other contacts that exist and are joined to other objects"""
|
||||||
# Populate the databse with a domain application that
|
# Populate the database with a domain application that
|
||||||
# has 1 "other contact" assigned to it
|
# has 1 "other contact" assigned to it
|
||||||
# We'll do it from scratch so we can reuse the other contact
|
# We'll do it from scratch so we can reuse the other contact
|
||||||
ao, _ = Contact.objects.get_or_create(
|
ao, _ = Contact.objects.get_or_create(
|
||||||
|
@ -1072,31 +1073,115 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
||||||
# Assert that it is returned, ie the contacts form is required
|
# Assert that it is returned, ie the contacts form is required
|
||||||
self.assertContains(response, "Enter the first name / given name of this contact.")
|
self.assertContains(response, "Enter the first name / given name of this contact.")
|
||||||
|
|
||||||
@skip("Repurpose when working on ticket 903")
|
def test_delete_other_contact(self):
|
||||||
def test_application_delete_other_contact(self):
|
"""Other contacts can be deleted after being saved to database.
|
||||||
"""Other contacts can be deleted after being saved to database."""
|
|
||||||
# Populate the databse with a domain application that
|
This formset uses the DJANGO DELETE widget. We'll test that by setting 2 contacts on an application,
|
||||||
# has 1 "other contact" assigned to it
|
loading the form and marking one contact up for deletion."""
|
||||||
|
# Populate the database with a domain application that
|
||||||
|
# has 2 "other contact" assigned to it
|
||||||
|
# We'll do it from scratch so we can reuse the other contact
|
||||||
ao, _ = Contact.objects.get_or_create(
|
ao, _ = Contact.objects.get_or_create(
|
||||||
first_name="Testy",
|
first_name="Testy",
|
||||||
last_name="Tester",
|
last_name="Tester",
|
||||||
title="Chief Tester",
|
title="Chief Tester",
|
||||||
email="testy@town.com",
|
email="testy@town.com",
|
||||||
phone="(555) 555 5555",
|
phone="(201) 555 5555",
|
||||||
)
|
)
|
||||||
you, _ = Contact.objects.get_or_create(
|
you, _ = Contact.objects.get_or_create(
|
||||||
first_name="Testy you",
|
first_name="Testy you",
|
||||||
last_name="Tester you",
|
last_name="Tester you",
|
||||||
title="Admin Tester",
|
title="Admin Tester",
|
||||||
email="testy-admin@town.com",
|
email="testy-admin@town.com",
|
||||||
phone="(555) 555 5556",
|
phone="(201) 555 5556",
|
||||||
)
|
)
|
||||||
other, _ = Contact.objects.get_or_create(
|
other, _ = Contact.objects.get_or_create(
|
||||||
first_name="Testy2",
|
first_name="Testy2",
|
||||||
last_name="Tester2",
|
last_name="Tester2",
|
||||||
title="Another Tester",
|
title="Another Tester",
|
||||||
email="testy2@town.com",
|
email="testy2@town.com",
|
||||||
phone="(555) 555 5557",
|
phone="(201) 555 5557",
|
||||||
|
)
|
||||||
|
other2, _ = Contact.objects.get_or_create(
|
||||||
|
first_name="Testy3",
|
||||||
|
last_name="Tester3",
|
||||||
|
title="Another Tester",
|
||||||
|
email="testy3@town.com",
|
||||||
|
phone="(201) 555 5557",
|
||||||
|
)
|
||||||
|
application, _ = DomainApplication.objects.get_or_create(
|
||||||
|
organization_type="federal",
|
||||||
|
federal_type="executive",
|
||||||
|
purpose="Purpose of the site",
|
||||||
|
anything_else="No",
|
||||||
|
is_policy_acknowledged=True,
|
||||||
|
organization_name="Testorg",
|
||||||
|
address_line1="address 1",
|
||||||
|
state_territory="NY",
|
||||||
|
zipcode="10002",
|
||||||
|
authorizing_official=ao,
|
||||||
|
submitter=you,
|
||||||
|
creator=self.user,
|
||||||
|
status="started",
|
||||||
|
)
|
||||||
|
application.other_contacts.add(other)
|
||||||
|
application.other_contacts.add(other2)
|
||||||
|
|
||||||
|
# prime the form by visiting /edit
|
||||||
|
self.app.get(reverse("edit-application", kwargs={"id": application.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)
|
||||||
|
|
||||||
|
other_contacts_page = self.app.get(reverse("application:other_contacts"))
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
|
||||||
|
other_contacts_form = other_contacts_page.forms[0]
|
||||||
|
|
||||||
|
# Minimal check to ensure the form is loaded with both other contacts
|
||||||
|
self.assertEqual(other_contacts_form["other_contacts-0-first_name"].value, "Testy2")
|
||||||
|
self.assertEqual(other_contacts_form["other_contacts-1-first_name"].value, "Testy3")
|
||||||
|
|
||||||
|
# Mark the first dude for deletion
|
||||||
|
other_contacts_form.set("other_contacts-0-DELETE", "on")
|
||||||
|
|
||||||
|
# Submit the form
|
||||||
|
other_contacts_form.submit()
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
|
||||||
|
# Verify that the first dude was deleted
|
||||||
|
application = DomainApplication.objects.get()
|
||||||
|
self.assertEqual(application.other_contacts.count(), 1)
|
||||||
|
self.assertEqual(application.other_contacts.first().first_name, "Testy3")
|
||||||
|
|
||||||
|
def test_delete_other_contact_does_not_allow_zero_contacts(self):
|
||||||
|
"""Delete Other Contact does not allow submission with zero contacts."""
|
||||||
|
# Populate the database with a domain application that
|
||||||
|
# has 1 "other contact" assigned to it
|
||||||
|
# We'll do it from scratch so we can reuse the other contact
|
||||||
|
ao, _ = Contact.objects.get_or_create(
|
||||||
|
first_name="Testy",
|
||||||
|
last_name="Tester",
|
||||||
|
title="Chief Tester",
|
||||||
|
email="testy@town.com",
|
||||||
|
phone="(201) 555 5555",
|
||||||
|
)
|
||||||
|
you, _ = Contact.objects.get_or_create(
|
||||||
|
first_name="Testy you",
|
||||||
|
last_name="Tester you",
|
||||||
|
title="Admin Tester",
|
||||||
|
email="testy-admin@town.com",
|
||||||
|
phone="(201) 555 5556",
|
||||||
|
)
|
||||||
|
other, _ = Contact.objects.get_or_create(
|
||||||
|
first_name="Testy2",
|
||||||
|
last_name="Tester2",
|
||||||
|
title="Another Tester",
|
||||||
|
email="testy2@town.com",
|
||||||
|
phone="(201) 555 5557",
|
||||||
)
|
)
|
||||||
application, _ = DomainApplication.objects.get_or_create(
|
application, _ = DomainApplication.objects.get_or_create(
|
||||||
organization_type="federal",
|
organization_type="federal",
|
||||||
|
@ -1129,35 +1214,97 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
||||||
|
|
||||||
other_contacts_form = other_contacts_page.forms[0]
|
other_contacts_form = other_contacts_page.forms[0]
|
||||||
|
|
||||||
# Minimal check to ensure the form is loaded with data (if this part of
|
# Minimal check to ensure the form is loaded
|
||||||
# the application doesn't work, we should be equipped with other unit
|
|
||||||
# tests to flag it)
|
|
||||||
self.assertEqual(other_contacts_form["other_contacts-0-first_name"].value, "Testy2")
|
self.assertEqual(other_contacts_form["other_contacts-0-first_name"].value, "Testy2")
|
||||||
|
|
||||||
# clear the form
|
# Mark the first dude for deletion
|
||||||
other_contacts_form["other_contacts-0-first_name"] = ""
|
other_contacts_form.set("other_contacts-0-DELETE", "on")
|
||||||
other_contacts_form["other_contacts-0-middle_name"] = ""
|
|
||||||
other_contacts_form["other_contacts-0-last_name"] = ""
|
|
||||||
other_contacts_form["other_contacts-0-title"] = ""
|
|
||||||
other_contacts_form["other_contacts-0-email"] = ""
|
|
||||||
other_contacts_form["other_contacts-0-phone"] = ""
|
|
||||||
|
|
||||||
# Submit the now empty form
|
# Submit the form
|
||||||
result = other_contacts_form.submit()
|
other_contacts_form.submit()
|
||||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
|
||||||
# Verify that the contact we saved earlier has been removed from the database
|
# Verify that the contact was not deleted
|
||||||
application = DomainApplication.objects.get() # There are no contacts anymore
|
application = DomainApplication.objects.get()
|
||||||
self.assertEqual(
|
self.assertEqual(application.other_contacts.count(), 1)
|
||||||
application.other_contacts.count(),
|
self.assertEqual(application.other_contacts.first().first_name, "Testy2")
|
||||||
0,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Verify that on submit, user is advanced to "no contacts" page
|
def test_delete_other_contact_sets_visible_empty_form_as_required_after_failed_submit(self):
|
||||||
no_contacts_page = result.follow()
|
"""When you:
|
||||||
expected_url_slug = str(Step.NO_OTHER_CONTACTS)
|
1. add an empty contact,
|
||||||
actual_url_slug = no_contacts_page.request.path.split("/")[-2]
|
2. delete existing contacts,
|
||||||
self.assertEqual(expected_url_slug, actual_url_slug)
|
3. then submit,
|
||||||
|
The forms on page reload shows all the required fields and their errors."""
|
||||||
|
|
||||||
|
# Populate the database with a domain application that
|
||||||
|
# has 1 "other contact" assigned to it
|
||||||
|
# We'll do it from scratch so we can reuse the other contact
|
||||||
|
ao, _ = Contact.objects.get_or_create(
|
||||||
|
first_name="Testy",
|
||||||
|
last_name="Tester",
|
||||||
|
title="Chief Tester",
|
||||||
|
email="testy@town.com",
|
||||||
|
phone="(201) 555 5555",
|
||||||
|
)
|
||||||
|
you, _ = Contact.objects.get_or_create(
|
||||||
|
first_name="Testy you",
|
||||||
|
last_name="Tester you",
|
||||||
|
title="Admin Tester",
|
||||||
|
email="testy-admin@town.com",
|
||||||
|
phone="(201) 555 5556",
|
||||||
|
)
|
||||||
|
other, _ = Contact.objects.get_or_create(
|
||||||
|
first_name="Testy2",
|
||||||
|
last_name="Tester2",
|
||||||
|
title="Another Tester",
|
||||||
|
email="testy2@town.com",
|
||||||
|
phone="(201) 555 5557",
|
||||||
|
)
|
||||||
|
application, _ = DomainApplication.objects.get_or_create(
|
||||||
|
organization_type="federal",
|
||||||
|
federal_type="executive",
|
||||||
|
purpose="Purpose of the site",
|
||||||
|
anything_else="No",
|
||||||
|
is_policy_acknowledged=True,
|
||||||
|
organization_name="Testorg",
|
||||||
|
address_line1="address 1",
|
||||||
|
state_territory="NY",
|
||||||
|
zipcode="10002",
|
||||||
|
authorizing_official=ao,
|
||||||
|
submitter=you,
|
||||||
|
creator=self.user,
|
||||||
|
status="started",
|
||||||
|
)
|
||||||
|
application.other_contacts.add(other)
|
||||||
|
|
||||||
|
# prime the form by visiting /edit
|
||||||
|
self.app.get(reverse("edit-application", kwargs={"id": application.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)
|
||||||
|
|
||||||
|
other_contacts_page = self.app.get(reverse("application:other_contacts"))
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
|
||||||
|
other_contacts_form = other_contacts_page.forms[0]
|
||||||
|
|
||||||
|
# Minimal check to ensure the form is loaded
|
||||||
|
self.assertEqual(other_contacts_form["other_contacts-0-first_name"].value, "Testy2")
|
||||||
|
|
||||||
|
# Set total forms to 2 indicating an additional formset was added.
|
||||||
|
# Submit no data though for the second formset.
|
||||||
|
# Set the first formset to be deleted.
|
||||||
|
other_contacts_form["other_contacts-TOTAL_FORMS"] = "2"
|
||||||
|
other_contacts_form.set("other_contacts-0-DELETE", "on")
|
||||||
|
|
||||||
|
response = other_contacts_form.submit()
|
||||||
|
|
||||||
|
# Assert that the response presents errors to the user, including to
|
||||||
|
# Enter the first name ...
|
||||||
|
self.assertContains(response, "Enter the first name / given name of this contact.")
|
||||||
|
|
||||||
def test_application_about_your_organiztion_interstate(self):
|
def test_application_about_your_organiztion_interstate(self):
|
||||||
"""Special districts have to answer an additional question."""
|
"""Special districts have to answer an additional question."""
|
||||||
|
|
|
@ -498,6 +498,13 @@ class OtherContacts(ApplicationWizard):
|
||||||
other_contacts_forms = forms[1]
|
other_contacts_forms = forms[1]
|
||||||
no_other_contacts_form = forms[2]
|
no_other_contacts_form = forms[2]
|
||||||
|
|
||||||
|
# set all the required other_contact fields as necessary since new forms
|
||||||
|
# were added through javascript
|
||||||
|
for form in forms[1].forms:
|
||||||
|
for field_item, field in form.fields.items():
|
||||||
|
if field.required:
|
||||||
|
field.widget.attrs["required"] = "required"
|
||||||
|
|
||||||
all_forms_valid = True
|
all_forms_valid = True
|
||||||
# test first for yes_no_form validity
|
# test first for yes_no_form validity
|
||||||
if other_contacts_yes_no_form.is_valid():
|
if other_contacts_yes_no_form.is_valid():
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue