Merge branch 'main' into za/1456-sorting-not-working-correctly

This commit is contained in:
zandercymatics 2023-12-19 10:05:48 -07:00
commit 8c9aaad42b
No known key found for this signature in database
GPG key ID: FF4636ABEC9682B7
14 changed files with 563 additions and 64 deletions

View file

@ -493,3 +493,34 @@ The `load_organization_data` script has five optional parameters. These are as f
| 3 | **directory** | Specifies the directory containing the files that will be parsed. Defaults to "migrationdata" |
| 4 | **domain_additional_filename** | Specifies the filename of domain_additional. Used as an override for the JSON. Has no default. |
| 5 | **organization_adhoc_filename** | Specifies the filename of organization_adhoc. Used as an override for the JSON. Has no default. |
## Extend Domain Extension Dates
This section outlines how to extend the expiration date of all ready domains (or a select subset) by a defined period of time.
### Running on sandboxes
#### Step 1: Login to CloudFoundry
```cf login -a api.fr.cloud.gov --sso```
#### Step 2: SSH into your environment
```cf ssh getgov-{space}```
Example: `cf ssh getgov-za`
#### Step 3: Create a shell instance
```/tmp/lifecycle/shell```
#### Step 4: Extend domains
```./manage.py extend_expiration_dates```
### Running locally
```docker-compose exec app ./manage.py extend_expiration_dates```
##### Optional parameters
| | Parameter | Description |
|:-:|:-------------------------- |:----------------------------------------------------------------------------|
| 1 | **extensionAmount** | Determines the period of time to extend by, in years. Defaults to 1 year. |
| 2 | **debug** | Increases logging detail. Defaults to False. |
| 3 | **limitParse** | Determines how many domains to parse. Defaults to all. |
| 4 | **disableIdempotentCheck** | Boolean that determines if we should check for idempotence or not. Compares the proposed extension date to the value in TransitionDomains. Defaults to False. |

View file

@ -19,7 +19,6 @@
"http://localhost:8080/register/other_contacts/",
"http://localhost:8080/register/anything_else/",
"http://localhost:8080/register/requirements/",
"http://localhost:8080/register/review/",
"http://localhost:8080/register/finished/"
]
}

View file

@ -91,15 +91,17 @@ def available(request, domain=""):
# validate that the given domain could be a domain name and fail early if
# not.
if not (DraftDomain.string_could_be_domain(domain) or DraftDomain.string_could_be_domain(domain + ".gov")):
return JsonResponse({"available": False, "message": DOMAIN_API_MESSAGES["invalid"]})
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, "message": DOMAIN_API_MESSAGES["success"]})
return JsonResponse({"available": True, "code": "success", "message": DOMAIN_API_MESSAGES["success"]})
else:
return JsonResponse({"available": False, "message": DOMAIN_API_MESSAGES["unavailable"]})
return JsonResponse(
{"available": False, "code": "unavailable", "message": DOMAIN_API_MESSAGES["unavailable"]}
)
except Exception:
return JsonResponse({"available": False, "message": DOMAIN_API_MESSAGES["error"]})
return JsonResponse({"available": False, "code": "error", "message": DOMAIN_API_MESSAGES["error"]})
@require_http_methods(["GET"])

View file

@ -0,0 +1,212 @@
"""Data migration: Extends expiration dates for valid domains"""
import argparse
from datetime import date
import logging
from django.core.management import BaseCommand
from epplibwrapper.errors import RegistryError
from registrar.models import Domain
from registrar.management.commands.utility.terminal_helper import TerminalColors, TerminalHelper
from registrar.models.transition_domain import TransitionDomain
try:
from epplib.exceptions import TransportError
except ImportError:
pass
logger = logging.getLogger(__name__)
class Command(BaseCommand):
help = "Extends expiration dates for valid domains"
def __init__(self):
"""Sets global variables for code tidyness"""
super().__init__()
self.update_success = []
self.update_skipped = []
self.update_failed = []
self.expiration_minimum_cutoff = date(2023, 11, 15)
self.expiration_maximum_cutoff = date(2023, 12, 30)
def add_arguments(self, parser):
"""Add command line arguments."""
parser.add_argument(
"--extensionAmount",
type=int,
default=1,
help="Determines the period (in years) to extend expiration dates by",
)
parser.add_argument(
"--limitParse",
type=int,
default=0,
help="Sets a cap on the number of records to parse",
)
parser.add_argument(
"--disableIdempotentCheck", action=argparse.BooleanOptionalAction, help="Disable script idempotence"
)
parser.add_argument("--debug", action=argparse.BooleanOptionalAction, help="Increases log chattiness")
def handle(self, **options):
"""
Extends the expiration dates for valid domains.
If a parse limit is set and it's less than the total number of valid domains,
the number of domains to change is set to the parse limit.
Includes an idempotence check.
"""
# Retrieve command line options
extension_amount = options.get("extensionAmount")
limit_parse = options.get("limitParse")
disable_idempotence = options.get("disableIdempotentCheck")
debug = options.get("debug")
# Does a check to see if parse_limit is a positive int.
# Raise an error if not.
self.check_if_positive_int(limit_parse, "limitParse")
valid_domains = Domain.objects.filter(
expiration_date__gte=self.expiration_minimum_cutoff,
expiration_date__lte=self.expiration_maximum_cutoff,
state=Domain.State.READY,
).order_by("name")
domains_to_change_count = valid_domains.count()
if limit_parse != 0:
domains_to_change_count = limit_parse
valid_domains = valid_domains[:limit_parse]
# Determines if we should continue code execution or not.
# If the user prompts 'N', a sys.exit() will be called.
self.prompt_user_to_proceed(extension_amount, domains_to_change_count)
for domain in valid_domains:
try:
is_idempotent = self.idempotence_check(domain, extension_amount)
if not disable_idempotence and not is_idempotent:
self.update_skipped.append(domain.name)
logger.info(f"{TerminalColors.YELLOW}" f"Skipping update for {domain}" f"{TerminalColors.ENDC}")
else:
domain.renew_domain(extension_amount)
self.update_success.append(domain.name)
logger.info(
f"{TerminalColors.OKCYAN}"
f"Successfully updated expiration date for {domain}"
f"{TerminalColors.ENDC}"
)
# Catches registry errors. Failures indicate bad data, or a faulty connection.
except (RegistryError, KeyError, TransportError) as err:
self.update_failed.append(domain.name)
logger.error(
f"{TerminalColors.FAIL}" f"Failed to update expiration date for {domain}" f"{TerminalColors.ENDC}"
)
logger.error(err)
except Exception as err:
self.log_script_run_summary(debug)
raise err
self.log_script_run_summary(debug)
# == Helper functions == #
def idempotence_check(self, domain: Domain, extension_amount):
"""Determines if the proposed operation violates idempotency"""
# Because our migration data had a hard stop date, we can determine if our change
# is valid simply checking the date is within a valid range and it was updated
# in epp or not.
# CAVEAT: This is a workaround. A more robust solution would be a db flag
current_expiration_date = domain.registry_expiration_date
transition_domains = TransitionDomain.objects.filter(
domain_name=domain.name, epp_expiration_date=current_expiration_date
)
return transition_domains.count() > 0
def prompt_user_to_proceed(self, extension_amount, domains_to_change_count):
"""Asks if the user wants to proceed with this action"""
TerminalHelper.prompt_for_execution(
system_exit_on_terminate=True,
info_to_inspect=f"""
==Extension Amount==
Period: {extension_amount} year(s)
==Proposed Changes==
Domains to change: {domains_to_change_count}
""",
prompt_title="Do you wish to proceed with these changes?",
)
logger.info(f"{TerminalColors.MAGENTA}" "Preparing to extend expiration dates..." f"{TerminalColors.ENDC}")
def check_if_positive_int(self, value: int, var_name: str):
"""
Determines if the given integer value is positive or not.
If not, it raises an ArgumentTypeError
"""
if value < 0:
raise argparse.ArgumentTypeError(
f"{value} is an invalid integer value for {var_name}. " "Must be positive."
)
return value
def log_script_run_summary(self, debug):
"""Prints success, failed, and skipped counts, as well as
all affected domains."""
update_success_count = len(self.update_success)
update_failed_count = len(self.update_failed)
update_skipped_count = len(self.update_skipped)
# Prepare debug messages
debug_messages = {
"success": (f"{TerminalColors.OKCYAN}Updated these Domains: {self.update_success}{TerminalColors.ENDC}\n"),
"skipped": (f"{TerminalColors.YELLOW}Skipped these Domains: {self.update_skipped}{TerminalColors.ENDC}\n"),
"failed": (
f"{TerminalColors.FAIL}Failed to update these Domains: {self.update_failed}{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} Domain entries
{TerminalColors.ENDC}
"""
)
elif update_failed_count == 0:
logger.info(
f"""{TerminalColors.YELLOW}
============= FINISHED ===============
Updated {update_success_count} Domain entries
----- IDEMPOTENCY CHECK FAILED -----
Skipped updating {update_skipped_count} Domain entries
{TerminalColors.ENDC}
"""
)
else:
logger.info(
f"""{TerminalColors.FAIL}
============= FINISHED ===============
Updated {update_success_count} Domain entries
----- UPDATE FAILED -----
Failed to update {update_failed_count} Domain entries,
Skipped updating {update_skipped_count} Domain entries
{TerminalColors.ENDC}
"""
)

View file

@ -1195,7 +1195,7 @@ class Domain(TimeStampedModel, DomainHelper):
logger.error(e)
logger.error(e.code)
raise e
if e.code == ErrorCode.OBJECT_DOES_NOT_EXIST and self.state != Domain.State.DELETED:
if e.code == ErrorCode.OBJECT_DOES_NOT_EXIST and self.state == Domain.State.UNKNOWN:
# avoid infinite loop
already_tried_to_create = True
self.dns_needed_from_unknown()

View file

@ -122,7 +122,7 @@ class DomainApplication(TimeStampedModel):
FEDERAL = (
"federal",
"Federal: an agency of the U.S. government's executive, legislative, or judicial branches",
"Federal: an agency of the U.S. governments legislative, executive, or judicial branches",
)
INTERSTATE = "interstate", "Interstate: an organization of two or more states"
STATE_OR_TERRITORY = (
@ -139,7 +139,7 @@ class DomainApplication(TimeStampedModel):
CITY = "city", "City: a city, town, township, village, etc."
SPECIAL_DISTRICT = (
"special_district",
"Special district: an independent organization within a single state",
"Special district: an independent government that delivers specialized, essential services",
)
SCHOOL_DISTRICT = (
"school_district",

View file

@ -85,16 +85,29 @@
class="usa-button usa-button--outline"
>Save and return to manage your domains</button>
{% else %}
<button
type="submit"
<a
href="#toggle-submit-domain-request"
class="usa-button usa-button--big dotgov-button--green"
>Submit your domain request</button>
aria-controls="toggle-submit-domain-request"
data-open-modal
>Submit your domain request</a
>
{% endif %}
</div>
{% endblock %}
</form>
<div
class="usa-modal"
id="toggle-submit-domain-request"
aria-labelledby="Are you sure you want to submit a domain request?"
aria-describedby="Are you sure you want to submit a domain request?"
data-force-action
>
{% include 'includes/modal.html' with modal_heading=modal_heading|safe modal_description="Once you submit this request, you wont be able to make further edits until its reviewed by our staff. Youll only be able to withdraw your request." modal_button=modal_button|safe %}
</div>
{% block after_form_content %}{% endblock %}
</main>

View file

@ -12,7 +12,7 @@
<p>You can enter your name servers, as well as other DNS-related information, in the following sections:</p>
{% url 'domain-dns-nameservers' pk=domain.id as url %}
<ul>
<ul class="usa-list">
<li><a href="{{ url }}">Name servers</a></li>
{% url 'domain-dns-dnssec' pk=domain.id as url %}

View file

@ -620,6 +620,17 @@ class MockEppLib(TestCase):
],
ex_date=datetime.date(2023, 5, 25),
)
mockDataExtensionDomain = fakedEppObject(
"fakePw",
cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35),
contacts=[common.DomainContact(contact="123", type=PublicContact.ContactTypeChoices.SECURITY)],
hosts=["fake.host.com"],
statuses=[
common.Status(state="serverTransferProhibited", description="", lang="en"),
common.Status(state="inactive", description="", lang="en"),
],
ex_date=datetime.date(2023, 11, 15),
)
mockDataInfoContact = mockDataInfoDomain.dummyInfoContactResultData(
"123", "123@mail.gov", datetime.datetime(2023, 5, 25, 19, 45, 35), "lastPw"
)
@ -820,6 +831,21 @@ class MockEppLib(TestCase):
ex_date=datetime.date(2023, 5, 25),
)
mockDnsNeededRenewedDomainExpDate = fakedEppObject(
"fakeneeded.gov",
ex_date=datetime.date(2023, 2, 15),
)
mockMaximumRenewedDomainExpDate = fakedEppObject(
"fakemaximum.gov",
ex_date=datetime.date(2024, 12, 31),
)
mockRecentRenewedDomainExpDate = fakedEppObject(
"waterbutpurple.gov",
ex_date=datetime.date(2024, 11, 15),
)
def _mockDomainName(self, _name, _avail=False):
return MagicMock(
res_data=[
@ -918,6 +944,21 @@ class MockEppLib(TestCase):
def mockRenewDomainCommand(self, _request, cleaned):
if getattr(_request, "name", None) == "fake-error.gov":
raise RegistryError(code=ErrorCode.PARAMETER_VALUE_RANGE_ERROR)
elif getattr(_request, "name", None) == "waterbutpurple.gov":
return MagicMock(
res_data=[self.mockRecentRenewedDomainExpDate],
code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY,
)
elif getattr(_request, "name", None) == "fakeneeded.gov":
return MagicMock(
res_data=[self.mockDnsNeededRenewedDomainExpDate],
code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY,
)
elif getattr(_request, "name", None) == "fakemaximum.gov":
return MagicMock(
res_data=[self.mockMaximumRenewedDomainExpDate],
code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY,
)
else:
return MagicMock(
res_data=[self.mockRenewedDomainExpDate],
@ -943,6 +984,7 @@ class MockEppLib(TestCase):
self.infoDomainTwoHosts if self.mockedSendFunction.call_count == 5 else self.infoDomainNoHost,
None,
),
"waterbutpurple.gov": (self.mockDataExtensionDomain, None),
"nameserverwithip.gov": (self.infoDomainHasIP, None),
"namerserversubdomain.gov": (self.infoDomainCheckHostIPCombo, None),
"freeman.gov": (self.InfoDomainWithContacts, None),

View file

@ -18,8 +18,8 @@ class TestDataUpdates(TestCase):
self.user2 = User.objects.create(username="user2")
self.user3 = User.objects.create(username="user3")
self.user4 = User.objects.create(username="user4")
# The last user created triggers the creation of a contact and attaches itself to it. @Neil wth is going on?
# This bs_user defuses that situation so we can test the code.
# The last user created triggers the creation of a contact and attaches itself to it. See signals.
# This bs_user defuses that situation.
self.bs_user = User.objects.create()
self.contact1 = Contact.objects.create(

View file

@ -18,7 +18,175 @@ from unittest.mock import patch
from registrar.models.contact import Contact
from .common import less_console_noise
from .common import MockEppLib, less_console_noise
class TestExtendExpirationDates(MockEppLib):
def setUp(self):
"""Defines the file name of migration_json and the folder its contained in"""
super().setUp()
# Create a valid domain that is updatable
Domain.objects.get_or_create(
name="waterbutpurple.gov", state=Domain.State.READY, expiration_date=datetime.date(2023, 11, 15)
)
TransitionDomain.objects.get_or_create(
username="testytester@mail.com",
domain_name="waterbutpurple.gov",
epp_expiration_date=datetime.date(2023, 11, 15),
)
# Create a domain with an invalid expiration date
Domain.objects.get_or_create(
name="fake.gov", state=Domain.State.READY, expiration_date=datetime.date(2022, 5, 25)
)
TransitionDomain.objects.get_or_create(
username="themoonisactuallycheese@mail.com",
domain_name="fake.gov",
epp_expiration_date=datetime.date(2022, 5, 25),
)
# Create a domain with an invalid state
Domain.objects.get_or_create(
name="fakeneeded.gov", state=Domain.State.DNS_NEEDED, expiration_date=datetime.date(2023, 11, 15)
)
TransitionDomain.objects.get_or_create(
username="fakeneeded@mail.com",
domain_name="fakeneeded.gov",
epp_expiration_date=datetime.date(2023, 11, 15),
)
# Create a domain with a date greater than the maximum
Domain.objects.get_or_create(
name="fakemaximum.gov", state=Domain.State.READY, expiration_date=datetime.date(2024, 12, 31)
)
TransitionDomain.objects.get_or_create(
username="fakemaximum@mail.com",
domain_name="fakemaximum.gov",
epp_expiration_date=datetime.date(2024, 12, 31),
)
def tearDown(self):
"""Deletes all DB objects related to migrations"""
super().tearDown()
# Delete domain information
Domain.objects.all().delete()
DomainInformation.objects.all().delete()
DomainInvitation.objects.all().delete()
TransitionDomain.objects.all().delete()
# Delete users
User.objects.all().delete()
UserDomainRole.objects.all().delete()
def run_extend_expiration_dates(self):
"""
This method executes the transfer_transition_domains_to_domains command.
The 'call_command' function from Django's management framework is then used to
execute the load_transition_domain command with the specified arguments.
"""
with patch(
"registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa
return_value=True,
):
call_command("extend_expiration_dates")
def test_extends_expiration_date_correctly(self):
"""
Tests that the extend_expiration_dates method extends dates as expected
"""
desired_domain = Domain.objects.filter(name="waterbutpurple.gov").get()
desired_domain.expiration_date = datetime.date(2024, 11, 15)
# Run the expiration date script
self.run_extend_expiration_dates()
current_domain = Domain.objects.filter(name="waterbutpurple.gov").get()
self.assertEqual(desired_domain, current_domain)
# Explicitly test the expiration date
self.assertEqual(current_domain.expiration_date, datetime.date(2024, 11, 15))
def test_extends_expiration_date_skips_non_current(self):
"""
Tests that the extend_expiration_dates method correctly skips domains
with an expiration date less than a certain threshold.
"""
desired_domain = Domain.objects.filter(name="fake.gov").get()
desired_domain.expiration_date = datetime.date(2022, 5, 25)
# Run the expiration date script
self.run_extend_expiration_dates()
current_domain = Domain.objects.filter(name="fake.gov").get()
self.assertEqual(desired_domain, current_domain)
# Explicitly test the expiration date. The extend_expiration_dates script
# will skip all dates less than date(2023, 11, 15), meaning that this domain
# should not be affected by the change.
self.assertEqual(current_domain.expiration_date, datetime.date(2022, 5, 25))
def test_extends_expiration_date_skips_maximum_date(self):
"""
Tests that the extend_expiration_dates method correctly skips domains
with an expiration date more than a certain threshold.
"""
desired_domain = Domain.objects.filter(name="fakemaximum.gov").get()
desired_domain.expiration_date = datetime.date(2024, 12, 31)
# Run the expiration date script
self.run_extend_expiration_dates()
current_domain = Domain.objects.filter(name="fakemaximum.gov").get()
self.assertEqual(desired_domain, current_domain)
# Explicitly test the expiration date. The extend_expiration_dates script
# will skip all dates less than date(2023, 11, 15), meaning that this domain
# should not be affected by the change.
self.assertEqual(current_domain.expiration_date, datetime.date(2024, 12, 31))
def test_extends_expiration_date_skips_non_ready(self):
"""
Tests that the extend_expiration_dates method correctly skips domains not in the state "ready"
"""
desired_domain = Domain.objects.filter(name="fakeneeded.gov").get()
desired_domain.expiration_date = datetime.date(2023, 11, 15)
# Run the expiration date script
self.run_extend_expiration_dates()
current_domain = Domain.objects.filter(name="fakeneeded.gov").get()
self.assertEqual(desired_domain, current_domain)
# Explicitly test the expiration date. The extend_expiration_dates script
# will skip all dates less than date(2023, 11, 15), meaning that this domain
# should not be affected by the change.
self.assertEqual(current_domain.expiration_date, datetime.date(2023, 11, 15))
def test_extends_expiration_date_idempotent(self):
"""
Tests the idempotency of the extend_expiration_dates command.
Verifies that running the method multiple times does not change the expiration date
of a domain beyond the initial extension.
"""
desired_domain = Domain.objects.filter(name="waterbutpurple.gov").get()
desired_domain.expiration_date = datetime.date(2024, 11, 15)
# Run the expiration date script
self.run_extend_expiration_dates()
current_domain = Domain.objects.filter(name="waterbutpurple.gov").get()
self.assertEqual(desired_domain, current_domain)
# Explicitly test the expiration date
self.assertEqual(desired_domain.expiration_date, datetime.date(2024, 11, 15))
# Run the expiration date script again
self.run_extend_expiration_dates()
# The old domain shouldn't have changed
self.assertEqual(desired_domain, current_domain)
# Explicitly test the expiration date - should be the same
self.assertEqual(desired_domain.expiration_date, datetime.date(2024, 11, 15))
class TestProcessedMigrations(TestCase):

View file

@ -141,7 +141,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
# 302 redirect to the first form
page = self.app.get(reverse("application:")).follow()
# submitting should get back the same page if the required field is empty
result = page.form.submit()
result = page.forms[0].submit()
self.assertIn("What kind of U.S.-based government organization do you represent?", result)
def test_application_multiple_applications_exist(self):
@ -164,6 +164,9 @@ class DomainApplicationTests(TestWithUser, WebTest):
this test work.
This test also looks for the long organization name on the summary page.
This also tests for the presence of a modal trigger and the dynamic test
in the modal header on the submit page.
"""
num_pages_tested = 0
# elections, type_of_work, tribal_government, no_other_contacts
@ -178,11 +181,11 @@ class DomainApplicationTests(TestWithUser, WebTest):
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
# ---- TYPE PAGE ----
type_form = type_page.form
type_form = type_page.forms[0]
type_form["organization_type-organization_type"] = "federal"
# test next button and validate data
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
type_result = type_page.form.submit()
type_result = type_form.submit()
# should see results in db
application = DomainApplication.objects.get() # there's only one
self.assertEqual(application.organization_type, "federal")
@ -197,7 +200,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
federal_page = type_result.follow()
federal_form = federal_page.form
federal_form = federal_page.forms[0]
federal_form["organization_federal-federal_type"] = "executive"
# test next button
@ -216,7 +219,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
# Follow the redirect to the next form page
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
org_contact_page = federal_result.follow()
org_contact_form = org_contact_page.form
org_contact_form = org_contact_page.forms[0]
# federal agency so we have to fill in federal_agency
org_contact_form["organization_contact-federal_agency"] = "General Services Administration"
org_contact_form["organization_contact-organization_name"] = "Testorg"
@ -249,7 +252,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
# Follow the redirect to the next form page
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
ao_page = org_contact_result.follow()
ao_form = ao_page.form
ao_form = ao_page.forms[0]
ao_form["authorizing_official-first_name"] = "Testy ATO"
ao_form["authorizing_official-last_name"] = "Tester ATO"
ao_form["authorizing_official-title"] = "Chief Tester"
@ -276,7 +279,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
# Follow the redirect to the next form page
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
current_sites_page = ao_result.follow()
current_sites_form = current_sites_page.form
current_sites_form = current_sites_page.forms[0]
current_sites_form["current_sites-0-website"] = "www.city.com"
# test next button
@ -298,7 +301,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
# Follow the redirect to the next form page
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
dotgov_page = current_sites_result.follow()
dotgov_form = dotgov_page.form
dotgov_form = dotgov_page.forms[0]
dotgov_form["dotgov_domain-requested_domain"] = "city"
dotgov_form["dotgov_domain-0-alternative_domain"] = "city1"
@ -318,7 +321,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
# Follow the redirect to the next form page
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
purpose_page = dotgov_result.follow()
purpose_form = purpose_page.form
purpose_form = purpose_page.forms[0]
purpose_form["purpose-purpose"] = "For all kinds of things."
# test next button
@ -337,7 +340,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
# Follow the redirect to the next form page
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
your_contact_page = purpose_result.follow()
your_contact_form = your_contact_page.form
your_contact_form = your_contact_page.forms[0]
your_contact_form["your_contact-first_name"] = "Testy you"
your_contact_form["your_contact-last_name"] = "Tester you"
@ -365,7 +368,7 @@ 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()
other_contacts_form = other_contacts_page.form
other_contacts_form = other_contacts_page.forms[0]
other_contacts_form["other_contacts-0-first_name"] = "Testy2"
other_contacts_form["other_contacts-0-last_name"] = "Tester2"
@ -398,7 +401,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
# Follow the redirect to the next form page
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
anything_else_page = other_contacts_result.follow()
anything_else_form = anything_else_page.form
anything_else_form = anything_else_page.forms[0]
anything_else_form["anything_else-anything_else"] = "Nothing else."
@ -418,7 +421,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
# Follow the redirect to the next form page
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
requirements_page = anything_else_result.follow()
requirements_form = requirements_page.form
requirements_form = requirements_page.forms[0]
requirements_form["requirements-is_policy_acknowledged"] = True
@ -438,7 +441,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
# Follow the redirect to the next form page
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
review_page = requirements_result.follow()
review_form = review_page.form
review_form = review_page.forms[0]
# Review page contains all the previously entered data
# Let's make sure the long org name is displayed
@ -472,6 +475,14 @@ class DomainApplicationTests(TestWithUser, WebTest):
self.assertContains(review_page, "(201) 555-5557")
self.assertContains(review_page, "Nothing else.")
# We can't test the modal itself as it relies on JS for init and triggering,
# but we can test for the existence of its trigger:
self.assertContains(review_page, "toggle-submit-domain-request")
# And the existence of the modal's data parked and ready for the js init.
# The next assert also tests for the passed requested domain context from
# the view > application_form > modal
self.assertContains(review_page, "You are about to submit a domain request for city.gov")
# final submission results in a redirect to the "finished" URL
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
with less_console_noise():
@ -540,7 +551,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
# the conditional step titles shouldn't appear initially
self.assertNotContains(type_page, self.TITLES["organization_federal"])
self.assertNotContains(type_page, self.TITLES["organization_election"])
type_form = type_page.form
type_form = type_page.forms[0]
type_form["organization_type-organization_type"] = "federal"
# set the session ID before .submit()
@ -561,9 +572,9 @@ class DomainApplicationTests(TestWithUser, WebTest):
# continuing on in the flow we need to see top-level agency on the
# contact page
federal_page.form["organization_federal-federal_type"] = "executive"
federal_page.forms[0]["organization_federal-federal_type"] = "executive"
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
federal_result = federal_page.form.submit()
federal_result = federal_page.forms[0].submit()
# the post request should return a redirect to the contact
# question
self.assertEqual(federal_result.status_code, 302)
@ -586,7 +597,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
# the conditional step titles shouldn't appear initially
self.assertNotContains(type_page, self.TITLES["organization_federal"])
self.assertNotContains(type_page, self.TITLES["organization_election"])
type_form = type_page.form
type_form = type_page.forms[0]
type_form["organization_type-organization_type"] = "county"
# set the session ID before .submit()
@ -606,9 +617,9 @@ class DomainApplicationTests(TestWithUser, WebTest):
# continuing on in the flow we need to NOT see top-level agency on the
# contact page
election_page.form["organization_election-is_election_board"] = "True"
election_page.forms[0]["organization_election-is_election_board"] = "True"
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
election_result = election_page.form.submit()
election_result = election_page.forms[0].submit()
# the post request should return a redirect to the contact
# question
self.assertEqual(election_result.status_code, 302)
@ -626,10 +637,10 @@ class DomainApplicationTests(TestWithUser, WebTest):
# and then setting the cookie on each request.
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
type_form = type_page.form
type_form = type_page.forms[0]
type_form["organization_type-organization_type"] = "federal"
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
type_result = type_page.form.submit()
type_result = type_form.submit()
# follow first redirect
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
@ -654,15 +665,15 @@ class DomainApplicationTests(TestWithUser, WebTest):
# and then setting the cookie on each request.
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
type_form = type_page.form
type_form = type_page.forms[0]
type_form["organization_type-organization_type"] = DomainApplication.OrganizationChoices.INTERSTATE
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
type_result = type_page.form.submit()
type_result = type_form.submit()
# follow first redirect
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
contact_page = type_result.follow()
org_contact_form = contact_page.form
org_contact_form = contact_page.forms[0]
self.assertNotIn("federal_agency", org_contact_form.fields)
@ -690,10 +701,10 @@ class DomainApplicationTests(TestWithUser, WebTest):
# and then setting the cookie on each request.
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
type_form = type_page.form
type_form = type_page.forms[0]
type_form["organization_type-organization_type"] = DomainApplication.OrganizationChoices.SPECIAL_DISTRICT
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
type_result = type_page.form.submit()
type_result = type_page.forms[0].submit()
# follow first redirect
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
contact_page = type_result.follow()
@ -710,7 +721,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
result = contacts_page.form.submit()
result = contacts_page.forms[0].submit()
# follow first redirect
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
no_contacts_page = result.follow()
@ -727,10 +738,10 @@ class DomainApplicationTests(TestWithUser, WebTest):
# and then setting the cookie on each request.
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
type_form = type_page.form
type_form = type_page.forms[0]
type_form["organization_type-organization_type"] = DomainApplication.OrganizationChoices.INTERSTATE
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
type_result = type_page.form.submit()
type_result = type_form.submit()
# follow first redirect
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
contact_page = type_result.follow()
@ -745,10 +756,10 @@ class DomainApplicationTests(TestWithUser, WebTest):
# 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]
type_form = type_page.form
type_form = type_page.forms[0]
type_form["organization_type-organization_type"] = DomainApplication.OrganizationChoices.TRIBAL
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
type_result = type_page.form.submit()
type_result = type_form.submit()
# the tribal government page comes immediately afterwards
self.assertIn("/tribal_government", type_result.headers["Location"])
# follow first redirect
@ -767,18 +778,18 @@ class DomainApplicationTests(TestWithUser, WebTest):
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
# ---- TYPE PAGE ----
type_form = type_page.form
type_form = type_page.forms[0]
type_form["organization_type-organization_type"] = "federal"
# test next button
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
type_result = type_page.form.submit()
type_result = type_form.submit()
# ---- FEDERAL BRANCH PAGE ----
# Follow the redirect to the next form page
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
federal_page = type_result.follow()
federal_form = federal_page.form
federal_form = federal_page.forms[0]
federal_form["organization_federal-federal_type"] = "executive"
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
federal_result = federal_form.submit()
@ -787,7 +798,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
# Follow the redirect to the next form page
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
org_contact_page = federal_result.follow()
org_contact_form = org_contact_page.form
org_contact_form = org_contact_page.forms[0]
# federal agency so we have to fill in federal_agency
org_contact_form["organization_contact-federal_agency"] = "General Services Administration"
org_contact_form["organization_contact-organization_name"] = "Testorg"
@ -828,18 +839,18 @@ class DomainApplicationTests(TestWithUser, WebTest):
# and then setting the cookie on each request.
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
# ---- TYPE PAGE ----
type_form = type_page.form
type_form = type_page.forms[0]
type_form["organization_type-organization_type"] = "federal"
# test next button
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
type_result = type_page.form.submit()
type_result = type_form.submit()
# ---- FEDERAL BRANCH PAGE ----
# Follow the redirect to the next form page
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
federal_page = type_result.follow()
federal_form = federal_page.form
federal_form = federal_page.forms[0]
federal_form["organization_federal-federal_type"] = "executive"
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
federal_result = federal_form.submit()
@ -848,7 +859,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
# Follow the redirect to the next form page
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
org_contact_page = federal_result.follow()
org_contact_form = org_contact_page.form
org_contact_form = org_contact_page.forms[0]
# federal agency so we have to fill in federal_agency
org_contact_form["organization_contact-federal_agency"] = "General Services Administration"
org_contact_form["organization_contact-organization_name"] = "Testorg"
@ -870,7 +881,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
# Follow the redirect to the next form page
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
ao_page = org_contact_result.follow()
ao_form = ao_page.form
ao_form = ao_page.forms[0]
ao_form["authorizing_official-first_name"] = "Testy ATO"
ao_form["authorizing_official-last_name"] = "Tester ATO"
ao_form["authorizing_official-title"] = "Chief Tester"
@ -884,7 +895,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
# Follow the redirect to the next form page
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
current_sites_page = ao_result.follow()
current_sites_form = current_sites_page.form
current_sites_form = current_sites_page.forms[0]
current_sites_form["current_sites-0-website"] = "www.city.com"
# test saving the page
@ -917,7 +928,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
current_sites_page = self.app.get(reverse("application:current_sites"))
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
# fill in the form field
current_sites_form = current_sites_page.form
current_sites_form = current_sites_page.forms[0]
self.assertIn("current_sites-0-website", current_sites_form.fields)
self.assertNotIn("current_sites-1-website", current_sites_form.fields)
current_sites_form["current_sites-0-website"] = "https://example.com"
@ -926,7 +937,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
current_sites_result = current_sites_form.submit("submit_button", value="save")
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
current_sites_form = current_sites_result.follow().form
current_sites_form = current_sites_result.follow().forms[0]
# verify that there are two form fields
value = current_sites_form["current_sites-0-website"].value
@ -1086,6 +1097,18 @@ class DomainApplicationTests(TestWithUser, WebTest):
detail_page = home_page.click("Manage", index=0)
self.assertContains(detail_page, "Federal: an agency of the U.S. government")
def test_submit_modal_no_domain_text_fallback(self):
"""When user clicks on submit your domain request and the requested domain
is null (possible through url direct access to the review page), present
fallback copy in the modal's header.
NOTE: This may be a moot point if we implement a more solid pattern in the
future, like not a submit action at all on the review page."""
review_page = self.app.get(reverse("application:review"))
self.assertContains(review_page, "toggle-submit-domain-request")
self.assertContains(review_page, "You are about to submit an incomplete request")
class TestWithDomainPermissions(TestWithUser):
def setUp(self):

View file

@ -43,11 +43,11 @@ 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,
contact help@get.gov.
""",
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, "
"contact help@get.gov."
),
GenericErrorCodes.GENERIC_ERROR: ("Value entered was wrong."),
}

View file

@ -321,12 +321,21 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView):
def get_context_data(self):
"""Define context for access on all wizard pages."""
# Build the submit button that we'll pass to the modal.
modal_button = '<button type="submit" ' 'class="usa-button" ' ">Submit request</button>"
# Concatenate the modal header that we'll pass to the modal.
if self.application.requested_domain:
modal_heading = "You are about to submit a domain request for " + str(self.application.requested_domain)
else:
modal_heading = "You are about to submit an incomplete request"
return {
"form_titles": self.TITLES,
"steps": self.steps,
# Add information about which steps should be unlocked
"visited": self.storage.get("step_history", []),
"is_federal": self.application.is_federal(),
"modal_button": modal_button,
"modal_heading": modal_heading,
}
def get_step_list(self) -> list: