Merge branch 'main' into za/1501-users-delete-domain-records

This commit is contained in:
zandercymatics 2024-01-18 15:45:41 -07:00
commit d41d8710c9
No known key found for this signature in database
GPG key ID: FF4636ABEC9682B7
35 changed files with 1502 additions and 369 deletions

View file

@ -1,10 +1,11 @@
"""Internal API views""" """Internal API views"""
from django.apps import apps from django.apps import apps
from django.views.decorators.http import require_http_methods from django.views.decorators.http import require_http_methods
from django.http import HttpResponse, JsonResponse from django.http import HttpResponse
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from registrar.templatetags.url_helpers import public_site_url from registrar.templatetags.url_helpers import public_site_url
from registrar.utility.enums import ValidationReturnType
from registrar.utility.errors import GenericError, GenericErrorCodes from registrar.utility.errors import GenericError, GenericErrorCodes
import requests import requests
@ -71,6 +72,7 @@ def check_domain_available(domain):
a match. If check fails, throws a RegistryError. a match. If check fails, throws a RegistryError.
""" """
Domain = apps.get_model("registrar.Domain") Domain = apps.get_model("registrar.Domain")
if domain.endswith(".gov"): if domain.endswith(".gov"):
return Domain.available(domain) return Domain.available(domain)
else: else:
@ -86,22 +88,14 @@ def available(request, domain=""):
Response is a JSON dictionary with the key "available" and value true or Response is a JSON dictionary with the key "available" and value true or
false. false.
""" """
Domain = apps.get_model("registrar.Domain")
domain = request.GET.get("domain", "") domain = request.GET.get("domain", "")
DraftDomain = apps.get_model("registrar.DraftDomain")
# validate that the given domain could be a domain name and fail early if _, json_response = Domain.validate_and_handle_errors(
# not. domain=domain,
if not (DraftDomain.string_could_be_domain(domain) or DraftDomain.string_could_be_domain(domain + ".gov")): return_type=ValidationReturnType.JSON_RESPONSE,
return JsonResponse({"available": False, "code": "invalid", "message": DOMAIN_API_MESSAGES["invalid"]})
# a domain is available if it is NOT in the list of current domains
try:
if check_domain_available(domain):
return JsonResponse({"available": True, "code": "success", "message": DOMAIN_API_MESSAGES["success"]})
else:
return JsonResponse(
{"available": False, "code": "unavailable", "message": DOMAIN_API_MESSAGES["unavailable"]}
) )
except Exception: return json_response
return JsonResponse({"available": False, "code": "error", "message": DOMAIN_API_MESSAGES["error"]})
@require_http_methods(["GET"]) @require_http_methods(["GET"])

View file

@ -134,10 +134,19 @@ function _checkDomainAvailability(el) {
const callback = (response) => { const callback = (response) => {
toggleInputValidity(el, (response && response.available), msg=response.message); toggleInputValidity(el, (response && response.available), msg=response.message);
announce(el.id, response.message); announce(el.id, response.message);
// Determines if we ignore the field if it is just blank
ignore_blank = el.classList.contains("blank-ok")
if (el.validity.valid) { if (el.validity.valid) {
el.classList.add('usa-input--success'); el.classList.add('usa-input--success');
// use of `parentElement` due to .gov inputs being wrapped in www/.gov decoration // use of `parentElement` due to .gov inputs being wrapped in www/.gov decoration
inlineToast(el.parentElement, el.id, SUCCESS, response.message); inlineToast(el.parentElement, el.id, SUCCESS, response.message);
} else if (ignore_blank && response.code == "required"){
// Visually remove the error
error = "usa-input--error"
if (el.classList.contains(error)){
el.classList.remove(error)
}
} else { } else {
inlineToast(el.parentElement, el.id, ERROR, response.message); inlineToast(el.parentElement, el.id, ERROR, response.message);
} }
@ -229,24 +238,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 +306,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");
} }
@ -321,7 +317,124 @@ 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 +444,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 +486,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 +519,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 +576,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 +622,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);

View file

@ -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;

View file

@ -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;
}
}

View file

@ -22,3 +22,9 @@ h2 {
margin: units(4) 0 units(1); margin: units(4) 0 units(1);
color: color('primary-darker'); color: color('primary-darker');
} }
// Normalize typography in forms
.usa-form,
.usa-form fieldset {
font-size: 1rem;
}

View file

@ -2,18 +2,17 @@ from __future__ import annotations # allows forward references in annotations
from itertools import zip_longest from itertools import zip_longest
import logging import logging
from typing import Callable from typing import Callable
from api.views import DOMAIN_API_MESSAGES
from phonenumber_field.formfields import PhoneNumberField # type: ignore from phonenumber_field.formfields import PhoneNumberField # type: ignore
from django import forms from django import forms
from django.core.validators import RegexValidator, MaxLengthValidator from django.core.validators import RegexValidator, MaxLengthValidator
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.db.models.fields.related import ForeignObjectRel, OneToOneField from django.db.models.fields.related import ForeignObjectRel
from api.views import DOMAIN_API_MESSAGES
from registrar.models import Contact, DomainApplication, DraftDomain, Domain from registrar.models import Contact, DomainApplication, DraftDomain, Domain
from registrar.templatetags.url_helpers import public_site_url from registrar.templatetags.url_helpers import public_site_url
from registrar.utility import errors from registrar.utility.enums import ValidationReturnType
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -96,39 +95,10 @@ class RegistrarFormSet(forms.BaseFormSet):
""" """
raise NotImplementedError 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( def _to_database(
self, self,
obj: DomainApplication, obj: DomainApplication,
join: str, join: str,
reverse_joins: list,
should_delete: Callable, should_delete: Callable,
pre_update: Callable, pre_update: Callable,
pre_create: Callable, pre_create: Callable,
@ -165,19 +135,25 @@ class RegistrarFormSet(forms.BaseFormSet):
# matching database object exists, update it # matching database object exists, update it
if db_obj is not None and cleaned: if db_obj is not None and cleaned:
if should_delete(cleaned): if should_delete(cleaned):
if any(self.has_more_than_one_join(db_obj, rel, related_name) for rel in reverse_joins): if hasattr(db_obj, "has_more_than_one_join") and db_obj.has_more_than_one_join(related_name):
# Remove the specific relationship without deleting the object # Remove the specific relationship without deleting the object
getattr(db_obj, related_name).remove(self.application) getattr(db_obj, related_name).remove(self.application)
else: else:
# If there are no other relationships, delete the object # If there are no other relationships, delete the object
db_obj.delete() db_obj.delete()
else:
if hasattr(db_obj, "has_more_than_one_join") and db_obj.has_more_than_one_join(related_name):
# create a new db_obj and disconnect existing one
getattr(db_obj, related_name).remove(self.application)
kwargs = pre_create(db_obj, cleaned)
getattr(obj, join).create(**kwargs)
else: else:
pre_update(db_obj, cleaned) pre_update(db_obj, cleaned)
db_obj.save() db_obj.save()
# 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)
@ -213,7 +189,7 @@ class TribalGovernmentForm(RegistrarForm):
) )
tribe_name = forms.CharField( tribe_name = forms.CharField(
label="What is the name of the tribe you represent?", label="Name of tribe",
error_messages={"required": "Enter the tribe you represent."}, error_messages={"required": "Enter the tribe you represent."},
) )
@ -351,13 +327,18 @@ class AboutYourOrganizationForm(RegistrarForm):
class AuthorizingOfficialForm(RegistrarForm): class AuthorizingOfficialForm(RegistrarForm):
JOIN = "authorizing_official"
def to_database(self, obj): def to_database(self, obj):
if not self.is_valid(): if not self.is_valid():
return return
contact = getattr(obj, "authorizing_official", None) contact = getattr(obj, "authorizing_official", None)
if contact is not None: if contact is not None and not contact.has_more_than_one_join("authorizing_official"):
# if contact exists in the database and is not joined to other entities
super().to_database(contact) super().to_database(contact)
else: else:
# no contact exists OR contact exists which is joined also to other entities;
# in either case, create a new contact and update it
contact = Contact() contact = Contact()
super().to_database(contact) super().to_database(contact)
obj.authorizing_official = contact obj.authorizing_official = contact
@ -411,7 +392,7 @@ class BaseCurrentSitesFormSet(RegistrarFormSet):
def to_database(self, obj: DomainApplication): def to_database(self, obj: DomainApplication):
# If we want to test against multiple joins for a website object, replace the empty array # 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 # 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) self._to_database(obj, self.JOIN, self.should_delete, self.pre_update, self.pre_create)
@classmethod @classmethod
def from_database(cls, obj): def from_database(cls, obj):
@ -429,17 +410,12 @@ CurrentSitesFormSet = forms.formset_factory(
class AlternativeDomainForm(RegistrarForm): class AlternativeDomainForm(RegistrarForm):
def clean_alternative_domain(self): def clean_alternative_domain(self):
"""Validation code for domain names.""" """Validation code for domain names."""
try:
requested = self.cleaned_data.get("alternative_domain", None) requested = self.cleaned_data.get("alternative_domain", None)
validated = DraftDomain.validate(requested, blank_ok=True) validated, _ = DraftDomain.validate_and_handle_errors(
except errors.ExtraDotsError: domain=requested,
raise forms.ValidationError(DOMAIN_API_MESSAGES["extra_dots"], code="extra_dots") return_type=ValidationReturnType.FORM_VALIDATION_ERROR,
except errors.DomainUnavailableError: blank_ok=True,
raise forms.ValidationError(DOMAIN_API_MESSAGES["unavailable"], code="unavailable") )
except errors.RegistrySystemError:
raise forms.ValidationError(DOMAIN_API_MESSAGES["error"], code="error")
except ValueError:
raise forms.ValidationError(DOMAIN_API_MESSAGES["invalid"], code="invalid")
return validated return validated
alternative_domain = forms.CharField( alternative_domain = forms.CharField(
@ -470,7 +446,7 @@ class BaseAlternativeDomainFormSet(RegistrarFormSet):
def to_database(self, obj: DomainApplication): def to_database(self, obj: DomainApplication):
# If we want to test against multiple joins for a website object, replace the empty array and # 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 # 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) self._to_database(obj, self.JOIN, self.should_delete, self.pre_update, self.pre_create)
@classmethod @classmethod
def on_fetch(cls, query): def on_fetch(cls, query):
@ -517,22 +493,19 @@ class DotGovDomainForm(RegistrarForm):
def clean_requested_domain(self): def clean_requested_domain(self):
"""Validation code for domain names.""" """Validation code for domain names."""
try:
requested = self.cleaned_data.get("requested_domain", None) requested = self.cleaned_data.get("requested_domain", None)
validated = DraftDomain.validate(requested) validated, _ = DraftDomain.validate_and_handle_errors(
except errors.BlankValueError: domain=requested,
raise forms.ValidationError(DOMAIN_API_MESSAGES["required"], code="required") return_type=ValidationReturnType.FORM_VALIDATION_ERROR,
except errors.ExtraDotsError: )
raise forms.ValidationError(DOMAIN_API_MESSAGES["extra_dots"], code="extra_dots")
except errors.DomainUnavailableError:
raise forms.ValidationError(DOMAIN_API_MESSAGES["unavailable"], code="unavailable")
except errors.RegistrySystemError:
raise forms.ValidationError(DOMAIN_API_MESSAGES["error"], code="error")
except ValueError:
raise forms.ValidationError(DOMAIN_API_MESSAGES["invalid"], code="invalid")
return validated return validated
requested_domain = forms.CharField(label="What .gov domain do you want?") requested_domain = forms.CharField(
label="What .gov domain do you want?",
error_messages={
"required": DOMAIN_API_MESSAGES["required"],
},
)
class PurposeForm(RegistrarForm): class PurposeForm(RegistrarForm):
@ -550,13 +523,18 @@ class PurposeForm(RegistrarForm):
class YourContactForm(RegistrarForm): class YourContactForm(RegistrarForm):
JOIN = "submitter"
def to_database(self, obj): def to_database(self, obj):
if not self.is_valid(): if not self.is_valid():
return return
contact = getattr(obj, "submitter", None) contact = getattr(obj, "submitter", None)
if contact is not None: if contact is not None and not contact.has_more_than_one_join("submitted_applications"):
# if contact exists in the database and is not joined to other entities
super().to_database(contact) super().to_database(contact)
else: else:
# no contact exists OR contact exists which is joined also to other entities;
# in either case, create a new contact and update it
contact = Contact() contact = Contact()
super().to_database(contact) super().to_database(contact)
obj.submitter = contact obj.submitter = contact
@ -610,9 +588,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 (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, initial=initial_value,
widget=forms.RadioSelect, widget=forms.RadioSelect,
error_messages={
"required": "This question is required.",
},
) )
@ -639,7 +620,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",
@ -650,8 +634,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
@ -660,12 +653,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
@ -679,24 +671,34 @@ 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.
"""
JOIN = "other_contacts" JOIN = "other_contacts"
REVERSE_JOINS = [
"user", def get_deletion_widget(self):
"authorizing_official", return forms.HiddenInput(attrs={"class": "deletion"})
"submitted_applications",
"contact_applications",
"information_authorizing_official",
"submitted_applications_information",
"contact_applications_information",
]
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)
@ -707,11 +709,20 @@ 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.should_delete, self.pre_update, self.pre_create)
@classmethod @classmethod
def from_database(cls, obj): def from_database(cls, obj):
@ -737,9 +748,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,
) )
@ -749,11 +761,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 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(), widget=forms.Textarea(),
validators=[ validators=[
MaxLengthValidator( MaxLengthValidator(

View file

@ -210,6 +210,8 @@ class ContactForm(forms.ModelForm):
class AuthorizingOfficialContactForm(ContactForm): class AuthorizingOfficialContactForm(ContactForm):
"""Form for updating authorizing official contacts.""" """Form for updating authorizing official contacts."""
JOIN = "authorizing_official"
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@ -230,6 +232,29 @@ class AuthorizingOfficialContactForm(ContactForm):
self.fields["email"].error_messages = { self.fields["email"].error_messages = {
"required": "Enter an email address in the required format, like name@example.com." "required": "Enter an email address in the required format, like name@example.com."
} }
self.domainInfo = None
def set_domain_info(self, domainInfo):
"""Set the domain information for the form.
The form instance is associated with the contact itself. In order to access the associated
domain information object, this needs to be set in the form by the view."""
self.domainInfo = domainInfo
def save(self, commit=True):
"""Override the save() method of the BaseModelForm."""
# Get the Contact object from the db for the Authorizing Official
db_ao = Contact.objects.get(id=self.instance.id)
if self.domainInfo and db_ao.has_more_than_one_join("information_authorizing_official"):
# Handle the case where the domain information object is available and the AO Contact
# has more than one joined object.
# In this case, create a new Contact, and update the new Contact with form data.
# Then associate with domain information object as the authorizing_official
data = dict(self.cleaned_data.items())
self.domainInfo.authorizing_official = Contact.objects.create(**data)
self.domainInfo.save()
else:
super().save()
class DomainSecurityEmailForm(forms.Form): class DomainSecurityEmailForm(forms.Form):

View file

@ -11,6 +11,7 @@ import os
import sys import sys
from typing import Dict, List from typing import Dict, List
from django.core.paginator import Paginator from django.core.paginator import Paginator
from registrar.utility.enums import LogCode
from registrar.models.transition_domain import TransitionDomain from registrar.models.transition_domain import TransitionDomain
from registrar.management.commands.utility.load_organization_error import ( from registrar.management.commands.utility.load_organization_error import (
LoadOrganizationError, LoadOrganizationError,
@ -28,7 +29,8 @@ from .epp_data_containers import (
) )
from .transition_domain_arguments import TransitionDomainArguments from .transition_domain_arguments import TransitionDomainArguments
from .terminal_helper import TerminalColors, TerminalHelper, LogCode from .terminal_helper import TerminalColors, TerminalHelper
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View file

@ -1,30 +1,12 @@
from enum import Enum
import logging import logging
import sys import sys
from django.core.paginator import Paginator from django.core.paginator import Paginator
from typing import List from typing import List
from registrar.utility.enums import LogCode
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class LogCode(Enum):
"""Stores the desired log severity
Overview of error codes:
- 1 ERROR
- 2 WARNING
- 3 INFO
- 4 DEBUG
- 5 DEFAULT
"""
ERROR = 1
WARNING = 2
INFO = 3
DEBUG = 4
DEFAULT = 5
class TerminalColors: class TerminalColors:
"""Colors for terminal outputs """Colors for terminal outputs
(makes reading the logs WAY easier)""" (makes reading the logs WAY easier)"""

View file

@ -54,6 +54,47 @@ class Contact(TimeStampedModel):
db_index=True, db_index=True,
) )
def _get_all_relations(self):
"""Returns an array of all fields which are relations"""
return [f.name for f in self._meta.get_fields() if f.is_relation]
def has_more_than_one_join(self, expected_relation):
"""Helper for finding whether an object is joined more than once.
expected_relation is the one relation with one expected join"""
# all_relations is the list of all_relations (from contact) to be checked for existing joins
all_relations = self._get_all_relations()
return any(self._has_more_than_one_join_per_relation(rel, expected_relation) for rel in all_relations)
def _has_more_than_one_join_per_relation(self, relation, expected_relation):
"""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 expected_relation, 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 relation == expected_relation else 0
# Raise a KeyError if rel is not a defined field on the db_obj model
# This will help catch any errors in relation passed.
if relation not in [field.name for field in self._meta.get_fields()]:
raise KeyError(f"{relation} is not a defined field on the {self._meta.model_name} model.")
# if attr rel in db_obj is not None, then test if reference object(s) exist
if getattr(self, relation) is not None:
field = self._meta.get_field(relation)
if isinstance(field, models.OneToOneField):
# if the rel field is a OneToOne field, then we have already
# determined that the object exists (is not None)
# so return True unless the relation being tested is the expected_relation
is_not_expected_relation = relation != expected_relation
return is_not_expected_relation
elif isinstance(field, models.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(self, relation).count() > threshold
return False
def get_formatted_name(self): def get_formatted_name(self):
"""Returns the contact's name in Western order.""" """Returns the contact's name in Western order."""
names = [n for n in [self.first_name, self.middle_name, self.last_name] if n] names = [n for n in [self.first_name, self.middle_name, self.last_name] if n]

View file

@ -1,8 +1,12 @@
import re import re
from api.views import check_domain_available from django import forms
from django.http import JsonResponse
from api.views import DOMAIN_API_MESSAGES, check_domain_available
from registrar.utility import errors from registrar.utility import errors
from epplibwrapper.errors import RegistryError from epplibwrapper.errors import RegistryError
from registrar.utility.enums import ValidationReturnType
class DomainHelper: class DomainHelper:
@ -23,21 +27,12 @@ class DomainHelper:
return bool(cls.DOMAIN_REGEX.match(domain)) return bool(cls.DOMAIN_REGEX.match(domain))
@classmethod @classmethod
def validate(cls, domain: str | None, blank_ok=False) -> str: def validate(cls, domain: str, blank_ok=False) -> str:
"""Attempt to determine if a domain name could be requested.""" """Attempt to determine if a domain name could be requested."""
if domain is None:
raise errors.BlankValueError() # Split into pieces for the linter
if not isinstance(domain, str): domain = cls._validate_domain_string(domain, blank_ok)
raise ValueError("Domain name must be a string")
domain = domain.lower().strip()
if domain == "" and not blank_ok:
raise errors.BlankValueError()
if domain.endswith(".gov"):
domain = domain[:-4]
if "." in domain:
raise errors.ExtraDotsError()
if not DomainHelper.string_could_be_domain(domain + ".gov"):
raise ValueError()
try: try:
if not check_domain_available(domain): if not check_domain_available(domain):
raise errors.DomainUnavailableError() raise errors.DomainUnavailableError()
@ -45,6 +40,110 @@ class DomainHelper:
raise errors.RegistrySystemError() from err raise errors.RegistrySystemError() from err
return domain return domain
@staticmethod
def _validate_domain_string(domain, blank_ok):
"""Normalize the domain string, and check its content"""
if domain is None:
raise errors.BlankValueError()
if not isinstance(domain, str):
raise errors.InvalidDomainError()
domain = domain.lower().strip()
if domain == "" and not blank_ok:
raise errors.BlankValueError()
elif domain == "":
# If blank ok is true, just return the domain
return domain
if domain.endswith(".gov"):
domain = domain[:-4]
if "." in domain:
raise errors.ExtraDotsError()
if not DomainHelper.string_could_be_domain(domain + ".gov"):
raise errors.InvalidDomainError()
return domain
@classmethod
def validate_and_handle_errors(cls, domain, return_type, blank_ok=False):
"""
Validates a domain and returns an appropriate response based on the validation result.
This method uses the `validate` method to validate the domain. If validation fails, it catches the exception,
maps it to a corresponding error code, and returns a response based on the `return_type` parameter.
Args:
domain (str): The domain to validate.
return_type (ValidationReturnType): Determines the type of response (JSON or form validation error).
blank_ok (bool, optional): If True, blank input does not raise an exception. Defaults to False.
Returns:
tuple: The validated domain (or None if validation failed), and the response (success or error).
""" # noqa
# Map each exception to a corresponding error code
error_map = {
errors.BlankValueError: "required",
errors.ExtraDotsError: "extra_dots",
errors.DomainUnavailableError: "unavailable",
errors.RegistrySystemError: "error",
errors.InvalidDomainError: "invalid",
}
validated = None
response = None
try:
# Attempt to validate the domain
validated = cls.validate(domain, blank_ok)
# Get a list of each possible exception, and the code to return
except tuple(error_map.keys()) as error:
# If an error is caught, get its type
error_type = type(error)
# Generate the response based on the error code and return type
response = DomainHelper._return_form_error_or_json_response(return_type, code=error_map.get(error_type))
else:
# For form validation, we do not need to display the success message
if return_type != ValidationReturnType.FORM_VALIDATION_ERROR:
response = DomainHelper._return_form_error_or_json_response(return_type, code="success", available=True)
# Return the validated domain and the response (either error or success)
return (validated, response)
@staticmethod
def _return_form_error_or_json_response(return_type: ValidationReturnType, code, available=False):
"""
Returns an error response based on the `return_type`.
If `return_type` is `FORM_VALIDATION_ERROR`, raises a form validation error.
If `return_type` is `JSON_RESPONSE`, returns a JSON response with 'available', 'code', and 'message' fields.
If `return_type` is neither, raises a ValueError.
Args:
return_type (ValidationReturnType): The type of error response.
code (str): The error code for the error message.
available (bool, optional): Availability, only used for JSON responses. Defaults to False.
Returns:
A JSON response or a form validation error.
Raises:
ValueError: If `return_type` is neither `FORM_VALIDATION_ERROR` nor `JSON_RESPONSE`.
""" # noqa
match return_type:
case ValidationReturnType.FORM_VALIDATION_ERROR:
raise forms.ValidationError(DOMAIN_API_MESSAGES[code], code=code)
case ValidationReturnType.JSON_RESPONSE:
return JsonResponse({"available": available, "code": code, "message": DOMAIN_API_MESSAGES[code]})
case _:
raise ValueError("Invalid return type specified")
@classmethod @classmethod
def sld(cls, domain: str): def sld(cls, domain: str):
""" """

View file

@ -2,14 +2,16 @@
{% load field_helpers %} {% load field_helpers %}
{% block form_instructions %} {% block form_instructions %}
<p>Wed like to know more about your organization. Include the following in your response: </p> <p>To help us determine your eligibility for a .gov domain, we need to know more about your organization. For example:</p>
<ul class="usa-list"> <ul class="usa-list">
<li>The type of work your organization does </li> <li>The type of work your organization does </li>
<li>How your organization is a government organization that is independent of a state government </li> <li>How your organization operates independently from a state government</li>
<li>Include links to authorizing legislation, applicable bylaws or charter, or other documentation to support your claims.</li> <li>A description of the specialized, essential services you offer (if applicable)</li>
<li>Links to authorizing legislation, applicable bylaws or charter, or other documentation to support your claims</li>
</ul> </ul>
</p> </p>
<h2>What can you tell us about your organization?</h2>
{% endblock %} {% endblock %}
{% block form_required_fields_help_text %} {% block form_required_fields_help_text %}

View file

@ -2,7 +2,9 @@
{% load field_helpers %} {% load field_helpers %}
{% block form_instructions %} {% block form_instructions %}
<p>Is there anything else you'd like us to know about your domain request? This question is optional.</p> <h2>Is there anything else youd like us to know about your domain request?</h2>
<p>This question is optional.</p>
{% endblock %} {% endblock %}
{% block form_required_fields_help_text %} {% block form_required_fields_help_text %}

View file

@ -14,7 +14,7 @@
{% include "includes/ao_example.html" %} {% include "includes/ao_example.html" %}
</div> </div>
<p>We typically dont reach out to the authorizing official, but if contact is necessary, our practice is to coordinate first with you, the requestor. Read more about <a class="usa-link" rel="noopener noreferrer" target="_blank" href="{% public_site_url 'domains/eligibility/#you-must-have-approval-from-an-authorizing-official-within-your-organization' %}">who can serve as an authorizing official</a>.</p> <p>We typically dont reach out to the authorizing official, but if contact is necessary, our practice is to coordinate with you, the requestor, first.</p>
{% endblock %} {% endblock %}

View file

@ -2,9 +2,9 @@
{% load static field_helpers %} {% load static field_helpers %}
{% block form_instructions %} {% block form_instructions %}
<p>Enter your organizations current public website, if you have one. For example, <p>We can better evaluate your request if we know about domains youre already using.</p>
www.city.com. We can better evaluate your domain request if we know about domains <h2>What are the current websites for your organization?</h2>
youre already using. If you already have any .gov domains please include them. This question is optional.</p> <p>Enter your organizations current public websites. If you already have a .gov domain, include that in your list. This question is optional.</p>
{% endblock %} {% endblock %}
{% block form_required_fields_help_text %} {% block form_required_fields_help_text %}

View file

@ -2,24 +2,22 @@
{% load static field_helpers url_helpers %} {% load static field_helpers url_helpers %}
{% block form_instructions %} {% block form_instructions %}
<p>Before requesting a .gov domain, <a class="usa-link" rel="noopener noreferrer" target="_blank" href="{% public_site_url 'domains/choosing' %}">please make sure it <p>Before requesting a .gov domain, please make sure it meets <a class="usa-link" rel="noopener noreferrer" target="_blank" href="{% public_site_url 'domains/choosing' %}">our naming requirements</a>. Your domain name must:
meets our naming requirements</a>. Your domain name must:
<ul class="usa-list"> <ul class="usa-list">
<li>Be available </li> <li>Be available </li>
<li>Be unique </li>
<li>Relate to your organizations name, location, and/or services </li> <li>Relate to your organizations name, location, and/or services </li>
<li>Be clear to the general public. Your domain name must not be easily confused <li>Be clear to the general public. Your domain name must not be easily confused
with other organizations.</li> with other organizations.</li>
</ul> </ul>
</p> </p>
<p>Names that <em>uniquely apply to your organization</em> are likely to be approved over names that could also apply to other organizations. In most instances, this requires including your states two-letter abbreviation.</p>
<p>Requests for your organizations initials or an abbreviated name might not be approved, but we encourage you to request the name you want.</p>
<p>Note that <strong>only federal agencies can request generic terms</strong> like <p>Note that <strong>only federal agencies can request generic terms</strong> like
vote.gov.</p> vote.gov.</p>
<p>Well try to give you the domain you want. We first need to make sure your request
meets our requirements. Well work with you to find the best domain for your
organization.</p>
<h2 class="margin-top-3">Domain examples for your type of organization</h2> <h2 class="margin-top-3">Domain examples for your type of organization</h2>
<div class="domain_example"> <div class="domain_example">
{% include "includes/domain_example.html" %} {% include "includes/domain_example.html" %}
@ -41,10 +39,7 @@
<h2>What .gov domain do you want?</h2> <h2>What .gov domain do you want?</h2>
</legend> </legend>
<p id="domain_instructions" class="margin-top-05">After you enter your domain, well make sure its <p id="domain_instructions" class="margin-top-05">After you enter your domain, well make sure its available and that it meets some of our naming requirements. If your domain passes these initial checks, well verify that it meets all our requirements after you complete the rest of this form.</p>
available and that it meets some of our naming requirements. If your domain passes
these initial checks, well verify that it meets all of our requirements once you
complete and submit the rest of this form.</p>
{% with attr_aria_describedby="domain_instructions domain_instructions2" %} {% with attr_aria_describedby="domain_instructions domain_instructions2" %}
{# attr_validate / validate="domain" invokes code in get-gov.js #} {# attr_validate / validate="domain" invokes code in get-gov.js #}
@ -53,6 +48,7 @@
{% endwith %} {% endwith %}
{% endwith %} {% endwith %}
<button <button
id="check-availability-button"
type="button" type="button"
class="usa-button" class="usa-button"
validate-for="{{ forms.0.requested_domain.auto_id }}" validate-for="{{ forms.0.requested_domain.auto_id }}"
@ -73,11 +69,13 @@
{# attr_validate / validate="domain" invokes code in get-gov.js #} {# attr_validate / validate="domain" invokes code in get-gov.js #}
{# attr_auto_validate likewise triggers behavior in get-gov.js #} {# attr_auto_validate likewise triggers behavior in get-gov.js #}
{% with append_gov=True attr_validate="domain" attr_auto_validate=True %} {% with append_gov=True attr_validate="domain" attr_auto_validate=True %}
{% with add_class="blank-ok alternate-domain-input" %}
{% for form in forms.1 %} {% for form in forms.1 %}
{% input_with_errors form.alternative_domain %} {% input_with_errors form.alternative_domain %}
{% endfor %} {% endfor %}
{% endwith %} {% endwith %}
{% endwith %} {% endwith %}
{% endwith %}
<button type="submit" name="submit_button" value="save" class="usa-button usa-button--unstyled"> <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"> <svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
@ -87,6 +85,4 @@
</fieldset> </fieldset>
<p id="domain_instructions2">If youre not sure this is the domain you want, thats
okay. You can change it later.</p>
{% endblock %} {% endblock %}

View file

@ -14,7 +14,7 @@
<h2>Time to complete the form</h2> <h2>Time to complete the form</h2>
<p>If you have <a href="{% public_site_url 'domains/before/#information-you%E2%80%99ll-need-to-complete-the-domain-request-form' %}" target="_blank" class="usa-link">all the information you need</a>, <p>If you have <a href="{% public_site_url 'domains/before/#information-you%E2%80%99ll-need-to-complete-the-domain-request-form' %}" target="_blank" class="usa-link">all the information you need</a>,
completing your domain request might take around 15 minutes.</p> completing your domain request might take around 15 minutes.</p>
<p><a href="{% public_site_url 'contact/' %}" target="_blank" rel="noopener noreferrer" class="usa-link">Contact us if you need help with your request</a>.</p>
{% block form_buttons %} {% block form_buttons %}
<div class="stepnav"> <div class="stepnav">
@ -29,6 +29,7 @@
</form> </form>
<div class="caption margin-top-5"><a href="{% public_site_url 'privacy-policy#pra' %}" target="_blank" rel="noopener noreferrer" class="usa-link">Paperwork Reduction Act statement</a> (OMB control number: 1670-0049; expiration date: 10/31/2026)</div>
</div> </div>
</main> </main>
{% endblock %} {% endblock %}

View file

@ -2,15 +2,12 @@
{% load field_helpers %} {% load field_helpers %}
{% block form_instructions %} {% block form_instructions %}
<h2 class="margin-bottom-05"> <p>If your domain request is approved, the name of your organization and your city/state will be listed in <a href="https://beta.get.gov/about/data/" target="_blank">.govs public data.</a></p>
What is the name and mailing address of your organization?
</h2>
<p>Enter the name of the organization you represent. Your organization might be part <h2>What is the name and mailing address of the organization you represent?</h2>
of a larger entity. If so, enter information about your part of the larger entity.</p>
<p>Your organization might be part of a larger entity. If so, enter the name of your part of the larger entity. </p>
<p>If your domain request is approved, the name of your organization will be publicly
listed as the domain registrant.</p>
{% endblock %} {% endblock %}

View file

@ -2,9 +2,11 @@
{% load field_helpers %} {% load field_helpers %}
{% block form_instructions %} {% block form_instructions %}
<h2 class="margin-bottom-05">Is your organization an election office?</h2>
<p>An election office is a government entity whose <em>primary</em> responsibility is overseeing elections and/or conducting voter registration.</p>
<p>An election office is a government entity whose primary responsibility is overseeing elections and/or conducting voter registration. If your organization is an election office, we'll prioritize your request.</p>
<h2>Is your organization an election office?</h2>
<p>Answer “yes” only if the <em>main purpose</em> of your organization is to serve as an election office.</p> <p>Answer “yes” only if the <em>main purpose</em> of your organization is to serve as an election office.</p>

View file

@ -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 %}

View file

@ -2,14 +2,10 @@
{% load field_helpers url_helpers %} {% load field_helpers url_helpers %}
{% block form_instructions %} {% block form_instructions %}
<p>.Gov domain names are for use on the internet. Dont register a .gov to simply reserve a <p>.Gov domains are intended for public use. Domains will not be given to organizations that only want to reserve a domain name (defensive registration) or that only intend to use the domain internally (as for an intranet).</p>
domain name or for mainly internal use.</p> <p>Read about <a class="usa-link" rel="noopener noreferrer" target="_blank" href="{% public_site_url 'domains/requirements/' %}">activities that are prohibited on .gov domains.</a></p>
<h2>What is the purpose of your requested domain?</h2>
<p>Describe the reason for your domain request. Explain how you plan to use this domain. <p>Describe how youll use your .gov domain. Will it be used for a website, email, or something else?</p>
Who is your intended audience? Will you use it for a website and/or email? Are you moving
your website from another top-level domain (like .com or .org)?
Read about <a class="usa-link" rel="noopener noreferrer" target="_blank" href="{% public_site_url 'domains/requirements/' %}">activities that are prohibited on .gov domains.</a></p>
{% endblock %} {% endblock %}
{% block form_required_fields_help_text %} {% block form_required_fields_help_text %}

View file

@ -3,51 +3,55 @@
{% block form_instructions %} {% block form_instructions %}
<p>Please read this page. Check the box at the bottom to show that you agree to the requirements for operating .gov domains.</p> <p>Please read this page. Check the box at the bottom to show that you agree to the requirements for operating .gov domains.</p>
<p>The .gov domain space exists to support a broad diversity of government missions. Generally, we dont examine how government organizations use their domains. However, misuse of a .gov domain can reflect upon the integrity of the entire .gov space. There are categories of misuse that are statutorily prohibited or abusive in nature.</p> <p>The .gov domain space exists to support a broad diversity of government missions. Generally, we dont review or audit how government organizations use their registered domains. However, misuse of a .gov domain can reflect upon the integrity of the entire .gov space. There are categories of misuse that are statutorily prohibited or abusive in nature.</p>
<h2>What you cant do with .gov domains</h2> <h2>What you cant do with a .gov domain</h2>
<h3>Commercial purposes</h3> <h3>Commercial purposes</h3>
<p>.Gov domains must not be used for commercial purposes, such as advertising that benefits private individuals or entities.</p> <p>A .gov domain must not be used for commercial purposes, such as advertising that benefits private individuals or entities.</p>
<h3>Political campaigns</h3> <h3>Political campaigns</h3>
<p>.Gov domains must not be used for political campaigns.</p> <p>A .gov domain must not be used for political campaign purposes, such as the website for a candidate seeking elected office.</p>
<h3>Illegal content</h3> <h3>Illegal content</h3>
<p>.Gov domains must not be used to distribute or promote material whose distribution violates applicable law.</p> <p>A .gov domain must not be used to distribute or promote material whose distribution violates applicable law.</p>
<h3>Malicious cyber activity </h3> <h3>Malicious cyber activity </h3>
<p>.Gov is a trusted and safe space. .Gov domains must not distribute malware, host <p>A .gov domain must not distribute malware, host open redirects, or engage in malicious cyber activity.</p>
open redirects, or otherwise engage in malicious cyber activity.</p>
<h2>What .gov domain registrants must do</h2> <h2>What .gov domain registrants must do</h2>
<h3>Keep your contact information updated</h3> <h3>Keep your contact information updated</h3>
<p>.Gov domain registrants must maintain accurate contact information in the .gov registrar.</p> <p>.Gov domain registrants must maintain accurate contact information in the .gov registrar. You will be asked to verify it as part of the renewal process.</p>
<h3>Be responsive if we contact you</h3> <h3>Be responsive if we contact you</h3>
<p>Registrants should respond promptly to communications about potential violations to these requirements.</p> <p>.Gov domain registrants must respond promptly to communications about potential violations to these requirements.</p>
<h2>Failure to comply with these requirements could result in domain suspension or termination</h2> <h2>Failure to comply could result in domain suspension or termination</h2>
<p>We may need to suspend or terminate a domain registration for violations. When we discover a violation, well make reasonable efforts to contact a registrant, including: <p>We may need to suspend or terminate a domain registration for violations of these requirements. When we discover a violation, well make reasonable efforts to contact a registrant, including emails or phone calls to:
<ul class="usa-list"> <ul class="usa-list">
<li>Emails to domain contacts</li> <li>Domain contacts</li>
<li>Phone calls to domain contacts</li> <li>The authorizing official</li>
<li>Email or phone call to the authorizing official</li> <li>The government organization, a parent organization, or affiliated entities</li>
<li>Emails or phone calls to the government organization, a parent organization,
or affiliated entities</li>
</ul> </ul>
</p> </p>
<p>We understand the critical importance of the availability of .gov domains. Suspending or terminating a .gov domain is reserved for prolonged, unresolved, serious violations where the registrant is non-responsive. We'll make extensive efforts to contact registrants and to identify potential solutions. We'll make reasonable accommodations for remediation timelines based on the severity of the issue.</p> <p>We understand the critical importance of availability for a .gov domain. Suspending or terminating a .gov domain is reserved for prolonged, unresolved, serious violations where the registrant is non-responsive. We'll make extensive efforts to contact registrants and to identify potential solutions. We'll make reasonable accommodations for remediation timelines based on the severity of the issue.</p>
<h2>Domain renewal</h2>
<p>.Gov domains are registered for a one-year period. To renew your domain, you'll be asked to verify your organizations eligibility and your contact information. </p>
<p>Though a domain may expire, it will not automatically be put on hold or deleted. Well make extensive efforts to contact your organization before holding or deleting a domain.</p>
{% endblock %} {% endblock %}
{% block form_required_fields_help_text %} {% block form_required_fields_help_text %}

View file

@ -1,24 +1,24 @@
{% extends 'application_form.html' %} {% extends 'application_form.html' %}
{% load field_helpers %} {% load field_helpers %}
{% block form_instructions %}
<p>To help us determine your eligibility for a .gov domain, we need to know more about your tribal government.</p>
{% endblock %}
{% block form_fields %} {% block form_fields %}
{% with sublabel_text="Please include the entire name of your tribe as recognized by the Bureau of Indian Affairs." %} <h2>What is the name of the tribe you represent?</h2>
{% with link_text="Bureau of Indian Affairs" %} <p>Please include the full name of your tribe as recognized by the <a rel="noopener noreferrer" class="usa-link usa-link--external" href="https://www.federalregister.gov/documents/2024/01/08/2024-00109/indian-entities-recognized-by-and-eligible-to-receive-services-from-the-united-states-bureau-of" target="_blank">Bureau of Indian Affairs</a>.</p>
{% with link_href="https://www.federalregister.gov/documents/2023/01/12/2023-00504/indian-entities-recognized-by-and-eligible-to-receive-services-from-the-united-states-bureau-of" %}
{% with external_link="true" target_blank="true" %} {% with external_link="true" target_blank="true" %}
{% input_with_errors forms.0.tribe_name %} {% input_with_errors forms.0.tribe_name %}
{% endwith %} {% endwith %}
{% endwith %}
{% endwith %}
{% endwith %}
<fieldset class="usa-fieldset"> <fieldset class="usa-fieldset">
<legend class="usa-legend"> <legend class="usa-legend">
<p>Is your organization a federally-recognized tribe or a state-recognized tribe? Check all that apply. <h2>Is your organization a federally-recognized tribe or a state-recognized tribe?</h2>
<abbr class="usa-hint usa-hint--required" title="required">*</abbr></p>
</legend> </legend>
<p>Check all that apply. <abbr class="usa-hint usa-hint--required" title="required">*</abbr></p>
{% input_with_errors forms.0.federally_recognized_tribe %} {% input_with_errors forms.0.federally_recognized_tribe %}
{% input_with_errors forms.0.state_recognized_tribe %} {% input_with_errors forms.0.state_recognized_tribe %}
</fieldset> </fieldset>

View file

@ -10,7 +10,7 @@
<h1>Withdraw request for {{ domainapplication.requested_domain.name }}?</h1> <h1>Withdraw request for {{ domainapplication.requested_domain.name }}?</h1>
<p>If you withdraw your request we won't review it. Once you withdraw your request you'll be able to edit it or completely remove it. </p> <p>If you withdraw your request, we won't review it. Once you withdraw your request, you can edit it and submit it again. </p>
<p><a href="{% url 'application-withdrawn' domainapplication.id %}" class="usa-button withdraw">Withdraw request</a> <p><a href="{% url 'application-withdrawn' domainapplication.id %}" class="usa-button withdraw">Withdraw request</a>
<a href="{% url 'application-status' domainapplication.id %}">Cancel</a></p> <a href="{% url 'application-status' domainapplication.id %}">Cancel</a></p>

View file

@ -2,14 +2,11 @@
{% load field_helpers %} {% load field_helpers %}
{% block form_instructions %} {% block form_instructions %}
<p>Well use this information to contact you about your domain request.</p> <p>While reviewing your domain request, we may need to reach out with questions. Well also email you when we complete our review.</p>
<p>If youd like us to use a different name, email, or phone number you can make those <h2>What contact information should we use to reach you?</h2>
changes below. Changing your contact information here wont affect your Login.gov
account information.</p>
<p>The contact information you provide here wont be public and will only be used to <p>Your contact information wont be made public and will be used only for .gov purposes. The information you provide here won't impact your Login.gov account information.</p>
support your domain request.</p>
{% endblock %} {% endblock %}

View file

@ -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 }}

View file

@ -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 }}

View file

@ -40,8 +40,7 @@
<ul class="usa-list"> <ul class="usa-list">
<li>AmericanSamoa.gov</li> <li>AmericanSamoa.gov</li>
<li>Colorado.gov</li> <li>Colorado.gov</li>
<li>Georgia.gov</li> <li>MN.gov</li>
<li>AmericanSamoa.gov </li>
<li>Guam.gov</li> <li>Guam.gov</li>
</ul> </ul>
@ -55,45 +54,49 @@
</ul> </ul>
{% elif organization_type == 'county' %} {% elif organization_type == 'county' %}
<p>Most county .gov domains must include the two-letter state abbreviation or the full state name. County names that arent shared by any other city, county, parish, town, borough, village or equivalent in the U.S., at the time a domain is granted, can be requested without referring to the state. Counties can include “county” in their domain to distinguish it from other places with similar names. We use the <a class="usa-link usa-link--external" rel="noopener noreferrer" target="_blank" href="https://www.census.gov/geographies/reference-files/time-series/geo/gazetteer-files.html">Census Bureaus National Places Gazetteer Files</a> to determine if county names are unique.</p> <p>Most county .gov domains must include the two-letter state abbreviation or the full state name. County names that arent shared by any other city, county, parish, town, borough, village or equivalent in the U.S. (at the time a domain is granted) dont have to refer to their state in their domain name. Counties can include “county” in their domain to distinguish it from other places with similar names.</p>
<p>We use the <a class="usa-link usa-link--external" rel="noopener noreferrer" target="_blank" href="https://www.census.gov/geographies/reference-files/time-series/geo/gazetteer-files.html">Census Bureaus National Places Gazetteer Files</a> to determine if county names are unique.</p>
<p><strong>Examples:</strong></p> <p><strong>Examples:</strong></p>
<ul class="usa-list"> <ul class="usa-list">
<li>AdamsCountyMS.gov</li> <li>LACounty.gov</li>
<li>Erie.gov</li>
<li>LivingstonParishLA.gov</li> <li>LivingstonParishLA.gov</li>
<li>MitchellCountyNC.gov</li> <li>MitchellCountyNC.gov</li>
<li>MiamiDade.gov</li>
</ul> </ul>
{% elif organization_type == 'city' %} {% elif organization_type == 'city' %}
<p>Most city domains must include the two-letter state abbreviation or clearly spell out the state name. Using phrases like “City of” or “Town of” is optional.</p> <p>Most city domains must include the two-letter state abbreviation or clearly spell out the state name. Using phrases like “City of” or “Town of” is optional.</p>
<p>Cities that meet one of the criteria below dont have to refer to their state in the domain name. <p>Cities that meet one of the criteria below dont have to refer to their state in their domain name.
<ul class="usa-list"> <ul class="usa-list">
<li>City names that are not shared by any other U.S. city, town, or village can be requested without referring to the state. We use the <a class="usa-link usa-link--external" rel="noopener noreferrer" target="_blank" href="https://www.census.gov/geographies/reference-files/time-series/geo/gazetteer-files.html">Census Bureaus National Places Gazetteer Files</a> to determine if names are unique.</li> <li>The city name is not shared by any other U.S. city, town, village, or county. We use the <a class="usa-link usa-link--external" rel="noopener noreferrer" target="_blank" href="https://www.census.gov/geographies/reference-files/time-series/geo/gazetteer-files.html">Census Bureaus National Places Gazetteer Files</a> to determine if names are unique.</li>
<li>Certain cities are so well-known that they may not require a state reference to communicate location. We use the list of U.S. “dateline cities” in the Associated Press Stylebook to make this determination.</li> <li>The city is so well known that it doesnt need a state reference to communicate location. We use the list of U.S. “dateline cities” in the Associated Press Stylebook as part of our decision.</li>
<li>The 50 largest cities, as measured by population according to the Census Bureau, can have .gov domain names that dont refer to their state.</li> <li>Its one of the 150 largest cities by population, <a class="usa-link usa-link--external" rel="noopener noreferrer" target="_blank" href="https://www.census.gov/data/tables/time-series/demo/popest/2020s-total-cities-and-towns.html">according to the Census Bureau.</a></li>
</ul> </ul>
<p><strong>Examples:</strong></p> <p><strong>Examples:</strong></p>
<ul class="usa-list"> <ul class="usa-list">
<li>CityofEudoraKS.gov</li> <li>CityofEudoraKS.gov</li>
<li>Pocatello.gov</li>
<li>WallaWallaWA.gov</li> <li>WallaWallaWA.gov</li>
<li>Pocatello.gov</li>
</ul> </ul>
</p> </p>
{% elif organization_type == 'special_district' %} {% elif organization_type == 'special_district' %}
<p>Domain names must represent your organization or institutional name, not solely the services you provide. It also needs to include your two-letter state abbreviation or clearly spell out the state name unless <a class="usa-link" rel="noopener noreferrer" target="_blank" href="{% public_site_url 'domains/choosing/#counties' %}">county or city exceptions apply</a>.</p> <p>Domain names must represent your organization or institutional name, not solely the services you provide. It also needs to include your two-letter state abbreviation or clearly spell out the state name.</p>
<p><strong>Examples:</strong></p> <p><strong>Examples:</strong></p>
<ul class="usa-list"> <ul class="usa-list">
<li>ElectionsShelbyTN.gov</li>
<li>GlacierViewFire.gov</li> <li>GlacierViewFire.gov</li>
<li>HVcoVote.gov</li>
<li>TechshareTX.gov</li> <li>TechshareTX.gov</li>
<li>UtahTrust.gov</li> <li>UtahTrust.gov</li>
</ul> </ul>
{% elif organization_type == 'school_district' %} {% elif organization_type == 'school_district' %}
<p>Domain names must represent your organization or institutional name.</p> <p>Domain names must represent your organization or institutional name.</p>
<p><strong>Example:</strong> mckinneyISDTX.gov </p> <p><strong>Examples:</strong></p>
<ul class="usa-list">
<li>mckinneyISDTX.gov</li>
<li>BooneCSDIA.gov</li>
</ul>
{%endif %} {%endif %}

View file

@ -1,8 +1,11 @@
"""Test form validation requirements.""" """Test form validation requirements."""
import json
from django.test import TestCase, RequestFactory from django.test import TestCase, RequestFactory
from api.views import available
from registrar.forms.application_wizard import ( from registrar.forms.application_wizard import (
AlternativeDomainForm,
CurrentSitesForm, CurrentSitesForm,
DotGovDomainForm, DotGovDomainForm,
AuthorizingOfficialForm, AuthorizingOfficialForm,
@ -23,6 +26,7 @@ from django.contrib.auth import get_user_model
class TestFormValidation(MockEppLib): class TestFormValidation(MockEppLib):
def setUp(self): def setUp(self):
super().setUp() super().setUp()
self.API_BASE_PATH = "/api/v1/available/?domain="
self.user = get_user_model().objects.create(username="username") self.user = get_user_model().objects.create(username="username")
self.factory = RequestFactory() self.factory = RequestFactory()
@ -74,6 +78,113 @@ class TestFormValidation(MockEppLib):
["Enter the .gov domain you want without any periods."], ["Enter the .gov domain you want without any periods."],
) )
def test_requested_domain_errors_consistent(self):
"""Tests if the errors on submit and with the check availability buttons are consistent
for requested_domains
"""
test_cases = [
# extra_dots
("top-level-agency.com", "Enter the .gov domain you want without any periods."),
# invalid
(
"underscores_forever",
"Enter a domain using only letters, numbers, " "or hyphens (though we don't recommend using hyphens).",
),
# required
(
"",
"Enter the .gov domain you want. Dont include “www” or “.gov.”"
" For example, if you want www.city.gov, you would enter “city”"
" (without the quotes).",
),
# unavailable
(
"whitehouse.gov",
"That domain isnt available. <a class='usa-link' "
"href='https://beta.get.gov/domains/choosing' target='_blank'>Read more about "
"choosing your .gov domain</a>.",
),
]
for domain, expected_error in test_cases:
with self.subTest(domain=domain, error=expected_error):
form = DotGovDomainForm(data={"requested_domain": domain})
form_error = list(form.errors["requested_domain"])
# Ensure the form returns what we expect
self.assertEqual(
form_error,
[expected_error],
)
request = self.factory.get(self.API_BASE_PATH + domain)
request.user = self.user
response = available(request, domain=domain)
# Ensure that we're getting the right kind of response
self.assertContains(response, "available")
response_object = json.loads(response.content)
json_error = response_object["message"]
# Test if the message is what we expect
self.assertEqual(json_error, expected_error)
# While its implied,
# for good measure, test if the two objects are equal anyway
self.assertEqual([json_error], form_error)
def test_alternate_domain_errors_consistent(self):
"""Tests if the errors on submit and with the check availability buttons are consistent
for alternative_domains
"""
test_cases = [
# extra_dots
("top-level-agency.com", "Enter the .gov domain you want without any periods."),
# invalid
(
"underscores_forever",
"Enter a domain using only letters, numbers, " "or hyphens (though we don't recommend using hyphens).",
),
# unavailable
(
"whitehouse.gov",
"That domain isnt available. <a class='usa-link' "
"href='https://beta.get.gov/domains/choosing' target='_blank'>Read more about "
"choosing your .gov domain</a>.",
),
]
for domain, expected_error in test_cases:
with self.subTest(domain=domain, error=expected_error):
form = AlternativeDomainForm(data={"alternative_domain": domain})
form_error = list(form.errors["alternative_domain"])
# Ensure the form returns what we expect
self.assertEqual(
form_error,
[expected_error],
)
request = self.factory.get(self.API_BASE_PATH + domain)
request.user = self.user
response = available(request, domain=domain)
# Ensure that we're getting the right kind of response
self.assertContains(response, "available")
response_object = json.loads(response.content)
json_error = response_object["message"]
# Test if the message is what we expect
self.assertEqual(json_error, expected_error)
# While its implied,
# for good measure, test if the two objects are equal anyway
self.assertEqual([json_error], form_error)
def test_requested_domain_two_dots_invalid(self): def test_requested_domain_two_dots_invalid(self):
"""don't accept domains that are subdomains""" """don't accept domains that are subdomains"""
form = DotGovDomainForm(data={"requested_domain": "sub.top-level-agency.gov"}) form = DotGovDomainForm(data={"requested_domain": "sub.top-level-agency.gov"})

View file

@ -691,8 +691,12 @@ class TestContact(TestCase):
self.user, _ = User.objects.get_or_create(email=self.email, first_name="Jeff", last_name="Lebowski") self.user, _ = User.objects.get_or_create(email=self.email, first_name="Jeff", last_name="Lebowski")
self.contact, _ = Contact.objects.get_or_create(user=self.user) self.contact, _ = Contact.objects.get_or_create(user=self.user)
self.contact_as_ao, _ = Contact.objects.get_or_create(email="newguy@igorville.gov")
self.application = DomainApplication.objects.create(creator=self.user, authorizing_official=self.contact_as_ao)
def tearDown(self): def tearDown(self):
super().tearDown() super().tearDown()
DomainApplication.objects.all().delete()
Contact.objects.all().delete() Contact.objects.all().delete()
User.objects.all().delete() User.objects.all().delete()
@ -766,3 +770,12 @@ class TestContact(TestCase):
# Updating the contact's email does not propagate # Updating the contact's email does not propagate
self.assertEqual(self.invalid_contact.email, "joey.baloney@diaperville.com") self.assertEqual(self.invalid_contact.email, "joey.baloney@diaperville.com")
self.assertEqual(self.invalid_user.email, "intern@igorville.gov") self.assertEqual(self.invalid_user.email, "intern@igorville.gov")
def test_has_more_than_one_join(self):
"""Test the Contact model method, has_more_than_one_join"""
# test for a contact which has one user defined
self.assertFalse(self.contact.has_more_than_one_join("user"))
self.assertTrue(self.contact.has_more_than_one_join("authorizing_official"))
# test for a contact which is assigned as an authorizing official on an application
self.assertFalse(self.contact_as_ao.has_more_than_one_join("authorizing_official"))
self.assertTrue(self.contact_as_ao.has_more_than_one_join("submitted_applications"))

View file

@ -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
@ -1032,7 +1033,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(
@ -1154,31 +1155,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",
@ -1211,35 +1296,531 @@ 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,
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_edit_other_contact_in_place(self):
"""When you:
1. edit an existing contact which is not joined to another model,
2. then submit,
The application is linked to the existing contact, and the existing contact updated."""
# Populate the database with a domain application that
# has 1 "other contact" assigned to it
# We'll do it from scratch
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)
# other_contact_pk is the initial pk of the other contact. set it before update
# to be able to verify after update that the same contact object is in place
other_contact_pk = other.id
# 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")
# update the first name of the contact
other_contacts_form["other_contacts-0-first_name"] = "Testy3"
# Submit the updated form
other_contacts_form.submit()
application.refresh_from_db()
# assert that the Other Contact is updated "in place"
other_contact = application.other_contacts.all()[0]
self.assertEquals(other_contact_pk, other_contact.id)
self.assertEquals("Testy3", other_contact.first_name)
def test_edit_other_contact_creates_new(self):
"""When you:
1. edit an existing contact which IS joined to another model,
2. then submit,
The application is linked to a new contact, and the new contact is updated."""
# Populate the database with a domain application that
# has 1 "other contact" assigned to it, the other contact is also
# the authorizing official initially
# We'll do it from scratch
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",
)
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(ao)
# other_contact_pk is the initial pk of the other contact. set it before update
# to be able to verify after update that the ao contact is still in place
# and not updated, and that the new contact has a new id
other_contact_pk = ao.id
# 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, "Testy")
# update the first name of the contact
other_contacts_form["other_contacts-0-first_name"] = "Testy2"
# Submit the updated form
other_contacts_form.submit()
application.refresh_from_db()
# assert that other contact info is updated, and that a new Contact
# is created for the other contact
other_contact = application.other_contacts.all()[0]
self.assertNotEquals(other_contact_pk, other_contact.id)
self.assertEquals("Testy2", other_contact.first_name)
# assert that the authorizing official is not updated
authorizing_official = application.authorizing_official
self.assertEquals("Testy", authorizing_official.first_name)
def test_edit_authorizing_official_in_place(self):
"""When you:
1. edit an authorizing official which is not joined to another model,
2. then submit,
The application is linked to the existing ao, and the ao updated."""
# Populate the database with a domain application that
# has an authorizing_official (ao)
# We'll do it from scratch
ao, _ = Contact.objects.get_or_create(
first_name="Testy",
last_name="Tester",
title="Chief Tester",
email="testy@town.com",
phone="(201) 555 5555",
)
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,
creator=self.user,
status="started",
) )
# Verify that on submit, user is advanced to "no contacts" page # ao_pk is the initial pk of the Authorizing Official. set it before update
no_contacts_page = result.follow() # to be able to verify after update that the same Contact object is in place
expected_url_slug = str(Step.NO_OTHER_CONTACTS) ao_pk = ao.id
actual_url_slug = no_contacts_page.request.path.split("/")[-2]
self.assertEqual(expected_url_slug, actual_url_slug) # 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)
ao_page = self.app.get(reverse("application:authorizing_official"))
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
ao_form = ao_page.forms[0]
# Minimal check to ensure the form is loaded
self.assertEqual(ao_form["authorizing_official-first_name"].value, "Testy")
# update the first name of the contact
ao_form["authorizing_official-first_name"] = "Testy2"
# Submit the updated form
ao_form.submit()
application.refresh_from_db()
# assert AO is updated "in place"
updated_ao = application.authorizing_official
self.assertEquals(ao_pk, updated_ao.id)
self.assertEquals("Testy2", updated_ao.first_name)
def test_edit_authorizing_official_creates_new(self):
"""When you:
1. edit an existing authorizing official which IS joined to another model,
2. then submit,
The application is linked to a new Contact, and the new Contact is updated."""
# Populate the database with a domain application that
# has authorizing official assigned to it, the authorizing offical is also
# an other contact initially
# We'll do it from scratch
ao, _ = Contact.objects.get_or_create(
first_name="Testy",
last_name="Tester",
title="Chief Tester",
email="testy@town.com",
phone="(201) 555 5555",
)
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,
creator=self.user,
status="started",
)
application.other_contacts.add(ao)
# ao_pk is the initial pk of the authorizing official. set it before update
# to be able to verify after update that the other contact is still in place
# and not updated, and that the new ao has a new id
ao_pk = ao.id
# 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)
ao_page = self.app.get(reverse("application:authorizing_official"))
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
ao_form = ao_page.forms[0]
# Minimal check to ensure the form is loaded
self.assertEqual(ao_form["authorizing_official-first_name"].value, "Testy")
# update the first name of the contact
ao_form["authorizing_official-first_name"] = "Testy2"
# Submit the updated form
ao_form.submit()
application.refresh_from_db()
# assert that the other contact is not updated
other_contacts = application.other_contacts.all()
other_contact = other_contacts[0]
self.assertEquals(ao_pk, other_contact.id)
self.assertEquals("Testy", other_contact.first_name)
# assert that the authorizing official is updated
authorizing_official = application.authorizing_official
self.assertEquals("Testy2", authorizing_official.first_name)
def test_edit_submitter_in_place(self):
"""When you:
1. edit a submitter (your contact) which is not joined to another model,
2. then submit,
The application is linked to the existing submitter, and the submitter updated."""
# Populate the database with a domain application that
# has a submitter
# We'll do it from scratch
you, _ = Contact.objects.get_or_create(
first_name="Testy",
last_name="Tester",
title="Chief Tester",
email="testy@town.com",
phone="(201) 555 5555",
)
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",
submitter=you,
creator=self.user,
status="started",
)
# submitter_pk is the initial pk of the submitter. set it before update
# to be able to verify after update that the same contact object is in place
submitter_pk = you.id
# 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)
your_contact_page = self.app.get(reverse("application:your_contact"))
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
your_contact_form = your_contact_page.forms[0]
# Minimal check to ensure the form is loaded
self.assertEqual(your_contact_form["your_contact-first_name"].value, "Testy")
# update the first name of the contact
your_contact_form["your_contact-first_name"] = "Testy2"
# Submit the updated form
your_contact_form.submit()
application.refresh_from_db()
updated_submitter = application.submitter
self.assertEquals(submitter_pk, updated_submitter.id)
self.assertEquals("Testy2", updated_submitter.first_name)
def test_edit_submitter_creates_new(self):
"""When you:
1. edit an existing your contact which IS joined to another model,
2. then submit,
The application is linked to a new Contact, and the new Contact is updated."""
# Populate the database with a domain application that
# has submitter assigned to it, the submitter is also
# an other contact initially
# We'll do it from scratch
submitter, _ = Contact.objects.get_or_create(
first_name="Testy",
last_name="Tester",
title="Chief Tester",
email="testy@town.com",
phone="(201) 555 5555",
)
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",
submitter=submitter,
creator=self.user,
status="started",
)
application.other_contacts.add(submitter)
# submitter_pk is the initial pk of the your contact. set it before update
# to be able to verify after update that the other contact is still in place
# and not updated, and that the new submitter has a new id
submitter_pk = submitter.id
# 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)
your_contact_page = self.app.get(reverse("application:your_contact"))
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
your_contact_form = your_contact_page.forms[0]
# Minimal check to ensure the form is loaded
self.assertEqual(your_contact_form["your_contact-first_name"].value, "Testy")
# update the first name of the contact
your_contact_form["your_contact-first_name"] = "Testy2"
# Submit the updated form
your_contact_form.submit()
application.refresh_from_db()
# assert that the other contact is not updated
other_contacts = application.other_contacts.all()
other_contact = other_contacts[0]
self.assertEquals(submitter_pk, other_contact.id)
self.assertEquals("Testy", other_contact.first_name)
# assert that the submitter is updated
submitter = application.submitter
self.assertEquals("Testy2", submitter.first_name)
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."""
@ -2621,6 +3202,65 @@ class TestDomainAuthorizingOfficial(TestDomainOverview):
page = self.app.get(reverse("domain-authorizing-official", kwargs={"pk": self.domain.id})) page = self.app.get(reverse("domain-authorizing-official", kwargs={"pk": self.domain.id}))
self.assertContains(page, "Testy") self.assertContains(page, "Testy")
def test_domain_edit_authorizing_official_in_place(self):
"""When editing an authorizing official for domain information and AO is not
joined to any other objects"""
self.domain_information.authorizing_official = Contact(
first_name="Testy", last_name="Tester", title="CIO", email="nobody@igorville.gov"
)
self.domain_information.authorizing_official.save()
self.domain_information.save()
ao_page = self.app.get(reverse("domain-authorizing-official", kwargs={"pk": self.domain.id}))
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
ao_form = ao_page.forms[0]
self.assertEqual(ao_form["first_name"].value, "Testy")
ao_form["first_name"] = "Testy2"
# ao_pk is the initial pk of the authorizing official. set it before update
# to be able to verify after update that the same contact object is in place
ao_pk = self.domain_information.authorizing_official.id
ao_form.submit()
# refresh domain information
self.domain_information.refresh_from_db()
self.assertEqual("Testy2", self.domain_information.authorizing_official.first_name)
self.assertEqual(ao_pk, self.domain_information.authorizing_official.id)
def test_domain_edit_authorizing_official_creates_new(self):
"""When editing an authorizing official for domain information and AO IS
joined to another object"""
# set AO and Other Contact to the same Contact object
self.domain_information.authorizing_official = Contact(
first_name="Testy", last_name="Tester", title="CIO", email="nobody@igorville.gov"
)
self.domain_information.authorizing_official.save()
self.domain_information.save()
self.domain_information.other_contacts.add(self.domain_information.authorizing_official)
self.domain_information.save()
# load the Authorizing Official in the web form
ao_page = self.app.get(reverse("domain-authorizing-official", kwargs={"pk": self.domain.id}))
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
ao_form = ao_page.forms[0]
# verify the first name is "Testy" and then change it to "Testy2"
self.assertEqual(ao_form["first_name"].value, "Testy")
ao_form["first_name"] = "Testy2"
# ao_pk is the initial pk of the authorizing official. set it before update
# to be able to verify after update that the same contact object is in place
ao_pk = self.domain_information.authorizing_official.id
ao_form.submit()
# refresh domain information
self.domain_information.refresh_from_db()
# assert that AO information is updated, and that the AO is a new Contact
self.assertEqual("Testy2", self.domain_information.authorizing_official.first_name)
self.assertNotEqual(ao_pk, self.domain_information.authorizing_official.id)
# assert that the Other Contact information is not updated and that the Other Contact
# is the original Contact object
other_contact = self.domain_information.other_contacts.all()[0]
self.assertEqual("Testy", other_contact.first_name)
self.assertEqual(ao_pk, other_contact.id)
class TestDomainOrganization(TestDomainOverview): class TestDomainOrganization(TestDomainOverview):
def test_domain_org_name_address(self): def test_domain_org_name_address(self):

View file

@ -0,0 +1,28 @@
"""Used for holding various enums"""
from enum import Enum
class ValidationReturnType(Enum):
"""Determines the return value of the validate_and_handle_errors class"""
JSON_RESPONSE = "JSON_RESPONSE"
FORM_VALIDATION_ERROR = "FORM_VALIDATION_ERROR"
class LogCode(Enum):
"""Stores the desired log severity
Overview of error codes:
- 1 ERROR
- 2 WARNING
- 3 INFO
- 4 DEBUG
- 5 DEFAULT
"""
ERROR = 1
WARNING = 2
INFO = 3
DEBUG = 4
DEFAULT = 5

View file

@ -17,6 +17,12 @@ class RegistrySystemError(ValueError):
pass pass
class InvalidDomainError(ValueError):
"""Error class for situations where an invalid domain is supplied"""
pass
class ActionNotAllowed(Exception): class ActionNotAllowed(Exception):
"""User accessed an action that is not """User accessed an action that is not
allowed by the current state""" allowed by the current state"""

View file

@ -505,6 +505,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():

View file

@ -222,6 +222,10 @@ class DomainAuthorizingOfficialView(DomainFormBaseView):
def form_valid(self, form): def form_valid(self, form):
"""The form is valid, save the authorizing official.""" """The form is valid, save the authorizing official."""
# Set the domain information in the form so that it can be accessible
# to associate a new Contact as authorizing official, if new Contact is needed
# in the save() method
form.set_domain_info(self.object.domain_info)
form.save() form.save()
messages.success(self.request, "The authorizing official for this domain has been updated.") messages.success(self.request, "The authorizing official for this domain has been updated.")