Merge pull request #1591 from cisagov/dk/661-other-contacts

Issue 661: Yes/No check on other contacts form, domain application
This commit is contained in:
dave-kennedy-ecs 2024-01-10 19:43:43 -05:00 committed by GitHub
commit 4e027373a6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 656 additions and 118 deletions

View file

@ -483,3 +483,57 @@ function prepareDeleteButtons(formLabel) {
}, 50);
}
})();
function toggleTwoDomElements(ele1, ele2, index) {
let element1 = document.getElementById(ele1);
let element2 = document.getElementById(ele2);
if (element1 && element2) {
// Toggle display based on the index
element1.style.display = index === 1 ? 'block' : 'none';
element2.style.display = index === 2 ? 'block' : 'none';
} else {
console.error('One or both elements not found.');
}
}
/**
* An IIFE that listens to the other contacts radio form on DAs and toggles the contacts/no other contacts forms
*
*/
(function otherContactsFormListener() {
// Get the radio buttons
let radioButtons = document.querySelectorAll('input[name="other_contacts-has_other_contacts"]');
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;
switch (selectedValue) {
case 'True':
toggleTwoDomElements('other-employees', 'no-other-employees', 1);
break;
case 'False':
toggleTwoDomElements('other-employees', 'no-other-employees', 2);
break;
default:
toggleTwoDomElements('other-employees', 'no-other-employees', 0);
}
}
if (radioButtons.length) {
// Add event listener to each radio button
radioButtons.forEach(function (radioButton) {
radioButton.addEventListener('change', handleRadioButtonChange);
});
// initialize
handleRadioButtonChange();
}
})();

View file

@ -39,7 +39,6 @@ for step, view in [
(Step.PURPOSE, views.Purpose),
(Step.YOUR_CONTACT, views.YourContact),
(Step.OTHER_CONTACTS, views.OtherContacts),
(Step.NO_OTHER_CONTACTS, views.NoOtherContacts),
(Step.ANYTHING_ELSE, views.AnythingElse),
(Step.REQUIREMENTS, views.Requirements),
(Step.REVIEW, views.Review),

View file

@ -7,6 +7,7 @@ from phonenumber_field.formfields import PhoneNumberField # type: ignore
from django import forms
from django.core.validators import RegexValidator, MaxLengthValidator
from django.utils.safestring import mark_safe
from django.db.models.fields.related import ForeignObjectRel, OneToOneField
from api.views import DOMAIN_API_MESSAGES
@ -95,10 +96,39 @@ class RegistrarFormSet(forms.BaseFormSet):
"""
raise NotImplementedError
def has_more_than_one_join(self, db_obj, rel, related_name):
"""Helper for finding whether an object is joined more than once."""
# threshold is the number of related objects that are acceptable
# when determining if related objects exist. threshold is 0 for most
# relationships. if the relationship is related_name, we know that
# there is already exactly 1 acceptable relationship (the one we are
# attempting to delete), so the threshold is 1
threshold = 1 if rel == related_name else 0
# Raise a KeyError if rel is not a defined field on the db_obj model
# This will help catch any errors in reverse_join config on forms
if rel not in [field.name for field in db_obj._meta.get_fields()]:
raise KeyError(f"{rel} is not a defined field on the {db_obj._meta.model_name} model.")
# if attr rel in db_obj is not None, then test if reference object(s) exist
if getattr(db_obj, rel) is not None:
field = db_obj._meta.get_field(rel)
if isinstance(field, OneToOneField):
# if the rel field is a OneToOne field, then we have already
# determined that the object exists (is not None)
return True
elif isinstance(field, ForeignObjectRel):
# if the rel field is a ManyToOne or ManyToMany, then we need
# to determine if the count of related objects is greater than
# the threshold
return getattr(db_obj, rel).count() > threshold
return False
def _to_database(
self,
obj: DomainApplication,
join: str,
reverse_joins: list,
should_delete: Callable,
pre_update: Callable,
pre_create: Callable,
@ -115,26 +145,39 @@ class RegistrarFormSet(forms.BaseFormSet):
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):
db_obj.delete()
continue
if any(self.has_more_than_one_join(db_obj, rel, related_name) for rel in reverse_joins):
# Remove the specific relationship without deleting the object
getattr(db_obj, related_name).remove(self.application)
else:
# If there are no other relationships, delete the object
db_obj.delete()
else:
pre_update(db_obj, cleaned)
db_obj.save()
# no matching database object, create it
elif db_obj is None and cleaned:
# 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)
@ -366,7 +409,9 @@ class BaseCurrentSitesFormSet(RegistrarFormSet):
return website.strip() == ""
def to_database(self, obj: DomainApplication):
self._to_database(obj, self.JOIN, self.should_delete, self.pre_update, self.pre_create)
# If we want to test against multiple joins for a website object, replace the empty array
# and change the JOIN in the models to allow for reverse references
self._to_database(obj, self.JOIN, [], self.should_delete, self.pre_update, self.pre_create)
@classmethod
def from_database(cls, obj):
@ -423,7 +468,9 @@ class BaseAlternativeDomainFormSet(RegistrarFormSet):
return {}
def to_database(self, obj: DomainApplication):
self._to_database(obj, self.JOIN, self.should_delete, self.pre_update, self.pre_create)
# If we want to test against multiple joins for a website object, replace the empty array and
# change the JOIN in the models to allow for reverse references
self._to_database(obj, self.JOIN, [], self.should_delete, self.pre_update, self.pre_create)
@classmethod
def on_fetch(cls, query):
@ -497,7 +544,7 @@ class PurposeForm(RegistrarForm):
message="Response must be less than 1000 characters.",
)
],
error_messages={"required": "Describe how you'll use the .gov domain youre requesting."},
error_messages={"required": "Describe how youll use the .gov domain youre requesting."},
)
@ -547,6 +594,27 @@ class YourContactForm(RegistrarForm):
)
class OtherContactsYesNoForm(RegistrarForm):
def __init__(self, *args, **kwargs):
"""Extend the initialization of the form from RegistrarForm __init__"""
super().__init__(*args, **kwargs)
# set the initial value based on attributes of application
if self.application and self.application.has_other_contacts():
initial_value = True
elif self.application and self.application.has_rationale():
initial_value = False
else:
# No pre-selection for new applications
initial_value = None
self.fields["has_other_contacts"] = forms.TypedChoiceField(
coerce=lambda x: x.lower() == "true" if x is not None else None, # coerce strings to bool, excepting None
choices=((True, "Yes, I can name other employees."), (False, "No (Well ask you to explain why).")),
initial=initial_value,
widget=forms.RadioSelect,
)
class OtherContactsForm(RegistrarForm):
first_name = forms.CharField(
label="First name / given name",
@ -580,6 +648,13 @@ class OtherContactsForm(RegistrarForm):
},
)
def __init__(self, *args, **kwargs):
self.form_data_marked_for_deletion = False
super().__init__(*args, **kwargs)
def mark_form_for_deletion(self):
self.form_data_marked_for_deletion = True
def clean(self):
"""
This method overrides the default behavior for forms.
@ -589,16 +664,124 @@ class OtherContactsForm(RegistrarForm):
validation
"""
# Set form_is_empty to True initially
form_is_empty = True
for name, field in self.fields.items():
# get the value of the field from the widget
value = field.widget.value_from_datadict(self.data, self.files, self.add_prefix(name))
# if any field in the submitted form is not empty, set form_is_empty to False
if value is not None and value != "":
form_is_empty = False
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 empty object with only 'delete' attribute defined.
# this will prevent _to_database from creating an empty
# database object
return {"delete": True}
if form_is_empty:
return self.cleaned_data
class BaseOtherContactsFormSet(RegistrarFormSet):
JOIN = "other_contacts"
REVERSE_JOINS = [
"user",
"authorizing_official",
"submitted_applications",
"contact_applications",
"information_authorizing_official",
"submitted_applications_information",
"contact_applications_information",
]
def __init__(self, *args, **kwargs):
self.formset_data_marked_for_deletion = False
self.application = kwargs.pop("application", None)
super(RegistrarFormSet, self).__init__(*args, **kwargs)
# quick workaround to ensure that the HTML `required`
# attribute shows up on required fields for the first form
# in the formset plus those that have data already.
for index in range(max(self.initial_form_count(), 1)):
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
def to_database(self, obj: DomainApplication):
self._to_database(obj, self.JOIN, self.REVERSE_JOINS, self.should_delete, self.pre_update, self.pre_create)
@classmethod
def from_database(cls, obj):
return super().from_database(obj, cls.JOIN, cls.on_fetch)
def mark_formset_for_deletion(self):
"""Mark other contacts formset for deletion.
Updates forms in formset as well to mark them for deletion.
This has an effect on validity checks and to_database methods.
"""
self.formset_data_marked_for_deletion = True
for form in self.forms:
form.mark_form_for_deletion()
def is_valid(self):
"""Extend is_valid from RegistrarFormSet. When marking this formset for deletion, set
validate_min to false so that validation does not attempt to enforce a minimum
number of other contacts when contacts marked for deletion"""
if self.formset_data_marked_for_deletion:
self.validate_min = False
return super().is_valid()
OtherContactsFormSet = forms.formset_factory(
OtherContactsForm,
extra=1,
absolute_max=1500, # django default; use `max_num` to limit entries
min_num=1,
validate_min=True,
formset=BaseOtherContactsFormSet,
)
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."
),
widget=forms.Textarea(),
validators=[
MaxLengthValidator(
1000,
message="Response must be less than 1000 characters.",
)
],
error_messages={"required": ("Rationale for no other employees is required.")},
)
def __init__(self, *args, **kwargs):
self.form_data_marked_for_deletion = False
super().__init__(*args, **kwargs)
def mark_form_for_deletion(self):
"""Marks no_other_contacts form for deletion.
This changes behavior of validity checks and to_database
methods."""
self.form_data_marked_for_deletion = True
def clean(self):
"""
This method overrides the default behavior for forms.
This cleans the form after field validation has already taken place.
In this override, remove errors associated with the form if form data
is marked for deletion.
"""
if self.form_data_marked_for_deletion:
# clear any errors raised by the form fields
# (before this clean() method is run, each field
# performs its own clean, which could result in
@ -612,46 +795,22 @@ class OtherContactsForm(RegistrarForm):
return self.cleaned_data
class BaseOtherContactsFormSet(RegistrarFormSet):
JOIN = "other_contacts"
def should_delete(self, cleaned):
empty = (isinstance(v, str) and (v.strip() == "" or v is None) for v in cleaned.values())
return all(empty)
def to_database(self, obj: DomainApplication):
self._to_database(obj, self.JOIN, self.should_delete, self.pre_update, self.pre_create)
@classmethod
def from_database(cls, obj):
return super().from_database(obj, cls.JOIN, cls.on_fetch)
OtherContactsFormSet = forms.formset_factory(
OtherContactsForm,
extra=1,
absolute_max=1500, # django default; use `max_num` to limit entries
formset=BaseOtherContactsFormSet,
)
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=(
"Please explain why there are no other employees from your organization "
"we can contact to help us assess your eligibility for a .gov domain."
),
widget=forms.Textarea(),
validators=[
MaxLengthValidator(
1000,
message="Response must be less than 1000 characters.",
)
],
)
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):

View file

@ -840,9 +840,13 @@ class DomainApplication(TimeStampedModel):
DomainApplication.OrganizationChoices.INTERSTATE,
]
def show_no_other_contacts_rationale(self) -> bool:
"""Show this step if the other contacts are blank."""
return not self.other_contacts.exists()
def has_rationale(self) -> bool:
"""Does this application have no_other_contacts_rationale?"""
return bool(self.no_other_contacts_rationale)
def has_other_contacts(self) -> bool:
"""Does this application have other contacts listed?"""
return self.other_contacts.exists()
def is_federal(self) -> Union[bool, None]:
"""Is this application for a federal agency?

View file

@ -1,8 +0,0 @@
{% extends 'application_form.html' %}
{% load static field_helpers %}
{% block form_fields %}
{% with attr_maxlength=1000 %}
{% input_with_errors forms.0.no_other_contacts_rationale %}
{% endwith %}
{% endblock %}

View file

@ -13,38 +13,69 @@
{% endblock %}
{% block form_required_fields_help_text %}
{% include "includes/required_fields.html" %}
{# commented out so it does not appear at this point on this page #}
{% endblock %}
{% block form_fields %}
{{ forms.0.management_form }}
{# forms.0 is a formset and this iterates over its forms #}
{% for form in forms.0.forms %}
<fieldset class="usa-fieldset">
<fieldset class="usa-fieldset margin-top-2">
<legend>
<h2>Organization contact {{ forloop.counter }} (optional)</h2>
<h2>Are there other employees who can help verify your request?</h2>
</legend>
{% input_with_errors form.first_name %}
{% input_with_errors form.middle_name %}
{% input_with_errors form.last_name %}
{% input_with_errors form.title %}
{% input_with_errors form.email %}
{% with add_class="usa-input--medium" %}
{% input_with_errors form.phone %}
{% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %}
{% input_with_errors forms.0.has_other_contacts %}
{% endwith %}
{# forms.0 is a small yes/no form that toggles the visibility of "other contact" formset #}
</fieldset>
{% endfor %}
<button type="submit" name="submit_button" value="save" class="usa-button usa-button--unstyled">
<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>
</button>
<div id="other-employees">
{% 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>
{% input_with_errors form.first_name %}
{% input_with_errors form.middle_name %}
{% input_with_errors form.last_name %}
{% input_with_errors form.title %}
{% comment %} There seems to be an issue with the character counter on emails.
It's not counting anywhere, and in this particular instance it's
affecting the margin of this block. The wrapper div is a
temporary workaround. {% endcomment %}
<div class="margin-top-3">
{% input_with_errors form.email %}
</div>
{% with add_class="usa-input--medium" %}
{% input_with_errors form.phone %}
{% endwith %}
</fieldset>
{% endfor %}
<button type="submit" name="submit_button" value="save" class="usa-button usa-button--unstyled">
<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>
</button>
</div>
<div id="no-other-employees">
<fieldset class="usa-fieldset margin-top-2">
<legend>
<h2>No other employees from your organization?</h2>
</legend>
{% with attr_maxlength=1000 add_label_class="usa-sr-only" %}
{% input_with_errors forms.2.no_other_contacts_rationale %}
{% endwith %}
</div>
{% endblock %}

View file

@ -99,16 +99,16 @@
{% if step == Step.OTHER_CONTACTS %}
{% for other in application.other_contacts.all %}
<div class="margin-bottom-105">
<div class="review__step__subheading">Contact {{ forloop.counter }}</div>
<p class="text-semibold margin-top-1 margin-bottom-0">Contact {{ forloop.counter }}</p>
{% include "includes/contact.html" with contact=other %}
</div>
{% empty %}
None
<div class="margin-bottom-105">
<p class="text-semibold margin-top-1 margin-bottom-0">No other employees from your organization?</p>
{{ application.no_other_contacts_rationale|default:"Incomplete" }}
</div>
{% endfor %}
{% endif %}
{% if step == Step.NO_OTHER_CONTACTS %}
{{ application.no_other_contacts_rationale|default:"Incomplete" }}
{% endif %}
{% if step == Step.ANYTHING_ELSE %}
{{ application.anything_else|default:"No" }}
{% endif %}

View file

@ -500,6 +500,28 @@ class TestDomainApplication(TestCase):
with self.assertRaises(TransitionNotAllowed):
self.approved_application.reject_with_prejudice()
def test_has_rationale_returns_true(self):
"""has_rationale() returns true when an application has no_other_contacts_rationale"""
self.started_application.no_other_contacts_rationale = "You talkin' to me?"
self.started_application.save()
self.assertEquals(self.started_application.has_rationale(), True)
def test_has_rationale_returns_false(self):
"""has_rationale() returns false when an application has no no_other_contacts_rationale"""
self.assertEquals(self.started_application.has_rationale(), False)
def test_has_other_contacts_returns_true(self):
"""has_other_contacts() returns true when an application has other_contacts"""
# completed_application has other contacts by default
self.assertEquals(self.started_application.has_other_contacts(), True)
def test_has_other_contacts_returns_false(self):
"""has_other_contacts() returns false when an application has no other_contacts"""
application = completed_application(
status=DomainApplication.ApplicationStatus.STARTED, name="no-others.gov", has_other_contacts=False
)
self.assertEquals(application.has_other_contacts(), False)
class TestPermissions(TestCase):
"""Test the User-Domain-Role connection."""

View file

@ -74,6 +74,7 @@ class TestWithUser(MockEppLib):
# delete any applications too
super().tearDown()
DomainApplication.objects.all().delete()
DomainInformation.objects.all().delete()
self.user.delete()
@ -216,8 +217,8 @@ class DomainApplicationTests(TestWithUser, WebTest):
in the modal header on the submit page.
"""
num_pages_tested = 0
# elections, type_of_work, tribal_government, no_other_contacts
SKIPPED_PAGES = 4
# elections, type_of_work, tribal_government
SKIPPED_PAGES = 3
num_pages = len(self.TITLES) - SKIPPED_PAGES
intro_page = self.app.get(reverse("application:"))
@ -422,8 +423,13 @@ class DomainApplicationTests(TestWithUser, WebTest):
# Follow the redirect to the next form page
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
other_contacts_page = your_contact_result.follow()
# This page has 3 forms in 1.
# Let's set the yes/no radios to enable the other contacts fieldsets
other_contacts_form = other_contacts_page.forms[0]
other_contacts_form["other_contacts-has_other_contacts"] = "True"
other_contacts_form["other_contacts-0-first_name"] = "Testy2"
other_contacts_form["other_contacts-0-last_name"] = "Tester2"
other_contacts_form["other_contacts-0-title"] = "Another Tester"
@ -561,8 +567,8 @@ class DomainApplicationTests(TestWithUser, WebTest):
@skip("WIP")
def test_application_form_started_allsteps(self):
num_pages_tested = 0
# elections, type_of_work, tribal_government, no_other_contacts
SKIPPED_PAGES = 4
# elections, type_of_work, tribal_government
SKIPPED_PAGES = 3
DASHBOARD_PAGE = 1
num_pages = len(self.TITLES) - SKIPPED_PAGES + DASHBOARD_PAGE
@ -809,24 +815,271 @@ class DomainApplicationTests(TestWithUser, WebTest):
self.assertContains(contact_page, self.TITLES[Step.ABOUT_YOUR_ORGANIZATION])
def test_application_no_other_contacts(self):
"""Applicants with no other contacts have to give a reason."""
contacts_page = self.app.get(reverse("application:other_contacts"))
def test_yes_no_form_inits_blank_for_new_application(self):
"""On the Other Contacts page, the yes/no form gets initialized with nothing selected for
new applications"""
other_contacts_page = self.app.get(reverse("application:other_contacts"))
other_contacts_form = other_contacts_page.forms[0]
self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, None)
def test_yes_no_form_inits_yes_for_application_with_other_contacts(self):
"""On the Other Contacts page, the yes/no form gets initialized with YES selected if the
application has other contacts"""
# Application has other contacts by default
application = completed_application(user=self.user)
# 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]
self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, "True")
def test_yes_no_form_inits_no_for_application_with_no_other_contacts_rationale(self):
"""On the Other Contacts page, the yes/no form gets initialized with NO selected if the
application has no other contacts"""
# Application has other contacts by default
application = completed_application(user=self.user, has_other_contacts=False)
application.no_other_contacts_rationale = "Hello!"
application.save()
# 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]
self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, "False")
def test_submitting_other_contacts_deletes_no_other_contacts_rationale(self):
"""When a user submits the Other Contacts form with other contacts selected, the application's
no other contacts rationale gets deleted"""
# Application has other contacts by default
application = completed_application(user=self.user, has_other_contacts=False)
application.no_other_contacts_rationale = "Hello!"
application.save()
# 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]
self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, "False")
other_contacts_form["other_contacts-has_other_contacts"] = "True"
other_contacts_form["other_contacts-0-first_name"] = "Testy"
other_contacts_form["other_contacts-0-middle_name"] = ""
other_contacts_form["other_contacts-0-last_name"] = "McTesterson"
other_contacts_form["other_contacts-0-title"] = "Lord"
other_contacts_form["other_contacts-0-email"] = "testy@abc.org"
other_contacts_form["other_contacts-0-phone"] = "(201) 555-0123"
# Submit the now empty form
other_contacts_form.submit()
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
result = contacts_page.forms[0].submit()
# follow first redirect
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
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)
# Verify that the no_other_contacts_rationale we saved earlier has been removed from the database
application = DomainApplication.objects.get()
self.assertEqual(
application.other_contacts.count(),
1,
)
self.assertEquals(
application.no_other_contacts_rationale,
None,
)
def test_submitting_no_other_contacts_rationale_deletes_other_contacts(self):
"""When a user submits the Other Contacts form with no other contacts selected, the application's
other contacts get deleted for other contacts that exist and are not joined to other objects
"""
# Application has other contacts by default
application = completed_application(user=self.user)
# 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]
self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, "True")
other_contacts_form["other_contacts-has_other_contacts"] = "False"
other_contacts_form["other_contacts-no_other_contacts_rationale"] = "Hello again!"
# Submit the now empty form
other_contacts_form.submit()
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
# Verify that the no_other_contacts_rationale we saved earlier has been removed from the database
application = DomainApplication.objects.get()
self.assertEqual(
application.other_contacts.count(),
0,
)
self.assertEquals(
application.no_other_contacts_rationale,
"Hello again!",
)
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
# 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="(555) 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",
)
other, _ = Contact.objects.get_or_create(
first_name="Testy2",
last_name="Tester2",
title="Another Tester",
email="testy2@town.com",
phone="(555) 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)
# Now let's join the other contact to another object
domain_info = DomainInformation.objects.create(creator=self.user)
domain_info.other_contacts.set([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]
self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, "True")
other_contacts_form["other_contacts-has_other_contacts"] = "False"
other_contacts_form["other_contacts-no_other_contacts_rationale"] = "Hello again!"
# Submit the now empty form
other_contacts_form.submit()
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
# Verify that the no_other_contacts_rationale we saved earlier is no longer associated with the application
application = DomainApplication.objects.get()
self.assertEqual(
application.other_contacts.count(),
0,
)
# Verify that the 'other' contact object still exists
domain_info = DomainInformation.objects.get()
self.assertEqual(
domain_info.other_contacts.count(),
1,
)
self.assertEqual(
domain_info.other_contacts.all()[0].first_name,
"Testy2",
)
self.assertEquals(
application.no_other_contacts_rationale,
"Hello again!",
)
def test_if_yes_no_form_is_no_then_no_other_contacts_required(self):
"""Applicants with no other contacts have to give a reason."""
other_contacts_page = self.app.get(reverse("application:other_contacts"))
other_contacts_form = other_contacts_page.forms[0]
other_contacts_form["other_contacts-has_other_contacts"] = "False"
response = other_contacts_page.forms[0].submit()
# The textarea for no other contacts returns this error message
# Assert that it is returned, ie the no other contacts form is required
self.assertContains(response, "Rationale for no other employees is required.")
# The first name field for other contacts returns this error message
# Assert that it is not returned, ie the contacts form is not required
self.assertNotContains(response, "Enter the first name / given name of this contact.")
def test_if_yes_no_form_is_yes_then_other_contacts_required(self):
"""Applicants with other contacts do not have to give a reason."""
other_contacts_page = self.app.get(reverse("application:other_contacts"))
other_contacts_form = other_contacts_page.forms[0]
other_contacts_form["other_contacts-has_other_contacts"] = "True"
response = other_contacts_page.forms[0].submit()
# The textarea for no other contacts returns this error message
# Assert that it is not returned, ie the no other contacts form is not required
self.assertNotContains(response, "Rationale for no other employees is required.")
# The first name field for other contacts returns this error message
# 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

View file

@ -42,7 +42,6 @@ class Step(StrEnum):
PURPOSE = "purpose"
YOUR_CONTACT = "your_contact"
OTHER_CONTACTS = "other_contacts"
NO_OTHER_CONTACTS = "no_other_contacts"
ANYTHING_ELSE = "anything_else"
REQUIREMENTS = "requirements"
REVIEW = "review"
@ -89,7 +88,6 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView):
Step.PURPOSE: _("Purpose of your domain"),
Step.YOUR_CONTACT: _("Your contact information"),
Step.OTHER_CONTACTS: _("Other employees from your organization"),
Step.NO_OTHER_CONTACTS: _("No other employees from your organization?"),
Step.ANYTHING_ELSE: _("Anything else?"),
Step.REQUIREMENTS: _("Requirements for operating .gov domains"),
Step.REVIEW: _("Review and submit your domain request"),
@ -102,7 +100,6 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView):
Step.TRIBAL_GOVERNMENT: lambda w: w.from_model("show_tribal_government", False),
Step.ORGANIZATION_ELECTION: lambda w: w.from_model("show_organization_election", False),
Step.ABOUT_YOUR_ORGANIZATION: lambda w: w.from_model("show_about_your_organization", False),
Step.NO_OTHER_CONTACTS: lambda w: w.from_model("show_no_other_contacts_rationale", False),
}
def __init__(self):
@ -488,12 +485,39 @@ class YourContact(ApplicationWizard):
class OtherContacts(ApplicationWizard):
template_name = "application_other_contacts.html"
forms = [forms.OtherContactsFormSet]
forms = [forms.OtherContactsYesNoForm, forms.OtherContactsFormSet, forms.NoOtherContactsForm]
def is_valid(self, forms: list) -> bool:
"""Overrides default behavior defined in ApplicationWizard.
Depending on value in other_contacts_yes_no_form, marks forms in
other_contacts or no_other_contacts for deletion. Then validates
all forms.
"""
other_contacts_yes_no_form = forms[0]
other_contacts_forms = forms[1]
no_other_contacts_form = forms[2]
class NoOtherContacts(ApplicationWizard):
template_name = "application_no_other_contacts.html"
forms = [forms.NoOtherContactsForm]
all_forms_valid = True
# test first for yes_no_form validity
if other_contacts_yes_no_form.is_valid():
# test for has_contacts
if other_contacts_yes_no_form.cleaned_data.get("has_other_contacts"):
# mark the no_other_contacts_form for deletion
no_other_contacts_form.mark_form_for_deletion()
# test that the other_contacts_forms and no_other_contacts_forms are valid
all_forms_valid = all(form.is_valid() for form in forms[1:])
else:
# mark the other_contacts_forms formset for deletion
other_contacts_forms.mark_formset_for_deletion()
all_forms_valid = all(form.is_valid() for form in forms[1:])
else:
# if yes no form is invalid, no choice has been made
# mark other forms for deletion so that their errors are not
# returned
other_contacts_forms.mark_formset_for_deletion()
no_other_contacts_form.mark_form_for_deletion()
all_forms_valid = False
return all_forms_valid
class AnythingElse(ApplicationWizard):