diff --git a/src/registrar/assets/sass/_theme/_base.scss b/src/registrar/assets/sass/_theme/_base.scss index b6d13cee3..127db5589 100644 --- a/src/registrar/assets/sass/_theme/_base.scss +++ b/src/registrar/assets/sass/_theme/_base.scss @@ -129,3 +129,28 @@ abbr[title] { .flex-end { align-items: flex-end; } + +// Only apply this custom wrapping to desktop +@include at-media(desktop) { + .usa-tooltip__body { + width: 350px; + white-space: normal; + text-align: center; + } +} + +@include at-media(tablet) { + .usa-tooltip__body { + width: 250px !important; + white-space: normal !important; + text-align: center !important; + } +} + +@include at-media(mobile) { + .usa-tooltip__body { + width: 250px !important; + white-space: normal !important; + text-align: center !important; + } +} diff --git a/src/registrar/assets/sass/_theme/_buttons.scss b/src/registrar/assets/sass/_theme/_buttons.scss index 2f4121399..ef8635b95 100644 --- a/src/registrar/assets/sass/_theme/_buttons.scss +++ b/src/registrar/assets/sass/_theme/_buttons.scss @@ -53,8 +53,8 @@ a.usa-button--unstyled.disabled-link:focus { } .usa-button--unstyled.disabled-button, -.usa-button--unstyled.disabled-link:hover, -.usa-button--unstyled.disabled-link:focus { +.usa-button--unstyled.disabled-button:hover, +.usa-button--unstyled.disabled-button:focus { cursor: not-allowed !important; outline: none !important; text-decoration: none !important; diff --git a/src/registrar/assets/sass/_theme/_tables.scss b/src/registrar/assets/sass/_theme/_tables.scss index 84c4791e5..0d58b5878 100644 --- a/src/registrar/assets/sass/_theme/_tables.scss +++ b/src/registrar/assets/sass/_theme/_tables.scss @@ -26,6 +26,16 @@ padding-bottom: units(2px); } + td .no-click-outline-and-cursor-help { + outline: none; + cursor: help; + use { + // USWDS has weird interactions with SVGs regarding tooltips, + // and other components. In this event, we need to disable pointer interactions. + pointer-events: none; + } + } + // Ticket #1510 // @include at-media('desktop') { // th:first-child { diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 8a7f9c6e6..7ebe3dc34 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -140,6 +140,24 @@ class Domain(TimeStampedModel, DomainHelper): # previously existed but has been deleted from the registry DELETED = "deleted", "Deleted" + @classmethod + def get_help_text(cls, state) -> str: + """Returns a help message for a desired state. If none is found, an empty string is returned""" + help_texts = { + # For now, unknown has the same message as DNS_NEEDED + cls.UNKNOWN: ("Before this domain can be used, " "you’ll need to add name server addresses."), + cls.DNS_NEEDED: ("Before this domain can be used, " "you’ll need to add name server addresses."), + cls.READY: "This domain has name servers and is ready for use.", + cls.ON_HOLD: ( + "This domain is administratively paused, " + "so it can’t be edited and won’t resolve in DNS. " + "Contact help@get.gov for details." + ), + cls.DELETED: ("This domain has been removed and " "is no longer registered to your organization."), + } + + return help_texts.get(state, "") + class Cache(property): """ Python descriptor to turn class methods into properties. @@ -1398,6 +1416,21 @@ class Domain(TimeStampedModel, DomainHelper): logger.info("Changing to DNS_NEEDED state") logger.info("able to transition to DNS_NEEDED state") + def get_state_help_text(self) -> str: + """Returns a str containing additional information about a given state. + Returns custom content for when the domain itself is expired.""" + + if self.is_expired() and self.state != self.State.UNKNOWN: + # Given expired is not a physical state, but it is displayed as such, + # We need custom logic to determine this message. + help_text = ( + "This domain has expired, but it is still online. " "To renew this domain, contact help@get.gov." + ) + else: + help_text = Domain.State.get_help_text(self.state) + + return help_text + def _disclose_fields(self, contact: PublicContact): """creates a disclose object that can be added to a contact Create using .disclose= on the command before sending. diff --git a/src/registrar/templates/domain_detail.html b/src/registrar/templates/domain_detail.html index 4dcef69ae..2b2d45695 100644 --- a/src/registrar/templates/domain_detail.html +++ b/src/registrar/templates/domain_detail.html @@ -6,7 +6,7 @@
@@ -17,6 +17,7 @@ Status: + {# UNKNOWN domains would not have an expiration date and thus would show 'Expired' #} {% if domain.is_expired and domain.state != domain.State.UNKNOWN %} Expired @@ -25,6 +26,12 @@ {% else %} {{ domain.state|title }} {% endif %} + + {% if domain.get_state_help_text %} +
+ {{ domain.get_state_help_text }} +
+ {% endif %}

diff --git a/src/registrar/templates/home.html b/src/registrar/templates/home.html index 2400e38f3..a79065f50 100644 --- a/src/registrar/templates/home.html +++ b/src/registrar/templates/home.html @@ -56,6 +56,16 @@ {% else %} {{ domain.state|capfirst }} {% endif %} + + + diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 1942a3839..891c254c5 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -1,5 +1,4 @@ from django.test import Client, TestCase -from django.urls import reverse from django.contrib.auth import get_user_model from .common import MockEppLib # type: ignore @@ -8,11 +7,7 @@ from .common import MockEppLib # type: ignore from registrar.models import ( DomainApplication, DomainInformation, - DraftDomain, - Contact, - User, ) -from .common import less_console_noise import logging logger = logging.getLogger(__name__) @@ -55,252 +50,3 @@ class TestWithUser(MockEppLib): DomainApplication.objects.all().delete() DomainInformation.objects.all().delete() self.user.delete() - - -class LoggedInTests(TestWithUser): - def setUp(self): - super().setUp() - self.client.force_login(self.user) - - def tearDown(self): - super().tearDown() - Contact.objects.all().delete() - - def test_home_lists_domain_applications(self): - response = self.client.get("/") - self.assertNotContains(response, "igorville.gov") - site = DraftDomain.objects.create(name="igorville.gov") - application = DomainApplication.objects.create(creator=self.user, requested_domain=site) - response = self.client.get("/") - - # count = 7 because of screenreader content - self.assertContains(response, "igorville.gov", count=7) - - # clean up - application.delete() - - def test_home_deletes_withdrawn_domain_application(self): - """Tests if the user can delete a DomainApplication in the 'withdrawn' status""" - - site = DraftDomain.objects.create(name="igorville.gov") - application = DomainApplication.objects.create( - creator=self.user, requested_domain=site, status=DomainApplication.ApplicationStatus.WITHDRAWN - ) - - # Ensure that igorville.gov exists on the page - home_page = self.client.get("/") - self.assertContains(home_page, "igorville.gov") - - # Check if the delete button exists. We can do this by checking for its id and text content. - self.assertContains(home_page, "Delete") - self.assertContains(home_page, "button-toggle-delete-domain-alert-1") - - # Trigger the delete logic - response = self.client.post(reverse("application-delete", kwargs={"pk": application.pk}), follow=True) - - self.assertNotContains(response, "igorville.gov") - - # clean up - application.delete() - - def test_home_deletes_started_domain_application(self): - """Tests if the user can delete a DomainApplication in the 'started' status""" - - site = DraftDomain.objects.create(name="igorville.gov") - application = DomainApplication.objects.create( - creator=self.user, requested_domain=site, status=DomainApplication.ApplicationStatus.STARTED - ) - - # Ensure that igorville.gov exists on the page - home_page = self.client.get("/") - self.assertContains(home_page, "igorville.gov") - - # Check if the delete button exists. We can do this by checking for its id and text content. - self.assertContains(home_page, "Delete") - self.assertContains(home_page, "button-toggle-delete-domain-alert-1") - - # Trigger the delete logic - response = self.client.post(reverse("application-delete", kwargs={"pk": application.pk}), follow=True) - - self.assertNotContains(response, "igorville.gov") - - # clean up - application.delete() - - def test_home_doesnt_delete_other_domain_applications(self): - """Tests to ensure the user can't delete Applications not in the status of STARTED or WITHDRAWN""" - - # Given that we are including a subset of items that can be deleted while excluding the rest, - # subTest is appropriate here as otherwise we would need many duplicate tests for the same reason. - with less_console_noise(): - draft_domain = DraftDomain.objects.create(name="igorville.gov") - for status in DomainApplication.ApplicationStatus: - if status not in [ - DomainApplication.ApplicationStatus.STARTED, - DomainApplication.ApplicationStatus.WITHDRAWN, - ]: - with self.subTest(status=status): - application = DomainApplication.objects.create( - creator=self.user, requested_domain=draft_domain, status=status - ) - - # Trigger the delete logic - response = self.client.post( - reverse("application-delete", kwargs={"pk": application.pk}), follow=True - ) - - # Check for a 403 error - the end user should not be allowed to do this - self.assertEqual(response.status_code, 403) - - desired_application = DomainApplication.objects.filter(requested_domain=draft_domain) - - # Make sure the DomainApplication wasn't deleted - self.assertEqual(desired_application.count(), 1) - - # clean up - application.delete() - - def test_home_deletes_domain_application_and_orphans(self): - """Tests if delete for DomainApplication deletes orphaned Contact objects""" - - # Create the site and contacts to delete (orphaned) - contact = Contact.objects.create( - first_name="Henry", - last_name="Mcfakerson", - ) - contact_shared = Contact.objects.create( - first_name="Relative", - last_name="Aether", - ) - - # Create two non-orphaned contacts - contact_2 = Contact.objects.create( - first_name="Saturn", - last_name="Mars", - ) - - # Attach a user object to a contact (should not be deleted) - contact_user, _ = Contact.objects.get_or_create(user=self.user) - - site = DraftDomain.objects.create(name="igorville.gov") - application = DomainApplication.objects.create( - creator=self.user, - requested_domain=site, - status=DomainApplication.ApplicationStatus.WITHDRAWN, - authorizing_official=contact, - submitter=contact_user, - ) - application.other_contacts.set([contact_2]) - - # Create a second application to attach contacts to - site_2 = DraftDomain.objects.create(name="teaville.gov") - application_2 = DomainApplication.objects.create( - creator=self.user, - requested_domain=site_2, - status=DomainApplication.ApplicationStatus.STARTED, - authorizing_official=contact_2, - submitter=contact_shared, - ) - application_2.other_contacts.set([contact_shared]) - - # Ensure that igorville.gov exists on the page - home_page = self.client.get("/") - self.assertContains(home_page, "igorville.gov") - - # Trigger the delete logic - response = self.client.post(reverse("application-delete", kwargs={"pk": application.pk}), follow=True) - - # igorville is now deleted - self.assertNotContains(response, "igorville.gov") - - # Check if the orphaned contact was deleted - orphan = Contact.objects.filter(id=contact.id) - self.assertFalse(orphan.exists()) - - # All non-orphan contacts should still exist and are unaltered - try: - current_user = Contact.objects.filter(id=contact_user.id).get() - except Contact.DoesNotExist: - self.fail("contact_user (a non-orphaned contact) was deleted") - - self.assertEqual(current_user, contact_user) - try: - edge_case = Contact.objects.filter(id=contact_2.id).get() - except Contact.DoesNotExist: - self.fail("contact_2 (a non-orphaned contact) was deleted") - - self.assertEqual(edge_case, contact_2) - - def test_home_deletes_domain_application_and_shared_orphans(self): - """Test the edge case for an object that will become orphaned after a delete - (but is not an orphan at the time of deletion)""" - - # Create the site and contacts to delete (orphaned) - contact = Contact.objects.create( - first_name="Henry", - last_name="Mcfakerson", - ) - contact_shared = Contact.objects.create( - first_name="Relative", - last_name="Aether", - ) - - # Create two non-orphaned contacts - contact_2 = Contact.objects.create( - first_name="Saturn", - last_name="Mars", - ) - - # Attach a user object to a contact (should not be deleted) - contact_user, _ = Contact.objects.get_or_create(user=self.user) - - site = DraftDomain.objects.create(name="igorville.gov") - application = DomainApplication.objects.create( - creator=self.user, - requested_domain=site, - status=DomainApplication.ApplicationStatus.WITHDRAWN, - authorizing_official=contact, - submitter=contact_user, - ) - application.other_contacts.set([contact_2]) - - # Create a second application to attach contacts to - site_2 = DraftDomain.objects.create(name="teaville.gov") - application_2 = DomainApplication.objects.create( - creator=self.user, - requested_domain=site_2, - status=DomainApplication.ApplicationStatus.STARTED, - authorizing_official=contact_2, - submitter=contact_shared, - ) - application_2.other_contacts.set([contact_shared]) - - home_page = self.client.get("/") - self.assertContains(home_page, "teaville.gov") - - # Trigger the delete logic - response = self.client.post(reverse("application-delete", kwargs={"pk": application_2.pk}), follow=True) - - self.assertNotContains(response, "teaville.gov") - - # Check if the orphaned contact was deleted - orphan = Contact.objects.filter(id=contact_shared.id) - self.assertFalse(orphan.exists()) - - def test_application_form_view(self): - response = self.client.get("/request/", follow=True) - self.assertContains( - response, - "You’re about to start your .gov domain request.", - ) - - def test_domain_application_form_with_ineligible_user(self): - """Application form not accessible for an ineligible user. - This test should be solid enough since all application wizard - views share the same permissions class""" - self.user.status = User.RESTRICTED - self.user.save() - - with less_console_noise(): - response = self.client.get("/request/", follow=True) - self.assertEqual(response.status_code, 403) diff --git a/src/registrar/tests/test_views_application.py b/src/registrar/tests/test_views_application.py index 02fe5ff76..593b8d31e 100644 --- a/src/registrar/tests/test_views_application.py +++ b/src/registrar/tests/test_views_application.py @@ -3,6 +3,7 @@ from unittest.mock import Mock from django.conf import settings from django.urls import reverse +from datetime import date from .common import MockSESClient, completed_application # type: ignore from django_webtest import WebTest # type: ignore @@ -16,6 +17,7 @@ from registrar.models import ( Contact, User, Website, + UserDomainRole, ) from registrar.views.application import ApplicationWizard, Step @@ -2327,3 +2329,364 @@ class TestWizardUnlockingSteps(TestWithUser, WebTest): else: self.fail(f"Expected a redirect, but got a different response: {response}") + + +class HomeTests(TestWithUser): + """A series of tests that target the two tables on home.html""" + + def setUp(self): + super().setUp() + self.client.force_login(self.user) + + def tearDown(self): + super().tearDown() + Contact.objects.all().delete() + + def test_home_lists_domain_applications(self): + response = self.client.get("/") + self.assertNotContains(response, "igorville.gov") + site = DraftDomain.objects.create(name="igorville.gov") + application = DomainApplication.objects.create(creator=self.user, requested_domain=site) + response = self.client.get("/") + + # count = 7 because of screenreader content + self.assertContains(response, "igorville.gov", count=7) + + # clean up + application.delete() + + def test_state_help_text(self): + """Tests if each domain state has help text""" + + # Get the expected text content of each state + deleted_text = "This domain has been removed and " "is no longer registered to your organization." + dns_needed_text = "Before this domain can be used, " "you’ll need to add name server addresses." + ready_text = "This domain has name servers and is ready for use." + on_hold_text = ( + "This domain is administratively paused, " + "so it can’t be edited and won’t resolve in DNS. " + "Contact help@get.gov for details." + ) + deleted_text = "This domain has been removed and " "is no longer registered to your organization." + # Generate a mapping of domain names, the state, and expected messages for the subtest + test_cases = [ + ("deleted.gov", Domain.State.DELETED, deleted_text), + ("dnsneeded.gov", Domain.State.DNS_NEEDED, dns_needed_text), + ("unknown.gov", Domain.State.UNKNOWN, dns_needed_text), + ("onhold.gov", Domain.State.ON_HOLD, on_hold_text), + ("ready.gov", Domain.State.READY, ready_text), + ] + for domain_name, state, expected_message in test_cases: + with self.subTest(domain_name=domain_name, state=state, expected_message=expected_message): + # Create a domain and a UserRole with the given params + test_domain, _ = Domain.objects.get_or_create(name=domain_name, state=state) + test_domain.expiration_date = date.today() + test_domain.save() + + user_role, _ = UserDomainRole.objects.get_or_create( + user=self.user, domain=test_domain, role=UserDomainRole.Roles.MANAGER + ) + + # Grab the home page + response = self.client.get("/") + + # Make sure the user can actually see the domain. + # We expect two instances because of SR content. + self.assertContains(response, domain_name, count=2) + + # Check that we have the right text content. + self.assertContains(response, expected_message, count=1) + + # Delete the role and domain to ensure we're testing in isolation + user_role.delete() + test_domain.delete() + + def test_state_help_text_expired(self): + """Tests if each domain state has help text when expired""" + expired_text = "This domain has expired, but it is still online. " "To renew this domain, contact help@get.gov." + test_domain, _ = Domain.objects.get_or_create(name="expired.gov", state=Domain.State.READY) + test_domain.expiration_date = date(2011, 10, 10) + test_domain.save() + + UserDomainRole.objects.get_or_create(user=self.user, domain=test_domain, role=UserDomainRole.Roles.MANAGER) + + # Grab the home page + response = self.client.get("/") + + # Make sure the user can actually see the domain. + # We expect two instances because of SR content. + self.assertContains(response, "expired.gov", count=2) + + # Check that we have the right text content. + self.assertContains(response, expired_text, count=1) + + def test_state_help_text_no_expiration_date(self): + """Tests if each domain state has help text when expiration date is None""" + + # == Test a expiration of None for state ready. This should be expired. == # + expired_text = "This domain has expired, but it is still online. " "To renew this domain, contact help@get.gov." + test_domain, _ = Domain.objects.get_or_create(name="imexpired.gov", state=Domain.State.READY) + test_domain.expiration_date = None + test_domain.save() + + UserDomainRole.objects.get_or_create(user=self.user, domain=test_domain, role=UserDomainRole.Roles.MANAGER) + + # Grab the home page + response = self.client.get("/") + + # Make sure the user can actually see the domain. + # We expect two instances because of SR content. + self.assertContains(response, "imexpired.gov", count=2) + + # Make sure the expiration date is None + self.assertEqual(test_domain.expiration_date, None) + + # Check that we have the right text content. + self.assertContains(response, expired_text, count=1) + + # == Test a expiration of None for state unknown. This should not display expired text. == # + unknown_text = "Before this domain can be used, " "you’ll need to add name server addresses." + test_domain_2, _ = Domain.objects.get_or_create(name="notexpired.gov", state=Domain.State.UNKNOWN) + test_domain_2.expiration_date = None + test_domain_2.save() + + UserDomainRole.objects.get_or_create(user=self.user, domain=test_domain_2, role=UserDomainRole.Roles.MANAGER) + + # Grab the home page + response = self.client.get("/") + + # Make sure the user can actually see the domain. + # We expect two instances because of SR content. + self.assertContains(response, "notexpired.gov", count=2) + + # Make sure the expiration date is None + self.assertEqual(test_domain_2.expiration_date, None) + + # Check that we have the right text content. + self.assertContains(response, unknown_text, count=1) + + def test_home_deletes_withdrawn_domain_application(self): + """Tests if the user can delete a DomainApplication in the 'withdrawn' status""" + + site = DraftDomain.objects.create(name="igorville.gov") + application = DomainApplication.objects.create( + creator=self.user, requested_domain=site, status=DomainApplication.ApplicationStatus.WITHDRAWN + ) + + # Ensure that igorville.gov exists on the page + home_page = self.client.get("/") + self.assertContains(home_page, "igorville.gov") + + # Check if the delete button exists. We can do this by checking for its id and text content. + self.assertContains(home_page, "Delete") + self.assertContains(home_page, "button-toggle-delete-domain-alert-1") + + # Trigger the delete logic + response = self.client.post(reverse("application-delete", kwargs={"pk": application.pk}), follow=True) + + self.assertNotContains(response, "igorville.gov") + + # clean up + application.delete() + + def test_home_deletes_started_domain_application(self): + """Tests if the user can delete a DomainApplication in the 'started' status""" + + site = DraftDomain.objects.create(name="igorville.gov") + application = DomainApplication.objects.create( + creator=self.user, requested_domain=site, status=DomainApplication.ApplicationStatus.STARTED + ) + + # Ensure that igorville.gov exists on the page + home_page = self.client.get("/") + self.assertContains(home_page, "igorville.gov") + + # Check if the delete button exists. We can do this by checking for its id and text content. + self.assertContains(home_page, "Delete") + self.assertContains(home_page, "button-toggle-delete-domain-alert-1") + + # Trigger the delete logic + response = self.client.post(reverse("application-delete", kwargs={"pk": application.pk}), follow=True) + + self.assertNotContains(response, "igorville.gov") + + # clean up + application.delete() + + def test_home_doesnt_delete_other_domain_applications(self): + """Tests to ensure the user can't delete Applications not in the status of STARTED or WITHDRAWN""" + + # Given that we are including a subset of items that can be deleted while excluding the rest, + # subTest is appropriate here as otherwise we would need many duplicate tests for the same reason. + with less_console_noise(): + draft_domain = DraftDomain.objects.create(name="igorville.gov") + for status in DomainApplication.ApplicationStatus: + if status not in [ + DomainApplication.ApplicationStatus.STARTED, + DomainApplication.ApplicationStatus.WITHDRAWN, + ]: + with self.subTest(status=status): + application = DomainApplication.objects.create( + creator=self.user, requested_domain=draft_domain, status=status + ) + + # Trigger the delete logic + response = self.client.post( + reverse("application-delete", kwargs={"pk": application.pk}), follow=True + ) + + # Check for a 403 error - the end user should not be allowed to do this + self.assertEqual(response.status_code, 403) + + desired_application = DomainApplication.objects.filter(requested_domain=draft_domain) + + # Make sure the DomainApplication wasn't deleted + self.assertEqual(desired_application.count(), 1) + + # clean up + application.delete() + + def test_home_deletes_domain_application_and_orphans(self): + """Tests if delete for DomainApplication deletes orphaned Contact objects""" + + # Create the site and contacts to delete (orphaned) + contact = Contact.objects.create( + first_name="Henry", + last_name="Mcfakerson", + ) + contact_shared = Contact.objects.create( + first_name="Relative", + last_name="Aether", + ) + + # Create two non-orphaned contacts + contact_2 = Contact.objects.create( + first_name="Saturn", + last_name="Mars", + ) + + # Attach a user object to a contact (should not be deleted) + contact_user, _ = Contact.objects.get_or_create(user=self.user) + + site = DraftDomain.objects.create(name="igorville.gov") + application = DomainApplication.objects.create( + creator=self.user, + requested_domain=site, + status=DomainApplication.ApplicationStatus.WITHDRAWN, + authorizing_official=contact, + submitter=contact_user, + ) + application.other_contacts.set([contact_2]) + + # Create a second application to attach contacts to + site_2 = DraftDomain.objects.create(name="teaville.gov") + application_2 = DomainApplication.objects.create( + creator=self.user, + requested_domain=site_2, + status=DomainApplication.ApplicationStatus.STARTED, + authorizing_official=contact_2, + submitter=contact_shared, + ) + application_2.other_contacts.set([contact_shared]) + + # Ensure that igorville.gov exists on the page + home_page = self.client.get("/") + self.assertContains(home_page, "igorville.gov") + + # Trigger the delete logic + response = self.client.post(reverse("application-delete", kwargs={"pk": application.pk}), follow=True) + + # igorville is now deleted + self.assertNotContains(response, "igorville.gov") + + # Check if the orphaned contact was deleted + orphan = Contact.objects.filter(id=contact.id) + self.assertFalse(orphan.exists()) + + # All non-orphan contacts should still exist and are unaltered + try: + current_user = Contact.objects.filter(id=contact_user.id).get() + except Contact.DoesNotExist: + self.fail("contact_user (a non-orphaned contact) was deleted") + + self.assertEqual(current_user, contact_user) + try: + edge_case = Contact.objects.filter(id=contact_2.id).get() + except Contact.DoesNotExist: + self.fail("contact_2 (a non-orphaned contact) was deleted") + + self.assertEqual(edge_case, contact_2) + + def test_home_deletes_domain_application_and_shared_orphans(self): + """Test the edge case for an object that will become orphaned after a delete + (but is not an orphan at the time of deletion)""" + + # Create the site and contacts to delete (orphaned) + contact = Contact.objects.create( + first_name="Henry", + last_name="Mcfakerson", + ) + contact_shared = Contact.objects.create( + first_name="Relative", + last_name="Aether", + ) + + # Create two non-orphaned contacts + contact_2 = Contact.objects.create( + first_name="Saturn", + last_name="Mars", + ) + + # Attach a user object to a contact (should not be deleted) + contact_user, _ = Contact.objects.get_or_create(user=self.user) + + site = DraftDomain.objects.create(name="igorville.gov") + application = DomainApplication.objects.create( + creator=self.user, + requested_domain=site, + status=DomainApplication.ApplicationStatus.WITHDRAWN, + authorizing_official=contact, + submitter=contact_user, + ) + application.other_contacts.set([contact_2]) + + # Create a second application to attach contacts to + site_2 = DraftDomain.objects.create(name="teaville.gov") + application_2 = DomainApplication.objects.create( + creator=self.user, + requested_domain=site_2, + status=DomainApplication.ApplicationStatus.STARTED, + authorizing_official=contact_2, + submitter=contact_shared, + ) + application_2.other_contacts.set([contact_shared]) + + home_page = self.client.get("/") + self.assertContains(home_page, "teaville.gov") + + # Trigger the delete logic + response = self.client.post(reverse("application-delete", kwargs={"pk": application_2.pk}), follow=True) + + self.assertNotContains(response, "teaville.gov") + + # Check if the orphaned contact was deleted + orphan = Contact.objects.filter(id=contact_shared.id) + self.assertFalse(orphan.exists()) + + def test_application_form_view(self): + response = self.client.get("/request/", follow=True) + self.assertContains( + response, + "You’re about to start your .gov domain request.", + ) + + def test_domain_application_form_with_ineligible_user(self): + """Application form not accessible for an ineligible user. + This test should be solid enough since all application wizard + views share the same permissions class""" + self.user.status = User.RESTRICTED + self.user.save() + + with less_console_noise(): + response = self.client.get("/request/", follow=True) + self.assertEqual(response.status_code, 403)