Merge pull request #1613 from cisagov/dk/903-delete-other-contacts

Issue 903: Delete other contacts (DK sandbox)
This commit is contained in:
rachidatecs 2024-01-16 18:10:25 -05:00 committed by GitHub
commit 66cf92e0dd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 503 additions and 151 deletions

View file

@ -229,99 +229,203 @@ function handleValidationClick(e) {
}
})();
/**
* Delete method for formsets that diff in the view and delete in the model (Nameservers, DS Data)
*
*/
function removeForm(e, formLabel, isNameserversForm, addButton, formIdentifier){
let totalForms = document.querySelector(`#id_${formIdentifier}-TOTAL_FORMS`);
let formToRemove = e.target.closest(".repeatable-form");
formToRemove.remove();
let forms = document.querySelectorAll(".repeatable-form");
totalForms.setAttribute('value', `${forms.length}`);
let formNumberRegex = RegExp(`form-(\\d){1}-`, 'g');
let formLabelRegex = RegExp(`${formLabel} (\\d+){1}`, 'g');
// For the example on Nameservers
let formExampleRegex = RegExp(`ns(\\d+){1}`, 'g');
forms.forEach((form, index) => {
// Iterate over child nodes of the current element
Array.from(form.querySelectorAll('label, input, select')).forEach((node) => {
// Iterate through the attributes of the current node
Array.from(node.attributes).forEach((attr) => {
// Check if the attribute value matches the regex
if (formNumberRegex.test(attr.value)) {
// Replace the attribute value with the updated value
attr.value = attr.value.replace(formNumberRegex, `form-${index}-`);
}
});
});
// h2 and legend for DS form, label for nameservers
Array.from(form.querySelectorAll('h2, legend, label, p')).forEach((node) => {
// If the node is a nameserver label, one of the first 2 which was previously 3 and up (not required)
// inject the USWDS required markup and make sure the INPUT is required
if (isNameserversForm && index <= 1 && node.innerHTML.includes('server') && !node.innerHTML.includes('*')) {
// Create a new element
const newElement = document.createElement('abbr');
newElement.textContent = '*';
newElement.setAttribute("title", "required");
newElement.classList.add("usa-hint", "usa-hint--required");
// Append the new element to the label
node.appendChild(newElement);
// Find the next sibling that is an input element
let nextInputElement = node.nextElementSibling;
while (nextInputElement) {
if (nextInputElement.tagName === 'INPUT') {
// Found the next input element
nextInputElement.setAttribute("required", "")
break;
}
nextInputElement = nextInputElement.nextElementSibling;
}
nextInputElement.required = true;
}
let innerSpan = node.querySelector('span')
if (innerSpan) {
innerSpan.textContent = innerSpan.textContent.replace(formLabelRegex, `${formLabel} ${index + 1}`);
} else {
node.textContent = node.textContent.replace(formLabelRegex, `${formLabel} ${index + 1}`);
node.textContent = node.textContent.replace(formExampleRegex, `ns${index + 1}`);
}
});
// Display the add more button if we have less than 13 forms
if (isNameserversForm && forms.length <= 13) {
addButton.removeAttribute("disabled");
}
if (isNameserversForm && forms.length < 3) {
// Hide the delete buttons on the remaining nameservers
Array.from(form.querySelectorAll('.delete-record')).forEach((deleteButton) => {
deleteButton.setAttribute("disabled", "true");
});
}
});
}
/**
* Prepare the namerservers and DS data forms delete buttons
* We will call this on the forms init, and also every time we add a form
* 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 totalForms = document.querySelector("#id_form-TOTAL_FORMS");
let isNameserversForm = document.title.includes("DNS name servers |");
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) => {
deleteButton.addEventListener('click', removeForm);
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);
});
}
});
}
function removeForm(e){
let formToRemove = e.target.closest(".repeatable-form");
formToRemove.remove();
let forms = document.querySelectorAll(".repeatable-form");
totalForms.setAttribute('value', `${forms.length}`);
/**
* 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"]');
let formNumberRegex = RegExp(`form-(\\d){1}-`, 'g');
let formLabelRegex = RegExp(`${formLabel} (\\d+){1}`, 'g');
// For the example on Nameservers
let formExampleRegex = RegExp(`ns(\\d+){1}`, 'g');
// 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');
forms.forEach((form, index) => {
// Iterate over child nodes of the current element
Array.from(form.querySelectorAll('label, input, select')).forEach((node) => {
// Iterate through the attributes of the current node
Array.from(node.attributes).forEach((attr) => {
// Check if the attribute value matches the regex
if (formNumberRegex.test(attr.value)) {
// Replace the attribute value with the updated value
attr.value = attr.value.replace(formNumberRegex, `form-${index}-`);
}
});
});
// h2 and legend for DS form, label for nameservers
Array.from(form.querySelectorAll('h2, legend, label, p')).forEach((node) => {
// If the node is a nameserver label, one of the first 2 which was previously 3 and up (not required)
// inject the USWDS required markup and make sure the INPUT is required
if (isNameserversForm && index <= 1 && node.innerHTML.includes('server') && !node.innerHTML.includes('*')) {
// Create a new element
const newElement = document.createElement('abbr');
newElement.textContent = '*';
newElement.setAttribute("title", "required");
newElement.classList.add("usa-hint", "usa-hint--required");
// Append the new element to the label
node.appendChild(newElement);
// Find the next sibling that is an input element
let nextInputElement = node.nextElementSibling;
while (nextInputElement) {
if (nextInputElement.tagName === 'INPUT') {
// Found the next input element
nextInputElement.setAttribute("required", "")
break;
}
nextInputElement = nextInputElement.nextElementSibling;
}
nextInputElement.required = true;
}
let innerSpan = node.querySelector('span')
if (innerSpan) {
innerSpan.textContent = innerSpan.textContent.replace(formLabelRegex, `${formLabel} ${index + 1}`);
} else {
node.textContent = node.textContent.replace(formLabelRegex, `${formLabel} ${index + 1}`);
node.textContent = node.textContent.replace(formExampleRegex, `ns${index + 1}`);
}
});
// Display the add more button if we have less than 13 forms
if (isNameserversForm && forms.length <= 13) {
console.log('remove disabled');
addButton.removeAttribute("disabled");
// 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';
}
if (isNameserversForm && forms.length < 3) {
// Hide the delete buttons on the remaining nameservers
Array.from(form.querySelectorAll('.delete-record')).forEach((deleteButton) => {
deleteButton.setAttribute("disabled", "true");
});
}
});
}
});
}
/**
@ -331,25 +435,38 @@ function prepareDeleteButtons(formLabel) {
* it everywhere.
*/
(function prepareFormsetsForms() {
let formIdentifier = "form"
let repeatableForm = document.querySelectorAll(".repeatable-form");
let container = document.querySelector("#form-container");
let addButton = document.querySelector("#add-form");
let totalForms = document.querySelector("#id_form-TOTAL_FORMS");
let cloneIndex = 0;
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) {
cloneIndex = 2;
formLabel = "Name server";
} else if ((document.title.includes("DS Data |")) || (document.title.includes("Key Data |"))) {
formLabel = "DS Data record";
// DNSSEC: DS Data
} 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
if (isNameserversForm && document.querySelectorAll(".repeatable-form").length == 13) {
addButton.setAttribute("disabled", "true");
}
// Hide forms which have previously been deleted
hideDeletedForms()
// Attach click event listener on the delete buttons of the existing forms
prepareDeleteButtons(formLabel);
@ -360,7 +477,7 @@ function prepareDeleteButtons(formLabel) {
let forms = document.querySelectorAll(".repeatable-form");
let formNum = forms.length;
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');
// For the eample on Nameservers
let formExampleRegex = RegExp(`ns(\\d){1}`, 'g');
@ -393,16 +510,27 @@ function prepareDeleteButtons(formLabel) {
}
formNum++;
newForm.innerHTML = newForm.innerHTML.replace(formNumberRegex, `form-${formNum-1}-`);
newForm.innerHTML = newForm.innerHTML.replace(formLabelRegex, `${formLabel} ${formNum}`);
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(formExampleRegex, `ns${formNum}`);
container.insertBefore(newForm, addButton);
newForm.style.display = 'block';
let inputs = newForm.querySelectorAll("input");
// Reset the values of each input to blank
inputs.forEach((input) => {
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
} else if (input.type === "checkbox" || input.type === "radio") {
@ -439,7 +567,8 @@ function prepareDeleteButtons(formLabel) {
totalForms.setAttribute('value', `${formNum}`);
// 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
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) {
let element1 = document.getElementById(ele1);
let element2 = document.getElementById(ele2);

View file

@ -5,6 +5,10 @@
@include sr-only;
}
.clear-both {
clear: both;
}
* {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;

View file

@ -31,3 +31,10 @@
padding-left: 0;
border-left: none;
}
legend.float-left-tablet + button.float-right-tablet {
margin-top: .5rem;
@include at-media('tablet') {
margin-top: 1rem;
}
}

View file

@ -177,7 +177,7 @@ class RegistrarFormSet(forms.BaseFormSet):
# 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):
elif db_obj is None and cleaned and not cleaned.get("DELETE", False):
kwargs = pre_create(db_obj, cleaned)
getattr(obj, join).create(**kwargs)
@ -609,9 +609,12 @@ class OtherContactsYesNoForm(RegistrarForm):
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 (Well ask you to explain why).")),
choices=((True, "Yes, I can name other employees."), (False, "No. (Well ask you to explain why.)")),
initial=initial_value,
widget=forms.RadioSelect,
error_messages={
"required": "This question is required.",
},
)
@ -638,7 +641,10 @@ class OtherContactsForm(RegistrarForm):
)
email = forms.EmailField(
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(
label="Phone",
@ -649,8 +655,17 @@ class OtherContactsForm(RegistrarForm):
)
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
super().__init__(*args, **kwargs)
self.empty_permitted = False
def mark_form_for_deletion(self):
self.form_data_marked_for_deletion = True
@ -659,12 +674,11 @@ class OtherContactsForm(RegistrarForm):
"""
This method overrides the default behavior for forms.
This cleans the form after field validation has already taken place.
In this override, allow for a form which is empty to be considered
valid even though certain required fields have not passed field
validation
In this override, allow for a form which is deleted by user or marked for
deletion by formset to be considered valid even though certain required fields have
not passed field validation
"""
if self.form_data_marked_for_deletion:
if self.form_data_marked_for_deletion or self.cleaned_data.get("DELETE"):
# clear any errors raised by the form fields
# (before this clean() method is run, each field
# performs its own clean, which could result in
@ -678,12 +692,26 @@ class OtherContactsForm(RegistrarForm):
# return empty object with only 'delete' attribute defined.
# this will prevent _to_database from creating an empty
# database object
return {"delete": True}
return {"DELETE": True}
return self.cleaned_data
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"
REVERSE_JOINS = [
"user",
@ -695,7 +723,13 @@ class BaseOtherContactsFormSet(RegistrarFormSet):
"contact_applications_information",
]
def get_deletion_widget(self):
return forms.HiddenInput(attrs={"class": "deletion"})
def __init__(self, *args, **kwargs):
"""
Override __init__ for RegistrarFormSet.
"""
self.formset_data_marked_for_deletion = False
self.application = kwargs.pop("application", None)
super(RegistrarFormSet, self).__init__(*args, **kwargs)
@ -706,8 +740,17 @@ class BaseOtherContactsFormSet(RegistrarFormSet):
self.forms[index].use_required_attribute = True
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):
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(
OtherContactsForm,
extra=1,
extra=0,
absolute_max=1500, # django default; use `max_num` to limit entries
min_num=1,
can_delete=True,
validate_min=True,
formset=BaseOtherContactsFormSet,
)
@ -748,11 +792,7 @@ class NoOtherContactsForm(RegistrarForm):
no_other_contacts_rationale = forms.CharField(
required=True,
# label has to end in a space to get the label_suffix to show
label=(
"You dont 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."
),
label=("No other employees rationale"),
widget=forms.Textarea(),
validators=[
MaxLengthValidator(

View file

@ -29,17 +29,31 @@
</fieldset>
<div id="other-employees">
<div id="other-employees" class="other-contacts-form">
{% include "includes/required_fields.html" %}
{{ forms.1.management_form }}
{# forms.1 is a formset and this iterates over its forms #}
{% for form in forms.1.forms %}
<fieldset class="usa-fieldset">
<legend>
<h2>Organization contact {{ forloop.counter }} (optional)</h2>
</legend>
<fieldset class="usa-fieldset repeatable-form padding-y-1">
{% input_with_errors form.first_name %}
<legend class="float-left-tablet">
<h2 class="margin-top-1">Organization contact {{ forloop.counter }}</h2>
</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 %}
</div>
{% input_with_errors form.middle_name %}
@ -52,17 +66,17 @@
affecting the margin of this block. The wrapper div is a
temporary workaround. {% endcomment %}
<div class="margin-top-3">
{% input_with_errors form.email %}
{% input_with_errors form.email %}
</div>
{% with add_class="usa-input--medium" %}
{% input_with_errors form.phone %}
{% input_with_errors form.phone %}
{% endwith %}
</fieldset>
{% 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">
<use xlink:href="{%static 'img/sprite.svg'%}#add_circle"></use>
</svg><span class="margin-left-05">Add another contact</span>
@ -70,10 +84,13 @@
</div>
<div id="no-other-employees">
<fieldset class="usa-fieldset margin-top-2">
<fieldset class="usa-fieldset margin-top-4">
<legend>
<h2>No other employees from your organization?</h2>
<h2 class="margin-bottom-0">No other employees from your organization?</h2>
</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" %}
{% input_with_errors forms.2.no_other_contacts_rationale %}
{% endwith %}

View file

@ -24,7 +24,7 @@
{% 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 %}
{{ formset.management_form }}

View file

@ -24,7 +24,7 @@
{% 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 %}
{{ formset.management_form }}

View file

@ -5,6 +5,7 @@ from django.conf import settings
from django.test import Client, TestCase
from django.urls import reverse
from django.contrib.auth import get_user_model
from .common import MockEppLib, MockSESClient, completed_application, create_user # type: ignore
from django_webtest import WebTest # 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):
"""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"""
# Populate the databse with a domain application that
# 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(
@ -1072,31 +1073,115 @@ class DomainApplicationTests(TestWithUser, WebTest):
# Assert that it is returned, ie the contacts form is required
self.assertContains(response, "Enter the first name / given name of this contact.")
@skip("Repurpose when working on ticket 903")
def test_application_delete_other_contact(self):
"""Other contacts can be deleted after being saved to database."""
# Populate the databse with a domain application that
# has 1 "other contact" assigned to it
def test_delete_other_contact(self):
"""Other contacts can be deleted after being saved to database.
This formset uses the DJANGO DELETE widget. We'll test that by setting 2 contacts on an application,
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(
first_name="Testy",
last_name="Tester",
title="Chief Tester",
email="testy@town.com",
phone="(555) 555 5555",
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="(555) 555 5556",
phone="(201) 555 5556",
)
other, _ = Contact.objects.get_or_create(
first_name="Testy2",
last_name="Tester2",
title="Another Tester",
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(
organization_type="federal",
@ -1129,35 +1214,97 @@ class DomainApplicationTests(TestWithUser, WebTest):
other_contacts_form = other_contacts_page.forms[0]
# Minimal check to ensure the form is loaded with data (if this part of
# the application doesn't work, we should be equipped with other unit
# tests to flag it)
# Minimal check to ensure the form is loaded
self.assertEqual(other_contacts_form["other_contacts-0-first_name"].value, "Testy2")
# clear the form
other_contacts_form["other_contacts-0-first_name"] = ""
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"] = ""
# Mark the first dude for deletion
other_contacts_form.set("other_contacts-0-DELETE", "on")
# Submit the now empty form
result = other_contacts_form.submit()
# Submit the form
other_contacts_form.submit()
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
# Verify that the contact we saved earlier has been removed from the database
application = DomainApplication.objects.get() # There are no contacts anymore
self.assertEqual(
application.other_contacts.count(),
0,
)
# Verify that the contact was not deleted
application = DomainApplication.objects.get()
self.assertEqual(application.other_contacts.count(), 1)
self.assertEqual(application.other_contacts.first().first_name, "Testy2")
# Verify that on submit, user is advanced to "no contacts" page
no_contacts_page = result.follow()
expected_url_slug = str(Step.NO_OTHER_CONTACTS)
actual_url_slug = no_contacts_page.request.path.split("/")[-2]
self.assertEqual(expected_url_slug, actual_url_slug)
def test_delete_other_contact_sets_visible_empty_form_as_required_after_failed_submit(self):
"""When you:
1. add an empty contact,
2. delete existing contacts,
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):
"""Special districts have to answer an additional question."""

View file

@ -498,6 +498,13 @@ class OtherContacts(ApplicationWizard):
other_contacts_forms = forms[1]
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
# test first for yes_no_form validity
if other_contacts_yes_no_form.is_valid():