Merge branch 'main' into za/1561-populate-first-ready-at

This commit is contained in:
zandercymatics 2024-01-11 11:57:41 -07:00
commit a58aa6c567
No known key found for this signature in database
GPG key ID: FF4636ABEC9682B7
45 changed files with 1282 additions and 456 deletions

View file

@ -29,7 +29,7 @@ DOMAIN_API_MESSAGES = {
"unavailable": mark_safe( # nosec
"That domain isnt available. "
"<a class='usa-link' href='{}' target='_blank'>"
"Read more about choosing your .gov domain.</a>".format(public_site_url("domains/choosing"))
"Read more about choosing your .gov domain</a>.".format(public_site_url("domains/choosing"))
),
"invalid": "Enter a domain using only letters, numbers, or hyphens (though we don't recommend using hyphens).",
"success": "That domain is available! Well try to give you the domain you want, \

View file

@ -51,7 +51,7 @@ class ViewsTest(TestCase):
# assert
self.assertEqual(response.status_code, 500)
self.assertTemplateUsed(response, "500.html")
self.assertIn("server error", response.content.decode("utf-8"))
self.assertIn("Server error", response.content.decode("utf-8"))
def test_login_callback_reads_next(self, mock_client):
# setup

View file

@ -401,6 +401,8 @@ class HostIPInline(admin.StackedInline):
class MyHostAdmin(AuditedAdmin):
"""Custom host admin class to use our inlines."""
search_fields = ["name", "domain__name"]
search_help_text = "Search by domain or hostname."
inlines = [HostIPInline]
@ -1251,8 +1253,8 @@ admin.site.register(models.DomainInformation, DomainInformationAdmin)
admin.site.register(models.Domain, DomainAdmin)
admin.site.register(models.DraftDomain, DraftDomainAdmin)
# Host and HostIP removed from django admin because changes in admin
# do not propogate to registry and logic not applied
# admin.site.register(models.Host, MyHostAdmin)
# do not propagate to registry and logic not applied
admin.site.register(models.Host, MyHostAdmin)
admin.site.register(models.Website, WebsiteAdmin)
admin.site.register(models.PublicContact, AuditedAdmin)
admin.site.register(models.DomainApplication, DomainApplicationAdmin)

View file

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

View file

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

View file

@ -7,6 +7,7 @@ from phonenumber_field.formfields import PhoneNumberField # type: ignore
from django import forms
from django.core.validators import RegexValidator, MaxLengthValidator
from django.utils.safestring import mark_safe
from django.db.models.fields.related import ForeignObjectRel, OneToOneField
from api.views import DOMAIN_API_MESSAGES
@ -95,10 +96,39 @@ class RegistrarFormSet(forms.BaseFormSet):
"""
raise NotImplementedError
def has_more_than_one_join(self, db_obj, rel, related_name):
"""Helper for finding whether an object is joined more than once."""
# threshold is the number of related objects that are acceptable
# when determining if related objects exist. threshold is 0 for most
# relationships. if the relationship is related_name, we know that
# there is already exactly 1 acceptable relationship (the one we are
# attempting to delete), so the threshold is 1
threshold = 1 if rel == related_name else 0
# Raise a KeyError if rel is not a defined field on the db_obj model
# This will help catch any errors in reverse_join config on forms
if rel not in [field.name for field in db_obj._meta.get_fields()]:
raise KeyError(f"{rel} is not a defined field on the {db_obj._meta.model_name} model.")
# if attr rel in db_obj is not None, then test if reference object(s) exist
if getattr(db_obj, rel) is not None:
field = db_obj._meta.get_field(rel)
if isinstance(field, OneToOneField):
# if the rel field is a OneToOne field, then we have already
# determined that the object exists (is not None)
return True
elif isinstance(field, ForeignObjectRel):
# if the rel field is a ManyToOne or ManyToMany, then we need
# to determine if the count of related objects is greater than
# the threshold
return getattr(db_obj, rel).count() > threshold
return False
def _to_database(
self,
obj: DomainApplication,
join: str,
reverse_joins: list,
should_delete: Callable,
pre_update: Callable,
pre_create: Callable,
@ -115,26 +145,39 @@ class RegistrarFormSet(forms.BaseFormSet):
query = getattr(obj, join).order_by("created_at").all() # order matters
# get the related name for the join defined for the db_obj for this form.
# the related name will be the reference on a related object back to db_obj
related_name = ""
field = obj._meta.get_field(join)
if isinstance(field, ForeignObjectRel) and callable(field.related_query_name):
related_name = field.related_query_name()
elif hasattr(field, "related_query_name") and callable(field.related_query_name):
related_name = field.related_query_name()
# the use of `zip` pairs the forms in the formset with the
# related objects gotten from the database -- there should always be
# at least as many forms as database entries: extra forms means new
# entries, but fewer forms is _not_ the correct way to delete items
# (likely a client-side error or an attempt at data tampering)
for db_obj, post_data in zip_longest(query, self.forms, fillvalue=None):
cleaned = post_data.cleaned_data if post_data is not None else {}
# matching database object exists, update it
if db_obj is not None and cleaned:
if should_delete(cleaned):
db_obj.delete()
continue
if any(self.has_more_than_one_join(db_obj, rel, related_name) for rel in reverse_joins):
# Remove the specific relationship without deleting the object
getattr(db_obj, related_name).remove(self.application)
else:
# If there are no other relationships, delete the object
db_obj.delete()
else:
pre_update(db_obj, cleaned)
db_obj.save()
# no matching database object, create it
elif db_obj is None and cleaned:
# make sure not to create a database object if cleaned has 'delete' attribute
elif db_obj is None and cleaned and not cleaned.get("delete", False):
kwargs = pre_create(db_obj, cleaned)
getattr(obj, join).create(**kwargs)
@ -262,7 +305,7 @@ class OrganizationContactForm(RegistrarForm):
validators=[
RegexValidator(
"^[0-9]{5}(?:-[0-9]{4})?$|^$",
message="Enter a zip code in the required format, like 12345 or 12345-6789.",
message="Enter a zip code in the form of 12345 or 12345-6789.",
)
],
)
@ -353,7 +396,7 @@ class CurrentSitesForm(RegistrarForm):
required=False,
label="Public website",
error_messages={
"invalid": ("Enter your organization's current website in the required format, like www.city.com.")
"invalid": ("Enter your organization's current website in the required format, like example.com.")
},
)
@ -366,7 +409,9 @@ class BaseCurrentSitesFormSet(RegistrarFormSet):
return website.strip() == ""
def to_database(self, obj: DomainApplication):
self._to_database(obj, self.JOIN, self.should_delete, self.pre_update, self.pre_create)
# If we want to test against multiple joins for a website object, replace the empty array
# and change the JOIN in the models to allow for reverse references
self._to_database(obj, self.JOIN, [], self.should_delete, self.pre_update, self.pre_create)
@classmethod
def from_database(cls, obj):
@ -423,7 +468,9 @@ class BaseAlternativeDomainFormSet(RegistrarFormSet):
return {}
def to_database(self, obj: DomainApplication):
self._to_database(obj, self.JOIN, self.should_delete, self.pre_update, self.pre_create)
# If we want to test against multiple joins for a website object, replace the empty array and
# change the JOIN in the models to allow for reverse references
self._to_database(obj, self.JOIN, [], self.should_delete, self.pre_update, self.pre_create)
@classmethod
def on_fetch(cls, query):
@ -497,7 +544,7 @@ class PurposeForm(RegistrarForm):
message="Response must be less than 1000 characters.",
)
],
error_messages={"required": "Describe how you'll use the .gov domain youre requesting."},
error_messages={"required": "Describe how youll use the .gov domain youre requesting."},
)
@ -543,10 +590,31 @@ class YourContactForm(RegistrarForm):
)
phone = PhoneNumberField(
label="Phone",
error_messages={"required": "Enter your phone number."},
error_messages={"invalid": "Enter a valid 10-digit phone number.", "required": "Enter your phone number."},
)
class OtherContactsYesNoForm(RegistrarForm):
def __init__(self, *args, **kwargs):
"""Extend the initialization of the form from RegistrarForm __init__"""
super().__init__(*args, **kwargs)
# set the initial value based on attributes of application
if self.application and self.application.has_other_contacts():
initial_value = True
elif self.application and self.application.has_rationale():
initial_value = False
else:
# No pre-selection for new applications
initial_value = None
self.fields["has_other_contacts"] = forms.TypedChoiceField(
coerce=lambda x: x.lower() == "true" if x is not None else None, # coerce strings to bool, excepting None
choices=((True, "Yes, I can name other employees."), (False, "No (Well ask you to explain why).")),
initial=initial_value,
widget=forms.RadioSelect,
)
class OtherContactsForm(RegistrarForm):
first_name = forms.CharField(
label="First name / given name",
@ -574,9 +642,19 @@ class OtherContactsForm(RegistrarForm):
)
phone = PhoneNumberField(
label="Phone",
error_messages={"required": "Enter a phone number for this contact."},
error_messages={
"invalid": "Enter a valid 10-digit phone number.",
"required": "Enter a phone number for this contact.",
},
)
def __init__(self, *args, **kwargs):
self.form_data_marked_for_deletion = False
super().__init__(*args, **kwargs)
def mark_form_for_deletion(self):
self.form_data_marked_for_deletion = True
def clean(self):
"""
This method overrides the default behavior for forms.
@ -586,16 +664,124 @@ class OtherContactsForm(RegistrarForm):
validation
"""
# Set form_is_empty to True initially
form_is_empty = True
for name, field in self.fields.items():
# get the value of the field from the widget
value = field.widget.value_from_datadict(self.data, self.files, self.add_prefix(name))
# if any field in the submitted form is not empty, set form_is_empty to False
if value is not None and value != "":
form_is_empty = False
if self.form_data_marked_for_deletion:
# clear any errors raised by the form fields
# (before this clean() method is run, each field
# performs its own clean, which could result in
# errors that we wish to ignore at this point)
#
# NOTE: we cannot just clear() the errors list.
# That causes problems.
for field in self.fields:
if field in self.errors:
del self.errors[field]
# return empty object with only 'delete' attribute defined.
# this will prevent _to_database from creating an empty
# database object
return {"delete": True}
if form_is_empty:
return self.cleaned_data
class BaseOtherContactsFormSet(RegistrarFormSet):
JOIN = "other_contacts"
REVERSE_JOINS = [
"user",
"authorizing_official",
"submitted_applications",
"contact_applications",
"information_authorizing_official",
"submitted_applications_information",
"contact_applications_information",
]
def __init__(self, *args, **kwargs):
self.formset_data_marked_for_deletion = False
self.application = kwargs.pop("application", None)
super(RegistrarFormSet, self).__init__(*args, **kwargs)
# quick workaround to ensure that the HTML `required`
# attribute shows up on required fields for the first form
# in the formset plus those that have data already.
for index in range(max(self.initial_form_count(), 1)):
self.forms[index].use_required_attribute = True
def should_delete(self, cleaned):
empty = (isinstance(v, str) and (v.strip() == "" or v is None) for v in cleaned.values())
return all(empty) or self.formset_data_marked_for_deletion
def to_database(self, obj: DomainApplication):
self._to_database(obj, self.JOIN, self.REVERSE_JOINS, self.should_delete, self.pre_update, self.pre_create)
@classmethod
def from_database(cls, obj):
return super().from_database(obj, cls.JOIN, cls.on_fetch)
def mark_formset_for_deletion(self):
"""Mark other contacts formset for deletion.
Updates forms in formset as well to mark them for deletion.
This has an effect on validity checks and to_database methods.
"""
self.formset_data_marked_for_deletion = True
for form in self.forms:
form.mark_form_for_deletion()
def is_valid(self):
"""Extend is_valid from RegistrarFormSet. When marking this formset for deletion, set
validate_min to false so that validation does not attempt to enforce a minimum
number of other contacts when contacts marked for deletion"""
if self.formset_data_marked_for_deletion:
self.validate_min = False
return super().is_valid()
OtherContactsFormSet = forms.formset_factory(
OtherContactsForm,
extra=1,
absolute_max=1500, # django default; use `max_num` to limit entries
min_num=1,
validate_min=True,
formset=BaseOtherContactsFormSet,
)
class NoOtherContactsForm(RegistrarForm):
no_other_contacts_rationale = forms.CharField(
required=True,
# label has to end in a space to get the label_suffix to show
label=(
"You dont need to provide names of other employees now, but it may "
"slow down our assessment of your eligibility. Describe why there are "
"no other employees who can help verify your request."
),
widget=forms.Textarea(),
validators=[
MaxLengthValidator(
1000,
message="Response must be less than 1000 characters.",
)
],
error_messages={"required": ("Rationale for no other employees is required.")},
)
def __init__(self, *args, **kwargs):
self.form_data_marked_for_deletion = False
super().__init__(*args, **kwargs)
def mark_form_for_deletion(self):
"""Marks no_other_contacts form for deletion.
This changes behavior of validity checks and to_database
methods."""
self.form_data_marked_for_deletion = True
def clean(self):
"""
This method overrides the default behavior for forms.
This cleans the form after field validation has already taken place.
In this override, remove errors associated with the form if form data
is marked for deletion.
"""
if self.form_data_marked_for_deletion:
# clear any errors raised by the form fields
# (before this clean() method is run, each field
# performs its own clean, which could result in
@ -609,46 +795,22 @@ class OtherContactsForm(RegistrarForm):
return self.cleaned_data
class BaseOtherContactsFormSet(RegistrarFormSet):
JOIN = "other_contacts"
def should_delete(self, cleaned):
empty = (isinstance(v, str) and (v.strip() == "" or v is None) for v in cleaned.values())
return all(empty)
def to_database(self, obj: DomainApplication):
self._to_database(obj, self.JOIN, self.should_delete, self.pre_update, self.pre_create)
@classmethod
def from_database(cls, obj):
return super().from_database(obj, cls.JOIN, cls.on_fetch)
OtherContactsFormSet = forms.formset_factory(
OtherContactsForm,
extra=1,
absolute_max=1500, # django default; use `max_num` to limit entries
formset=BaseOtherContactsFormSet,
)
class NoOtherContactsForm(RegistrarForm):
no_other_contacts_rationale = forms.CharField(
required=True,
# label has to end in a space to get the label_suffix to show
label=(
"Please explain why there are no other employees from your organization "
"we can contact to help us assess your eligibility for a .gov domain."
),
widget=forms.Textarea(),
validators=[
MaxLengthValidator(
1000,
message="Response must be less than 1000 characters.",
)
],
)
def to_database(self, obj):
"""
This method overrides the behavior of RegistrarForm.
If form data is marked for deletion, set relevant fields
to None before saving.
Do nothing if form is not valid.
"""
if not self.is_valid():
return
if self.form_data_marked_for_deletion:
for field_name, _ in self.fields.items():
setattr(obj, field_name, None)
else:
for name, value in self.cleaned_data.items():
setattr(obj, name, value)
obj.save()
class AnythingElseForm(RegistrarForm):

View file

@ -59,7 +59,7 @@ class DomainNameserverForm(forms.Form):
# add custom error messages
self.fields["server"].error_messages.update(
{
"required": "A minimum of 2 name servers are required.",
"required": "At least two name servers are required.",
}
)

View file

@ -0,0 +1,262 @@
"""Loops through each valid DomainInformation object and updates its agency value"""
import argparse
import csv
import logging
import os
from typing import List
from django.core.management import BaseCommand
from registrar.management.commands.utility.terminal_helper import TerminalColors, TerminalHelper
from registrar.models.domain_information import DomainInformation
from django.db.models import Q
from registrar.models.transition_domain import TransitionDomain
logger = logging.getLogger(__name__)
class Command(BaseCommand):
help = "Loops through each valid DomainInformation object and updates its agency value"
def __init__(self):
super().__init__()
self.di_to_update: List[DomainInformation] = []
self.di_failed_to_update: List[DomainInformation] = []
self.di_skipped: List[DomainInformation] = []
def add_arguments(self, parser):
"""Adds command line arguments"""
parser.add_argument(
"current_full_filepath",
help="TBD",
)
parser.add_argument("--debug", action=argparse.BooleanOptionalAction)
parser.add_argument("--sep", default=",", help="Delimiter character")
def handle(self, current_full_filepath, **kwargs):
"""Loops through each valid DomainInformation object and updates its agency value"""
debug = kwargs.get("debug")
separator = kwargs.get("sep")
# Check if the provided file path is valid
if not os.path.isfile(current_full_filepath):
raise argparse.ArgumentTypeError(f"Invalid file path '{current_full_filepath}'")
# === Update the "federal_agency" field === #
was_success = self.patch_agency_info(debug)
# === Try to process anything that was skipped === #
# We should only correct skipped records if the previous step was successful.
# If something goes wrong, then we risk corrupting data, so skip this step.
if len(self.di_skipped) > 0 and was_success:
# Flush out the list of DomainInformations to update
self.di_to_update.clear()
self.process_skipped_records(current_full_filepath, separator, debug)
# Clear the old skipped list, and log the run summary
self.di_skipped.clear()
self.log_script_run_summary(debug)
elif not was_success:
# This code should never execute. This can only occur if bulk_update somehow fails,
# which may indicate some sort of data corruption.
logger.error(
f"{TerminalColors.FAIL}"
"Could not automatically patch skipped records. The initial update failed."
"An error was encountered when running this script, please inspect the following "
f"records for accuracy and completeness: {self.di_failed_to_update}"
f"{TerminalColors.ENDC}"
)
def patch_agency_info(self, debug):
"""
Updates the federal_agency field of each valid DomainInformation object based on the corresponding
TransitionDomain object. Skips the update if the TransitionDomain object does not exist or its
federal_agency field is None. Logs the update, skip, and failure actions if debug mode is on.
After all updates, logs a summary of the results.
"""
# Grab all DomainInformation objects (and their associated TransitionDomains)
# that need to be updated
empty_agency_query = Q(federal_agency=None) | Q(federal_agency="")
domain_info_to_fix = DomainInformation.objects.filter(empty_agency_query)
domain_names = domain_info_to_fix.values_list("domain__name", flat=True)
transition_domains = TransitionDomain.objects.filter(domain_name__in=domain_names).exclude(empty_agency_query)
# Get the domain names from TransitionDomain
td_agencies = transition_domains.values_list("domain_name", "federal_agency").distinct()
human_readable_domain_names = list(domain_names)
# Code execution will stop here if the user prompts "N"
TerminalHelper.prompt_for_execution(
system_exit_on_terminate=True,
info_to_inspect=f"""
==Proposed Changes==
Number of DomainInformation objects to change: {len(human_readable_domain_names)}
The following DomainInformation objects will be modified: {human_readable_domain_names}
""",
prompt_title="Do you wish to patch federal_agency data?",
)
logger.info("Updating...")
# Create a dictionary mapping of domain_name to federal_agency
td_dict = dict(td_agencies)
for di in domain_info_to_fix:
domain_name = di.domain.name
federal_agency = td_dict.get(domain_name)
log_message = None
# If agency exists on a TransitionDomain, update the related DomainInformation object
if domain_name in td_dict:
di.federal_agency = federal_agency
self.di_to_update.append(di)
log_message = f"{TerminalColors.OKCYAN}Updated {di}{TerminalColors.ENDC}"
else:
self.di_skipped.append(di)
log_message = f"{TerminalColors.YELLOW}Skipping update for {di}{TerminalColors.ENDC}"
# Log the action if debug mode is on
if debug and log_message is not None:
logger.info(log_message)
# Bulk update the federal agency field in DomainInformation objects
DomainInformation.objects.bulk_update(self.di_to_update, ["federal_agency"])
# Get a list of each domain we changed
corrected_domains = DomainInformation.objects.filter(domain__name__in=domain_names)
# After the update has happened, do a sweep of what we get back.
# If the fields we expect to update are still None, then something is wrong.
for di in corrected_domains:
if di not in self.di_skipped and di.federal_agency is None:
logger.info(f"{TerminalColors.FAIL}Failed to update {di}{TerminalColors.ENDC}")
self.di_failed_to_update.append(di)
# === Log results and return data === #
self.log_script_run_summary(debug)
# Tracks if this script was successful. If any errors are found, something went very wrong.
was_success = len(self.di_failed_to_update) == 0
return was_success
def process_skipped_records(self, file_path, separator, debug):
"""If we encounter any DomainInformation records that do not have data in the associated
TransitionDomain record, then check the associated current-full.csv file for this
information."""
# Code execution will stop here if the user prompts "N"
TerminalHelper.prompt_for_execution(
system_exit_on_terminate=True,
info_to_inspect=f"""
==File location==
current-full.csv filepath: {file_path}
==Proposed Changes==
Number of DomainInformation objects to change: {len(self.di_skipped)}
The following DomainInformation objects will be modified if agency data exists in file: {self.di_skipped}
""",
prompt_title="Do you wish to patch skipped records?",
)
logger.info("Updating...")
file_data = self.read_current_full(file_path, separator)
for di in self.di_skipped:
domain_name = di.domain.name
row = file_data.get(domain_name)
fed_agency = None
if row is not None and "agency" in row:
fed_agency = row.get("agency")
# Determine if we should update this record or not.
# If we don't get any data back, something went wrong.
if fed_agency is not None:
di.federal_agency = fed_agency
self.di_to_update.append(di)
if debug:
logger.info(f"{TerminalColors.OKCYAN}" f"Updating {di}" f"{TerminalColors.ENDC}")
else:
self.di_failed_to_update.append(di)
logger.error(
f"{TerminalColors.FAIL}" f"Could not update {di}. No information found." f"{TerminalColors.ENDC}"
)
# Bulk update the federal agency field in DomainInformation objects
DomainInformation.objects.bulk_update(self.di_to_update, ["federal_agency"])
def read_current_full(self, file_path, separator):
"""Reads the current-full.csv file and stores it in a dictionary"""
with open(file_path, "r") as requested_file:
old_reader = csv.DictReader(requested_file, delimiter=separator)
# Some variants of current-full.csv have key casing differences for fields
# such as "Domain name" or "Domain Name". This corrects that.
reader = self.lowercase_fieldnames(old_reader)
# Return a dictionary with the domain name as the key,
# and the row information as the value
dict_data = {}
for row in reader:
domain_name = row.get("domain name")
if domain_name is not None:
domain_name = domain_name.lower()
dict_data[domain_name] = row
return dict_data
def lowercase_fieldnames(self, reader):
"""Lowercases all field keys in a dictreader to account for potential casing differences"""
for row in reader:
yield {k.lower(): v for k, v in row.items()}
def log_script_run_summary(self, debug):
"""Prints success, failed, and skipped counts, as well as
all affected objects."""
update_success_count = len(self.di_to_update)
update_failed_count = len(self.di_failed_to_update)
update_skipped_count = len(self.di_skipped)
# Prepare debug messages
debug_messages = {
"success": (f"{TerminalColors.OKCYAN}Updated: {self.di_to_update}{TerminalColors.ENDC}\n"),
"skipped": (f"{TerminalColors.YELLOW}Skipped: {self.di_skipped}{TerminalColors.ENDC}\n"),
"failed": (f"{TerminalColors.FAIL}Failed: {self.di_failed_to_update}{TerminalColors.ENDC}\n"),
}
# Print out a list of everything that was changed, if we have any changes to log.
# Otherwise, don't print anything.
TerminalHelper.print_conditional(
debug,
f"{debug_messages.get('success') if update_success_count > 0 else ''}"
f"{debug_messages.get('skipped') if update_skipped_count > 0 else ''}"
f"{debug_messages.get('failed') if update_failed_count > 0 else ''}",
)
if update_failed_count == 0 and update_skipped_count == 0:
logger.info(
f"""{TerminalColors.OKGREEN}
============= FINISHED ===============
Updated {update_success_count} DomainInformation entries
{TerminalColors.ENDC}
"""
)
elif update_failed_count == 0:
logger.warning(
f"""{TerminalColors.YELLOW}
============= FINISHED ===============
Updated {update_success_count} DomainInformation entries
----- SOME AGENCY DATA WAS NONE (WILL BE PATCHED AUTOMATICALLY) -----
Skipped updating {update_skipped_count} DomainInformation entries
{TerminalColors.ENDC}
"""
)
else:
logger.error(
f"""{TerminalColors.FAIL}
============= FINISHED ===============
Updated {update_success_count} DomainInformation entries
----- UPDATE FAILED -----
Failed to update {update_failed_count} DomainInformation entries,
Skipped updating {update_skipped_count} DomainInformation entries
{TerminalColors.ENDC}
"""
)

View file

@ -0,0 +1,17 @@
# Generated by Django 4.2.7 on 2024-01-09 02:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("registrar", "0061_domain_security_contact_registry_id"),
]
operations = [
migrations.AlterField(
model_name="host",
name="name",
field=models.CharField(default=None, help_text="Fully qualified domain name", max_length=253),
),
]

View file

@ -653,13 +653,11 @@ class DomainApplication(TimeStampedModel):
def in_review(self):
"""Investigate an application that has been submitted.
As a side effect, an email notification is sent."""
self._send_status_update_email(
"application in review",
"emails/status_change_in_review.txt",
"emails/status_change_in_review_subject.txt",
)
This action is logged."""
literal = DomainApplication.ApplicationStatus.IN_REVIEW
# Check if the tuple exists, then grab its value
in_review = literal if literal is not None else "In Review"
logger.info(f"A status change occurred. {self} was changed to '{in_review}'")
@transition(
field="status",
@ -674,13 +672,11 @@ class DomainApplication(TimeStampedModel):
def action_needed(self):
"""Send back an application that is under investigation or rejected.
As a side effect, an email notification is sent."""
self._send_status_update_email(
"action needed",
"emails/status_change_action_needed.txt",
"emails/status_change_action_needed_subject.txt",
)
This action is logged."""
literal = DomainApplication.ApplicationStatus.ACTION_NEEDED
# Check if the tuple is setup correctly, then grab its value
action_needed = literal if literal is not None else "Action Needed"
logger.info(f"A status change occurred. {self} was changed to '{action_needed}'")
@transition(
field="status",
@ -840,9 +836,13 @@ class DomainApplication(TimeStampedModel):
DomainApplication.OrganizationChoices.INTERSTATE,
]
def show_no_other_contacts_rationale(self) -> bool:
"""Show this step if the other contacts are blank."""
return not self.other_contacts.exists()
def has_rationale(self) -> bool:
"""Does this application have no_other_contacts_rationale?"""
return bool(self.no_other_contacts_rationale)
def has_other_contacts(self) -> bool:
"""Does this application have other contacts listed?"""
return self.other_contacts.exists()
def is_federal(self) -> Union[bool, None]:
"""Is this application for a federal agency?

View file

@ -20,7 +20,7 @@ class Host(TimeStampedModel):
null=False,
blank=False,
default=None, # prevent saving without a value
unique=True,
unique=False,
help_text="Fully qualified domain name",
)
@ -30,3 +30,6 @@ class Host(TimeStampedModel):
related_name="host", # access this Host via the Domain as `domain.host`
help_text="Domain to which this host belongs",
)
def __str__(self):
return f"{self.domain.name} {self.name}"

View file

@ -9,10 +9,10 @@
<div class="grid-row grow-gap">
<div class="tablet:grid-col-6 usa-prose margin-bottom-3">
<h1>
{% translate "You do not have the right permissions to view this page." %}
{% translate "You're not authorized to view this page." %}
</h1>
<h2>
{% translate "Status 403" %}
{% translate "403 error" %}
</h2>
@ -23,7 +23,7 @@
{% endif %}
<p>
You must be an authorized user and need to be signed in to view this page.
<a href="{% url 'login' %}"> Try logging in again</a>.
<a href="{% url 'login' %}"> Try signing in again</a>.
</p>
<p>
If you'd like help with this error <a class="usa-link" rel="noopener noreferrer" target="_blank" href="{% public_site_url 'contact' %}">contact us</a>.

View file

@ -9,10 +9,10 @@
<div class="grid-row grid-gap">
<div class="tablet:grid-col-6 usa-prose margin-bottom-3">
<h1>
{% translate "We're having some trouble" %}
{% translate "We're having some trouble." %}
</h1>
<h2>
{% translate "Status 500 server error" %}
{% translate "500 error" %}
</h2>
{% if friendly_message %}
<p>{{ friendly_message }}</p>

View file

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

View file

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

View file

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

View file

@ -114,7 +114,7 @@
aria-describedby="Your DNSSEC records will be deleted from the registry."
data-force-action
>
{% include 'includes/modal.html' with cancel_button_resets_ds_form=True modal_heading="Warning: You are about to remove all DS records on your domain" modal_description="To fully disable DNSSEC: In addition to removing your DS records here youll also need to delete the DS records at your DNS host. To avoid causing your domain to appear offline you should wait to delete your DS records at your DNS host until the Time to Live (TTL) expires. This is often less than 24 hours, but confirm with your provider." modal_button=modal_button|safe %}
{% include 'includes/modal.html' with cancel_button_resets_ds_form=True modal_heading="Warning: You are about to remove all DS records on your domain." modal_description="To fully disable DNSSEC: In addition to removing your DS records here, youll need to delete the DS records at your DNS host. To avoid causing your domain to appear offline, you should wait to delete your DS records at your DNS host until the Time to Live (TTL) expires. This is often less than 24 hours, but confirm with your provider." modal_button=modal_button|safe %}
</div>
{% endblock %} {# domain_content #}

View file

@ -3,30 +3,38 @@ Hi.
{{ requester_email }} has added you as a manager on {{ domain.name }}.
You can manage this domain on the .gov registrar <https://manage.get.gov>.
----------------------------------------------------------------
YOU NEED A LOGIN.GOV ACCOUNT
Youll need a Login.gov account to manage your .gov domain. Login.gov provides
a simple and secure process for signing into many government services with one
account. If you dont already have one, follow these steps to create your
a simple and secure process for signing in to many government services with one
account.
If you dont already have one, follow these steps to create your
Login.gov account <https://login.gov/help/get-started/create-your-account/>.
DOMAIN MANAGEMENT
As a .gov domain manager you can add or update information about your domain.
As a .gov domain manager, you can add or update information about your domain.
Youll also serve as a contact for your .gov domain. Please keep your contact
information updated. Learn more about domain management <https://get.gov/help/>.
information updated.
Learn more about domain management <https://get.gov/help/domain-management>.
SOMETHING WRONG?
If youre not affiliated with {{ domain.name }} or think you received this
message in error, contact the .gov team <https://get.gov/help/#contact-us>.
message in error, reply to this email.
THANK YOU
.Gov helps the public identify official, trusted information. Thank you for
using a .gov domain.
.Gov helps the public identify official, trusted information. Thank you for using a .gov domain.
----------------------------------------------------------------
The .gov team
Contact us: <https://get.gov/contact/>
Visit <https://get.gov>
{% endautoescape %}
Learn about .gov <https://get.gov>
{% endautoescape %}

View file

@ -1,26 +1,28 @@
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
Hi {{ application.submitter.first_name }}.
Hi, {{ application.submitter.first_name }}.
Your .gov domain request has been withdrawn and will not be reviewed by our team.
Your .gov domain request has been withdrawn.
DOMAIN REQUESTED: {{ application.requested_domain.name }}
REQUEST #: {{ application.id }}
REQUEST RECEIVED ON: {{ application.submission_date|date }}
STATUS: Withdrawn
----------------------------------------------------------------
YOU CAN EDIT YOUR WITHDRAWN REQUEST
You can edit and resubmit this request by signing in to the registrar <https://manage.get.gov/>.
SOMETHING WRONG?
If you didnt ask for this domain request to be withdrawn or think you received this message in error, reply to this email.
The details of your withdrawn request are included below. You can edit and resubmit this application by logging into the registrar. <https://manage.get.gov/>.
THANK YOU
.Gov helps the public identify official, trusted information. Thank you for requesting a .gov domain.
----------------------------------------------------------------
{% include 'emails/includes/application_summary.txt' %}
----------------------------------------------------------------
The .gov team
Contact us: <https://get.gov/contact/>
Visit <https://get.gov>
{% endautoescape %}
Learn about .gov <https://get.gov>
{% endautoescape %}

View file

@ -1 +1 @@
Your .gov domain request has been withdrawn
Update on your .gov request: {{ application.requested_domain.name }}

View file

@ -1,42 +0,0 @@
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
Hi {{ application.submitter.first_name }}.
We've identified an action needed to complete the review of your .gov domain request.
DOMAIN REQUESTED: {{ application.requested_domain.name }}
REQUEST RECEIVED ON: {{ application.submission_date|date }}
REQUEST #: {{ application.id }}
STATUS: Action needed
NEED TO MAKE CHANGES?
If you need to change your request you have to first withdraw it. Once you
withdraw the request you can edit it and submit it again. Changing your request
might add to the wait time. Learn more about withdrawing your request.
<https://get.gov/help/domain-requests/#withdraw-your-domain-request>.
NEXT STEPS
- You will receive a separate email from our team that provides details about the action needed.
You may need to update your application or provide additional information.
- If you do not receive a separate email with these details within one business day, please contact us:
<https://forms.office.com/pages/responsepage.aspx?id=bOfNPG2UEkq7evydCEI1SqHke9Gh6wJEl3kQ5EjWUKlUQzRJWDlBNTBCQUxTTzBaNlhTWURSSTBLTC4u>
THANK YOU
.Gov helps the public identify official, trusted information. Thank you for
requesting a .gov domain.
----------------------------------------------------------------
{% include 'emails/includes/application_summary.txt' %}
----------------------------------------------------------------
The .gov team
Contact us: <https://get.gov/contact/>
Visit <https://get.gov>
{% endautoescape %}

View file

@ -1 +0,0 @@
Action needed for your .gov domain request

View file

@ -1,40 +1,43 @@
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
Hi {{ application.submitter.first_name }}.
Hi, {{ application.submitter.first_name }}.
Congratulations! Your .gov domain request has been approved.
DOMAIN REQUESTED: {{ application.requested_domain.name }}
REQUEST RECEIVED ON: {{ application.submission_date|date }}
REQUEST #: {{ application.id }}
STATUS: In review
STATUS: Approved
Now that your .gov domain has been approved, there are a few more things to do before your domain can be used.
You can manage your approved domain on the .gov registrar <https://manage.get.gov>.
----------------------------------------------------------------
YOU MUST ADD DOMAIN NAME SERVER INFORMATION
ADD DOMAIN NAME SERVER INFORMATION
Before your .gov domain can be used, youll first need to connect it to a Domain Name System (DNS) hosting service. At this time, we dont provide DNS hosting services.
Before your .gov domain can be used, you have to connect it to your Domain Name System (DNS) hosting service. At this time, we dont provide DNS hosting services.
Go to the domain management page to add your domain name server information <https://manage.get.gov/domain/{{ application.approved_domain.id }}/nameservers>.
After youve set up hosting, youll need to enter your name server information on the .gov registrar.
Get help with adding your domain name server information <https://get.gov/help/domain-management/#manage-dns-information-for-your-domain>.
Learn more about:
- Finding a DNS hosting service <https://get.gov/domains/moving/#find-dns-hosting-services>
- Adding name servers <https://get.gov/help/domain-management/#manage-dns-name-servers>.
ADD DOMAIN MANAGERS, SECURITY EMAIL
Currently, youre the only person who can manage this domain. Please keep your contact information updated.
We strongly recommend that you add other points of contact who will help manage your domain. We also recommend that you provide a security email. This email will allow the public to report security issues on your domain. Security emails are made public.
We strongly recommend adding other domain managers who can serve as additional contacts. We also recommend providing a security email that the public can use to report security issues on your domain. You can add domain managers and a security email on the .gov registrar.
Go to the domain management page to add domain contacts <https://manage.get.gov/domain/{{ application.approved_domain.id }}/your-contact-information> and a security email <https://manage.get.gov/domain/{{ application.approved_domain.id }}/security-email>.
Get help with managing your .gov domain <https://get.gov/help/domain-management/>.
Learn more about:
- Adding domain managers <https://get.gov/help/domain-management/#add-a-domain-manager-to-your-.gov-domain>
- Adding a security email <https://get.gov/help/domain-management/#add-or-update-the-security-email-for-your-.gov-domain>
- Domain security best practices <https://get.gov/domains/security/>
THANK YOU
.Gov helps the public identify official, trusted information. Thank you for using a .gov domain.
----------------------------------------------------------------
The .gov team
Contact us: <https://get.gov/contact/>
Visit <https://get.gov>
Learn about .gov <https://get.gov>
{% endautoescape %}

View file

@ -1 +1 @@
Your .gov domain request is approved
Update on your .gov request: {{ application.requested_domain.name }}

View file

@ -1,43 +0,0 @@
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
Hi {{ application.submitter.first_name }}.
Your .gov domain request is being reviewed.
DOMAIN REQUESTED: {{ application.requested_domain.name }}
REQUEST RECEIVED ON: {{ application.submission_date|date }}
REQUEST #: {{ application.id }}
STATUS: In review
NEED TO MAKE CHANGES?
If you need to change your request you have to first withdraw it. Once you
withdraw the request you can edit it and submit it again. Changing your request
might add to the wait time. Learn more about withdrawing your request.
<https://get.gov/help/domain-requests/#withdraw-your-domain-request>.
NEXT STEPS
- Were reviewing your request. This usually takes 20 business days.
- You can check the status of your request at any time.
<https://manage.get.gov/application/{{ application.id }}>
- Well email you with questions or when we complete our review.
THANK YOU
.Gov helps the public identify official, trusted information. Thank you for
requesting a .gov domain.
----------------------------------------------------------------
{% include 'emails/includes/application_summary.txt' %}
----------------------------------------------------------------
The .gov team
Contact us: <https://get.gov/contact/>
Visit <https://get.gov>
{% endautoescape %}

View file

@ -1 +0,0 @@
Your .gov domain request is being reviewed

View file

@ -1,32 +1,32 @@
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
Hi {{ application.submitter.first_name }}.
Hi, {{ application.submitter.first_name }}.
Your .gov domain request has been rejected.
DOMAIN REQUESTED: {{ application.requested_domain.name }}
REQUEST RECEIVED ON: {{ application.submission_date|date }}
REQUEST #: {{ application.id }}
STATUS: Rejected
YOU CAN SUBMIT A NEW REQUEST
The details of your request are included below. If your organization is eligible for a .gov
domain and you meet our other requirements, you can submit a new request. Learn
more about .gov domains <https://get.gov/help/domains/>.
THANK YOU
.Gov helps the public identify official, trusted information. Thank you for
requesting a .gov domain.
----------------------------------------------------------------
{% include 'emails/includes/application_summary.txt' %}
YOU CAN SUBMIT A NEW REQUEST
If your organization is eligible for a .gov domain and you meet our other requirements, you can submit a new request.
Learn more about:
- Eligibility for a .gov domain <https://get.gov/domains/eligibility>
- Choosing a .gov domain name <https://get.gov/domains/choosing>
NEED ASSISTANCE?
If you have questions about this domain request or need help choosing a new domain name, reply to this email.
THANK YOU
.Gov helps the public identify official, trusted information. Thank you for requesting a .gov domain.
----------------------------------------------------------------
The .gov team
Contact us: <https://get.gov/contact/>
Visit <https://get.gov>
Learn about .gov <https://get.gov>
{% endautoescape %}

View file

@ -1 +1 @@
Your .gov domain request has been rejected
Update on your .gov request: {{ application.requested_domain.name }}

View file

@ -1,35 +1,31 @@
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
Hi {{ application.submitter.first_name }}.
Hi, {{ application.submitter.first_name }}.
We received your .gov domain request.
DOMAIN REQUESTED: {{ application.requested_domain.name }}
REQUEST RECEIVED ON: {{ application.submission_date|date }}
REQUEST #: {{ application.id }}
STATUS: Received
STATUS: Submitted
----------------------------------------------------------------
NEXT STEPS
Well review your request. This usually takes 20 business days. During this review well verify that:
- Your organization is eligible for a .gov domain
- You work at the organization and/or can make requests on its behalf
- Your requested domain meets our naming requirements
Well email you if we have questions and when we complete our review. You can check the status of your request at any time on the registrar homepage. <https://manage.get.gov>
NEED TO MAKE CHANGES?
To make changes to your domain request, you have to withdraw it first. Withdrawing your request may extend the time it takes for the .gov team to complete their review.
If you need to change your request you have to first withdraw it. Once you
withdraw the request you can edit it and submit it again. Changing your request
might add to the wait time. Learn more about withdrawing your request.
NEXT STEPS
- Well review your request. This usually takes 20 business days.
- You can check the status of your request at any time.
<https://manage.get.gov/application/{{ application.id }}>
- Well email you with questions or when we complete our review.
Learn more about withdrawing your request <https://get.gov/help/domain-requests/#withdraw-your-domain-request>.
THANK YOU
.Gov helps the public identify official, trusted information. Thank you for
requesting a .gov domain.
.Gov helps the public identify official, trusted information. Thank you for requesting a .gov domain.
----------------------------------------------------------------
@ -38,5 +34,5 @@ requesting a .gov domain.
The .gov team
Contact us: <https://get.gov/contact/>
Visit <https://get.gov>
Learn about .gov <https://get.gov>
{% endautoescape %}

View file

@ -1 +1 @@
Thank you for applying for a .gov domain
Update on your .gov request: {{ application.requested_domain.name }}

View file

@ -743,6 +743,25 @@ class MockEppLib(TestCase):
],
)
mockVerisignDataInfoContact = mockDataInfoDomain.dummyInfoContactResultData(
"defaultVeri", "registrar@dotgov.gov", datetime.datetime(2023, 5, 25, 19, 45, 35), "lastPw"
)
InfoDomainWithVerisignSecurityContact = fakedEppObject(
"fakepw",
cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35),
contacts=[
common.DomainContact(
contact="defaultVeri",
type=PublicContact.ContactTypeChoices.SECURITY,
)
],
hosts=["fake.host.com"],
statuses=[
common.Status(state="serverTransferProhibited", description="", lang="en"),
common.Status(state="inactive", description="", lang="en"),
],
)
InfoDomainWithDefaultTechnicalContact = fakedEppObject(
"fakepw",
cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35),
@ -1058,6 +1077,7 @@ class MockEppLib(TestCase):
"freeman.gov": (self.InfoDomainWithContacts, None),
"threenameserversDomain.gov": (self.infoDomainThreeHosts, None),
"defaultsecurity.gov": (self.InfoDomainWithDefaultSecurityContact, None),
"adomain2.gov": (self.InfoDomainWithVerisignSecurityContact, None),
"defaulttechnical.gov": (self.InfoDomainWithDefaultTechnicalContact, None),
"justnameserver.com": (self.justNameserver, None),
}
@ -1087,6 +1107,8 @@ class MockEppLib(TestCase):
mocked_result = self.mockDefaultSecurityContact
case "defaultTech":
mocked_result = self.mockDefaultTechnicalContact
case "defaultVeri":
mocked_result = self.mockVerisignDataInfoContact
case _:
# Default contact return
mocked_result = self.mockDataInfoContact

View file

@ -457,44 +457,6 @@ class TestDomainApplicationAdmin(MockEppLib):
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
@boto3_mocking.patching
def test_save_model_sends_in_review_email(self):
# make sure there is no user with this email
EMAIL = "mayor@igorville.gov"
User.objects.filter(email=EMAIL).delete()
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
with less_console_noise():
# Create a sample application
application = completed_application(status=DomainApplication.ApplicationStatus.SUBMITTED)
# Create a mock request
request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk))
# Modify the application's property
application.status = DomainApplication.ApplicationStatus.IN_REVIEW
# Use the model admin's save_model method
self.admin.save_model(request, application, form=None, change=True)
# Access the arguments passed to send_email
call_args = self.mock_client.EMAILS_SENT
kwargs = call_args[0]["kwargs"]
# Retrieve the email details from the arguments
from_email = kwargs.get("FromEmailAddress")
to_email = kwargs["Destination"]["ToAddresses"][0]
email_content = kwargs["Content"]
email_body = email_content["Simple"]["Body"]["Text"]["Data"]
# Assert or perform other checks on the email details
expected_string = "Your .gov domain request is being reviewed."
self.assertEqual(from_email, settings.DEFAULT_FROM_EMAIL)
self.assertEqual(to_email, EMAIL)
self.assertIn(expected_string, email_body)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
@boto3_mocking.patching
def test_save_model_sends_approved_email(self):
# make sure there is no user with this email
@ -556,44 +518,6 @@ class TestDomainApplicationAdmin(MockEppLib):
# Test that approved domain exists and equals requested domain
self.assertEqual(application.requested_domain.name, application.approved_domain.name)
@boto3_mocking.patching
def test_save_model_sends_action_needed_email(self):
# make sure there is no user with this email
EMAIL = "mayor@igorville.gov"
User.objects.filter(email=EMAIL).delete()
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
with less_console_noise():
# Create a sample application
application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
# Create a mock request
request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk))
# Modify the application's property
application.status = DomainApplication.ApplicationStatus.ACTION_NEEDED
# Use the model admin's save_model method
self.admin.save_model(request, application, form=None, change=True)
# Access the arguments passed to send_email
call_args = self.mock_client.EMAILS_SENT
kwargs = call_args[0]["kwargs"]
# Retrieve the email details from the arguments
from_email = kwargs.get("FromEmailAddress")
to_email = kwargs["Destination"]["ToAddresses"][0]
email_content = kwargs["Content"]
email_body = email_content["Simple"]["Body"]["Text"]["Data"]
# Assert or perform other checks on the email details
expected_string = "We've identified an action needed to complete the review of your .gov domain request."
self.assertEqual(from_email, settings.DEFAULT_FROM_EMAIL)
self.assertEqual(to_email, EMAIL)
self.assertIn(expected_string, email_body)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
@boto3_mocking.patching
def test_save_model_sends_rejected_email(self):
# make sure there is no user with this email

View file

@ -30,7 +30,7 @@ class TestFormValidation(MockEppLib):
form = OrganizationContactForm(data={"zipcode": "nah"})
self.assertEqual(
form.errors["zipcode"],
["Enter a zip code in the required format, like 12345 or 12345-6789."],
["Enter a zip code in the form of 12345 or 12345-6789."],
)
def test_org_contact_zip_valid(self):
@ -42,7 +42,7 @@ class TestFormValidation(MockEppLib):
form = CurrentSitesForm(data={"website": "nah"})
self.assertEqual(
form.errors["website"],
["Enter your organization's current website in the required format, like www.city.com."],
["Enter your organization's current website in the required format, like example.com."],
)
def test_website_valid(self):
@ -207,7 +207,7 @@ class TestFormValidation(MockEppLib):
def test_your_contact_phone_invalid(self):
"""Must be a valid phone number."""
form = YourContactForm(data={"phone": "boss@boss"})
self.assertTrue(form.errors["phone"][0].startswith("Enter a valid phone number "))
self.assertTrue(form.errors["phone"][0].startswith("Enter a valid 10-digit phone number."))
def test_other_contact_email_invalid(self):
"""must be a valid email address."""
@ -220,7 +220,7 @@ class TestFormValidation(MockEppLib):
def test_other_contact_phone_invalid(self):
"""Must be a valid phone number."""
form = OtherContactsForm(data={"phone": "super@boss"})
self.assertTrue(form.errors["phone"][0].startswith("Enter a valid phone number "))
self.assertTrue(form.errors["phone"][0].startswith("Enter a valid 10-digit phone number."))
def test_requirements_form_blank(self):
"""Requirements box unchecked is an error."""

View file

@ -268,14 +268,12 @@ class TestDomainApplication(TestCase):
(self.ineligible_application, TransitionNotAllowed),
]
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
with less_console_noise():
for application, exception_type in test_cases:
with self.subTest(application=application, exception_type=exception_type):
try:
application.action_needed()
except TransitionNotAllowed:
self.fail("TransitionNotAllowed was raised, but it was not expected.")
for application, exception_type in test_cases:
with self.subTest(application=application, exception_type=exception_type):
try:
application.action_needed()
except TransitionNotAllowed:
self.fail("TransitionNotAllowed was raised, but it was not expected.")
def test_action_needed_transition_not_allowed(self):
"""
@ -288,12 +286,10 @@ class TestDomainApplication(TestCase):
(self.withdrawn_application, TransitionNotAllowed),
]
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
with less_console_noise():
for application, exception_type in test_cases:
with self.subTest(application=application, exception_type=exception_type):
with self.assertRaises(exception_type):
application.action_needed()
for application, exception_type in test_cases:
with self.subTest(application=application, exception_type=exception_type):
with self.assertRaises(exception_type):
application.action_needed()
def test_approved_transition_allowed(self):
"""
@ -500,6 +496,28 @@ class TestDomainApplication(TestCase):
with self.assertRaises(TransitionNotAllowed):
self.approved_application.reject_with_prejudice()
def test_has_rationale_returns_true(self):
"""has_rationale() returns true when an application has no_other_contacts_rationale"""
self.started_application.no_other_contacts_rationale = "You talkin' to me?"
self.started_application.save()
self.assertEquals(self.started_application.has_rationale(), True)
def test_has_rationale_returns_false(self):
"""has_rationale() returns false when an application has no no_other_contacts_rationale"""
self.assertEquals(self.started_application.has_rationale(), False)
def test_has_other_contacts_returns_true(self):
"""has_other_contacts() returns true when an application has other_contacts"""
# completed_application has other contacts by default
self.assertEquals(self.started_application.has_other_contacts(), True)
def test_has_other_contacts_returns_false(self):
"""has_other_contacts() returns false when an application has no other_contacts"""
application = completed_application(
status=DomainApplication.ApplicationStatus.STARTED, name="no-others.gov", has_other_contacts=False
)
self.assertEquals(application.has_other_contacts(), False)
class TestPermissions(TestCase):
"""Test the User-Domain-Role connection."""

View file

@ -10,7 +10,7 @@ class TestNameserverError(TestCase):
def test_with_no_ip(self):
"""Test NameserverError when no ip address is passed"""
nameserver = "nameserver val"
expected = "Using your domain for a name server requires an IP address"
expected = "Using your domain for a name server requires an IP address."
nsException = NameserverError(code=nsErrorCodes.MISSING_IP, nameserver=nameserver)
self.assertEqual(nsException.message, expected)
@ -20,7 +20,7 @@ class TestNameserverError(TestCase):
"""Test NameserverError when no ip address
and no nameserver is passed"""
nameserver = "nameserver val"
expected = "Too many hosts provided, you may not have more than 13 nameservers."
expected = "You can't have more than 13 nameservers."
nsException = NameserverError(code=nsErrorCodes.TOO_MANY_HOSTS, nameserver=nameserver)
self.assertEqual(nsException.message, expected)

View file

@ -4,8 +4,10 @@ from django.test import Client, RequestFactory, TestCase
from io import StringIO
from registrar.models.domain_information import DomainInformation
from registrar.models.domain import Domain
from registrar.models.public_contact import PublicContact
from registrar.models.user import User
from django.contrib.auth import get_user_model
from registrar.tests.common import MockEppLib
from registrar.utility.csv_export import (
write_header,
write_body,
@ -221,8 +223,9 @@ class CsvReportsTest(TestCase):
self.assertEqual(expected_file_content, response.content)
class ExportDataTest(TestCase):
class ExportDataTest(MockEppLib):
def setUp(self):
super().setUp()
username = "test_user"
first_name = "First"
last_name = "Last"
@ -327,11 +330,85 @@ class ExportDataTest(TestCase):
)
def tearDown(self):
PublicContact.objects.all().delete()
Domain.objects.all().delete()
DomainInformation.objects.all().delete()
User.objects.all().delete()
super().tearDown()
def test_export_domains_to_writer_security_emails(self):
"""Test that export_domains_to_writer returns the
expected security email"""
# Add security email information
self.domain_1.name = "defaultsecurity.gov"
self.domain_1.save()
# Invoke setter
self.domain_1.security_contact
# Invoke setter
self.domain_2.security_contact
# Invoke setter
self.domain_3.security_contact
# Create a CSV file in memory
csv_file = StringIO()
writer = csv.writer(csv_file)
# Define columns, sort fields, and filter condition
columns = [
"Domain name",
"Domain type",
"Agency",
"Organization name",
"City",
"State",
"AO",
"AO email",
"Security contact email",
"Status",
"Expiration date",
]
sort_fields = ["domain__name"]
filter_condition = {
"domain__state__in": [
Domain.State.READY,
Domain.State.DNS_NEEDED,
Domain.State.ON_HOLD,
],
}
self.maxDiff = None
# Call the export functions
write_header(writer, columns)
write_body(writer, columns, sort_fields, filter_condition)
# Reset the CSV file's position to the beginning
csv_file.seek(0)
# Read the content into a variable
csv_content = csv_file.read()
# We expect READY domains,
# sorted alphabetially by domain name
expected_content = (
"Domain name,Domain type,Agency,Organization name,City,State,AO,"
"AO email,Security contact email,Status,Expiration date\n"
"adomain10.gov,Federal,Armed Forces Retirement Home,Ready\n"
"adomain2.gov,Interstate,(blank),Dns needed\n"
"ddomain3.gov,Federal,Armed Forces Retirement Home,123@mail.gov,On hold,2023-05-25\n"
"defaultsecurity.gov,Federal - Executive,World War I Centennial Commission,dotgov@cisa.dhs.gov,Ready"
)
# Normalize line endings and remove commas,
# spaces and leading/trailing whitespace
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
self.assertEqual(csv_content, expected_content)
def test_write_body(self):
"""Test that write_body returns the
existing domain, test that sort by domain name works,

View file

@ -77,6 +77,7 @@ class TestWithUser(MockEppLib):
# delete any applications too
super().tearDown()
DomainApplication.objects.all().delete()
DomainInformation.objects.all().delete()
self.user.delete()
@ -219,8 +220,8 @@ class DomainApplicationTests(TestWithUser, WebTest):
in the modal header on the submit page.
"""
num_pages_tested = 0
# elections, type_of_work, tribal_government, no_other_contacts
SKIPPED_PAGES = 4
# elections, type_of_work, tribal_government
SKIPPED_PAGES = 3
num_pages = len(self.TITLES) - SKIPPED_PAGES
intro_page = self.app.get(reverse("application:"))
@ -425,8 +426,13 @@ class DomainApplicationTests(TestWithUser, WebTest):
# Follow the redirect to the next form page
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
other_contacts_page = your_contact_result.follow()
# This page has 3 forms in 1.
# Let's set the yes/no radios to enable the other contacts fieldsets
other_contacts_form = other_contacts_page.forms[0]
other_contacts_form["other_contacts-has_other_contacts"] = "True"
other_contacts_form["other_contacts-0-first_name"] = "Testy2"
other_contacts_form["other_contacts-0-last_name"] = "Tester2"
other_contacts_form["other_contacts-0-title"] = "Another Tester"
@ -564,8 +570,8 @@ class DomainApplicationTests(TestWithUser, WebTest):
@skip("WIP")
def test_application_form_started_allsteps(self):
num_pages_tested = 0
# elections, type_of_work, tribal_government, no_other_contacts
SKIPPED_PAGES = 4
# elections, type_of_work, tribal_government
SKIPPED_PAGES = 3
DASHBOARD_PAGE = 1
num_pages = len(self.TITLES) - SKIPPED_PAGES + DASHBOARD_PAGE
@ -812,24 +818,271 @@ class DomainApplicationTests(TestWithUser, WebTest):
self.assertContains(contact_page, self.TITLES[Step.ABOUT_YOUR_ORGANIZATION])
def test_application_no_other_contacts(self):
"""Applicants with no other contacts have to give a reason."""
contacts_page = self.app.get(reverse("application:other_contacts"))
def test_yes_no_form_inits_blank_for_new_application(self):
"""On the Other Contacts page, the yes/no form gets initialized with nothing selected for
new applications"""
other_contacts_page = self.app.get(reverse("application:other_contacts"))
other_contacts_form = other_contacts_page.forms[0]
self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, None)
def test_yes_no_form_inits_yes_for_application_with_other_contacts(self):
"""On the Other Contacts page, the yes/no form gets initialized with YES selected if the
application has other contacts"""
# Application has other contacts by default
application = completed_application(user=self.user)
# prime the form by visiting /edit
self.app.get(reverse("edit-application", kwargs={"id": application.pk}))
# django-webtest does not handle cookie-based sessions well because it keeps
# resetting the session key on each new request, thus destroying the concept
# of a "session". We are going to do it manually, saving the session ID here
# and then setting the cookie on each request.
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
other_contacts_page = self.app.get(reverse("application:other_contacts"))
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
other_contacts_form = other_contacts_page.forms[0]
self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, "True")
def test_yes_no_form_inits_no_for_application_with_no_other_contacts_rationale(self):
"""On the Other Contacts page, the yes/no form gets initialized with NO selected if the
application has no other contacts"""
# Application has other contacts by default
application = completed_application(user=self.user, has_other_contacts=False)
application.no_other_contacts_rationale = "Hello!"
application.save()
# prime the form by visiting /edit
self.app.get(reverse("edit-application", kwargs={"id": application.pk}))
# django-webtest does not handle cookie-based sessions well because it keeps
# resetting the session key on each new request, thus destroying the concept
# of a "session". We are going to do it manually, saving the session ID here
# and then setting the cookie on each request.
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
other_contacts_page = self.app.get(reverse("application:other_contacts"))
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
other_contacts_form = other_contacts_page.forms[0]
self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, "False")
def test_submitting_other_contacts_deletes_no_other_contacts_rationale(self):
"""When a user submits the Other Contacts form with other contacts selected, the application's
no other contacts rationale gets deleted"""
# Application has other contacts by default
application = completed_application(user=self.user, has_other_contacts=False)
application.no_other_contacts_rationale = "Hello!"
application.save()
# prime the form by visiting /edit
self.app.get(reverse("edit-application", kwargs={"id": application.pk}))
# django-webtest does not handle cookie-based sessions well because it keeps
# resetting the session key on each new request, thus destroying the concept
# of a "session". We are going to do it manually, saving the session ID here
# and then setting the cookie on each request.
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
other_contacts_page = self.app.get(reverse("application:other_contacts"))
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
other_contacts_form = other_contacts_page.forms[0]
self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, "False")
other_contacts_form["other_contacts-has_other_contacts"] = "True"
other_contacts_form["other_contacts-0-first_name"] = "Testy"
other_contacts_form["other_contacts-0-middle_name"] = ""
other_contacts_form["other_contacts-0-last_name"] = "McTesterson"
other_contacts_form["other_contacts-0-title"] = "Lord"
other_contacts_form["other_contacts-0-email"] = "testy@abc.org"
other_contacts_form["other_contacts-0-phone"] = "(201) 555-0123"
# Submit the now empty form
other_contacts_form.submit()
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
result = contacts_page.forms[0].submit()
# follow first redirect
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
no_contacts_page = result.follow()
expected_url_slug = str(Step.NO_OTHER_CONTACTS)
actual_url_slug = no_contacts_page.request.path.split("/")[-2]
self.assertEqual(expected_url_slug, actual_url_slug)
# Verify that the no_other_contacts_rationale we saved earlier has been removed from the database
application = DomainApplication.objects.get()
self.assertEqual(
application.other_contacts.count(),
1,
)
self.assertEquals(
application.no_other_contacts_rationale,
None,
)
def test_submitting_no_other_contacts_rationale_deletes_other_contacts(self):
"""When a user submits the Other Contacts form with no other contacts selected, the application's
other contacts get deleted for other contacts that exist and are not joined to other objects
"""
# Application has other contacts by default
application = completed_application(user=self.user)
# prime the form by visiting /edit
self.app.get(reverse("edit-application", kwargs={"id": application.pk}))
# django-webtest does not handle cookie-based sessions well because it keeps
# resetting the session key on each new request, thus destroying the concept
# of a "session". We are going to do it manually, saving the session ID here
# and then setting the cookie on each request.
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
other_contacts_page = self.app.get(reverse("application:other_contacts"))
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
other_contacts_form = other_contacts_page.forms[0]
self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, "True")
other_contacts_form["other_contacts-has_other_contacts"] = "False"
other_contacts_form["other_contacts-no_other_contacts_rationale"] = "Hello again!"
# Submit the now empty form
other_contacts_form.submit()
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
# Verify that the no_other_contacts_rationale we saved earlier has been removed from the database
application = DomainApplication.objects.get()
self.assertEqual(
application.other_contacts.count(),
0,
)
self.assertEquals(
application.no_other_contacts_rationale,
"Hello again!",
)
def test_submitting_no_other_contacts_rationale_removes_reference_other_contacts_when_joined(self):
"""When a user submits the Other Contacts form with no other contacts selected, the application's
other contacts references get removed for other contacts that exist and are joined to other objects"""
# Populate the databse with a domain application that
# has 1 "other contact" assigned to it
# We'll do it from scratch so we can reuse the other contact
ao, _ = Contact.objects.get_or_create(
first_name="Testy",
last_name="Tester",
title="Chief Tester",
email="testy@town.com",
phone="(555) 555 5555",
)
you, _ = Contact.objects.get_or_create(
first_name="Testy you",
last_name="Tester you",
title="Admin Tester",
email="testy-admin@town.com",
phone="(555) 555 5556",
)
other, _ = Contact.objects.get_or_create(
first_name="Testy2",
last_name="Tester2",
title="Another Tester",
email="testy2@town.com",
phone="(555) 555 5557",
)
application, _ = DomainApplication.objects.get_or_create(
organization_type="federal",
federal_type="executive",
purpose="Purpose of the site",
anything_else="No",
is_policy_acknowledged=True,
organization_name="Testorg",
address_line1="address 1",
state_territory="NY",
zipcode="10002",
authorizing_official=ao,
submitter=you,
creator=self.user,
status="started",
)
application.other_contacts.add(other)
# Now let's join the other contact to another object
domain_info = DomainInformation.objects.create(creator=self.user)
domain_info.other_contacts.set([other])
# prime the form by visiting /edit
self.app.get(reverse("edit-application", kwargs={"id": application.pk}))
# django-webtest does not handle cookie-based sessions well because it keeps
# resetting the session key on each new request, thus destroying the concept
# of a "session". We are going to do it manually, saving the session ID here
# and then setting the cookie on each request.
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
other_contacts_page = self.app.get(reverse("application:other_contacts"))
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
other_contacts_form = other_contacts_page.forms[0]
self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, "True")
other_contacts_form["other_contacts-has_other_contacts"] = "False"
other_contacts_form["other_contacts-no_other_contacts_rationale"] = "Hello again!"
# Submit the now empty form
other_contacts_form.submit()
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
# Verify that the no_other_contacts_rationale we saved earlier is no longer associated with the application
application = DomainApplication.objects.get()
self.assertEqual(
application.other_contacts.count(),
0,
)
# Verify that the 'other' contact object still exists
domain_info = DomainInformation.objects.get()
self.assertEqual(
domain_info.other_contacts.count(),
1,
)
self.assertEqual(
domain_info.other_contacts.all()[0].first_name,
"Testy2",
)
self.assertEquals(
application.no_other_contacts_rationale,
"Hello again!",
)
def test_if_yes_no_form_is_no_then_no_other_contacts_required(self):
"""Applicants with no other contacts have to give a reason."""
other_contacts_page = self.app.get(reverse("application:other_contacts"))
other_contacts_form = other_contacts_page.forms[0]
other_contacts_form["other_contacts-has_other_contacts"] = "False"
response = other_contacts_page.forms[0].submit()
# The textarea for no other contacts returns this error message
# Assert that it is returned, ie the no other contacts form is required
self.assertContains(response, "Rationale for no other employees is required.")
# The first name field for other contacts returns this error message
# Assert that it is not returned, ie the contacts form is not required
self.assertNotContains(response, "Enter the first name / given name of this contact.")
def test_if_yes_no_form_is_yes_then_other_contacts_required(self):
"""Applicants with other contacts do not have to give a reason."""
other_contacts_page = self.app.get(reverse("application:other_contacts"))
other_contacts_form = other_contacts_page.forms[0]
other_contacts_form["other_contacts-has_other_contacts"] = "True"
response = other_contacts_page.forms[0].submit()
# The textarea for no other contacts returns this error message
# Assert that it is not returned, ie the no other contacts form is not required
self.assertNotContains(response, "Rationale for no other employees is required.")
# The first name field for other contacts returns this error message
# Assert that it is returned, ie the contacts form is required
self.assertContains(response, "Enter the first name / given name of this contact.")
@skip("Repurpose when working on ticket 903")
def test_application_delete_other_contact(self):
"""Other contacts can be deleted after being saved to database."""
# Populate the databse with a domain application that
@ -1982,7 +2235,7 @@ class TestDomainNameservers(TestDomainOverview):
# the required field. form requires a minimum of 2 name servers
self.assertContains(
result,
"A minimum of 2 name servers are required.",
"At least two name servers are required.",
count=2,
status_code=200,
)
@ -2223,7 +2476,7 @@ class TestDomainNameservers(TestDomainOverview):
# once around each required field.
self.assertContains(
result,
"A minimum of 2 name servers are required.",
"At least two name servers are required.",
count=4,
status_code=200,
)

View file

@ -26,12 +26,23 @@ def get_domain_infos(filter_condition, sort_fields):
def write_row(writer, columns, domain_info):
security_contacts = domain_info.domain.contacts.filter(contact_type=PublicContact.ContactTypeChoices.SECURITY)
# For linter
ao = " "
if domain_info.authorizing_official:
first_name = domain_info.authorizing_official.first_name or ""
last_name = domain_info.authorizing_official.last_name or ""
ao = first_name + " " + last_name
security_email = " "
if security_contacts:
security_email = security_contacts[0].email
invalid_emails = {"registrar@dotgov.gov"}
# These are default emails that should not be displayed in the csv report
if security_email is not None and security_email.lower() in invalid_emails:
security_email = "(blank)"
# create a dictionary of fields which can be included in output
FIELDS = {
"Domain name": domain_info.domain.name,
@ -44,13 +55,14 @@ def write_row(writer, columns, domain_info):
"State": domain_info.state_territory,
"AO": ao,
"AO email": domain_info.authorizing_official.email if domain_info.authorizing_official else " ",
"Security contact email": security_contacts[0].email if security_contacts else " ",
"Security contact email": security_email,
"Status": domain_info.domain.get_state_display(),
"Expiration date": domain_info.domain.expiration_date,
"Created at": domain_info.domain.created_at,
"First ready": domain_info.domain.first_ready,
"Deleted": domain_info.domain.deleted,
}
writer.writerow([FIELDS.get(column, "") for column in columns])

View file

@ -44,8 +44,8 @@ class GenericError(Exception):
_error_mapping = {
GenericErrorCodes.CANNOT_CONTACT_REGISTRY: (
"Were experiencing a system connection error. Please wait a few minutes "
"and try again. If you continue to receive this error after a few tries, "
"Were experiencing a system error. Please wait a few minutes "
"and try again. If you continue to get this error, "
"contact help@get.gov."
),
GenericErrorCodes.GENERIC_ERROR: ("Value entered was wrong."),
@ -97,13 +97,15 @@ class NameserverError(Exception):
"""
_error_mapping = {
NameserverErrorCodes.MISSING_IP: ("Using your domain for a name server requires an IP address"),
NameserverErrorCodes.MISSING_IP: ("Using your domain for a name server requires an IP address."),
NameserverErrorCodes.GLUE_RECORD_NOT_ALLOWED: ("Name server address does not match domain name"),
NameserverErrorCodes.INVALID_IP: ("{}: Enter an IP address in the required format."),
NameserverErrorCodes.TOO_MANY_HOSTS: ("Too many hosts provided, you may not have more than 13 nameservers."),
NameserverErrorCodes.MISSING_HOST: ("Name server must be provided to enter IP address."),
NameserverErrorCodes.TOO_MANY_HOSTS: ("You can't have more than 13 nameservers."),
NameserverErrorCodes.MISSING_HOST: ("You must provide a name server to enter an IP address."),
NameserverErrorCodes.INVALID_HOST: ("Enter a name server in the required format, like ns1.example.com"),
NameserverErrorCodes.DUPLICATE_HOST: ("Remove duplicate entry"),
NameserverErrorCodes.DUPLICATE_HOST: (
"You already entered this name server address. Name server addresses must be unique."
),
NameserverErrorCodes.BAD_DATA: (
"Theres something wrong with the name server information you provided. "
"If you need help email us at help@get.gov."
@ -156,8 +158,8 @@ class DsDataError(Exception):
),
DsDataErrorCodes.INVALID_DIGEST_SHA1: ("SHA-1 digest must be exactly 40 characters."),
DsDataErrorCodes.INVALID_DIGEST_SHA256: ("SHA-256 digest must be exactly 64 characters."),
DsDataErrorCodes.INVALID_DIGEST_CHARS: ("Digest must contain only alphanumeric characters [0-9,a-f]."),
DsDataErrorCodes.INVALID_KEYTAG_SIZE: ("Key tag must be less than 65535"),
DsDataErrorCodes.INVALID_DIGEST_CHARS: ("Digest must contain only alphanumeric characters (0-9, a-f)."),
DsDataErrorCodes.INVALID_KEYTAG_SIZE: ("Key tag must be less than 65535."),
}
def __init__(self, *args, code=None, **kwargs):
@ -187,7 +189,7 @@ class SecurityEmailError(Exception):
"""
_error_mapping = {
SecurityEmailErrorCodes.BAD_DATA: ("Enter an email address in the required format, like name@example.com.")
SecurityEmailErrorCodes.BAD_DATA: ("Enter an email address in the required format, " "like name@example.com."),
}
def __init__(self, *args, code=None, **kwargs):

View file

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

View file

@ -196,7 +196,7 @@ class DomainOrgNameAddressView(DomainFormBaseView):
"""The form is valid, save the organization name and mailing address."""
form.save()
messages.success(self.request, "The organization information has been updated.")
messages.success(self.request, "The organization information for this domain has been updated.")
# superclass has the redirect
return super().form_valid(form)
@ -348,9 +348,8 @@ class DomainNameserversView(DomainFormBaseView):
messages.success(
self.request,
"The name servers for this domain have been updated. "
"Keep in mind that DNS changes may take some time to "
"propagate across the internet. It can take anywhere "
"from a few minutes to 48 hours for your changes to take place.",
"Note that DNS changes could take anywhere from a few minutes to "
"48 hours to propagate across the internet.",
)
# superclass has the redirect
@ -549,7 +548,7 @@ class DomainYourContactInformationView(DomainFormBaseView):
# Post to DB using values from the form
form.save()
messages.success(self.request, "Your contact information for this domain has been updated.")
messages.success(self.request, "Your contact information has been updated.")
# superclass has the redirect
return super().form_valid(form)
@ -686,7 +685,7 @@ class DomainAddUserView(DomainFormBaseView):
)
else:
if add_success:
messages.success(self.request, f"Invited {email} to this domain.")
messages.success(self.request, f"{email} has been invited to this domain.")
def _make_invitation(self, email_address: str, requester: User):
"""Make a Domain invitation for this email and redirect with a message."""