From 67d177dc75b5b9881ee67fc796462e95894cea93 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 3 Oct 2023 11:18:38 -0600 Subject: [PATCH 01/67] #1102 - Changed disclose behaviour --- src/registrar/models/domain.py | 15 ++++++--------- src/registrar/tests/common.py | 8 ++------ 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index e45724a9b..744c54a56 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -850,18 +850,15 @@ class Domain(TimeStampedModel, DomainHelper): """creates a disclose object that can be added to a contact Create using .disclose= on the command before sending. if item is security email then make sure email is visable""" - isSecurity = contact.contact_type == contact.ContactTypeChoices.SECURITY + is_security = contact.contact_type == contact.ContactTypeChoices.SECURITY DF = epp.DiscloseField - fields = {DF.FAX, DF.VOICE, DF.ADDR} - - if not isSecurity or ( - isSecurity and contact.email == PublicContact.get_default_security().email - ): - fields.add(DF.EMAIL) + fields = {DF.EMAIL} + disclose = ( + is_security and contact.email != PublicContact.get_default_security().email + ) return epp.Disclose( - flag=False, + flag=disclose, fields=fields, - types={DF.ADDR: "loc"}, ) def _make_epp_contact_postal_info(self, contact: PublicContact): # type: ignore diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index 10c387099..3770bbe09 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -626,15 +626,11 @@ class MockEppLib(TestCase): self, contact: PublicContact, disclose_email=False, createContact=True ): DF = common.DiscloseField - fields = {DF.FAX, DF.VOICE, DF.ADDR} - - if not disclose_email: - fields.add(DF.EMAIL) + fields = {DF.EMAIL} di = common.Disclose( - flag=False, + flag=disclose_email, fields=fields, - types={DF.ADDR: "loc"}, ) # check docs here looks like we may have more than one address field but From 914b2da187e7daa87fd12231363900af74306bb4 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 3 Oct 2023 11:36:56 -0600 Subject: [PATCH 02/67] Update domain.py --- src/registrar/models/domain.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 744c54a56..2d75da42c 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -856,6 +856,7 @@ class Domain(TimeStampedModel, DomainHelper): disclose = ( is_security and contact.email != PublicContact.get_default_security().email ) + # Will only disclose DF.EMAIL if its not the default return epp.Disclose( flag=disclose, fields=fields, From fb5faf35f1c6d7b1a207ccd963e815f746e8f59a Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 4 Oct 2023 10:50:07 -0600 Subject: [PATCH 03/67] Added tests --- src/registrar/tests/test_models_domain.py | 96 +++++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py index bf258db31..9068863b0 100644 --- a/src/registrar/tests/test_models_domain.py +++ b/src/registrar/tests/test_models_domain.py @@ -629,6 +629,102 @@ class TestRegistrantContacts(MockEppLib): self.mockedSendFunction.assert_has_calls(expected_calls, any_order=True) self.assertEqual(PublicContact.objects.filter(domain=self.domain).count(), 1) + @skip("Dependent on #850") + def test_not_disclosed_on_other_contacts(self): + """ + Scenario: Registrant creates a new domain with multiple contacts + When `domain` has registrant, admin, technical, + and security contacts + Then Domain sends `commands.CreateContact` to the registry + And the field `disclose` is set to false for DF.EMAIL + on all fields except security + """ + # Generates a domain with four existing contacts + domain, _ = Domain.objects.get_or_create(name="igorville.gov") + # Adds default emails to all fields + domain.addAllDefaults() + # Security contact should be disclosed + domain.security_contact.email = "test123@mail.gov" + # TODO - uncomment below when #850 is merged + domain.registrant_contact = domain.get_default_registrant_contact() + + expected_admin = domain.get_default_administrative_contact() + expected_registrant = domain.get_default_registrant_contact() + expected_security = domain.get_default_security_contact() + expected_tech = domain.get_default_technical_contact() + + contacts = [ + expected_admin, + expected_registrant, + expected_security, + expected_tech + ] + + for contact in contacts: + id = PublicContact.objects.get( + domain=self.domain, + contact_type=contact.contact_type_choice, + ).registry_id + contact.registry_id = id + + expectedCreateCommand = self._convertPublicContactToEpp( + contact, disclose_email=False + ) + + self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True) + + @skip("Dependent on #850") + def test_not_disclosed_on_default_security_contact(self): + """ + Scenario: Registrant creates a new domain with no security email + When `domain.security_contact.email` is equal to the default + Then Domain sends `commands.CreateContact` to the registry + And the field `disclose` is set to false for DF.EMAIL + """ + expectedSecContact = PublicContact.get_default_security() + expectedSecContact.domain = self.domain + self.domain.security_contact.email = "test123@mail.gov" + + id = PublicContact.objects.get( + domain=self.domain, + contact_type=PublicContact.ContactTypeChoices.SECURITY, + ).registry_id + + expectedSecContact.registry_id = id + expectedCreateCommand = self._convertPublicContactToEpp( + expectedSecContact, disclose_email=True + ) + + self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True) + # Confirm that we are getting a default object + self.assertEqual(self.domain.security_contact, expectedSecContact) + + @skip("Dependent on #850") + def test_is_disclosed_on_security_contact(self): + """ + Scenario: Registrant creates a new domain with a security email + When `domain.security_contact.email` is set to a valid email + and is not the default + Then Domain sends `commands.CreateContact` to the registry + And the field `disclose` is set to true for DF.EMAIL + """ + expectedSecContact = PublicContact.get_default_security() + expectedSecContact.domain = self.domain + + id = PublicContact.objects.get( + domain=self.domain, + contact_type=PublicContact.ContactTypeChoices.SECURITY, + ).registry_id + + expectedSecContact.registry_id = id + expectedCreateCommand = self._convertPublicContactToEpp( + expectedSecContact, disclose_email=False + ) + + self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True) + # Confirm that we are getting a default object + self.assertEqual(self.domain.security_contact, expectedSecContact) + @skip("not implemented yet") def test_update_is_unsuccessful(self): """ From bb1e7bdf78b883224d9479afb57f6b8d0ebe58fe Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 4 Oct 2023 12:23:08 -0600 Subject: [PATCH 04/67] Test cases --- src/registrar/tests/common.py | 23 +++++++++ src/registrar/tests/test_models_domain.py | 63 +++++++++-------------- 2 files changed, 48 insertions(+), 38 deletions(-) diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index 7a98f224d..1257abe29 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -647,6 +647,25 @@ class MockEppLib(TestCase): registrant="regContact", ) + InfoDomainWithDefaultSecurityContact = fakedEppObject( + "fakepw", + cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35), + contacts=[ + common.DomainContact( + contact="defaultSec", + type=PublicContact.ContactTypeChoices.SECURITY, + ) + ], + hosts=["fake.host.com"], + statuses=[ + common.Status(state="serverTransferProhibited", description="", lang="en"), + common.Status(state="inactive", description="", lang="en"), + ], + ) + + mockDefaultSecurityContact = InfoDomainWithContacts.dummyInfoContactResultData( + "defaultSec", "dotgov@cisa.dhs.gov" + ) mockSecurityContact = InfoDomainWithContacts.dummyInfoContactResultData( "securityContact", "security@mail.gov" ) @@ -681,6 +700,8 @@ class MockEppLib(TestCase): return MagicMock(res_data=[self.infoDomainNoContact]) elif getattr(_request, "name", None) == "freeman.gov": return MagicMock(res_data=[self.InfoDomainWithContacts]) + elif getattr(_request, "name", None) == "defaultsecurity.gov": + return MagicMock(res_data=[self.InfoDomainWithDefaultSecurityContact]) else: return MagicMock(res_data=[self.mockDataInfoDomain]) elif isinstance(_request, commands.InfoContact): @@ -696,6 +717,8 @@ class MockEppLib(TestCase): mocked_result = self.mockAdministrativeContact case "regContact": mocked_result = self.mockRegistrantContact + case "defaultSec": + mocked_result = self.mockDefaultSecurityContact case _: # Default contact return mocked_result = self.mockDataInfoContact diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py index 51748fdfd..b61e3551e 100644 --- a/src/registrar/tests/test_models_domain.py +++ b/src/registrar/tests/test_models_domain.py @@ -705,7 +705,6 @@ class TestRegistrantContacts(MockEppLib): self.mockedSendFunction.assert_has_calls(expected_calls, any_order=True) self.assertEqual(PublicContact.objects.filter(domain=self.domain).count(), 1) - @skip("Dependent on #850") def test_not_disclosed_on_other_contacts(self): """ Scenario: Registrant creates a new domain with multiple contacts @@ -716,19 +715,19 @@ class TestRegistrantContacts(MockEppLib): on all fields except security """ # Generates a domain with four existing contacts - domain, _ = Domain.objects.get_or_create(name="igorville.gov") - # Adds default emails to all fields - domain.addAllDefaults() - # Security contact should be disclosed - domain.security_contact.email = "test123@mail.gov" - # TODO - uncomment below when #850 is merged - domain.registrant_contact = domain.get_default_registrant_contact() + domain, _ = Domain.objects.get_or_create(name="freeman.gov") expected_admin = domain.get_default_administrative_contact() expected_registrant = domain.get_default_registrant_contact() expected_security = domain.get_default_security_contact() + expected_security.email = "security@mail.gov" expected_tech = domain.get_default_technical_contact() + domain.administrative_contact = expected_admin + domain.registrant_contact = expected_registrant + domain.security_contact = expected_security + domain.technical_contact = expected_tech + contacts = [ expected_admin, expected_registrant, @@ -737,19 +736,14 @@ class TestRegistrantContacts(MockEppLib): ] for contact in contacts: - id = PublicContact.objects.get( - domain=self.domain, - contact_type=contact.contact_type_choice, - ).registry_id - contact.registry_id = id - + is_security = contact.contact_type == "security" expectedCreateCommand = self._convertPublicContactToEpp( - contact, disclose_email=False + contact, + disclose_email=is_security ) self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True) - @skip("Dependent on #850") def test_not_disclosed_on_default_security_contact(self): """ Scenario: Registrant creates a new domain with no security email @@ -757,25 +751,21 @@ class TestRegistrantContacts(MockEppLib): Then Domain sends `commands.CreateContact` to the registry And the field `disclose` is set to false for DF.EMAIL """ + self.maxDiff = None + domain, _ = Domain.objects.get_or_create(name="defaultsecurity.gov") expectedSecContact = PublicContact.get_default_security() - expectedSecContact.domain = self.domain - self.domain.security_contact.email = "test123@mail.gov" + expectedSecContact.domain = domain + expectedSecContact.registry_id="defaultSec" + domain.security_contact = expectedSecContact - id = PublicContact.objects.get( - domain=self.domain, - contact_type=PublicContact.ContactTypeChoices.SECURITY, - ).registry_id - - expectedSecContact.registry_id = id expectedCreateCommand = self._convertPublicContactToEpp( - expectedSecContact, disclose_email=True + expectedSecContact, disclose_email=False ) self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True) - # Confirm that we are getting a default object - self.assertEqual(self.domain.security_contact, expectedSecContact) + # Confirm that we are getting a default email + self.assertEqual(domain.security_contact.email, expectedSecContact.email) - @skip("Dependent on #850") def test_is_disclosed_on_security_contact(self): """ Scenario: Registrant creates a new domain with a security email @@ -784,22 +774,19 @@ class TestRegistrantContacts(MockEppLib): Then Domain sends `commands.CreateContact` to the registry And the field `disclose` is set to true for DF.EMAIL """ + domain, _ = Domain.objects.get_or_create(name="igorville.gov") expectedSecContact = PublicContact.get_default_security() - expectedSecContact.domain = self.domain + expectedSecContact.domain = domain + expectedSecContact.email = "123@mail.gov" + domain.security_contact = expectedSecContact - id = PublicContact.objects.get( - domain=self.domain, - contact_type=PublicContact.ContactTypeChoices.SECURITY, - ).registry_id - - expectedSecContact.registry_id = id expectedCreateCommand = self._convertPublicContactToEpp( - expectedSecContact, disclose_email=False + expectedSecContact, disclose_email=True ) self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True) - # Confirm that we are getting a default object - self.assertEqual(self.domain.security_contact, expectedSecContact) + # Confirm that we are getting the desired email + self.assertEqual(domain.security_contact.email, expectedSecContact.email) @skip("not implemented yet") def test_update_is_unsuccessful(self): From f0acb978b995a307180dbfc50602e98cccdf37ba Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 4 Oct 2023 12:25:16 -0600 Subject: [PATCH 05/67] Running black for formatting --- src/registrar/tests/test_models_domain.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py index b61e3551e..666d93c93 100644 --- a/src/registrar/tests/test_models_domain.py +++ b/src/registrar/tests/test_models_domain.py @@ -708,7 +708,7 @@ class TestRegistrantContacts(MockEppLib): def test_not_disclosed_on_other_contacts(self): """ Scenario: Registrant creates a new domain with multiple contacts - When `domain` has registrant, admin, technical, + When `domain` has registrant, admin, technical, and security contacts Then Domain sends `commands.CreateContact` to the registry And the field `disclose` is set to false for DF.EMAIL @@ -732,16 +732,15 @@ class TestRegistrantContacts(MockEppLib): expected_admin, expected_registrant, expected_security, - expected_tech + expected_tech, ] for contact in contacts: is_security = contact.contact_type == "security" expectedCreateCommand = self._convertPublicContactToEpp( - contact, - disclose_email=is_security + contact, disclose_email=is_security ) - + self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True) def test_not_disclosed_on_default_security_contact(self): @@ -755,13 +754,13 @@ class TestRegistrantContacts(MockEppLib): domain, _ = Domain.objects.get_or_create(name="defaultsecurity.gov") expectedSecContact = PublicContact.get_default_security() expectedSecContact.domain = domain - expectedSecContact.registry_id="defaultSec" + expectedSecContact.registry_id = "defaultSec" domain.security_contact = expectedSecContact expectedCreateCommand = self._convertPublicContactToEpp( expectedSecContact, disclose_email=False ) - + self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True) # Confirm that we are getting a default email self.assertEqual(domain.security_contact.email, expectedSecContact.email) @@ -783,7 +782,7 @@ class TestRegistrantContacts(MockEppLib): expectedCreateCommand = self._convertPublicContactToEpp( expectedSecContact, disclose_email=True ) - + self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True) # Confirm that we are getting the desired email self.assertEqual(domain.security_contact.email, expectedSecContact.email) From b824b6725fc7e0731405187ab8bc27dc73a938b4 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 4 Oct 2023 12:46:05 -0600 Subject: [PATCH 06/67] Test for technical contact on its own --- src/registrar/tests/common.py | 23 +++++++++++++++++++++++ src/registrar/tests/test_models_domain.py | 22 +++++++++++++++++++++- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index 1257abe29..20c08b9f4 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -663,6 +663,25 @@ class MockEppLib(TestCase): ], ) + InfoDomainWithDefaultTechnicalContact = fakedEppObject( + "fakepw", + cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35), + contacts=[ + common.DomainContact( + contact="defaultTech", + type=PublicContact.ContactTypeChoices.TECHNICAL, + ) + ], + hosts=["fake.host.com"], + statuses=[ + common.Status(state="serverTransferProhibited", description="", lang="en"), + common.Status(state="inactive", description="", lang="en"), + ], + ) + + mockDefaultTechnicalContact = InfoDomainWithContacts.dummyInfoContactResultData( + "defaultTech", "dotgov@cisa.dhs.gov" + ) mockDefaultSecurityContact = InfoDomainWithContacts.dummyInfoContactResultData( "defaultSec", "dotgov@cisa.dhs.gov" ) @@ -702,6 +721,8 @@ class MockEppLib(TestCase): return MagicMock(res_data=[self.InfoDomainWithContacts]) elif getattr(_request, "name", None) == "defaultsecurity.gov": return MagicMock(res_data=[self.InfoDomainWithDefaultSecurityContact]) + elif getattr(_request, "name", None) == "defaulttechnical.gov": + return MagicMock(res_data=[self.InfoDomainWithDefaultTechnicalContact]) else: return MagicMock(res_data=[self.mockDataInfoDomain]) elif isinstance(_request, commands.InfoContact): @@ -719,6 +740,8 @@ class MockEppLib(TestCase): mocked_result = self.mockRegistrantContact case "defaultSec": mocked_result = self.mockDefaultSecurityContact + case "defaultTech": + mocked_result = self.mockDefaultTechnicalContact case _: # Default contact return mocked_result = self.mockDataInfoContact diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py index 666d93c93..296389808 100644 --- a/src/registrar/tests/test_models_domain.py +++ b/src/registrar/tests/test_models_domain.py @@ -750,7 +750,6 @@ class TestRegistrantContacts(MockEppLib): Then Domain sends `commands.CreateContact` to the registry And the field `disclose` is set to false for DF.EMAIL """ - self.maxDiff = None domain, _ = Domain.objects.get_or_create(name="defaultsecurity.gov") expectedSecContact = PublicContact.get_default_security() expectedSecContact.domain = domain @@ -765,6 +764,27 @@ class TestRegistrantContacts(MockEppLib): # Confirm that we are getting a default email self.assertEqual(domain.security_contact.email, expectedSecContact.email) + def test_not_disclosed_on_default_technical_contact(self): + """ + Scenario: Registrant creates a new domain with no technical contact + When `domain.technical_contact.email` is equal to the default + Then Domain sends `commands.CreateContact` to the registry + And the field `disclose` is set to false for DF.EMAIL + """ + domain, _ = Domain.objects.get_or_create(name="defaulttechnical.gov") + expectedTechContact = PublicContact.get_default_technical() + expectedTechContact.domain = domain + expectedTechContact.registry_id = "defaultTech" + domain.technical_contact = expectedTechContact + + expectedCreateCommand = self._convertPublicContactToEpp( + expectedTechContact, disclose_email=False + ) + + self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True) + # Confirm that we are getting a default email + self.assertEqual(domain.technical_contact.email, expectedTechContact.email) + def test_is_disclosed_on_security_contact(self): """ Scenario: Registrant creates a new domain with a security email From bb193bd500371832f3eec509fbc767e89a32da6c Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 4 Oct 2023 14:15:36 -0600 Subject: [PATCH 07/67] Added comment --- src/registrar/tests/test_models_domain.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py index 296389808..0c75ddb88 100644 --- a/src/registrar/tests/test_models_domain.py +++ b/src/registrar/tests/test_models_domain.py @@ -741,6 +741,7 @@ class TestRegistrantContacts(MockEppLib): contact, disclose_email=is_security ) + # Should only be disclosed if the type is security self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True) def test_not_disclosed_on_default_security_contact(self): From c788200ed3751d05c565083f96cc91dc47568621 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 4 Oct 2023 15:59:13 -0600 Subject: [PATCH 08/67] Update test_models_domain.py --- src/registrar/tests/test_models_domain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py index 0c75ddb88..0e0db2810 100644 --- a/src/registrar/tests/test_models_domain.py +++ b/src/registrar/tests/test_models_domain.py @@ -741,7 +741,7 @@ class TestRegistrantContacts(MockEppLib): contact, disclose_email=is_security ) - # Should only be disclosed if the type is security + # Should only be disclosed if the type is security, as the email is valid self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True) def test_not_disclosed_on_default_security_contact(self): From 9ad183712b23d960c60d49f3a27a0bb41af7b916 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 5 Oct 2023 09:59:46 -0600 Subject: [PATCH 09/67] Test on emails --- src/registrar/tests/test_models_domain.py | 28 +++++++++++++++++------ 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py index 0e0db2810..7cfc6e4b4 100644 --- a/src/registrar/tests/test_models_domain.py +++ b/src/registrar/tests/test_models_domain.py @@ -717,11 +717,18 @@ class TestRegistrantContacts(MockEppLib): # Generates a domain with four existing contacts domain, _ = Domain.objects.get_or_create(name="freeman.gov") + # Contact setup expected_admin = domain.get_default_administrative_contact() + expected_admin.email = self.mockAdministrativeContact.email + expected_registrant = domain.get_default_registrant_contact() + expected_registrant.email = self.mockRegistrantContact.email + expected_security = domain.get_default_security_contact() - expected_security.email = "security@mail.gov" + expected_security.email = self.mockSecurityContact.email + expected_tech = domain.get_default_technical_contact() + expected_tech.email = self.mockTechnicalContact.email domain.administrative_contact = expected_admin domain.registrant_contact = expected_registrant @@ -729,21 +736,28 @@ class TestRegistrantContacts(MockEppLib): domain.technical_contact = expected_tech contacts = [ - expected_admin, - expected_registrant, - expected_security, - expected_tech, + (expected_admin, domain.administrative_contact), + (expected_registrant, domain.registrant_contact), + (expected_security, domain.security_contact), + (expected_tech, domain.technical_contact), ] + # Test for each contact for contact in contacts: - is_security = contact.contact_type == "security" + expected_contact = contact[0] + actual_contact = contact[1] + is_security = actual_contact.contact_type == "security" + expectedCreateCommand = self._convertPublicContactToEpp( - contact, disclose_email=is_security + expected_contact, disclose_email=is_security ) # Should only be disclosed if the type is security, as the email is valid self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True) + # The emails should match on both items + self.assertEqual(expected_contact.email, actual_contact.email) + def test_not_disclosed_on_default_security_contact(self): """ Scenario: Registrant creates a new domain with no security email From e6e0c2c416857763d83fef8be032613e73fee0a0 Mon Sep 17 00:00:00 2001 From: Cameron Dixon Date: Fri, 6 Oct 2023 12:16:20 -0400 Subject: [PATCH 10/67] Content revision, add :emoji: names --- .github/ISSUE_TEMPLATE/issue-default.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/issue-default.yml b/.github/ISSUE_TEMPLATE/issue-default.yml index 27ec10415..2252845bf 100644 --- a/.github/ISSUE_TEMPLATE/issue-default.yml +++ b/.github/ISSUE_TEMPLATE/issue-default.yml @@ -6,13 +6,13 @@ body: id: title-help attributes: value: | - > Titles should be short, descriptive, and compelling. + > Titles should be short, descriptive, and compelling. Use sentence case. - type: textarea id: issue-description attributes: label: Issue description and context description: | - Describe the issue so that someone who wasn't present for its discovery can understand the problem and why it matters. Use full sentences, plain language, and good [formatting](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax). Share desired outcomes or potential next steps. Images or links to other content/context (like documents or Slack discussions) are welcome. + Describe the issue so that someone who wasn't present for its discovery can understand why it matters. Use full sentences, plain language, and good [formatting](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax). Screenshots and links to documents/discussions are welcome. validations: required: true - type: textarea @@ -20,13 +20,13 @@ body: attributes: label: Acceptance criteria description: "If known, share 1-3 statements that would need to be true for this issue to be considered resolved. Use a [task list](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/about-task-lists#creating-task-lists) if appropriate." - placeholder: "- [ ] The button does the thing." + placeholder: "- [ ]" - type: textarea id: links-to-other-issues attributes: label: Links to other issues description: | - Add the issue #number of other issues this relates to and how (e.g., 🚧 Blocks, ⛔️ Is blocked by, 🔄 Relates to). + "Add issue #numbers this relates to and how (e.g., 🚧 :construction: Blocks, ⛔️ :no_entry: Is blocked by, 🔄 :repeat: Relates to)." placeholder: 🔄 Relates to... - type: markdown id: note From ad607547a0b1bfe289c05860533ab6a7f061006a Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 11 Oct 2023 10:45:59 -0600 Subject: [PATCH 11/67] Fix typo --- src/registrar/tests/test_models_domain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py index 7cfc6e4b4..0165000d0 100644 --- a/src/registrar/tests/test_models_domain.py +++ b/src/registrar/tests/test_models_domain.py @@ -746,7 +746,7 @@ class TestRegistrantContacts(MockEppLib): for contact in contacts: expected_contact = contact[0] actual_contact = contact[1] - is_security = actual_contact.contact_type == "security" + is_security = expected_contact.contact_type == "security" expectedCreateCommand = self._convertPublicContactToEpp( expected_contact, disclose_email=is_security From 003db40e58d73a205d027fd8674b89a716370708 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Thu, 12 Oct 2023 15:32:10 -0400 Subject: [PATCH 12/67] use shortened names for orgs in the choicefield which satisfies the admin requirement, replace short name with a long name in the user facing app in the form, summary page, manage app page --- src/registrar/admin.py | 4 +- src/registrar/forms/application_wizard.py | 3 +- ...napplication_organization_type_and_more.py | 52 +++++++++++++++++ src/registrar/models/domain_application.py | 56 ++++++++++++++----- src/registrar/models/domain_information.py | 1 + .../templates/application_review.html | 9 ++- .../templates/application_status.html | 6 +- src/registrar/templatetags/custom_filters.py | 15 +++++ src/registrar/tests/test_admin.py | 17 ++++++ src/registrar/tests/test_views.py | 28 +++++++++- 10 files changed, 171 insertions(+), 20 deletions(-) create mode 100644 src/registrar/migrations/0038_alter_domainapplication_organization_type_and_more.py diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 174500f28..aef56e0b3 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -219,9 +219,9 @@ class MyUserAdmin(BaseUserAdmin): # (which should in theory be the ONLY group) def group(self, obj): if obj.groups.filter(name="full_access_group").exists(): - return "Full access" + return "full_access_group" elif obj.groups.filter(name="cisa_analysts_group").exists(): - return "Analyst" + return "cisa_analysts_group" return "" def get_list_display(self, request): diff --git a/src/registrar/forms/application_wizard.py b/src/registrar/forms/application_wizard.py index 516683247..2fd78cdd8 100644 --- a/src/registrar/forms/application_wizard.py +++ b/src/registrar/forms/application_wizard.py @@ -153,7 +153,8 @@ class RegistrarFormSet(forms.BaseFormSet): class OrganizationTypeForm(RegistrarForm): organization_type = forms.ChoiceField( - choices=DomainApplication.OrganizationChoices.choices, + # use the long names in the application form + choices=DomainApplication.OrganizationChoicesVerbose.choices, widget=forms.RadioSelect, error_messages={"required": "Select the type of organization you represent."}, ) diff --git a/src/registrar/migrations/0038_alter_domainapplication_organization_type_and_more.py b/src/registrar/migrations/0038_alter_domainapplication_organization_type_and_more.py new file mode 100644 index 000000000..bdbca82d8 --- /dev/null +++ b/src/registrar/migrations/0038_alter_domainapplication_organization_type_and_more.py @@ -0,0 +1,52 @@ +# Generated by Django 4.2.1 on 2023-10-12 19:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("registrar", "0037_create_groups_v01"), + ] + + operations = [ + migrations.AlterField( + model_name="domainapplication", + name="organization_type", + field=models.CharField( + blank=True, + choices=[ + ("federal", "Federal"), + ("interstate", "Interstate"), + ("state_or_territory", "State or territory"), + ("tribal", "Tribal"), + ("county", "County"), + ("city", "City"), + ("special_district", "Special district"), + ("school_district", "School district"), + ], + help_text="Type of organization", + max_length=255, + null=True, + ), + ), + migrations.AlterField( + model_name="domaininformation", + name="organization_type", + field=models.CharField( + blank=True, + choices=[ + ("federal", "Federal"), + ("interstate", "Interstate"), + ("state_or_territory", "State or territory"), + ("tribal", "Tribal"), + ("county", "County"), + ("city", "City"), + ("special_district", "Special district"), + ("school_district", "School district"), + ], + help_text="Type of Organization", + max_length=255, + null=True, + ), + ), + ] diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py index 68429d381..a4752aa88 100644 --- a/src/registrar/models/domain_application.py +++ b/src/registrar/models/domain_application.py @@ -105,28 +105,57 @@ class DomainApplication(TimeStampedModel): ARMED_FORCES_AP = "AP", "Armed Forces Pacific (AP)" class OrganizationChoices(models.TextChoices): + + """ + Primary organization choices: + For use in django admin + Keys need to match OrganizationChoicesVerbose + """ + + FEDERAL = "federal", "Federal" + INTERSTATE = "interstate", "Interstate" + STATE_OR_TERRITORY = "state_or_territory", "State or territory" + TRIBAL = "tribal", "Tribal" + COUNTY = "county", "County" + CITY = "city", "City" + SPECIAL_DISTRICT = "special_district", "Special district" + SCHOOL_DISTRICT = "school_district", "School district" + + class OrganizationChoicesVerbose(models.TextChoices): + + """ + Secondary organization choices + For use in the application form and on the templates + Keys need to match OrganizationChoices + """ + FEDERAL = ( "federal", - "Federal: an agency of the U.S. government's executive, legislative, " - "or judicial branches", + "Federal: an agency of the U.S. government's executive, " + "legislative, or judicial branches", ) INTERSTATE = "interstate", "Interstate: an organization of two or more states" - STATE_OR_TERRITORY = "state_or_territory", ( - "State or territory: one of the 50 U.S. states, the District of " - "Columbia, American Samoa, Guam, Northern Mariana Islands, " - "Puerto Rico, or the U.S. Virgin Islands" + STATE_OR_TERRITORY = ( + "state_or_territory", + "State or territory: one of the 50 U.S. states, the District of Columbia, " + "American Samoa, Guam, Northern Mariana Islands, Puerto Rico, or the U.S. " + "Virgin Islands", ) - TRIBAL = "tribal", ( - "Tribal: a tribal government recognized by the federal or " - "a state government" + TRIBAL = ( + "tribal", + "Tribal: a tribal government recognized by the federal or a state " + "government", ) COUNTY = "county", "County: a county, parish, or borough" CITY = "city", "City: a city, town, township, village, etc." - SPECIAL_DISTRICT = "special_district", ( - "Special district: an independent organization within a single state" + SPECIAL_DISTRICT = ( + "special_district", + "Special district: an independent organization within a single state", ) - SCHOOL_DISTRICT = "school_district", ( - "School district: a school district that is not part of a local government" + SCHOOL_DISTRICT = ( + "school_district", + "School district: a school district that is not part of a local " + "government", ) class BranchChoices(models.TextChoices): @@ -297,6 +326,7 @@ class DomainApplication(TimeStampedModel): # ##### data fields from the initial form ##### organization_type = models.CharField( max_length=255, + # use the short names in Django admin choices=OrganizationChoices.choices, null=True, blank=True, diff --git a/src/registrar/models/domain_information.py b/src/registrar/models/domain_information.py index 3b93aff48..d2bc5c53d 100644 --- a/src/registrar/models/domain_information.py +++ b/src/registrar/models/domain_information.py @@ -21,6 +21,7 @@ class DomainInformation(TimeStampedModel): StateTerritoryChoices = DomainApplication.StateTerritoryChoices + # use the short names in Django admin OrganizationChoices = DomainApplication.OrganizationChoices BranchChoices = DomainApplication.BranchChoices diff --git a/src/registrar/templates/application_review.html b/src/registrar/templates/application_review.html index be81303b8..6a4dcbffd 100644 --- a/src/registrar/templates/application_review.html +++ b/src/registrar/templates/application_review.html @@ -1,5 +1,6 @@ {% extends 'application_form.html' %} {% load static url_helpers %} +{% load custom_filters %} {% block form_required_fields_help_text %} {# there are no required fields on this page so don't show this #} @@ -26,7 +27,13 @@
{{ form_titles|get_item:step }}
{% if step == Step.ORGANIZATION_TYPE %} - {{ application.get_organization_type_display|default:"Incomplete" }} + {% if application.organization_type is not None %} + {% with long_org_type=application.organization_type|get_organization_long_name %} + {{ long_org_type }} + {% endwith %} + {% else %} + Incomplete + {% endif %} {% endif %} {% if step == Step.TRIBAL_GOVERNMENT %} {{ application.tribe_name|default:"Incomplete" }} diff --git a/src/registrar/templates/application_status.html b/src/registrar/templates/application_status.html index a68c07c8a..79d0f7ff9 100644 --- a/src/registrar/templates/application_status.html +++ b/src/registrar/templates/application_status.html @@ -1,5 +1,7 @@ {% extends 'base.html' %} +{% load custom_filters %} + {% block title %}Domain request status | {{ domainapplication.requested_domain.name }} | {% endblock %} {% load static url_helpers %} @@ -50,7 +52,9 @@

Summary of your domain request

{% with heading_level='h3' %} - {% include "includes/summary_item.html" with title='Type of organization' value=domainapplication.get_organization_type_display heading_level=heading_level %} + {% with long_org_type=domainapplication.organization_type|get_organization_long_name %} + {% include "includes/summary_item.html" with title='Type of organization' value=long_org_type heading_level=heading_level %} + {% endwith %} {% if domainapplication.tribe_name %} {% include "includes/summary_item.html" with title='Tribal government' value=domainapplication.tribe_name heading_level=heading_level %} diff --git a/src/registrar/templatetags/custom_filters.py b/src/registrar/templatetags/custom_filters.py index 3614db18e..e90c3166d 100644 --- a/src/registrar/templatetags/custom_filters.py +++ b/src/registrar/templatetags/custom_filters.py @@ -1,5 +1,6 @@ from django import template import re +from registrar.models.domain_application import DomainApplication register = template.Library() @@ -48,3 +49,17 @@ def contains_checkbox(html_list): if re.search(r']*type="checkbox"', html_string): return True return False + + +@register.filter +def get_organization_long_name(organization_type): + organization_choices_dict = {} + + for name, value in DomainApplication.OrganizationChoicesVerbose.choices: + organization_choices_dict[name] = value + + long_form_type = organization_choices_dict[organization_type] + if long_form_type is not None: + return long_form_type + + return "Error" diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 51ace34f7..b5827d3e9 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -300,6 +300,23 @@ class TestDomainApplicationAdmin(MockEppLib): self.superuser = create_superuser() self.staffuser = create_user() + def test_short_org_name_in_applications_list(self): + """ + Make sure the short name is displaying in admin on the list page + """ + self.client.force_login(self.superuser) + completed_application() + response = self.client.get("/admin/registrar/domainapplication/") + # There are 3 template references to Federal (3) plus one reference in the table + # for our actual application + self.assertContains(response, "Federal", count=4) + # This may be a bit more robust + self.assertContains( + response, 'Federal', count=1 + ) + # Now let's make sure the long description does not exist + self.assertNotContains(response, "Federal: an agency of the U.S. government") + @boto3_mocking.patching def test_save_model_sends_submitted_email(self): # make sure there is no user with this email diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 2194b42db..32a22916e 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -141,9 +141,12 @@ class DomainApplicationTests(TestWithUser, WebTest): @boto3_mocking.patching def test_application_form_submission(self): - """Can fill out the entire form and submit. + """ + Can fill out the entire form and submit. As we add additional form pages, we need to include them here to make this test work. + + This test also looks for the long organization name on the summary page. """ num_pages_tested = 0 # elections, type_of_work, tribal_government, no_other_contacts @@ -427,7 +430,8 @@ class DomainApplicationTests(TestWithUser, WebTest): review_form = review_page.form # Review page contains all the previously entered data - self.assertContains(review_page, "Federal") + # Let's make sure the long org name is displayed + self.assertContains(review_page, "Federal: an agency of the U.S. government") self.assertContains(review_page, "Executive") self.assertContains(review_page, "Testorg") self.assertContains(review_page, "address 1") @@ -1065,6 +1069,26 @@ class DomainApplicationTests(TestWithUser, WebTest): # page = self.app.get(url) # self.assertNotContains(page, "VALUE") + def test_long_org_name_in_application(self): + """ + Make sure the long name is displaying in the application form, + org step + """ + request = self.app.get(reverse("application:")).follow() + self.assertContains(request, "Federal: an agency of the U.S. government") + + def test_long_org_name_in_application_manage(self): + """ + Make sure the long name is displaying in the application summary + page (manage your application) + """ + completed_application(status=DomainApplication.SUBMITTED, user=self.user) + home_page = self.app.get("/") + self.assertContains(home_page, "city.gov") + # click the "Edit" link + detail_page = home_page.click("Manage") + self.assertContains(detail_page, "Federal: an agency of the U.S. government") + class TestWithDomainPermissions(TestWithUser): def setUp(self): From 7b13dd4b3a68e973e0d6a204a3ff3398dff47737 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Thu, 12 Oct 2023 16:14:51 -0400 Subject: [PATCH 13/67] trigger PR pipeline --- src/registrar/models/domain_application.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py index a4752aa88..4da32ad18 100644 --- a/src/registrar/models/domain_application.py +++ b/src/registrar/models/domain_application.py @@ -107,9 +107,9 @@ class DomainApplication(TimeStampedModel): class OrganizationChoices(models.TextChoices): """ - Primary organization choices: - For use in django admin - Keys need to match OrganizationChoicesVerbose + Primary organization choices: + For use in django admin + Keys need to match OrganizationChoicesVerbose """ FEDERAL = "federal", "Federal" @@ -124,9 +124,9 @@ class DomainApplication(TimeStampedModel): class OrganizationChoicesVerbose(models.TextChoices): """ - Secondary organization choices - For use in the application form and on the templates - Keys need to match OrganizationChoices + Secondary organization choices + For use in the application form and on the templates + Keys need to match OrganizationChoices """ FEDERAL = ( From 6709ac9ddb91259f060657970086c05c50cde98e Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Fri, 13 Oct 2023 16:20:38 -0400 Subject: [PATCH 14/67] trick the sandbox by deleting a conflicting migration --- .../migrations/0035_alter_user_options.py | 42 ++++++++++++++- ...napplication_organization_type_and_more.py | 52 ------------------- 2 files changed, 41 insertions(+), 53 deletions(-) delete mode 100644 src/registrar/migrations/0038_alter_domainapplication_organization_type_and_more.py diff --git a/src/registrar/migrations/0035_alter_user_options.py b/src/registrar/migrations/0035_alter_user_options.py index 7ed81cdf5..b76f07c5d 100644 --- a/src/registrar/migrations/0035_alter_user_options.py +++ b/src/registrar/migrations/0035_alter_user_options.py @@ -1,6 +1,6 @@ # Generated by Django 4.2.1 on 2023-09-27 18:53 -from django.db import migrations +from django.db import migrations, models class Migration(migrations.Migration): @@ -18,4 +18,44 @@ class Migration(migrations.Migration): ] }, ), + migrations.AlterField( + model_name="domainapplication", + name="organization_type", + field=models.CharField( + blank=True, + choices=[ + ("federal", "Federal"), + ("interstate", "Interstate"), + ("state_or_territory", "State or territory"), + ("tribal", "Tribal"), + ("county", "County"), + ("city", "City"), + ("special_district", "Special district"), + ("school_district", "School district"), + ], + help_text="Type of organization", + max_length=255, + null=True, + ), + ), + migrations.AlterField( + model_name="domaininformation", + name="organization_type", + field=models.CharField( + blank=True, + choices=[ + ("federal", "Federal"), + ("interstate", "Interstate"), + ("state_or_territory", "State or territory"), + ("tribal", "Tribal"), + ("county", "County"), + ("city", "City"), + ("special_district", "Special district"), + ("school_district", "School district"), + ], + help_text="Type of Organization", + max_length=255, + null=True, + ), + ), ] diff --git a/src/registrar/migrations/0038_alter_domainapplication_organization_type_and_more.py b/src/registrar/migrations/0038_alter_domainapplication_organization_type_and_more.py deleted file mode 100644 index bdbca82d8..000000000 --- a/src/registrar/migrations/0038_alter_domainapplication_organization_type_and_more.py +++ /dev/null @@ -1,52 +0,0 @@ -# Generated by Django 4.2.1 on 2023-10-12 19:30 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("registrar", "0037_create_groups_v01"), - ] - - operations = [ - migrations.AlterField( - model_name="domainapplication", - name="organization_type", - field=models.CharField( - blank=True, - choices=[ - ("federal", "Federal"), - ("interstate", "Interstate"), - ("state_or_territory", "State or territory"), - ("tribal", "Tribal"), - ("county", "County"), - ("city", "City"), - ("special_district", "Special district"), - ("school_district", "School district"), - ], - help_text="Type of organization", - max_length=255, - null=True, - ), - ), - migrations.AlterField( - model_name="domaininformation", - name="organization_type", - field=models.CharField( - blank=True, - choices=[ - ("federal", "Federal"), - ("interstate", "Interstate"), - ("state_or_territory", "State or territory"), - ("tribal", "Tribal"), - ("county", "County"), - ("city", "City"), - ("special_district", "Special district"), - ("school_district", "School district"), - ], - help_text="Type of Organization", - max_length=255, - null=True, - ), - ), - ] From a2574124ffaae7eccaf3d94bb3e67aefdffe2ec7 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Fri, 13 Oct 2023 16:27:23 -0400 Subject: [PATCH 15/67] return migrations to pervious state --- .../migrations/0035_alter_user_options.py | 42 +-------------- ...napplication_organization_type_and_more.py | 52 +++++++++++++++++++ 2 files changed, 53 insertions(+), 41 deletions(-) create mode 100644 src/registrar/migrations/0038_alter_domainapplication_organization_type_and_more.py diff --git a/src/registrar/migrations/0035_alter_user_options.py b/src/registrar/migrations/0035_alter_user_options.py index b76f07c5d..7ed81cdf5 100644 --- a/src/registrar/migrations/0035_alter_user_options.py +++ b/src/registrar/migrations/0035_alter_user_options.py @@ -1,6 +1,6 @@ # Generated by Django 4.2.1 on 2023-09-27 18:53 -from django.db import migrations, models +from django.db import migrations class Migration(migrations.Migration): @@ -18,44 +18,4 @@ class Migration(migrations.Migration): ] }, ), - migrations.AlterField( - model_name="domainapplication", - name="organization_type", - field=models.CharField( - blank=True, - choices=[ - ("federal", "Federal"), - ("interstate", "Interstate"), - ("state_or_territory", "State or territory"), - ("tribal", "Tribal"), - ("county", "County"), - ("city", "City"), - ("special_district", "Special district"), - ("school_district", "School district"), - ], - help_text="Type of organization", - max_length=255, - null=True, - ), - ), - migrations.AlterField( - model_name="domaininformation", - name="organization_type", - field=models.CharField( - blank=True, - choices=[ - ("federal", "Federal"), - ("interstate", "Interstate"), - ("state_or_territory", "State or territory"), - ("tribal", "Tribal"), - ("county", "County"), - ("city", "City"), - ("special_district", "Special district"), - ("school_district", "School district"), - ], - help_text="Type of Organization", - max_length=255, - null=True, - ), - ), ] diff --git a/src/registrar/migrations/0038_alter_domainapplication_organization_type_and_more.py b/src/registrar/migrations/0038_alter_domainapplication_organization_type_and_more.py new file mode 100644 index 000000000..a06ea0451 --- /dev/null +++ b/src/registrar/migrations/0038_alter_domainapplication_organization_type_and_more.py @@ -0,0 +1,52 @@ +# Generated by Django 4.2.1 on 2023-10-13 20:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("registrar", "0037_create_groups_v01"), + ] + + operations = [ + migrations.AlterField( + model_name="domainapplication", + name="organization_type", + field=models.CharField( + blank=True, + choices=[ + ("federal", "Federal"), + ("interstate", "Interstate"), + ("state_or_territory", "State or territory"), + ("tribal", "Tribal"), + ("county", "County"), + ("city", "City"), + ("special_district", "Special district"), + ("school_district", "School district"), + ], + help_text="Type of organization", + max_length=255, + null=True, + ), + ), + migrations.AlterField( + model_name="domaininformation", + name="organization_type", + field=models.CharField( + blank=True, + choices=[ + ("federal", "Federal"), + ("interstate", "Interstate"), + ("state_or_territory", "State or territory"), + ("tribal", "Tribal"), + ("county", "County"), + ("city", "City"), + ("special_district", "Special district"), + ("school_district", "School district"), + ], + help_text="Type of Organization", + max_length=255, + null=True, + ), + ), + ] From 5e787eeed324f8789c8a1e3e9dcf0694d13d588a Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Fri, 13 Oct 2023 16:30:29 -0400 Subject: [PATCH 16/67] merge diverging migrations --- src/registrar/migrations/0039_merge_20231013_2029.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 src/registrar/migrations/0039_merge_20231013_2029.py diff --git a/src/registrar/migrations/0039_merge_20231013_2029.py b/src/registrar/migrations/0039_merge_20231013_2029.py new file mode 100644 index 000000000..aed231bdc --- /dev/null +++ b/src/registrar/migrations/0039_merge_20231013_2029.py @@ -0,0 +1,12 @@ +# Generated by Django 4.2.1 on 2023-10-13 20:29 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("registrar", "0038_alter_domainapplication_organization_type_and_more"), + ("registrar", "0038_create_groups_v02"), + ] + + operations = [] From 372ead1121372d885b19fc56fce641f1cb6b7541 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Fri, 13 Oct 2023 17:05:33 -0400 Subject: [PATCH 17/67] refactor org literal mapping to dict, tweak error handing, lint --- src/registrar/models/domain_application.py | 12 ++++++------ src/registrar/templatetags/custom_filters.py | 17 ++++++++++------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py index 4da32ad18..a4752aa88 100644 --- a/src/registrar/models/domain_application.py +++ b/src/registrar/models/domain_application.py @@ -107,9 +107,9 @@ class DomainApplication(TimeStampedModel): class OrganizationChoices(models.TextChoices): """ - Primary organization choices: - For use in django admin - Keys need to match OrganizationChoicesVerbose + Primary organization choices: + For use in django admin + Keys need to match OrganizationChoicesVerbose """ FEDERAL = "federal", "Federal" @@ -124,9 +124,9 @@ class DomainApplication(TimeStampedModel): class OrganizationChoicesVerbose(models.TextChoices): """ - Secondary organization choices - For use in the application form and on the templates - Keys need to match OrganizationChoices + Secondary organization choices + For use in the application form and on the templates + Keys need to match OrganizationChoices """ FEDERAL = ( diff --git a/src/registrar/templatetags/custom_filters.py b/src/registrar/templatetags/custom_filters.py index e90c3166d..158b7269e 100644 --- a/src/registrar/templatetags/custom_filters.py +++ b/src/registrar/templatetags/custom_filters.py @@ -1,8 +1,10 @@ +import logging from django import template import re from registrar.models.domain_application import DomainApplication register = template.Library() +logger = logging.getLogger(__name__) @register.filter(name="extract_value") @@ -53,13 +55,14 @@ def contains_checkbox(html_list): @register.filter def get_organization_long_name(organization_type): - organization_choices_dict = {} - - for name, value in DomainApplication.OrganizationChoicesVerbose.choices: - organization_choices_dict[name] = value + # https://gist.github.com/OmenApps/3eef60ba4204f3d1842d9d7477efcce1#file-django_choices-txt-L28 + organization_choices_dict = dict( + DomainApplication.OrganizationChoicesVerbose.choices + ) long_form_type = organization_choices_dict[organization_type] - if long_form_type is not None: - return long_form_type + if long_form_type is None: + logger.error("Organization type error, triggered by a template's custom filter") + return "Error" - return "Error" + return long_form_type From 35570b6423d8a31472254a445733799218fd2bd2 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 13 Oct 2023 15:33:30 -0600 Subject: [PATCH 18/67] Progress save --- ...2-submit-domain-request-user-flow copy.md} | 0 .../decisions/0023-use-geventconnpool..md | 50 +++++++++++++++++++ 2 files changed, 50 insertions(+) rename docs/architecture/decisions/{0022-submit-domain-request-user-flow.md => 0022-submit-domain-request-user-flow copy.md} (100%) create mode 100644 docs/architecture/decisions/0023-use-geventconnpool..md diff --git a/docs/architecture/decisions/0022-submit-domain-request-user-flow.md b/docs/architecture/decisions/0022-submit-domain-request-user-flow copy.md similarity index 100% rename from docs/architecture/decisions/0022-submit-domain-request-user-flow.md rename to docs/architecture/decisions/0022-submit-domain-request-user-flow copy.md diff --git a/docs/architecture/decisions/0023-use-geventconnpool..md b/docs/architecture/decisions/0023-use-geventconnpool..md new file mode 100644 index 000000000..512004ac7 --- /dev/null +++ b/docs/architecture/decisions/0023-use-geventconnpool..md @@ -0,0 +1,50 @@ +# 22. Use geventconnpool library for Connection Pooling + +Date: 2023-13-10 + +## Status + +In Review + +## Context + +When sending and receiving data from the registry, we use the [EPPLib](https://github.com/cisagov/epplib) library to facilitate that process. To manage these connections within our application, we utilize a module named `epplibwrapper` which serves as a bridge between getgov and the EPPLib library. As part of this process, `epplibwrapper` will instantiate a client that handles sending/receiving data. + +At present, each time we need to send a command to the registry, the client establishes a new connection to handle this task. This becomes inefficient when dealing with multiple calls in parallel or in series, as we have to initiate a handshake for each of them. To mitigate this issue, a widely adopted solution is to use a [connection pool](https://en.wikipedia.org/wiki/Connection_pool). In general, a connection pool stores a cache of active connections so that rather than restarting the handshake process when it is unnecessary, we can utilize an existing connection to avoid this problem. + +In practice, the lack of a connection pool has resulted in performance issues when dealing with connections to and from the registry. Given the unique nature of our development stack, our options for prebuilt libraries are limited. Out of our available options, a library called [`geventconnpool`](https://github.com/rasky/geventconnpool) was identified that most closely matched our needs. + +## Considered Options + +**Option 1:** Use the existing connection pool library `geventconnpool` as a foundation for connection pooling. + +**Option 2:** Write our own connection pool logic. + +**Option 3:** Modify an existing library which we will tailor to our needs. + +## Tradeoffs + +**Option 1:** +Pros: +- Subtantially saves development time and effort. +- It is a tiny library that is easy to audit and understand. +- This library has been used for [EPP before](https://github.com/rasky/geventconnpool/issues/9) +- Uses [`gevent`](http://www.gevent.org/) for coroutines, which is reliable and well maintained. +- +- Cons: May not be tailored to our specific needs, could introduce unwanted dependencies. + +**Option 2:** +- Pros: Full control over functionality, can be tailored to our specific needs. +- Cons: Requires significant development time and effort, needs thorough testing. + +**Option 3:** +- Pros: Savings in development time and effort, can be tailored to our specific needs. +- Cons: Could introduce complexity, potential issues with maintaining the modified library. + +## Decision + +We have decided to go with option 1. New users of the registrar will need to have at least one approved application OR prior registered .gov domain in order to submit another application. We chose this option because we would like to allow users be able to work on applications, even if they are unable to submit them. + +A [user flow diagram](https://miro.com/app/board/uXjVM3jz3Bs=/?share_link_id=875307531981) demonstrates our decision. + +## Consequences \ No newline at end of file From becaedc117b1b58602035292608760375de89163 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 16 Oct 2023 09:59:12 -0600 Subject: [PATCH 19/67] Update 0023-use-geventconnpool..md --- .../decisions/0023-use-geventconnpool..md | 79 ++++++++++++++----- 1 file changed, 59 insertions(+), 20 deletions(-) diff --git a/docs/architecture/decisions/0023-use-geventconnpool..md b/docs/architecture/decisions/0023-use-geventconnpool..md index 512004ac7..c24318b4f 100644 --- a/docs/architecture/decisions/0023-use-geventconnpool..md +++ b/docs/architecture/decisions/0023-use-geventconnpool..md @@ -16,35 +16,74 @@ In practice, the lack of a connection pool has resulted in performance issues wh ## Considered Options -**Option 1:** Use the existing connection pool library `geventconnpool` as a foundation for connection pooling. +**Option 1:** Use the existing connection pool library `geventconnpool`. +
+➕ Pros + +- Saves development time and effort. +- A tiny library that is easy to audit and understand. +- Built to be flexible, so every built-in function can be overridden with minimal effort. +- This library has been used for [EPP before](https://github.com/rasky/geventconnpool/issues/9). +- Uses [`gevent`](http://www.gevent.org/) for coroutines, which is reliable and well maintained. +- [`gevent`](http://www.gevent.org/) is used in our WSGI web server. +- This library is the closest match to our needs that we have found. + +
+
+➖ Cons + +- Not a well maintained library, could require a fork if a dependency breaks. +- Heavily reliant on `gevent`. + +
**Option 2:** Write our own connection pool logic. +
+➕ Pros -**Option 3:** Modify an existing library which we will tailor to our needs. +- Full control over functionality, can be tailored to our specific needs. +- Highly specific to our stack, could be fine tuned for performance. -## Tradeoffs +
+
+➖ Cons -**Option 1:** -Pros: -- Subtantially saves development time and effort. -- It is a tiny library that is easy to audit and understand. -- This library has been used for [EPP before](https://github.com/rasky/geventconnpool/issues/9) -- Uses [`gevent`](http://www.gevent.org/) for coroutines, which is reliable and well maintained. -- -- Cons: May not be tailored to our specific needs, could introduce unwanted dependencies. +- Requires significant development time and effort, needs thorough testing. +- Would require managing with and developing around concurrency. +- Introduces the potential for many unseen bugs. -**Option 2:** -- Pros: Full control over functionality, can be tailored to our specific needs. -- Cons: Requires significant development time and effort, needs thorough testing. +
-**Option 3:** -- Pros: Savings in development time and effort, can be tailored to our specific needs. -- Cons: Could introduce complexity, potential issues with maintaining the modified library. +**Option 3:** Modify an existing library which we will then tailor to our needs. +
+➕ Pros + +- Savings in development time and effort, can be tailored to our specific needs. +- Good middleground between the first two options. + +
+
+➖ Cons + +- Could introduce complexity, potential issues with maintaining the modified library. +- May not be necessary if the given library is flexible enough. + +
## Decision -We have decided to go with option 1. New users of the registrar will need to have at least one approved application OR prior registered .gov domain in order to submit another application. We chose this option because we would like to allow users be able to work on applications, even if they are unable to submit them. +We have decided to go with option 1, which is to use the `geventconnpool` library. It closely matches our needs and offers several advantages. Of note, it significantly saves on development time and it is inherently flexible. This allows us to easily change functionality with minimal effort. In addition, the gevent library (which this uses) offers performance benefits due to it being a) written in [cython](https://cython.org/), b) very well maintained and purpose built for tasks such as these, and c) used in our WGSI server. -A [user flow diagram](https://miro.com/app/board/uXjVM3jz3Bs=/?share_link_id=875307531981) demonstrates our decision. +In summary, this decision was driven by the library's flexibility, simplicity, and compatibility with our tech stack. We acknowledge the risk associated with its maintenance status, but believe that the benefit outweighs the risk. -## Consequences \ No newline at end of file +## Consequences + +While its small size makes it easy to work around, `geventconnpool` is not actively maintained. Its last update was in 2021, and as such there is a risk that its dependencies (gevent) will outpace this library and cause it to break. If such an event occurs, it would require that we fork the library and fix those issues. See option 3 pros/cons. + +## Mitigation Plan +To manage this risk, we'll: + +1. Monitor the gevent library for updates. +2. Design the connection pool logic abstractly such that we can easily swap the underlying logic out without needing (or minimizing the need) to rewrite code in `epplibwrapper`. +3. Document a process for forking and maintaining the library if it becomes necessary, including testing procedures. +4. Establish a contingency plan for reverting to a previous system state or switching to a different library if significant issues arise with `gevent` or `geventconnpool`. \ No newline at end of file From 605e75dc48fa2dff17015e39caffe141a0fa1a43 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 16 Oct 2023 10:00:10 -0600 Subject: [PATCH 20/67] Fix file rename --- ...-user-flow copy.md => 0022-submit-domain-request-user-flow.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/architecture/decisions/{0022-submit-domain-request-user-flow copy.md => 0022-submit-domain-request-user-flow.md} (100%) diff --git a/docs/architecture/decisions/0022-submit-domain-request-user-flow copy.md b/docs/architecture/decisions/0022-submit-domain-request-user-flow.md similarity index 100% rename from docs/architecture/decisions/0022-submit-domain-request-user-flow copy.md rename to docs/architecture/decisions/0022-submit-domain-request-user-flow.md From d4ec7eca8a734c94b2dabf2c9417dd811184cef2 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 16 Oct 2023 10:11:15 -0600 Subject: [PATCH 21/67] Fix weird file name --- .../{0023-use-geventconnpool..md => 0023-use-geventconnpool.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/architecture/decisions/{0023-use-geventconnpool..md => 0023-use-geventconnpool.md} (100%) diff --git a/docs/architecture/decisions/0023-use-geventconnpool..md b/docs/architecture/decisions/0023-use-geventconnpool.md similarity index 100% rename from docs/architecture/decisions/0023-use-geventconnpool..md rename to docs/architecture/decisions/0023-use-geventconnpool.md From 8e700e0ecbb088e7ede30f66c8e56a140ec11507 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Mon, 16 Oct 2023 16:57:31 -0400 Subject: [PATCH 22/67] added debugging; removed duplicate get_object calls from views --- src/registrar/models/domain.py | 30 ++++++++++++++++++++++++++++++ src/registrar/views/domain.py | 15 +++++++++------ 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 59edb707a..1980bd087 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -1,5 +1,6 @@ from itertools import zip_longest import logging +import inspect from datetime import date from string import digits from django_fsm import FSMField, transition, TransitionNotAllowed # type: ignore @@ -50,8 +51,33 @@ class Domain(TimeStampedModel, DomainHelper): def __init__(self, *args, **kwargs): self._cache = {} + #self.print_calling_function() + logger.info("__init__ being called on domain") super(Domain, self).__init__(*args, **kwargs) + def print_calling_function(self): + # Get the current frame in the call stack + current_frame = inspect.currentframe() + + i = 1 + while True: + try: + # Get the calling frame + calling_frame = inspect.getouterframes(current_frame, 2)[i] + + # Extract information about the calling function + calling_function_name = calling_frame.function + calling_module_name = calling_frame[0].f_globals['__name__'] + calling_line_number = calling_frame[2] + + # Print information about the calling function + print(f"Calling function: {calling_function_name} in module {calling_module_name} at line {calling_line_number}") + + i+=1 + except Exception as err: + print("========================================================") + break + class Status(models.TextChoices): """ The status codes we can receive from the registry. @@ -144,10 +170,12 @@ class Domain(TimeStampedModel, DomainHelper): def __get__(self, obj, objtype=None): """Called during get. Example: `r = domain.registrant`.""" + logger.info("domain __get__ is called: %s", obj) return super().__get__(obj, objtype) def __set__(self, obj, value): """Called during set. Example: `domain.registrant = 'abc123'`.""" + logger.info("domain __set__ is called: %s", obj) super().__set__(obj, value) # always invalidate cache after sending updates to the registry obj._invalidate_cache() @@ -1223,6 +1251,7 @@ class Domain(TimeStampedModel, DomainHelper): raise NotImplementedError() def _fetch_cache(self, fetch_hosts=False, fetch_contacts=False): + logger.info("fetch_cache called") """Contact registry for info about a domain.""" try: # get info from registry @@ -1354,6 +1383,7 @@ class Domain(TimeStampedModel, DomainHelper): def _invalidate_cache(self): """Remove cache data when updates are made.""" + logger.debug("_invalidate_cache called") self._cache = {} def _get_property(self, property): diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index d8c3c80fa..8838407f4 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -54,7 +54,7 @@ class DomainOrgNameAddressView(DomainPermissionView, FormMixin): def get_form_kwargs(self, *args, **kwargs): """Add domain_info.organization_name instance to make a bound form.""" form_kwargs = super().get_form_kwargs(*args, **kwargs) - form_kwargs["instance"] = self.get_object().domain_info + form_kwargs["instance"] = self.object.domain_info return form_kwargs def get_success_url(self): @@ -97,7 +97,7 @@ class DomainAuthorizingOfficialView(DomainPermissionView, FormMixin): def get_form_kwargs(self, *args, **kwargs): """Add domain_info.authorizing_official instance to make a bound form.""" form_kwargs = super().get_form_kwargs(*args, **kwargs) - form_kwargs["instance"] = self.get_object().domain_info.authorizing_official + form_kwargs["instance"] = self.object.domain_info.authorizing_official return form_kwargs def get_success_url(self): @@ -137,8 +137,11 @@ class DomainNameserversView(DomainPermissionView, FormMixin): def get_initial(self): """The initial value for the form (which is a formset here).""" - domain = self.get_object() + logger.info("DomainNameserversView.get_initial()") + domain = self.object + logger.info("DomainNameserversView.get_initial:: after get_object") nameservers = domain.nameservers + logger.info("DomainNameserversView.get_initial:: after set nameservers") initial_data = [] if nameservers is not None: @@ -196,7 +199,7 @@ class DomainNameserversView(DomainPermissionView, FormMixin): except KeyError: # no server information in this field, skip it pass - domain = self.get_object() + domain = self.object domain.nameservers = nameservers messages.success( @@ -257,7 +260,7 @@ class DomainSecurityEmailView(DomainPermissionView, FormMixin): def get_initial(self): """The initial value for the form.""" - domain = self.get_object() + domain = self.object initial = super().get_initial() security_contact = domain.security_contact if security_contact is None or security_contact.email == "dotgov@cisa.dhs.gov": @@ -286,7 +289,7 @@ class DomainSecurityEmailView(DomainPermissionView, FormMixin): # Set the security email from the form new_email = form.cleaned_data.get("security_email", "") - domain = self.get_object() + domain = self.object contact = domain.security_contact contact.email = new_email contact.save() From 2d4ab0c7bd24571ff4ecefe36eb8ae671b33e5bd Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Tue, 17 Oct 2023 07:57:16 -0400 Subject: [PATCH 23/67] added DomainFormBaseView and DomainBaseView and session cache for caching domains --- src/registrar/config/settings.py | 8 ++ src/registrar/models/domain.py | 4 +- src/registrar/views/domain.py | 151 ++++++++++++++++--------------- 3 files changed, 88 insertions(+), 75 deletions(-) diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index ceb215a4d..8de4c6caa 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -169,6 +169,11 @@ WSGI_APPLICATION = "registrar.config.wsgi.application" # "BACKEND": "django.core.cache.backends.db.DatabaseCache", # } # } +CACHES = { + 'default': { + 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', + } +} # Absolute path to the directory where `collectstatic` # will place static files for deployment. @@ -652,6 +657,9 @@ SESSION_COOKIE_SAMESITE = "Lax" # instruct browser to only send cookie via HTTPS SESSION_COOKIE_SECURE = True +# session engine to cache session information +SESSION_ENGINE = 'django.contrib.sessions.backends.cache' + # ~ Set by django.middleware.clickjacking.XFrameOptionsMiddleware # prevent clickjacking by instructing the browser not to load # our site within an iframe diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index ae9d80c25..822451a49 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -61,7 +61,7 @@ class Domain(TimeStampedModel, DomainHelper): def __init__(self, *args, **kwargs): self._cache = {} - #self.print_calling_function() + self.print_calling_function() logger.info("__init__ being called on domain") super(Domain, self).__init__(*args, **kwargs) @@ -180,7 +180,7 @@ class Domain(TimeStampedModel, DomainHelper): def __get__(self, obj, objtype=None): """Called during get. Example: `r = domain.registrant`.""" - logger.info("domain __get__ is called: %s", obj) + logger.info("domain __get__ is called: %s: %s", obj, objtype) return super().__get__(obj, objtype) def __set__(self, obj, value): diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index bc1f42b88..13ca3774a 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -36,8 +36,76 @@ from .utility import DomainPermissionView, DomainInvitationPermissionDeleteView logger = logging.getLogger(__name__) +class DomainBaseView(DomainPermissionView): -class DomainView(DomainPermissionView): + def get(self, request, *args, **kwargs): + logger.info("DomainBaseView::get") + self._get_domain(request) + # pk = self.kwargs.get('pk') + # cached_domain = request.session.get(pk) + + # if cached_domain: + # logger.info("reading object from session cache") + # self.object = cached_domain + # else: + # logger.info("reading object from db") + # self.object = self.get_object() + # logger.info("writing object to session cache") + # request.session[pk] = self.object + context = self.get_context_data(object=self.object) + return self.render_to_response(context) + + def _get_domain(self, request): + # get domain from session cache or from db + # and set to self.object + # set session to self for downstream functions to + # update session cache + self.session = request.session + pk = self.kwargs.get('pk') + cached_domain = self.session.get(pk) + + if cached_domain: + logger.info("reading object from session cache") + self.object = cached_domain + else: + logger.info("reading object from db") + self.object = self.get_object() + self._update_session_with_domain() + + def _update_session_with_domain(self): + pk = self.kwargs.get('pk') + logger.info("writing object to session cache") + self.session[pk] = self.object + + +class DomainFormBaseView(DomainBaseView, FormMixin): + + def post(self, request, *args, **kwargs): + """Form submission posts to this view. + + This post method harmonizes using DetailView and FormMixin together. + """ + self._get_domain(request) + form = self.get_form() + if form.is_valid(): + return self.form_valid(form) + else: + return self.form_invalid(form) + + def form_valid(self, form): + self._update_session_with_domain() + + # superclass has the redirect + return super().form_valid(form) + + def form_invalid(self, form): + self._update_session_with_domain() + + # superclass has the redirect + return super().form_invalid(form) + + +class DomainView(DomainBaseView): """Domain detail overview page.""" @@ -46,10 +114,10 @@ class DomainView(DomainPermissionView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - default_email = Domain().get_default_security_contact().email + default_email = self.object.get_default_security_contact().email context["default_security_email"] = default_email - security_email = self.get_object().get_security_email() + security_email = self.object.get_security_email() if security_email is None or security_email == default_email: context["security_email"] = None return context @@ -57,7 +125,7 @@ class DomainView(DomainPermissionView): return context -class DomainOrgNameAddressView(DomainPermissionView, FormMixin): +class DomainOrgNameAddressView(DomainFormBaseView): """Organization name and mailing address view""" model = Domain @@ -75,18 +143,6 @@ class DomainOrgNameAddressView(DomainPermissionView, FormMixin): """Redirect to the overview page for the domain.""" return reverse("domain-org-name-address", kwargs={"pk": self.object.pk}) - def post(self, request, *args, **kwargs): - """Form submission posts to this view. - - This post method harmonizes using DetailView and FormMixin together. - """ - self.object = self.get_object() - form = self.get_form() - if form.is_valid(): - return self.form_valid(form) - else: - return self.form_invalid(form) - def form_valid(self, form): """The form is valid, save the organization name and mailing address.""" form.save() @@ -99,7 +155,7 @@ class DomainOrgNameAddressView(DomainPermissionView, FormMixin): return super().form_valid(form) -class DomainAuthorizingOfficialView(DomainPermissionView, FormMixin): +class DomainAuthorizingOfficialView(DomainFormBaseView): """Domain authorizing official editing view.""" @@ -118,18 +174,6 @@ class DomainAuthorizingOfficialView(DomainPermissionView, FormMixin): """Redirect to the overview page for the domain.""" return reverse("domain-authorizing-official", kwargs={"pk": self.object.pk}) - def post(self, request, *args, **kwargs): - """Form submission posts to this view. - - This post method harmonizes using DetailView and FormMixin together. - """ - self.object = self.get_object() - form = self.get_form() - if form.is_valid(): - return self.form_valid(form) - else: - return self.form_invalid(form) - def form_valid(self, form): """The form is valid, save the authorizing official.""" form.save() @@ -142,7 +186,7 @@ class DomainAuthorizingOfficialView(DomainPermissionView, FormMixin): return super().form_valid(form) -class DomainNameserversView(DomainPermissionView, FormMixin): +class DomainNameserversView(DomainFormBaseView): """Domain nameserver editing view.""" @@ -191,16 +235,6 @@ class DomainNameserversView(DomainPermissionView, FormMixin): form.fields["server"].required = False return formset - def post(self, request, *args, **kwargs): - """Formset submission posts to this view.""" - self.object = self.get_object() - formset = self.get_form() - - if formset.is_valid(): - return self.form_valid(formset) - else: - return self.form_invalid(formset) - def form_valid(self, formset): """The formset is valid, perform something with it.""" @@ -224,7 +258,7 @@ class DomainNameserversView(DomainPermissionView, FormMixin): return super().form_valid(formset) -class DomainYourContactInformationView(DomainPermissionView, FormMixin): +class DomainYourContactInformationView(DomainFormBaseView): """Domain your contact information editing view.""" @@ -241,16 +275,6 @@ class DomainYourContactInformationView(DomainPermissionView, FormMixin): """Redirect to the your contact information for the domain.""" return reverse("domain-your-contact-information", kwargs={"pk": self.object.pk}) - def post(self, request, *args, **kwargs): - """Form submission posts to this view.""" - self.object = self.get_object() - form = self.get_form() - if form.is_valid(): - # there is a valid email address in the form - return self.form_valid(form) - else: - return self.form_invalid(form) - def form_valid(self, form): """The form is valid, call setter in model.""" @@ -265,7 +289,7 @@ class DomainYourContactInformationView(DomainPermissionView, FormMixin): return super().form_valid(form) -class DomainSecurityEmailView(DomainPermissionView, FormMixin): +class DomainSecurityEmailView(DomainFormBaseView): """Domain security email editing view.""" @@ -287,16 +311,6 @@ class DomainSecurityEmailView(DomainPermissionView, FormMixin): """Redirect to the security email page for the domain.""" return reverse("domain-security-email", kwargs={"pk": self.object.pk}) - def post(self, request, *args, **kwargs): - """Form submission posts to this view.""" - self.object = self.get_object() - form = self.get_form() - if form.is_valid(): - # there is a valid email address in the form - return self.form_valid(form) - else: - return self.form_invalid(form) - def form_valid(self, form): """The form is valid, call setter in model.""" @@ -327,14 +341,14 @@ class DomainSecurityEmailView(DomainPermissionView, FormMixin): return redirect(self.get_success_url()) -class DomainUsersView(DomainPermissionView): +class DomainUsersView(DomainBaseView): """User management page in the domain details.""" template_name = "domain_users.html" -class DomainAddUserView(DomainPermissionView, FormMixin): +class DomainAddUserView(DomainFormBaseView): """Inside of a domain's user management, a form for adding users. @@ -348,15 +362,6 @@ class DomainAddUserView(DomainPermissionView, FormMixin): def get_success_url(self): return reverse("domain-users", kwargs={"pk": self.object.pk}) - def post(self, request, *args, **kwargs): - self.object = self.get_object() - form = self.get_form() - if form.is_valid(): - # there is a valid email address in the form - return self.form_valid(form) - else: - return self.form_invalid(form) - def _domain_abs_url(self): """Get an absolute URL for this domain.""" return self.request.build_absolute_uri( From 5b4103e1eed42c5662ee78018af6d911a9b71318 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Tue, 17 Oct 2023 08:43:48 -0400 Subject: [PATCH 24/67] more debugging of domain model --- src/registrar/models/domain.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 822451a49..05cae5add 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -1736,6 +1736,7 @@ class Domain(TimeStampedModel, DomainHelper): ) if property in self._cache: + logger.info("writing %s to cache", property) return self._cache[property] else: raise KeyError( From 3ed4c0e4fbbbfdf5b69d7a6d4c1af0d5ec90a61c Mon Sep 17 00:00:00 2001 From: Erin <121973038+erinysong@users.noreply.github.com> Date: Tue, 17 Oct 2023 08:37:03 -0700 Subject: [PATCH 25/67] Update availability API to use EPP availability check --- src/! | 1 + src/api/tests/test_available.py | 78 +++++++++++++++++++++++-------- src/api/views.py | 6 +-- src/registrar/tests/common.py | 37 ++++++++++++++- src/registrar/tests/test_forms.py | 11 ++++- 5 files changed, 108 insertions(+), 25 deletions(-) create mode 100644 src/! diff --git a/src/! b/src/! new file mode 100644 index 000000000..19765bd50 --- /dev/null +++ b/src/! @@ -0,0 +1 @@ +null diff --git a/src/api/tests/test_available.py b/src/api/tests/test_available.py index 0bbe01f03..3d01228c3 100644 --- a/src/api/tests/test_available.py +++ b/src/api/tests/test_available.py @@ -7,21 +7,34 @@ from django.test import TestCase, RequestFactory from ..views import available, _domains, in_domains from .common import less_console_noise +from registrar.tests.common import MockEppLib +from unittest.mock import MagicMock, patch, call + +from epplibwrapper import ( + commands, + common, + extensions, + responses, + RegistryError, + ErrorCode, +) API_BASE_PATH = "/api/v1/available/" +from registrar.models import Domain - -class AvailableViewTest(TestCase): +class AvailableViewTest(MockEppLib): """Test that the view function works as expected.""" def setUp(self): + super().setUp() self.user = get_user_model().objects.create(username="username") self.factory = RequestFactory() def test_view_function(self): request = self.factory.get(API_BASE_PATH + "test.gov") request.user = self.user + response = available(request, domain="test.gov") # has the right text in it self.assertContains(response, "available") @@ -29,28 +42,43 @@ class AvailableViewTest(TestCase): response_object = json.loads(response.content) self.assertIn("available", response_object) - def test_domain_list(self): - """Test the domain list that is returned from Github. + def test_makes_calls(self): + gsa_available = in_domains("gsa.gov") + igorville_available = in_domains("igorvilleremixed.gov") - This does not mock out the external file, it is actually fetched from - the internet. - """ - domains = _domains() - self.assertIn("gsa.gov", domains) - # entries are all lowercase so GSA.GOV is not in the set - self.assertNotIn("GSA.GOV", domains) - self.assertNotIn("igorvilleremixed.gov", domains) - # all the entries have dots - self.assertNotIn("gsa", domains) + self.mockedSendFunction.assert_has_calls( + [ + call( + commands.CheckDomain( + ["gsa.gov"], + ), + cleaned=True, + ), + call( + commands.CheckDomain( + ["igorvilleremixed.gov"], + ), + cleaned=True, + ) + ] + ) def test_in_domains(self): - self.assertTrue(in_domains("gsa.gov")) + gsa_available = in_domains("gsa.gov") + gsa_caps_available = in_domains("GSA.gov") + igorville_available = in_domains("igorvilleremixed.gov") + + self.assertTrue(gsa_available) # input is lowercased so GSA.GOV should be found - self.assertTrue(in_domains("GSA.GOV")) + self.assertTrue(gsa_caps_available) # This domain should not have been registered - self.assertFalse(in_domains("igorvilleremixed.gov")) - + self.assertFalse(igorville_available) + def test_in_domains_dotgov(self): + gsa_available = in_domains("gsa.gov") + gsa_caps_available = in_domains("GSA.gov") + igorville_available = in_domains("igorvilleremixed.gov") + """Domain searches work without trailing .gov""" self.assertTrue(in_domains("gsa")) # input is lowercased so GSA.GOV should be found @@ -58,6 +86,14 @@ class AvailableViewTest(TestCase): # This domain should not have been registered self.assertFalse(in_domains("igorvilleremixed")) + def test_in_domains_capitalized(self): + gsa_available = in_domains("gsa.gov") + capitalized_gsa_available = in_domains("GSA.gov") + + """Domain searches work without case sensitivity""" + self.assertTrue(in_domains("gsa.gov")) + self.assertTrue(in_domains("GSA.gov")) + def test_not_available_domain(self): """gsa.gov is not available""" request = self.factory.get(API_BASE_PATH + "gsa.gov") @@ -86,13 +122,17 @@ class AvailableViewTest(TestCase): request.user = self.user response = available(request, domain=bad_string) self.assertFalse(json.loads(response.content)["available"]) + # domain set to raise error successfully raises error + with self.assertRaises(RegistryError): + error_domain_available = available(request, "errordomain.gov") -class AvailableAPITest(TestCase): +class AvailableAPITest(MockEppLib): """Test that the API can be called as expected.""" def setUp(self): + super().setUp() self.user = get_user_model().objects.create(username="username") def test_available_get(self): diff --git a/src/api/views.py b/src/api/views.py index e19e060ef..02e419a91 100644 --- a/src/api/views.py +++ b/src/api/views.py @@ -59,12 +59,12 @@ def in_domains(domain): given domain doesn't end with .gov, ".gov" is added when looking for a match. """ - domain = domain.lower() + Domain = apps.get_model("registrar.Domain") if domain.endswith(".gov"): - return domain.lower() in _domains() + return Domain.available(domain) else: # domain search string doesn't end with .gov, add it on here - return (domain + ".gov") in _domains() + return Domain.available(domain + ".gov") @require_http_methods(["GET"]) diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index b8fea7f93..0144738e2 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -30,6 +30,7 @@ from epplibwrapper import ( info, RegistryError, ErrorCode, + responses, ) logger = logging.getLogger(__name__) @@ -824,7 +825,41 @@ class MockEppLib(TestCase): raise RegistryError( code=ErrorCode.OBJECT_ASSOCIATION_PROHIBITS_OPERATION ) - + elif isinstance(_request, commands.CheckDomain): + if "gsa.gov" in getattr(_request, "names", None): + return MagicMock( + res_data=[ + responses.check.CheckDomainResultData( + name="gsa.gov", avail=True, reason=None + ), + ] + ) + elif "GSA.gov" in getattr(_request, "names", None): + return MagicMock( + res_data=[ + responses.check.CheckDomainResultData( + name="GSA.gov", avail=True, reason=None + ), + ] + ) + elif "igorvilleremixed.gov" in getattr(_request, "names", None): + return MagicMock( + res_data=[ + responses.check.CheckDomainResultData( + name="igorvilleremixed.gov", avail=False, reason=None + ), + ] + ) + elif "errordomain.gov" in getattr(_request, "names", None): + raise RegistryError("Registry cannot find domain availability.") + else: + return MagicMock( + res_data=[ + responses.check.CheckDomainResultData( + name="domainnotfound.gov", avail=False, reason="In Use" + ) + ], + ) return MagicMock(res_data=[self.mockDataInfoHosts]) def setUp(self): diff --git a/src/registrar/tests/test_forms.py b/src/registrar/tests/test_forms.py index 95be195ba..4b1aeb12c 100644 --- a/src/registrar/tests/test_forms.py +++ b/src/registrar/tests/test_forms.py @@ -1,6 +1,6 @@ """Test form validation requirements.""" -from django.test import TestCase +from django.test import TestCase, RequestFactory from registrar.forms.application_wizard import ( CurrentSitesForm, @@ -16,9 +16,16 @@ from registrar.forms.application_wizard import ( AboutYourOrganizationForm, ) from registrar.forms.domain import ContactForm +from registrar.tests.common import MockEppLib +from django.contrib.auth import get_user_model -class TestFormValidation(TestCase): +class TestFormValidation(MockEppLib): + def setUp(self): + super().setUp() + self.user = get_user_model().objects.create(username="username") + self.factory = RequestFactory() + def test_org_contact_zip_invalid(self): form = OrganizationContactForm(data={"zipcode": "nah"}) self.assertEqual( From dcaaa7099bd8ec1bdb22a18bae46b7e69819974e Mon Sep 17 00:00:00 2001 From: Erin <121973038+erinysong@users.noreply.github.com> Date: Tue, 17 Oct 2023 08:53:04 -0700 Subject: [PATCH 26/67] Delete unknown ! file added --- src/! | 1 - 1 file changed, 1 deletion(-) delete mode 100644 src/! diff --git a/src/! b/src/! deleted file mode 100644 index 19765bd50..000000000 --- a/src/! +++ /dev/null @@ -1 +0,0 @@ -null From bcbf0699241c95d9634d5a8c3959b1cf1b1f4692 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Tue, 17 Oct 2023 12:24:52 -0400 Subject: [PATCH 27/67] added more logging; set hosts properly in cache when no hosts exist --- src/registrar/models/domain.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 05cae5add..8b8c8b7ce 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -1450,8 +1450,10 @@ class Domain(TimeStampedModel, DomainHelper): def _fetch_hosts(self, host_data): """Fetch host info.""" + logger.info("calling _fetch_hosts on %s hosts", len(host_data)) hosts = [] for name in host_data: + logger.info("calling InfoHost on %s", name) req = commands.InfoHost(name=name) data = registry.send(req, cleaned=True).res_data[0] host = { @@ -1463,6 +1465,7 @@ class Domain(TimeStampedModel, DomainHelper): "up_date": getattr(data, "up_date", ...), } hosts.append({k: v for k, v in host.items() if v is not ...}) + logger.info("successfully called InfoHost on host_data, and have %s hosts to set to cache", len(hosts)) return hosts def _convert_ips(self, ip_list: list[str]): @@ -1593,6 +1596,8 @@ class Domain(TimeStampedModel, DomainHelper): def _fetch_cache(self, fetch_hosts=False, fetch_contacts=False): logger.info("fetch_cache called") + logger.info("fetch_hosts = %s", fetch_hosts) + logger.info("fetch_contacts = %s", fetch_contacts) """Contact registry for info about a domain.""" try: # get info from registry @@ -1629,6 +1634,7 @@ class Domain(TimeStampedModel, DomainHelper): cleaned["dnssecdata"] = extension # Capture and store old hosts and contacts from cache if they exist old_cache_hosts = self._cache.get("hosts") + logger.info("old_cache_hosts is %s", old_cache_hosts) old_cache_contacts = self._cache.get("contacts") # get contact info, if there are any @@ -1643,22 +1649,30 @@ class Domain(TimeStampedModel, DomainHelper): # hosts that existed in cache (if they existed) # and pass them along. if old_cache_hosts is not None: + logger.debug("resetting cleaned['hosts'] to old_cache_hosts") cleaned["hosts"] = old_cache_hosts # get nameserver info, if there are any if ( fetch_hosts - and "_hosts" in cleaned - and isinstance(cleaned["_hosts"], list) - and len(cleaned["_hosts"]) ): - cleaned["hosts"] = self._fetch_hosts(cleaned["_hosts"]) + if ( + "_hosts" in cleaned + and isinstance(cleaned["_hosts"], list) + and len(cleaned["_hosts"]) + ): + cleaned["hosts"] = self._fetch_hosts(cleaned["_hosts"]) + else: + cleaned["hosts"] = [] # We're only getting hosts, so retain the old # contacts that existed in cache (if they existed) # and pass them along. + logger.info("set cleaned['hosts'] to %s", cleaned["hosts"]) if old_cache_contacts is not None: + logger.info("resetting cleaned['contacts'] to old_cache_contacts") cleaned["contacts"] = old_cache_contacts # replace the prior cache with new data + logger.info("replacing the prior cache with new data") self._cache = cleaned except RegistryError as e: @@ -1729,6 +1743,7 @@ class Domain(TimeStampedModel, DomainHelper): def _get_property(self, property): """Get some piece of info about a domain.""" + logger.info("__get_property(%s)", property) if property not in self._cache: self._fetch_cache( fetch_hosts=(property == "hosts"), From 0e5978011dec06809c982396fd981666227da2c8 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Tue, 17 Oct 2023 14:23:42 -0400 Subject: [PATCH 28/67] Error handling on the post for security contact --- src/epplibwrapper/__init__.py | 4 ++- src/epplibwrapper/errors.py | 3 ++ src/registrar/models/domain.py | 2 +- src/registrar/tests/common.py | 16 +++++++++ src/registrar/tests/test_views.py | 60 +++++++++++++++++++++++++++++++ src/registrar/views/domain.py | 30 +++++++++++++--- 6 files changed, 108 insertions(+), 7 deletions(-) diff --git a/src/epplibwrapper/__init__.py b/src/epplibwrapper/__init__.py index dd6664a3a..d0138d73c 100644 --- a/src/epplibwrapper/__init__.py +++ b/src/epplibwrapper/__init__.py @@ -45,7 +45,7 @@ except NameError: # Attn: these imports should NOT be at the top of the file try: from .client import CLIENT, commands - from .errors import RegistryError, ErrorCode + from .errors import RegistryError, ErrorCode, CANNOT_CONTACT_REGISTRY, GENERIC_ERROR from epplib.models import common, info from epplib.responses import extensions from epplib import responses @@ -61,4 +61,6 @@ __all__ = [ "info", "ErrorCode", "RegistryError", + "CANNOT_CONTACT_REGISTRY", + "GENERIC_ERROR", ] diff --git a/src/epplibwrapper/errors.py b/src/epplibwrapper/errors.py index d34ed5e91..dba5f328c 100644 --- a/src/epplibwrapper/errors.py +++ b/src/epplibwrapper/errors.py @@ -1,5 +1,8 @@ from enum import IntEnum +CANNOT_CONTACT_REGISTRY = "Update failed. Cannot contact the registry." +GENERIC_ERROR = "Value entered was wrong." + class ErrorCode(IntEnum): """ diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index d6dd5e287..fa3ff443c 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -701,7 +701,7 @@ class Domain(TimeStampedModel, DomainHelper): and errorCode != ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY ): # TODO- ticket #433 look here for error handling - raise Exception("Unable to add contact to registry") + raise RegistryError(code=errorCode) # contact doesn't exist on the domain yet logger.info("_set_singleton_contact()-> contact has been added to the registry") diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index b8fea7f93..803c2f069 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -32,6 +32,8 @@ from epplibwrapper import ( ErrorCode, ) +from registrar.models.utility.contact_error import ContactError, ContactErrorCodes + logger = logging.getLogger(__name__) @@ -794,6 +796,20 @@ class MockEppLib(TestCase): # use this for when a contact is being updated # sets the second send() to fail raise RegistryError(code=ErrorCode.OBJECT_EXISTS) + elif ( + isinstance(_request, commands.CreateContact) + and getattr(_request, "email", None) == "test@failCreate.gov" + ): + # use this for when a contact is being updated + # mocks a registry error on creation + raise RegistryError(code=None) + elif ( + isinstance(_request, commands.CreateContact) + and getattr(_request, "email", None) == "test@contactError.gov" + ): + # use this for when a contact is being updated + # mocks a registry error on creation + raise ContactError(code=ContactErrorCodes.CONTACT_TYPE_NONE) elif isinstance(_request, commands.CreateHost): return MagicMock( res_data=[self.mockDataHostChange], diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 2194b42db..bda23546b 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -1490,6 +1490,66 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest, MockEppLib): success_page, "The security email for this domain has been updated" ) + def test_security_email_form_messages(self): + """ + Test against the success and error messages that are defined in the view + """ + p = "adminpass" + self.client.login(username="superuser", password=p) + + form_data_registry_error = { + "security_email": "test@failCreate.gov", + } + + form_data_contact_error = { + "security_email": "test@contactError.gov", + } + + form_data_success = { + "security_email": "test@something.gov", + } + + test_cases = [ + ( + "RegistryError", + form_data_registry_error, + "Update failed. Cannot contact the registry.", + ), + ("ContactError", form_data_contact_error, "Value entered was wrong."), + ( + "RegistrySuccess", + form_data_success, + "The security email for this domain has been updated.", + ), + # Add more test cases with different scenarios here + ] + + for test_name, data, expected_message in test_cases: + response = self.client.post( + reverse("domain-security-email", kwargs={"pk": self.domain.id}), + data=data, + follow=True, + ) + + # Check the response status code, content, or any other relevant assertions + self.assertEqual(response.status_code, 200) + + # Check if the expected message tag is set + if test_name == "RegistryError" or test_name == "ContactError": + message_tag = "error" + elif test_name == "RegistrySuccess": + message_tag = "success" + else: + # Handle other cases if needed + message_tag = "info" # Change to the appropriate default + + # Check the message tag + messages = list(response.context["messages"]) + self.assertEqual(len(messages), 1) + message = messages[0] + self.assertEqual(message.tags, message_tag) + self.assertEqual(message.message, expected_message) + def test_domain_overview_blocked_for_ineligible_user(self): """We could easily duplicate this test for all domain management views, but a single url test should be solid enough since all domain diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 4ea3d2fbc..e993a7c1a 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -22,6 +22,7 @@ from registrar.models import ( UserDomainRole, ) from registrar.models.public_contact import PublicContact +from registrar.models.utility.contact_error import ContactError from ..forms import ( ContactForm, @@ -30,6 +31,13 @@ from ..forms import ( DomainSecurityEmailForm, NameserverFormset, ) + +from epplibwrapper import ( + RegistryError, + CANNOT_CONTACT_REGISTRY, + GENERIC_ERROR, +) + from ..utility.email import send_templated_email, EmailSendingError from .utility import DomainPermissionView, DomainInvitationPermissionDeleteView @@ -310,15 +318,27 @@ class DomainSecurityEmailView(DomainPermissionView, FormMixin): # If no default is created for security_contact, # then we cannot connect to the registry. if contact is None: - messages.error(self.request, "Update failed. Cannot contact the registry.") + messages.error(self.request, CANNOT_CONTACT_REGISTRY) return redirect(self.get_success_url()) contact.email = new_email - contact.save() - messages.success( - self.request, "The security email for this domain has been updated." - ) + try: + contact.save() + except RegistryError as Err: + if Err.is_connection_error(): + messages.error(self.request, CANNOT_CONTACT_REGISTRY) + logger.error(f"Registry connection error: {Err}") + else: + messages.error(self.request, GENERIC_ERROR) + logger.error(f"Registry error: {Err}") + except ContactError as Err: + messages.error(self.request, GENERIC_ERROR) + logger.error(f"Generic registry error: {Err}") + else: + messages.success( + self.request, "The security email for this domain has been updated." + ) # superclass has the redirect return redirect(self.get_success_url()) From 7ed916a86489c295b50e6f1cda06ac4e7bc7c6e1 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Tue, 17 Oct 2023 14:56:50 -0400 Subject: [PATCH 29/67] lint --- src/registrar/tests/common.py | 42 +++++++++++++++---------------- src/registrar/tests/test_views.py | 1 + 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index f73082ab4..a9f38db03 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -795,28 +795,8 @@ class MockEppLib(TestCase): return self.mockInfoContactCommands(_request, cleaned) elif isinstance(_request, commands.UpdateDomain): return self.mockUpdateDomainCommands(_request, cleaned) - elif ( - isinstance(_request, commands.CreateContact) - and getattr(_request, "id", None) == "fail" - and self.mockedSendFunction.call_count == 3 - ): - # use this for when a contact is being updated - # sets the second send() to fail - raise RegistryError(code=ErrorCode.OBJECT_EXISTS) - elif ( - isinstance(_request, commands.CreateContact) - and getattr(_request, "email", None) == "test@failCreate.gov" - ): - # use this for when a contact is being updated - # mocks a registry error on creation - raise RegistryError(code=None) - elif ( - isinstance(_request, commands.CreateContact) - and getattr(_request, "email", None) == "test@contactError.gov" - ): - # use this for when a contact is being updated - # mocks a registry error on creation - raise ContactError(code=ContactErrorCodes.CONTACT_TYPE_NONE) + elif isinstance(_request, commands.CreateContact): + return self.mockCreateContactCommands(_request, cleaned) elif isinstance(_request, commands.CreateHost): return MagicMock( res_data=[self.mockDataHostChange], @@ -913,6 +893,24 @@ class MockEppLib(TestCase): return MagicMock(res_data=[mocked_result]) + def mockCreateContactCommands(self, _request, cleaned): + if ( + getattr(_request, "id", None) == "fail" + and self.mockedSendFunction.call_count == 3 + ): + # use this for when a contact is being updated + # sets the second send() to fail + raise RegistryError(code=ErrorCode.OBJECT_EXISTS) + elif getattr(_request, "email", None) == "test@failCreate.gov": + # use this for when a contact is being updated + # mocks a registry error on creation + raise RegistryError(code=None) + elif getattr(_request, "email", None) == "test@contactError.gov": + # use this for when a contact is being updated + # mocks a contact error on creation + raise ContactError(code=ContactErrorCodes.CONTACT_TYPE_NONE) + return MagicMock(res_data=[self.mockDataInfoHosts]) + def setUp(self): """mock epp send function as this will fail locally""" self.mockSendPatch = patch("registrar.models.domain.registry.send") diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index ce901626b..0e8f895af 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -1626,6 +1626,7 @@ class TestDomainSecurityEmail(TestDomainOverview): response = self.client.get(reverse("domain", kwargs={"pk": self.domain.id})) self.assertEqual(response.status_code, 403) + class TestDomainDNSSEC(TestDomainOverview): """MockEPPLib is already inherited.""" From 981af109dff15247c62144d779f8dd0838544b5e Mon Sep 17 00:00:00 2001 From: Erin <121973038+erinysong@users.noreply.github.com> Date: Tue, 17 Oct 2023 14:00:34 -0700 Subject: [PATCH 30/67] Fix subset of linter errors --- src/api/tests/test_available.py | 50 ++++++++++------------------- src/registrar/tests/common.py | 57 +++++++++++++-------------------- 2 files changed, 40 insertions(+), 67 deletions(-) diff --git a/src/api/tests/test_available.py b/src/api/tests/test_available.py index 3d01228c3..9eab17bf7 100644 --- a/src/api/tests/test_available.py +++ b/src/api/tests/test_available.py @@ -3,24 +3,20 @@ import json from django.contrib.auth import get_user_model -from django.test import TestCase, RequestFactory +from django.test import RequestFactory -from ..views import available, _domains, in_domains +from ..views import available, in_domains from .common import less_console_noise from registrar.tests.common import MockEppLib -from unittest.mock import MagicMock, patch, call +from unittest.mock import call from epplibwrapper import ( commands, - common, - extensions, - responses, RegistryError, - ErrorCode, ) API_BASE_PATH = "/api/v1/available/" -from registrar.models import Domain + class AvailableViewTest(MockEppLib): @@ -34,7 +30,6 @@ class AvailableViewTest(MockEppLib): def test_view_function(self): request = self.factory.get(API_BASE_PATH + "test.gov") request.user = self.user - response = available(request, domain="test.gov") # has the right text in it self.assertContains(response, "available") @@ -42,10 +37,12 @@ class AvailableViewTest(MockEppLib): response_object = json.loads(response.content) self.assertIn("available", response_object) - def test_makes_calls(self): + def test_in_domains_makes_calls_(self): + """Domain searches successfully make correct mock EPP calls""" gsa_available = in_domains("gsa.gov") igorville_available = in_domains("igorvilleremixed.gov") + """Domain searches successfully make mock EPP calls""" self.mockedSendFunction.assert_has_calls( [ call( @@ -59,26 +56,20 @@ class AvailableViewTest(MockEppLib): ["igorvilleremixed.gov"], ), cleaned=True, - ) + ), ] ) - - def test_in_domains(self): - gsa_available = in_domains("gsa.gov") - gsa_caps_available = in_domains("GSA.gov") - igorville_available = in_domains("igorvilleremixed.gov") - + """Domain searches return correct availability results""" self.assertTrue(gsa_available) - # input is lowercased so GSA.GOV should be found - self.assertTrue(gsa_caps_available) - # This domain should not have been registered self.assertFalse(igorville_available) - - def test_in_domains_dotgov(self): - gsa_available = in_domains("gsa.gov") - gsa_caps_available = in_domains("GSA.gov") - igorville_available = in_domains("igorvilleremixed.gov") + def test_in_domains_capitalized(self): + """Domain searches work without case sensitivity""" + self.assertTrue(in_domains("gsa.gov")) + # input is lowercased so GSA.GOV should be found + self.assertTrue(in_domains("GSA.gov")) + + def test_in_domains_dotgov(self): """Domain searches work without trailing .gov""" self.assertTrue(in_domains("gsa")) # input is lowercased so GSA.GOV should be found @@ -86,14 +77,6 @@ class AvailableViewTest(MockEppLib): # This domain should not have been registered self.assertFalse(in_domains("igorvilleremixed")) - def test_in_domains_capitalized(self): - gsa_available = in_domains("gsa.gov") - capitalized_gsa_available = in_domains("GSA.gov") - - """Domain searches work without case sensitivity""" - self.assertTrue(in_domains("gsa.gov")) - self.assertTrue(in_domains("GSA.gov")) - def test_not_available_domain(self): """gsa.gov is not available""" request = self.factory.get(API_BASE_PATH + "gsa.gov") @@ -125,6 +108,7 @@ class AvailableViewTest(MockEppLib): # domain set to raise error successfully raises error with self.assertRaises(RegistryError): error_domain_available = available(request, "errordomain.gov") + self.assertFalse(json.loads(error_domain_available.content)["available"]) class AvailableAPITest(MockEppLib): diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index 0144738e2..32117e2db 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -761,6 +761,28 @@ class MockEppLib(TestCase): return MagicMock(res_data=[self.infoDomainThreeHosts]) return MagicMock(res_data=[self.mockDataInfoDomain]) + def _mockDomainName(self, _name, _avail=False): + return MagicMock( + res_data=[ + responses.check.CheckDomainResultData( + name=_name, avail=_avail, reason=None + ), + ] + ) + + def _handleCheckDomain(self, _request): + print(getattr(_request, "names", None)) + if "gsa.gov" in getattr(_request, "names", None): + return self._mockDomainName("gsa.gov", True) + elif "GSA.gov" in getattr(_request, "names", None): + return self._mockDomainName("GSA.gov", True) + elif "igorvilleremixed.gov" in getattr(_request, "names", None): + return self._mockDomainName("igorvilleremixed.gov", False) + elif "errordomain.gov" in getattr(_request, "names", None): + raise RegistryError("Registry cannot find domain availability.") + else: + return self._mockDomainName("domainnotfound.gov", False) + def mockSend(self, _request, cleaned): """Mocks the registry.send function used inside of domain.py registry is imported from epplibwrapper @@ -826,40 +848,7 @@ class MockEppLib(TestCase): code=ErrorCode.OBJECT_ASSOCIATION_PROHIBITS_OPERATION ) elif isinstance(_request, commands.CheckDomain): - if "gsa.gov" in getattr(_request, "names", None): - return MagicMock( - res_data=[ - responses.check.CheckDomainResultData( - name="gsa.gov", avail=True, reason=None - ), - ] - ) - elif "GSA.gov" in getattr(_request, "names", None): - return MagicMock( - res_data=[ - responses.check.CheckDomainResultData( - name="GSA.gov", avail=True, reason=None - ), - ] - ) - elif "igorvilleremixed.gov" in getattr(_request, "names", None): - return MagicMock( - res_data=[ - responses.check.CheckDomainResultData( - name="igorvilleremixed.gov", avail=False, reason=None - ), - ] - ) - elif "errordomain.gov" in getattr(_request, "names", None): - raise RegistryError("Registry cannot find domain availability.") - else: - return MagicMock( - res_data=[ - responses.check.CheckDomainResultData( - name="domainnotfound.gov", avail=False, reason="In Use" - ) - ], - ) + return self._handleCheckDomain(_request) return MagicMock(res_data=[self.mockDataInfoHosts]) def setUp(self): From 34294782c3a887f3ef1b5f99d45202bac4a59365 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Tue, 17 Oct 2023 15:24:38 -0700 Subject: [PATCH 31/67] Update Staff permissions for contacts, websites, addresses and domain information and application --- src/registrar/admin.py | 26 ++++++++++--- .../migrations/0040_create_groups_v03.py | 37 +++++++++++++++++++ src/registrar/models/user_group.py | 7 +++- 3 files changed, 63 insertions(+), 7 deletions(-) create mode 100644 src/registrar/migrations/0040_create_groups_v03.py diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 8d0ed8c2e..0533929f6 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -294,6 +294,26 @@ class ContactAdmin(ListHeaderAdmin): contact.admin_order_field = "first_name" # type: ignore + # Read only that we'll leverage for CISA Analysts + analyst_readonly_fields = [ + "user", + ] + + def get_readonly_fields(self, request, obj=None): + """Set the read-only state on form elements. + We have 1 conditions that determine which fields are read-only: + admin user permissions. + """ + + readonly_fields = list(self.readonly_fields) + + if request.user.has_perm("registrar.full_access_permission"): + return readonly_fields + # Return restrictive Read-only fields for analysts and + # users who might not belong to groups + readonly_fields.extend([field for field in self.analyst_readonly_fields]) + return readonly_fields # Read-only fields for analysts + class WebsiteAdmin(ListHeaderAdmin): """Custom website admin class.""" @@ -420,9 +440,6 @@ class DomainInformationAdmin(ListHeaderAdmin): "creator", "type_of_work", "more_organization_information", - "address_line1", - "address_line2", - "zipcode", "domain", "submitter", "no_other_contacts_rationale", @@ -557,9 +574,6 @@ class DomainApplicationAdmin(ListHeaderAdmin): analyst_readonly_fields = [ "creator", "about_your_organization", - "address_line1", - "address_line2", - "zipcode", "requested_domain", "alternative_domains", "purpose", diff --git a/src/registrar/migrations/0040_create_groups_v03.py b/src/registrar/migrations/0040_create_groups_v03.py new file mode 100644 index 000000000..6885b9dfc --- /dev/null +++ b/src/registrar/migrations/0040_create_groups_v03.py @@ -0,0 +1,37 @@ +# This migration creates the create_full_access_group and create_cisa_analyst_group groups +# It is dependent on 0035 (which populates ContentType and Permissions) +# If permissions on the groups need changing, edit CISA_ANALYST_GROUP_PERMISSIONS +# in the user_group model then: +# [NOT RECOMMENDED] +# step 1: docker-compose exec app ./manage.py migrate --fake registrar 0035_contenttypes_permissions +# step 2: docker-compose exec app ./manage.py migrate registrar 0036_create_groups +# step 3: fake run the latest migration in the migrations list +# [RECOMMENDED] +# Alternatively: +# step 1: duplicate the migration that loads data +# step 2: docker-compose exec app ./manage.py migrate + +from django.db import migrations +from registrar.models import UserGroup +from typing import Any + + +# For linting: RunPython expects a function reference, +# so let's give it one +def create_groups(apps, schema_editor) -> Any: + UserGroup.create_cisa_analyst_group(apps, schema_editor) + UserGroup.create_full_access_group(apps, schema_editor) + + +class Migration(migrations.Migration): + dependencies = [ + ("registrar", "0039_alter_transitiondomain_status"), + ] + + operations = [ + migrations.RunPython( + create_groups, + reverse_code=migrations.RunPython.noop, + atomic=True, + ), + ] \ No newline at end of file diff --git a/src/registrar/models/user_group.py b/src/registrar/models/user_group.py index 5cdb1f2ec..568741786 100644 --- a/src/registrar/models/user_group.py +++ b/src/registrar/models/user_group.py @@ -24,7 +24,7 @@ class UserGroup(Group): { "app_label": "registrar", "model": "contact", - "permissions": ["view_contact"], + "permissions": ["change_contact"], }, { "app_label": "registrar", @@ -56,6 +56,11 @@ class UserGroup(Group): "model": "domaininvitation", "permissions": ["add_domaininvitation", "view_domaininvitation"], }, + { + "app_label": "registrar", + "model": "website", + "permissions": ["change_website"], + }, ] # Avoid error: You can't execute queries until the end From a87ebfc287fef3f886133471a4b6bb8f37adf822 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Tue, 17 Oct 2023 15:40:11 -0700 Subject: [PATCH 32/67] Fix linter errors and tests --- src/registrar/migrations/0040_create_groups_v03.py | 2 +- src/registrar/tests/test_admin.py | 3 --- src/registrar/tests/test_migrations.py | 3 ++- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/registrar/migrations/0040_create_groups_v03.py b/src/registrar/migrations/0040_create_groups_v03.py index 6885b9dfc..cad2cadc5 100644 --- a/src/registrar/migrations/0040_create_groups_v03.py +++ b/src/registrar/migrations/0040_create_groups_v03.py @@ -34,4 +34,4 @@ class Migration(migrations.Migration): reverse_code=migrations.RunPython.noop, atomic=True, ), - ] \ No newline at end of file + ] diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 51ace34f7..7dbc8ff38 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -620,9 +620,6 @@ class TestDomainApplicationAdmin(MockEppLib): expected_fields = [ "creator", "about_your_organization", - "address_line1", - "address_line2", - "zipcode", "requested_domain", "alternative_domains", "purpose", diff --git a/src/registrar/tests/test_migrations.py b/src/registrar/tests/test_migrations.py index 95e5853ff..165ef6f71 100644 --- a/src/registrar/tests/test_migrations.py +++ b/src/registrar/tests/test_migrations.py @@ -36,7 +36,7 @@ class TestGroups(TestCase): # Define the expected permission codenames expected_permissions = [ "view_logentry", - "view_contact", + "change_contact", "view_domain", "change_domainapplication", "change_domaininformation", @@ -45,6 +45,7 @@ class TestGroups(TestCase): "change_draftdomain", "analyst_access_permission", "change_user", + "change_website", ] # Get the codenames of actual permissions associated with the group From 7d6155de1340a0dfba51b87af878200f39502c0f Mon Sep 17 00:00:00 2001 From: Erin <121973038+erinysong@users.noreply.github.com> Date: Tue, 17 Oct 2023 16:06:57 -0700 Subject: [PATCH 33/67] Refactor mockSend to match linter --- src/registrar/tests/common.py | 98 ++++++++++++++++++----------------- 1 file changed, 50 insertions(+), 48 deletions(-) diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index 32117e2db..61444d37f 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -769,9 +769,8 @@ class MockEppLib(TestCase): ), ] ) - + def _handleCheckDomain(self, _request): - print(getattr(_request, "names", None)) if "gsa.gov" in getattr(_request, "names", None): return self._mockDomainName("gsa.gov", True) elif "GSA.gov" in getattr(_request, "names", None): @@ -788,28 +787,8 @@ class MockEppLib(TestCase): registry is imported from epplibwrapper returns objects that simulate what would be in a epp response but only relevant pieces for tests""" - if isinstance(_request, commands.InfoDomain): - return self._getattrInfoDomain(_request) - - elif isinstance(_request, commands.InfoContact): - mocked_result: info.InfoContactResultData - - # For testing contact types - match getattr(_request, "id", None): - case "securityContact": - mocked_result = self.mockSecurityContact - case "technicalContact": - mocked_result = self.mockTechnicalContact - case "adminContact": - mocked_result = self.mockAdministrativeContact - case "regContact": - mocked_result = self.mockRegistrantContact - case _: - # Default contact return - mocked_result = self.mockDataInfoContact - - return MagicMock(res_data=[mocked_result]) - elif ( + print(type(_request) == commands.CheckDomain) + if ( isinstance(_request, commands.CreateContact) and getattr(_request, "id", None) == "fail" and self.mockedSendFunction.call_count == 3 @@ -817,27 +796,8 @@ class MockEppLib(TestCase): # use this for when a contact is being updated # sets the second send() to fail raise RegistryError(code=ErrorCode.OBJECT_EXISTS) - elif isinstance(_request, commands.CreateHost): - return MagicMock( - res_data=[self.mockDataHostChange], - code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY, - ) - elif isinstance(_request, commands.UpdateHost): - return MagicMock( - res_data=[self.mockDataHostChange], - code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY, - ) - elif isinstance(_request, commands.UpdateDomain): - return MagicMock( - res_data=[self.mockDataHostChange], - code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY, - ) - elif isinstance(_request, commands.DeleteHost): - return MagicMock( - res_data=[self.mockDataHostChange], - code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY, - ) - elif ( + + if ( isinstance(_request, commands.DeleteDomain) and getattr(_request, "name", None) == "failDelete.gov" ): @@ -847,9 +807,51 @@ class MockEppLib(TestCase): raise RegistryError( code=ErrorCode.OBJECT_ASSOCIATION_PROHIBITS_OPERATION ) - elif isinstance(_request, commands.CheckDomain): - return self._handleCheckDomain(_request) - return MagicMock(res_data=[self.mockDataInfoHosts]) + + match type(_request): + case commands.InfoDomain: + return self._getattrInfoDomain(_request) + case commands.InfoContact: + mocked_result: info.InfoContactResultData + + # For testing contact types + match getattr(_request, "id", None): + case "securityContact": + mocked_result = self.mockSecurityContact + case "technicalContact": + mocked_result = self.mockTechnicalContact + case "adminContact": + mocked_result = self.mockAdministrativeContact + case "regContact": + mocked_result = self.mockRegistrantContact + case _: + # Default contact return + mocked_result = self.mockDataInfoContact + return MagicMock(res_data=[mocked_result]) + case commands.CreateHost: + return MagicMock( + res_data=[self.mockDataHostChange], + code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY, + ) + case commands.UpdateHost: + return MagicMock( + res_data=[self.mockDataHostChange], + code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY, + ) + case commands.UpdateDomain: + return MagicMock( + res_data=[self.mockDataHostChange], + code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY, + ) + case commands.DeleteHost: + return MagicMock( + res_data=[self.mockDataHostChange], + code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY, + ) + case commands.CheckDomain: + return self._handleCheckDomain(_request) + case _: + return MagicMock(res_data=[self.mockDataInfoHosts]) def setUp(self): """mock epp send function as this will fail locally""" From e248ae7760ca27adac0a8e735e70544df8a34021 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Tue, 17 Oct 2023 21:42:43 -0400 Subject: [PATCH 34/67] remoed CACHES from settings (unnecessary); refactored _fetch_cache; added comments --- src/registrar/config/settings.py | 5 - src/registrar/models/domain.py | 154 +++++++++++-------------------- src/registrar/views/domain.py | 85 ++++++++--------- 3 files changed, 99 insertions(+), 145 deletions(-) diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index d5f2b53cf..7b96af5ee 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -169,11 +169,6 @@ WSGI_APPLICATION = "registrar.config.wsgi.application" # "BACKEND": "django.core.cache.backends.db.DatabaseCache", # } # } -CACHES = { - "default": { - "BACKEND": "django.core.cache.backends.locmem.LocMemCache", - } -} # Absolute path to the directory where `collectstatic` # will place static files for deployment. diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 408721ae4..f32da2403 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -1,6 +1,5 @@ from itertools import zip_longest import logging -import inspect import ipaddress import re from datetime import date @@ -62,35 +61,8 @@ class Domain(TimeStampedModel, DomainHelper): def __init__(self, *args, **kwargs): self._cache = {} - self.print_calling_function() - logger.info("__init__ being called on domain") super(Domain, self).__init__(*args, **kwargs) - def print_calling_function(self): - # Get the current frame in the call stack - current_frame = inspect.currentframe() - - i = 1 - while True: - try: - # Get the calling frame - calling_frame = inspect.getouterframes(current_frame, 2)[i] - - # Extract information about the calling function - calling_function_name = calling_frame.function - calling_module_name = calling_frame[0].f_globals["__name__"] - calling_line_number = calling_frame[2] - - # Print information about the calling function - print( - f"Calling function: {calling_function_name} in module {calling_module_name} at line {calling_line_number}" - ) - - i += 1 - except Exception as err: - print("========================================================") - break - class Status(models.TextChoices): """ The status codes we can receive from the registry. @@ -183,12 +155,10 @@ class Domain(TimeStampedModel, DomainHelper): def __get__(self, obj, objtype=None): """Called during get. Example: `r = domain.registrant`.""" - logger.info("domain __get__ is called: %s: %s", obj, objtype) return super().__get__(obj, objtype) def __set__(self, obj, value): """Called during set. Example: `domain.registrant = 'abc123'`.""" - logger.info("domain __set__ is called: %s", obj) super().__set__(obj, value) # always invalidate cache after sending updates to the registry obj._invalidate_cache() @@ -290,7 +260,6 @@ class Domain(TimeStampedModel, DomainHelper): """Creates the host object in the registry doesn't add the created host to the domain returns ErrorCode (int)""" - logger.info("Creating host") if addrs is not None: addresses = [epp.Ip(addr=addr) for addr in addrs] request = commands.CreateHost(name=host, addrs=addresses) @@ -1275,7 +1244,6 @@ class Domain(TimeStampedModel, DomainHelper): count = 0 while not exitEarly and count < 3: try: - logger.info("Getting domain info from epp") req = commands.InfoDomain(name=self.name) domainInfoResponse = registry.send(req, cleaned=True) exitEarly = True @@ -1569,10 +1537,8 @@ class Domain(TimeStampedModel, DomainHelper): def _fetch_hosts(self, host_data): """Fetch host info.""" - logger.info("calling _fetch_hosts on %s hosts", len(host_data)) hosts = [] for name in host_data: - logger.info("calling InfoHost on %s", name) req = commands.InfoHost(name=name) data = registry.send(req, cleaned=True).res_data[0] host = { @@ -1584,10 +1550,6 @@ class Domain(TimeStampedModel, DomainHelper): "up_date": getattr(data, "up_date", ...), } hosts.append({k: v for k, v in host.items() if v is not ...}) - logger.info( - "successfully called InfoHost on host_data, and have %s hosts to set to cache", - len(hosts), - ) return hosts def _convert_ips(self, ip_list: list[str]): @@ -1717,87 +1679,85 @@ class Domain(TimeStampedModel, DomainHelper): ) def _fetch_cache(self, fetch_hosts=False, fetch_contacts=False): - logger.info("fetch_cache called") - logger.info("fetch_hosts = %s", fetch_hosts) - logger.info("fetch_contacts = %s", fetch_contacts) """Contact registry for info about a domain.""" try: # get info from registry - dataResponse = self._get_or_create_domain() - data = dataResponse.res_data[0] - # extract properties from response - # (Ellipsis is used to mean "null") - cache = { - "auth_info": getattr(data, "auth_info", ...), - "_contacts": getattr(data, "contacts", ...), - "cr_date": getattr(data, "cr_date", ...), - "ex_date": getattr(data, "ex_date", ...), - "_hosts": getattr(data, "hosts", ...), - "name": getattr(data, "name", ...), - "registrant": getattr(data, "registrant", ...), - "statuses": getattr(data, "statuses", ...), - "tr_date": getattr(data, "tr_date", ...), - "up_date": getattr(data, "up_date", ...), - } - # remove null properties (to distinguish between "a value of None" and null) - cleaned = {k: v for k, v in cache.items() if v is not ...} + data_response = self._get_or_create_domain() + cache = self._extract_data_from_response(data_response) + + # remove null properties (to distinguish between "a value of None" and null) + cleaned = self._remove_null_properties(cache) - # statuses can just be a list no need to keep the epp object if "statuses" in cleaned: cleaned["statuses"] = [status.state for status in cleaned["statuses"]] - # get extensions info, if there is any - # DNSSECExtension is one possible extension, make sure to handle - # only DNSSECExtension and not other type extensions - returned_extensions = dataResponse.extensions - cleaned["dnssecdata"] = None - for extension in returned_extensions: - if isinstance(extension, extensions.DNSSECExtension): - cleaned["dnssecdata"] = extension + cleaned["dnssecdata"] = self._get_dnssec_data(data_response.extensions) + # Capture and store old hosts and contacts from cache if they exist old_cache_hosts = self._cache.get("hosts") - logger.info("old_cache_hosts is %s", old_cache_hosts) old_cache_contacts = self._cache.get("contacts") - # get contact info, if there are any - if ( - fetch_contacts - and "_contacts" in cleaned - and isinstance(cleaned["_contacts"], list) - and len(cleaned["_contacts"]) > 0 - ): - cleaned["contacts"] = self._fetch_contacts(cleaned["_contacts"]) - # We're only getting contacts, so retain the old - # hosts that existed in cache (if they existed) - # and pass them along. + if fetch_contacts: + cleaned["contacts"] = self._get_contacts( + cleaned.get("_contacts", []) + ) if old_cache_hosts is not None: logger.debug("resetting cleaned['hosts'] to old_cache_hosts") cleaned["hosts"] = old_cache_hosts - # get nameserver info, if there are any if fetch_hosts: - if ( - "_hosts" in cleaned - and isinstance(cleaned["_hosts"], list) - and len(cleaned["_hosts"]) - ): - cleaned["hosts"] = self._fetch_hosts(cleaned["_hosts"]) - else: - cleaned["hosts"] = [] - # We're only getting hosts, so retain the old - # contacts that existed in cache (if they existed) - # and pass them along. - logger.info("set cleaned['hosts'] to %s", cleaned["hosts"]) + cleaned["hosts"] = self._get_hosts( + cleaned.get("_hosts", []) + ) if old_cache_contacts is not None: - logger.info("resetting cleaned['contacts'] to old_cache_contacts") cleaned["contacts"] = old_cache_contacts - # replace the prior cache with new data - logger.info("replacing the prior cache with new data") + self._cache = cleaned except RegistryError as e: logger.error(e) + def _extract_data_from_response(self, data_response): + data = data_response.res_data[0] + cache = { + "auth_info": getattr(data, "auth_info", ...), + "_contacts": getattr(data, "contacts", ...), + "cr_date": getattr(data, "cr_date", ...), + "ex_date": getattr(data, "ex_date", ...), + "_hosts": getattr(data, "hosts", ...), + "name": getattr(data, "name", ...), + "registrant": getattr(data, "registrant", ...), + "statuses": getattr(data, "statuses", ...), + "tr_date": getattr(data, "tr_date", ...), + "up_date": getattr(data, "up_date", ...), + } + return {k: v for k, v in cache.items() if v is not ...} + + def _remove_null_properties(self, cache): + return {k: v for k, v in cache.items() if v is not ...} + + def _get_dnssec_data(self, response_extensions): + # get extensions info, if there is any + # DNSSECExtension is one possible extension, make sure to handle + # only DNSSECExtension and not other type extensions + dnssec_data = None + for extension in response_extensions: + if isinstance(extension, extensions.DNSSECExtension): + dnssec_data = extension + return dnssec_data + + def _get_contacts(self, contacts): + cleaned_contacts = {} + if contacts and isinstance(contacts, list) and len(contacts) > 0: + cleaned_contacts = self._fetch_contacts(contacts) + return cleaned_contacts + + def _get_hosts(self, hosts): + cleaned_hosts = [] + if hosts and isinstance(hosts, list): + cleaned_hosts = self._fetch_hosts(hosts) + return cleaned_hosts + def _get_or_create_public_contact(self, public_contact: PublicContact): """Tries to find a PublicContact object in our DB. If it can't, it'll create it. Returns PublicContact""" @@ -1863,7 +1823,6 @@ class Domain(TimeStampedModel, DomainHelper): def _get_property(self, property): """Get some piece of info about a domain.""" - logger.info("__get_property(%s)", property) if property not in self._cache: self._fetch_cache( fetch_hosts=(property == "hosts"), @@ -1871,7 +1830,6 @@ class Domain(TimeStampedModel, DomainHelper): ) if property in self._cache: - logger.info("writing %s to cache", property) return self._cache[property] else: raise KeyError( diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 5e6894bb2..fbe6a3c20 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -51,40 +51,55 @@ logger = logging.getLogger(__name__) class DomainBaseView(DomainPermissionView): + """ + Base View for the Domain. Handles getting and setting the domain + in session cache on GETs. Also provides methods for getting + and setting the domain in cache + """ + def get(self, request, *args, **kwargs): - logger.info("DomainBaseView::get") self._get_domain(request) context = self.get_context_data(object=self.object) return self.render_to_response(context) def _get_domain(self, request): - # get domain from session cache or from db - # and set to self.object - # set session to self for downstream functions to - # update session cache + """ + get domain from session cache or from db and set + to self.object + set session to self for downstream functions to + update session cache + """ self.session = request.session - pk = self.kwargs.get("pk") - cached_domain = self.session.get(pk) + # domain:private_key is the session key to use for + # caching the domain in the session + domain_pk = "domain:" + str(self.kwargs.get("pk")) + cached_domain = self.session.get(domain_pk) if cached_domain: - logger.info("reading object from session cache") self.object = cached_domain else: - logger.info("reading object from db") self.object = self.get_object() self._update_session_with_domain() def _update_session_with_domain(self): - pk = self.kwargs.get("pk") - logger.info("writing object to session cache") - self.session[pk] = self.object + """ + update domain in the session cache + """ + domain_pk = "domain:" + str(self.kwargs.get("pk")) + self.session[domain_pk] = self.object class DomainFormBaseView(DomainBaseView, FormMixin): + """ + Form Base View for the Domain. Handles getting and setting + domain in cache when dealing with domain forms. Provides + implementations of post, form_valid and form_invalid. + """ + def post(self, request, *args, **kwargs): """Form submission posts to this view. - This post method harmonizes using DetailView and FormMixin together. + This post method harmonizes using DomainBaseView and FormMixin """ self._get_domain(request) form = self.get_form() @@ -94,12 +109,14 @@ class DomainFormBaseView(DomainBaseView, FormMixin): return self.form_invalid(form) def form_valid(self, form): + # updates session cache with domain self._update_session_with_domain() # superclass has the redirect return super().form_valid(form) def form_invalid(self, form): + # updates session cache with domain self._update_session_with_domain() # superclass has the redirect @@ -200,11 +217,7 @@ class DomainNameserversView(DomainFormBaseView): def get_initial(self): """The initial value for the form (which is a formset here).""" - logger.info("DomainNameserversView.get_initial()") - domain = self.object - logger.info("DomainNameserversView.get_initial:: after get_object") - nameservers = domain.nameservers - logger.info("DomainNameserversView.get_initial:: after set nameservers") + nameservers = self.object.nameservers initial_data = [] if nameservers is not None: @@ -252,8 +265,7 @@ class DomainNameserversView(DomainFormBaseView): except KeyError: # no server information in this field, skip it pass - domain = self.object - domain.nameservers = nameservers + self.object.nameservers = nameservers messages.success( self.request, "The name servers for this domain have been updated." @@ -273,9 +285,7 @@ class DomainDNSSECView(DomainFormBaseView): """The initial value for the form (which is a formset here).""" context = super().get_context_data(**kwargs) - self.domain = self.object - - has_dnssec_records = self.domain.dnssecdata is not None + has_dnssec_records = self.object.dnssecdata is not None # Create HTML for the modal button modal_button = ( @@ -292,17 +302,16 @@ class DomainDNSSECView(DomainFormBaseView): def get_success_url(self): """Redirect to the DNSSEC page for the domain.""" - return reverse("domain-dns-dnssec", kwargs={"pk": self.domain.pk}) + return reverse("domain-dns-dnssec", kwargs={"pk": self.object.pk}) def post(self, request, *args, **kwargs): """Form submission posts to this view.""" self._get_domain(request) - self.domain = self.object form = self.get_form() if form.is_valid(): if "disable_dnssec" in request.POST: try: - self.domain.dnssecdata = {} + self.object.dnssecdata = {} except RegistryError as err: errmsg = "Error removing existing DNSSEC record(s)." logger.error(errmsg + ": " + err) @@ -326,8 +335,7 @@ class DomainDsDataView(DomainFormBaseView): def get_initial(self): """The initial value for the form (which is a formset here).""" - domain = self.object - dnssecdata: extensions.DNSSECExtension = domain.dnssecdata + dnssecdata: extensions.DNSSECExtension = self.object.dnssecdata initial_data = [] if dnssecdata is not None: @@ -368,8 +376,7 @@ class DomainDsDataView(DomainFormBaseView): # set the dnssec_ds_confirmed flag in the context for this view # based either on the existence of DS Data in the domain, # or on the flag stored in the session - domain = self.object - dnssecdata: extensions.DNSSECExtension = domain.dnssecdata + dnssecdata: extensions.DNSSECExtension = self.object.dnssecdata if dnssecdata is not None and dnssecdata.dsData is not None: self.request.session["dnssec_ds_confirmed"] = True @@ -421,9 +428,8 @@ class DomainDsDataView(DomainFormBaseView): # as valid; this can happen if form has been added but # not been interacted with; in that case, want to ignore pass - domain = self.object try: - domain.dnssecdata = dnssecdata + self.object.dnssecdata = dnssecdata except RegistryError as err: errmsg = "Error updating DNSSEC data in the registry." logger.error(errmsg) @@ -447,8 +453,7 @@ class DomainKeyDataView(DomainFormBaseView): def get_initial(self): """The initial value for the form (which is a formset here).""" - domain = self.object - dnssecdata: extensions.DNSSECExtension = domain.dnssecdata + dnssecdata: extensions.DNSSECExtension = self.object.dnssecdata initial_data = [] if dnssecdata is not None: @@ -489,8 +494,7 @@ class DomainKeyDataView(DomainFormBaseView): # set the dnssec_key_confirmed flag in the context for this view # based either on the existence of Key Data in the domain, # or on the flag stored in the session - domain = self.object - dnssecdata: extensions.DNSSECExtension = domain.dnssecdata + dnssecdata: extensions.DNSSECExtension = self.object.dnssecdata if dnssecdata is not None and dnssecdata.keyData is not None: self.request.session["dnssec_key_confirmed"] = True @@ -541,9 +545,8 @@ class DomainKeyDataView(DomainFormBaseView): except KeyError: # no server information in this field, skip it pass - domain = self.object try: - domain.dnssecdata = dnssecdata + self.object.dnssecdata = dnssecdata except RegistryError as err: errmsg = "Error updating DNSSEC data in the registry." logger.error(errmsg) @@ -596,9 +599,8 @@ class DomainSecurityEmailView(DomainFormBaseView): def get_initial(self): """The initial value for the form.""" - domain = self.object initial = super().get_initial() - security_contact = domain.security_contact + security_contact = self.object.security_contact if security_contact is None or security_contact.email == "dotgov@cisa.dhs.gov": initial["security_email"] = None return initial @@ -619,8 +621,7 @@ class DomainSecurityEmailView(DomainFormBaseView): if new_email is None or new_email.strip() == "": new_email = PublicContact.get_default_security().email - domain = self.object - contact = domain.security_contact + contact = self.object.security_contact # If no default is created for security_contact, # then we cannot connect to the registry. From a587920c4554e14c2a32a00db62d6f21419fb189 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Tue, 17 Oct 2023 21:59:33 -0400 Subject: [PATCH 35/67] bow down before the almighty linter --- src/registrar/models/domain.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index f32da2403..e444f2a61 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -1698,17 +1698,13 @@ class Domain(TimeStampedModel, DomainHelper): old_cache_contacts = self._cache.get("contacts") if fetch_contacts: - cleaned["contacts"] = self._get_contacts( - cleaned.get("_contacts", []) - ) + cleaned["contacts"] = self._get_contacts(cleaned.get("_contacts", [])) if old_cache_hosts is not None: logger.debug("resetting cleaned['hosts'] to old_cache_hosts") cleaned["hosts"] = old_cache_hosts if fetch_hosts: - cleaned["hosts"] = self._get_hosts( - cleaned.get("_hosts", []) - ) + cleaned["hosts"] = self._get_hosts(cleaned.get("_hosts", [])) if old_cache_contacts is not None: cleaned["contacts"] = old_cache_contacts From 2b55e40d5793cdda88bd5991a08de2b5b10d7c5a Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Wed, 18 Oct 2023 11:04:05 -0400 Subject: [PATCH 36/67] refactored _get_contacts to return default dict when no contacts exist rather then empty dict --- src/registrar/models/domain.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index e444f2a61..a212480e7 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -1743,7 +1743,14 @@ class Domain(TimeStampedModel, DomainHelper): return dnssec_data def _get_contacts(self, contacts): - cleaned_contacts = {} + choices = PublicContact.ContactTypeChoices + # We expect that all these fields get populated, + # so we can create these early, rather than waiting. + cleaned_contacts = { + choices.ADMINISTRATIVE: None, + choices.SECURITY: None, + choices.TECHNICAL: None, + } if contacts and isinstance(contacts, list) and len(contacts) > 0: cleaned_contacts = self._fetch_contacts(contacts) return cleaned_contacts From 94ac0e703d7cb4b8c5b42aa23c03ca9950078486 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Wed, 18 Oct 2023 13:28:50 -0400 Subject: [PATCH 37/67] small refactor to remove redundant code --- src/registrar/models/domain.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index a212480e7..d0e6f1edd 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -1715,7 +1715,7 @@ class Domain(TimeStampedModel, DomainHelper): def _extract_data_from_response(self, data_response): data = data_response.res_data[0] - cache = { + return { "auth_info": getattr(data, "auth_info", ...), "_contacts": getattr(data, "contacts", ...), "cr_date": getattr(data, "cr_date", ...), @@ -1727,7 +1727,6 @@ class Domain(TimeStampedModel, DomainHelper): "tr_date": getattr(data, "tr_date", ...), "up_date": getattr(data, "up_date", ...), } - return {k: v for k, v in cache.items() if v is not ...} def _remove_null_properties(self, cache): return {k: v for k, v in cache.items() if v is not ...} @@ -1821,7 +1820,6 @@ class Domain(TimeStampedModel, DomainHelper): def _invalidate_cache(self): """Remove cache data when updates are made.""" - logger.debug("_invalidate_cache called") self._cache = {} def _get_property(self, property): From 02a9c98a570f0c1d58a5d47f53e947990065a081 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Wed, 18 Oct 2023 17:49:49 -0400 Subject: [PATCH 38/67] fix org in domain table and write a unit test for it --- src/registrar/admin.py | 2 +- src/registrar/tests/test_admin.py | 25 ++++++++++++++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 88f0de869..417d23708 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -721,7 +721,7 @@ class DomainAdmin(ListHeaderAdmin): ] def organization_type(self, obj): - return obj.domain_info.organization_type + return obj.domain_info.get_organization_type_display() organization_type.admin_order_field = ( # type: ignore "domain_info__organization_type" diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index b5827d3e9..805e97171 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -52,6 +52,26 @@ class TestDomainAdmin(MockEppLib): self.factory = RequestFactory() super().setUp() + def test_short_org_name_in_domains_list(self): + """ + Make sure the short name is displaying in admin on the list page + """ + self.client.force_login(self.superuser) + application = completed_application(status=DomainApplication.IN_REVIEW) + application.approve() + + response = self.client.get("/admin/registrar/domain/") + + # There are 3 template references to Federal (3) plus one reference in the table + # for our actual application + self.assertContains(response, "Federal", count=4) + # This may be a bit more robust + self.assertContains( + response, 'Federal', count=1 + ) + # Now let's make sure the long description does not exist + self.assertNotContains(response, "Federal: an agency of the U.S. government") + @skip("Why did this test stop working, and is is a good test") def test_place_and_remove_hold(self): domain = create_ready_domain() @@ -243,8 +263,11 @@ class TestDomainAdmin(MockEppLib): raise def tearDown(self): - User.objects.all().delete() super().tearDown() + Domain.objects.all().delete() + DomainInformation.objects.all().delete() + DomainApplication.objects.all().delete() + User.objects.all().delete() class TestDomainApplicationAdminForm(TestCase): From d01bebec4110dc247fd3a882a86cdd07c9a9af4f Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Wed, 18 Oct 2023 18:03:22 -0400 Subject: [PATCH 39/67] merge conflicting migrations --- src/registrar/migrations/0040_merge_20231018_2203.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 src/registrar/migrations/0040_merge_20231018_2203.py diff --git a/src/registrar/migrations/0040_merge_20231018_2203.py b/src/registrar/migrations/0040_merge_20231018_2203.py new file mode 100644 index 000000000..fad679098 --- /dev/null +++ b/src/registrar/migrations/0040_merge_20231018_2203.py @@ -0,0 +1,12 @@ +# Generated by Django 4.2.1 on 2023-10-18 22:03 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("registrar", "0039_alter_transitiondomain_status"), + ("registrar", "0039_merge_20231013_2029"), + ] + + operations = [] From 1791db42035c19a85bef66bed4a99fdcc0f99a52 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Wed, 18 Oct 2023 18:57:58 -0400 Subject: [PATCH 40/67] add more specificity to CSS selectors causing a btn color bug --- src/registrar/assets/sass/_theme/_buttons.scss | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/registrar/assets/sass/_theme/_buttons.scss b/src/registrar/assets/sass/_theme/_buttons.scss index 718bd5792..0857ec603 100644 --- a/src/registrar/assets/sass/_theme/_buttons.scss +++ b/src/registrar/assets/sass/_theme/_buttons.scss @@ -22,15 +22,15 @@ a.breadcrumb__back { } } -a.usa-button { +a.usa-button:not(.usa-button--unstyled, .usa-button--outline) { text-decoration: none; color: color('white'); } -a.usa-button:visited, -a.usa-button:hover, -a.usa-button:focus, -a.usa-button:active { +a.usa-button:not(.usa-button--unstyled, .usa-button--outline):visited, +a.usa-button:not(.usa-button--unstyled, .usa-button--outline):hover, +a.usa-button:not(.usa-button--unstyled, .usa-button--outline):focus, +a.usa-button:not(.usa-button--unstyled, .usa-button--outline):active { color: color('white'); } From eebe6e117efb9f2ef9e5cdf1fe81e9882a45959c Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 19 Oct 2023 10:50:18 -0600 Subject: [PATCH 41/67] Fix merge issue --- src/registrar/tests/common.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index 94e3e2d3e..f6c6c1f2d 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -900,11 +900,10 @@ class MockEppLib(TestCase): "namerserversubdomain.gov": (self.infoDomainCheckHostIPCombo, None), "freeman.gov": (self.InfoDomainWithContacts, None), "threenameserversDomain.gov": (self.infoDomainThreeHosts, None), + "defaultsecurity.gov": (self.InfoDomainWithDefaultSecurityContact, None), + "defaulttechnical.gov": (self.InfoDomainWithDefaultTechnicalContact, None) } - TODO = elif getattr(_request, "name", None) == "defaultsecurity.gov": - return MagicMock(res_data=[self.InfoDomainWithDefaultSecurityContact]) - elif getattr(_request, "name", None) == "defaulttechnical.gov": - return MagicMock(res_data=[self.InfoDomainWithDefaultTechnicalContact]) + # Retrieve the corresponding values from the dictionary res_data, extensions = request_mappings.get( request_name, (self.mockDataInfoDomain, None) From e26db14a29b441d632f083ffcbc0acb62233c186 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 19 Oct 2023 10:53:54 -0600 Subject: [PATCH 42/67] Fix merge weirdness --- src/registrar/tests/common.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index f6c6c1f2d..acda90a75 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -917,23 +917,23 @@ class MockEppLib(TestCase): def mockInfoContactCommands(self, _request, cleaned): mocked_result: info.InfoContactResultData - # For testing contact types - match getattr(_request, "id", None): - case "securityContact": - mocked_result = self.mockSecurityContact - case "technicalContact": - mocked_result = self.mockTechnicalContact - case "adminContact": - mocked_result = self.mockAdministrativeContact - case "regContact": - mocked_result = self.mockRegistrantContact - case "defaultSec": - mocked_result = self.mockDefaultSecurityContact - case "defaultTech": - mocked_result = self.mockDefaultTechnicalContact - case _: - # Default contact return - mocked_result = self.mockDataInfoContact + # For testing contact types + match getattr(_request, "id", None): + case "securityContact": + mocked_result = self.mockSecurityContact + case "technicalContact": + mocked_result = self.mockTechnicalContact + case "adminContact": + mocked_result = self.mockAdministrativeContact + case "regContact": + mocked_result = self.mockRegistrantContact + case "defaultSec": + mocked_result = self.mockDefaultSecurityContact + case "defaultTech": + mocked_result = self.mockDefaultTechnicalContact + case _: + # Default contact return + mocked_result = self.mockDataInfoContact return MagicMock(res_data=[mocked_result]) From afd7b1c3f332f619677fa13fc1fd23fb1bc9f4e5 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 19 Oct 2023 11:51:52 -0600 Subject: [PATCH 43/67] Reformat + add test case for eppdisclose --- src/registrar/tests/common.py | 2 +- src/registrar/tests/test_models_domain.py | 69 ++++++++++++++++++++++- 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index acda90a75..bd6a6336b 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -901,7 +901,7 @@ class MockEppLib(TestCase): "freeman.gov": (self.InfoDomainWithContacts, None), "threenameserversDomain.gov": (self.infoDomainThreeHosts, None), "defaultsecurity.gov": (self.InfoDomainWithDefaultSecurityContact, None), - "defaulttechnical.gov": (self.InfoDomainWithDefaultTechnicalContact, None) + "defaulttechnical.gov": (self.InfoDomainWithDefaultTechnicalContact, None), } # Retrieve the corresponding values from the dictionary diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py index 7a66c1106..76f8f1d2a 100644 --- a/src/registrar/tests/test_models_domain.py +++ b/src/registrar/tests/test_models_domain.py @@ -19,7 +19,7 @@ from registrar.utility.errors import ActionNotAllowed, NameserverError from registrar.models.utility.contact_error import ContactError, ContactErrorCodes -from .common import MockEppLib + from django_fsm import TransitionNotAllowed # type: ignore from epplibwrapper import ( commands, @@ -29,6 +29,7 @@ from epplibwrapper import ( RegistryError, ErrorCode, ) +from .common import MockEppLib import logging logger = logging.getLogger(__name__) @@ -813,6 +814,72 @@ class TestRegistrantContacts(MockEppLib): # The emails should match on both items self.assertEqual(expected_contact.email, actual_contact.email) + def test_convert_public_contact_to_epp(self): + self.maxDiff = None + domain, _ = Domain.objects.get_or_create(name="freeman.gov") + dummy_contact = domain.get_default_security_contact() + test_disclose = self._convertPublicContactToEpp( + dummy_contact, disclose_email=True + ).__dict__ + test_not_disclose = self._convertPublicContactToEpp( + dummy_contact, disclose_email=False + ).__dict__ + expected_disclose = { + "auth_info": common.ContactAuthInfo(pw='2fooBAR123fooBaz'), + "disclose": common.Disclose(flag=True, fields={common.DiscloseField.EMAIL}, types=None), + "email": "dotgov@cisa.dhs.gov", + "extensions": [], + "fax": None, + "id": "ThIq2NcRIDN7PauO", + "ident": None, + "notify_email": None, + "postal_info": common.PostalInfo( + name='Registry Customer Service', + addr=common.ContactAddr( + street=['4200 Wilson Blvd.', None, None], + city='Arlington', + pc='22201', + cc='US', + sp='VA' + ), + org='Cybersecurity and Infrastructure Security Agency', + type='loc' + ), + "vat": None, + "voice": "+1.8882820870" + } + expected_not_disclose = { + "auth_info": common.ContactAuthInfo(pw='2fooBAR123fooBaz'), + "disclose": common.Disclose(flag=False, fields={common.DiscloseField.EMAIL}, types=None), + "email": "dotgov@cisa.dhs.gov", + "extensions": [], + "fax": None, + "id": "ThrECENCHI76PGLh", + "ident": None, + "notify_email": None, + "postal_info": common.PostalInfo( + name='Registry Customer Service', + addr=common.ContactAddr( + street=['4200 Wilson Blvd.', None, None], + city='Arlington', + pc='22201', + cc='US', + sp='VA' + ), + org='Cybersecurity and Infrastructure Security Agency', + type='loc' + ), + "vat": None, + "voice": "+1.8882820870" + } + + # Set the ids equal, since this value changes + test_disclose["id"] = expected_disclose["id"] + test_not_disclose["id"] = expected_not_disclose["id"] + + self.assertEqual(test_disclose, expected_disclose) + self.assertEqual(test_not_disclose, expected_not_disclose) + def test_not_disclosed_on_default_security_contact(self): """ Scenario: Registrant creates a new domain with no security email From 81b58167a61b7ceee66230af314f73dba3da0be1 Mon Sep 17 00:00:00 2001 From: Erin <121973038+erinysong@users.noreply.github.com> Date: Thu, 19 Oct 2023 11:13:55 -0700 Subject: [PATCH 44/67] Fix lint errors --- src/registrar/tests/common.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index c61b827fc..f6539466d 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -851,7 +851,7 @@ class MockEppLib(TestCase): res_data=[self.mockDataHostChange], code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY, ) - + def mockDeleteDomainCommands(self, _request, cleaned): if getattr(_request, "name", None) == "failDelete.gov": name = getattr(_request, "name", None) @@ -862,7 +862,6 @@ class MockEppLib(TestCase): ) return None - def mockInfoDomainCommands(self, _request, cleaned): request_name = getattr(_request, "name", None) From 257bc0f0ba2def600a45eb94be0d8486781ca3a8 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Thu, 19 Oct 2023 17:36:22 -0700 Subject: [PATCH 45/67] Add tests --- src/registrar/tests/test_admin.py | 36 +++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 7dbc8ff38..4737909fe 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -11,6 +11,7 @@ from registrar.admin import ( ListHeaderAdmin, MyUserAdmin, AuditedAdmin, + ContactAdmin, ) from registrar.models import ( Domain, @@ -1310,3 +1311,38 @@ class DomainSessionVariableTest(TestCase): {"_edit_domain": "true"}, follow=True, ) + + +class ContactAdminTest(TestCase): + def setUp(self): + self.site = AdminSite() + self.factory = RequestFactory() + self.client = Client(HTTP_HOST="localhost:8080") + self.admin = ContactAdmin(model=get_user_model(), admin_site=None) + self.superuser = create_superuser() + self.staffuser = create_user() + + def test_readonly_when_restricted_staffuser(self): + request = self.factory.get("/") + request.user = self.staffuser + + readonly_fields = self.admin.get_readonly_fields(request) + + expected_fields = [ + "user", + ] + + self.assertEqual(readonly_fields, expected_fields) + + def test_readonly_when_restricted_superuser(self): + request = self.factory.get("/") + request.user = self.superuser + + readonly_fields = self.admin.get_readonly_fields(request) + + expected_fields = [] + + self.assertEqual(readonly_fields, expected_fields) + + def tearDown(self): + User.objects.all().delete() From 15ecaa83e70f0c40f8a5a06dc0f506a54967cbdc Mon Sep 17 00:00:00 2001 From: Cameron Dixon Date: Fri, 20 Oct 2023 06:33:11 -0400 Subject: [PATCH 46/67] Separate additional context from the description --- .github/ISSUE_TEMPLATE/issue-default.yml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/issue-default.yml b/.github/ISSUE_TEMPLATE/issue-default.yml index 2252845bf..701742f72 100644 --- a/.github/ISSUE_TEMPLATE/issue-default.yml +++ b/.github/ISSUE_TEMPLATE/issue-default.yml @@ -10,9 +10,9 @@ body: - type: textarea id: issue-description attributes: - label: Issue description and context + label: Issue description description: | - Describe the issue so that someone who wasn't present for its discovery can understand why it matters. Use full sentences, plain language, and good [formatting](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax). Screenshots and links to documents/discussions are welcome. + Describe the issue so that someone who wasn't present for its discovery can understand why it matters. Use full sentences, plain language, and good [formatting](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax). validations: required: true - type: textarea @@ -21,6 +21,11 @@ body: label: Acceptance criteria description: "If known, share 1-3 statements that would need to be true for this issue to be considered resolved. Use a [task list](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/about-task-lists#creating-task-lists) if appropriate." placeholder: "- [ ]" + - type: textarea + id: additional-context + attributes: + label: Additional context + description: "Share any other thoughts, like how this might be implemented or fixed. Screenshots and links to documents/discussions are welcome." - type: textarea id: links-to-other-issues attributes: @@ -32,4 +37,5 @@ body: id: note attributes: value: | - > We may edit this issue's text to document our understanding and clarify the product work. + > We may edit the text in this issue to document our understanding and clarify the product work. + From 0b087a81517c4e2a66e14844de18adeb2441ad41 Mon Sep 17 00:00:00 2001 From: Cameron Dixon Date: Fri, 20 Oct 2023 06:38:34 -0400 Subject: [PATCH 47/67] fix spacing --- .github/ISSUE_TEMPLATE/issue-default.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/issue-default.yml b/.github/ISSUE_TEMPLATE/issue-default.yml index 701742f72..26384ceda 100644 --- a/.github/ISSUE_TEMPLATE/issue-default.yml +++ b/.github/ISSUE_TEMPLATE/issue-default.yml @@ -24,8 +24,8 @@ body: - type: textarea id: additional-context attributes: - label: Additional context - description: "Share any other thoughts, like how this might be implemented or fixed. Screenshots and links to documents/discussions are welcome." + label: Additional context + description: "Share any other thoughts, like how this might be implemented or fixed. Screenshots and links to documents/discussions are welcome." - type: textarea id: links-to-other-issues attributes: From 94a8bf7793c3b5b4ffccde707027488c9ee430c2 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 20 Oct 2023 07:56:00 -0600 Subject: [PATCH 48/67] Linting --- src/registrar/tests/test_models_domain.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py index 76f8f1d2a..939d765b6 100644 --- a/src/registrar/tests/test_models_domain.py +++ b/src/registrar/tests/test_models_domain.py @@ -824,9 +824,13 @@ class TestRegistrantContacts(MockEppLib): test_not_disclose = self._convertPublicContactToEpp( dummy_contact, disclose_email=False ).__dict__ + + # Separated for linter + disclose_email_field = {common.DiscloseField.EMAIL} + disclose = common.Disclose(flag=True, fields=disclose_email_field, types=None), expected_disclose = { "auth_info": common.ContactAuthInfo(pw='2fooBAR123fooBaz'), - "disclose": common.Disclose(flag=True, fields={common.DiscloseField.EMAIL}, types=None), + "disclose": disclose, "email": "dotgov@cisa.dhs.gov", "extensions": [], "fax": None, @@ -848,9 +852,12 @@ class TestRegistrantContacts(MockEppLib): "vat": None, "voice": "+1.8882820870" } + + # Separated for linter + not_disclose = common.Disclose(flag=False, fields=disclose_email_field, types=None) expected_not_disclose = { "auth_info": common.ContactAuthInfo(pw='2fooBAR123fooBaz'), - "disclose": common.Disclose(flag=False, fields={common.DiscloseField.EMAIL}, types=None), + "disclose": not_disclose, "email": "dotgov@cisa.dhs.gov", "extensions": [], "fax": None, From d5b47a22df78eaa0e8ec22620ab68eae933abcf3 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 20 Oct 2023 08:04:21 -0600 Subject: [PATCH 49/67] Update test_models_domain.py --- src/registrar/tests/test_models_domain.py | 50 ++++++++++++----------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py index 939d765b6..5759df1be 100644 --- a/src/registrar/tests/test_models_domain.py +++ b/src/registrar/tests/test_models_domain.py @@ -827,10 +827,11 @@ class TestRegistrantContacts(MockEppLib): # Separated for linter disclose_email_field = {common.DiscloseField.EMAIL} - disclose = common.Disclose(flag=True, fields=disclose_email_field, types=None), expected_disclose = { - "auth_info": common.ContactAuthInfo(pw='2fooBAR123fooBaz'), - "disclose": disclose, + "auth_info": common.ContactAuthInfo(pw="2fooBAR123fooBaz"), + "disclose": common.Disclose( + flag=True, fields=disclose_email_field, types=None + ), "email": "dotgov@cisa.dhs.gov", "extensions": [], "fax": None, @@ -838,26 +839,27 @@ class TestRegistrantContacts(MockEppLib): "ident": None, "notify_email": None, "postal_info": common.PostalInfo( - name='Registry Customer Service', + name="Registry Customer Service", addr=common.ContactAddr( - street=['4200 Wilson Blvd.', None, None], - city='Arlington', - pc='22201', - cc='US', - sp='VA' + street=["4200 Wilson Blvd.", None, None], + city="Arlington", + pc="22201", + cc="US", + sp="VA", ), - org='Cybersecurity and Infrastructure Security Agency', - type='loc' + org="Cybersecurity and Infrastructure Security Agency", + type="loc", ), "vat": None, - "voice": "+1.8882820870" + "voice": "+1.8882820870", } # Separated for linter - not_disclose = common.Disclose(flag=False, fields=disclose_email_field, types=None) expected_not_disclose = { - "auth_info": common.ContactAuthInfo(pw='2fooBAR123fooBaz'), - "disclose": not_disclose, + "auth_info": common.ContactAuthInfo(pw="2fooBAR123fooBaz"), + "disclose": common.Disclose( + flag=False, fields=disclose_email_field, types=None + ), "email": "dotgov@cisa.dhs.gov", "extensions": [], "fax": None, @@ -865,19 +867,19 @@ class TestRegistrantContacts(MockEppLib): "ident": None, "notify_email": None, "postal_info": common.PostalInfo( - name='Registry Customer Service', + name="Registry Customer Service", addr=common.ContactAddr( - street=['4200 Wilson Blvd.', None, None], - city='Arlington', - pc='22201', - cc='US', - sp='VA' + street=["4200 Wilson Blvd.", None, None], + city="Arlington", + pc="22201", + cc="US", + sp="VA", ), - org='Cybersecurity and Infrastructure Security Agency', - type='loc' + org="Cybersecurity and Infrastructure Security Agency", + type="loc", ), "vat": None, - "voice": "+1.8882820870" + "voice": "+1.8882820870", } # Set the ids equal, since this value changes From 485a05e45e133b508cdd0cf64d18078eb9d0e828 Mon Sep 17 00:00:00 2001 From: Alysia Broddrick Date: Fri, 20 Oct 2023 08:44:12 -0700 Subject: [PATCH 50/67] Fixed manager role still showing as admin --- .../0040_alter_userdomainrole_role.py | 17 +++++++++++++++++ src/registrar/models/user_domain_role.py | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 src/registrar/migrations/0040_alter_userdomainrole_role.py diff --git a/src/registrar/migrations/0040_alter_userdomainrole_role.py b/src/registrar/migrations/0040_alter_userdomainrole_role.py new file mode 100644 index 000000000..39e539f55 --- /dev/null +++ b/src/registrar/migrations/0040_alter_userdomainrole_role.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.1 on 2023-10-20 15:40 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("registrar", "0039_alter_transitiondomain_status"), + ] + + operations = [ + migrations.AlterField( + model_name="userdomainrole", + name="role", + field=models.TextField(choices=[("manager", "Manager")]), + ), + ] diff --git a/src/registrar/models/user_domain_role.py b/src/registrar/models/user_domain_role.py index e5cb01cc1..7b1f550d3 100644 --- a/src/registrar/models/user_domain_role.py +++ b/src/registrar/models/user_domain_role.py @@ -15,7 +15,7 @@ class UserDomainRole(TimeStampedModel): elsewhere. """ - ADMIN = "manager" + MANAGER = "manager" user = models.ForeignKey( "registrar.User", From 34e0ce955ab54384b2404fac4ef4adafc493f49d Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Fri, 20 Oct 2023 11:51:11 -0400 Subject: [PATCH 51/67] clean up a bad comment --- src/registrar/templatetags/custom_filters.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/registrar/templatetags/custom_filters.py b/src/registrar/templatetags/custom_filters.py index 158b7269e..14e2c9e3e 100644 --- a/src/registrar/templatetags/custom_filters.py +++ b/src/registrar/templatetags/custom_filters.py @@ -55,11 +55,9 @@ def contains_checkbox(html_list): @register.filter def get_organization_long_name(organization_type): - # https://gist.github.com/OmenApps/3eef60ba4204f3d1842d9d7477efcce1#file-django_choices-txt-L28 organization_choices_dict = dict( DomainApplication.OrganizationChoicesVerbose.choices ) - long_form_type = organization_choices_dict[organization_type] if long_form_type is None: logger.error("Organization type error, triggered by a template's custom filter") From 649a22456232f77f75f98bd6ecedc09d3ac62529 Mon Sep 17 00:00:00 2001 From: Alysia Broddrick Date: Fri, 20 Oct 2023 09:15:46 -0700 Subject: [PATCH 52/67] updated related mentions to admin to be Manager --- docs/developer/user-permissions.md | 2 +- src/registrar/models/domain_application.py | 2 +- src/registrar/models/domain_invitation.py | 2 +- src/registrar/tests/test_models.py | 2 +- src/registrar/tests/test_views.py | 12 ++++++------ src/registrar/views/domain.py | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/developer/user-permissions.md b/docs/developer/user-permissions.md index 31b69d3b3..f7c41492d 100644 --- a/docs/developer/user-permissions.md +++ b/docs/developer/user-permissions.md @@ -42,7 +42,7 @@ as health checks used by our platform). ## Adding roles The current MVP design uses only a single role called -`UserDomainRole.Roles.ADMIN` that has all access on a domain. As such, the +`UserDomainRole.Roles.MANAGER` that has all access on a domain. As such, the permission mixin doesn't need to examine the `role` field carefully. In the future, as we add additional roles that our product vision calls for (read-only? editing only some information?), we need to add conditional diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py index 68429d381..f15474117 100644 --- a/src/registrar/models/domain_application.py +++ b/src/registrar/models/domain_application.py @@ -582,7 +582,7 @@ class DomainApplication(TimeStampedModel): # create the permission for the user UserDomainRole = apps.get_model("registrar.UserDomainRole") UserDomainRole.objects.get_or_create( - user=self.creator, domain=created_domain, role=UserDomainRole.Roles.ADMIN + user=self.creator, domain=created_domain, role=UserDomainRole.Roles.MANAGER ) self._send_status_update_email( diff --git a/src/registrar/models/domain_invitation.py b/src/registrar/models/domain_invitation.py index 7cc2a5432..dff03fb87 100644 --- a/src/registrar/models/domain_invitation.py +++ b/src/registrar/models/domain_invitation.py @@ -63,7 +63,7 @@ class DomainInvitation(TimeStampedModel): # and create a role for that user on this domain _, created = UserDomainRole.objects.get_or_create( - user=user, domain=self.domain, role=UserDomainRole.Roles.ADMIN + user=user, domain=self.domain, role=UserDomainRole.Roles.MANAGER ) if not created: # something strange happened and this role already existed when diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py index 2c6f78ef5..e76dea035 100644 --- a/src/registrar/tests/test_models.py +++ b/src/registrar/tests/test_models.py @@ -601,7 +601,7 @@ class TestInvitations(TestCase): def test_retrieve_existing_role_no_error(self): # make the overlapping role UserDomainRole.objects.get_or_create( - user=self.user, domain=self.domain, role=UserDomainRole.Roles.ADMIN + user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER ) # this is not an error but does produce a console warning with less_console_noise(): diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 0e8f895af..2a14f3466 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -89,7 +89,7 @@ class LoggedInTests(TestWithUser): domain, _ = Domain.objects.get_or_create(name="igorville.gov") self.assertNotContains(response, "igorville.gov") role, _ = UserDomainRole.objects.get_or_create( - user=self.user, domain=domain, role=UserDomainRole.Roles.ADMIN + user=self.user, domain=domain, role=UserDomainRole.Roles.MANAGER ) response = self.client.get("/") # count = 2 because it is also in screenreader content @@ -1097,23 +1097,23 @@ class TestWithDomainPermissions(TestWithUser): creator=self.user, domain=self.domain_dnssec_none ) self.role, _ = UserDomainRole.objects.get_or_create( - user=self.user, domain=self.domain, role=UserDomainRole.Roles.ADMIN + user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER ) UserDomainRole.objects.get_or_create( - user=self.user, domain=self.domain_dsdata, role=UserDomainRole.Roles.ADMIN + user=self.user, domain=self.domain_dsdata, role=UserDomainRole.Roles.MANAGER ) UserDomainRole.objects.get_or_create( user=self.user, domain=self.domain_multdsdata, - role=UserDomainRole.Roles.ADMIN, + role=UserDomainRole.Roles.MANAGER, ) UserDomainRole.objects.get_or_create( - user=self.user, domain=self.domain_keydata, role=UserDomainRole.Roles.ADMIN + user=self.user, domain=self.domain_keydata, role=UserDomainRole.Roles.MANAGER ) UserDomainRole.objects.get_or_create( user=self.user, domain=self.domain_dnssec_none, - role=UserDomainRole.Roles.ADMIN, + role=UserDomainRole.Roles.MANAGER, ) def tearDown(self): diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index aa71a7551..5590698be 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -736,7 +736,7 @@ class DomainAddUserView(DomainFormBaseView): try: UserDomainRole.objects.create( - user=requested_user, domain=self.object, role=UserDomainRole.Roles.ADMIN + user=requested_user, domain=self.object, role=UserDomainRole.Roles.MANAGER ) except IntegrityError: # User already has the desired role! Do nothing?? From 555a1e7ae95a8e8ea6f3debce1ab65c94ca34c29 Mon Sep 17 00:00:00 2001 From: Alysia Broddrick Date: Fri, 20 Oct 2023 09:21:12 -0700 Subject: [PATCH 53/67] ran linter --- src/registrar/tests/test_views.py | 4 +++- src/registrar/views/domain.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 2a14f3466..7cc616889 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -1108,7 +1108,9 @@ class TestWithDomainPermissions(TestWithUser): role=UserDomainRole.Roles.MANAGER, ) UserDomainRole.objects.get_or_create( - user=self.user, domain=self.domain_keydata, role=UserDomainRole.Roles.MANAGER + user=self.user, + domain=self.domain_keydata, + role=UserDomainRole.Roles.MANAGER, ) UserDomainRole.objects.get_or_create( user=self.user, diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 5590698be..fce94f175 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -736,7 +736,9 @@ class DomainAddUserView(DomainFormBaseView): try: UserDomainRole.objects.create( - user=requested_user, domain=self.object, role=UserDomainRole.Roles.MANAGER + user=requested_user, + domain=self.object, + role=UserDomainRole.Roles.MANAGER, ) except IntegrityError: # User already has the desired role! Do nothing?? From 8c0d88df2ac1a97b3f889b4437fa41904224c578 Mon Sep 17 00:00:00 2001 From: Erin <121973038+erinysong@users.noreply.github.com> Date: Fri, 20 Oct 2023 09:35:28 -0700 Subject: [PATCH 54/67] Remove login requirement on domain availability API --- src/api/views.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/api/views.py b/src/api/views.py index 02e419a91..e8b8431de 100644 --- a/src/api/views.py +++ b/src/api/views.py @@ -3,8 +3,6 @@ from django.apps import apps from django.views.decorators.http import require_http_methods from django.http import JsonResponse -from django.contrib.auth.decorators import login_required - import requests from cachetools.func import ttl_cache @@ -68,7 +66,6 @@ def in_domains(domain): @require_http_methods(["GET"]) -@login_required def available(request, domain=""): """Is a given domain available or not. From 3cfdacccfc3bc5aaf7aa71eb97da959b2cf1fc44 Mon Sep 17 00:00:00 2001 From: Neil Martinsen-Burrell Date: Fri, 20 Oct 2023 12:00:17 -0500 Subject: [PATCH 55/67] Change User management to Domain managers --- src/registrar/templates/domain_detail.html | 2 +- src/registrar/templates/domain_sidebar.html | 2 +- src/registrar/templates/domain_users.html | 17 +++++++++++++++-- src/registrar/tests/test_views.py | 2 +- src/registrar/views/domain.py | 2 +- 5 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/registrar/templates/domain_detail.html b/src/registrar/templates/domain_detail.html index e0d672093..4ddbd673a 100644 --- a/src/registrar/templates/domain_detail.html +++ b/src/registrar/templates/domain_detail.html @@ -52,7 +52,7 @@ {% include "includes/summary_item.html" with title='Security email' value='None provided' edit_link=url %} {% endif %} {% url 'domain-users' pk=domain.id as url %} - {% include "includes/summary_item.html" with title='User management' users='true' list=True value=domain.permissions.all edit_link=url %} + {% include "includes/summary_item.html" with title='Domain managers' users='true' list=True value=domain.permissions.all edit_link=url %}
{% endblock %} {# domain_content #} diff --git a/src/registrar/templates/domain_sidebar.html b/src/registrar/templates/domain_sidebar.html index 1acd87eeb..ac45ad04c 100644 --- a/src/registrar/templates/domain_sidebar.html +++ b/src/registrar/templates/domain_sidebar.html @@ -100,7 +100,7 @@ - User management + Domain managers diff --git a/src/registrar/templates/domain_users.html b/src/registrar/templates/domain_users.html index 22b9d18d1..5cb7acffd 100644 --- a/src/registrar/templates/domain_users.html +++ b/src/registrar/templates/domain_users.html @@ -1,10 +1,23 @@ {% extends "domain_base.html" %} {% load static %} -{% block title %}User management | {{ domain.name }} | {% endblock %} +{% block title %}Domain managers | {{ domain.name }} | {% endblock %} {% block domain_content %} -

User management

+

Domain managers

+ +

+ Domain managers can update all information related to a domain within the + .gov registrar, including contact details, authorizing official, security + email, and DNS name servers. +

+ +
    +
  • There is no limit to the number of domain managers you can add.
  • +
  • After adding a domain manager, an email invitation will be sent to that user with + instructions on how to set up an account.
  • +
  • To remove a domain manager, contact us for assistance. +
{% if domain.permissions %}
diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 0e8f895af..8ad855433 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -1204,7 +1204,7 @@ class TestDomainUserManagement(TestDomainOverview): response = self.client.get( reverse("domain-users", kwargs={"pk": self.domain.id}) ) - self.assertContains(response, "User management") + self.assertContains(response, "Domain managers") def test_domain_user_management_add_link(self): """Button to get to user add page works.""" diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index aa71a7551..d9b671a65 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -656,7 +656,7 @@ class DomainSecurityEmailView(DomainFormBaseView): class DomainUsersView(DomainBaseView): - """User management page in the domain details.""" + """Domain managers page in the domain details.""" template_name = "domain_users.html" From 8e753796ec166e1dc467a4db2b6429623602425a Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Fri, 20 Oct 2023 10:10:14 -0700 Subject: [PATCH 56/67] Update migration to match main --- .../migrations/0041_create_groups_v03.py | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 src/registrar/migrations/0041_create_groups_v03.py diff --git a/src/registrar/migrations/0041_create_groups_v03.py b/src/registrar/migrations/0041_create_groups_v03.py new file mode 100644 index 000000000..4ddbb651a --- /dev/null +++ b/src/registrar/migrations/0041_create_groups_v03.py @@ -0,0 +1,37 @@ +# This migration creates the create_full_access_group and create_cisa_analyst_group groups +# It is dependent on 0035 (which populates ContentType and Permissions) +# If permissions on the groups need changing, edit CISA_ANALYST_GROUP_PERMISSIONS +# in the user_group model then: +# [NOT RECOMMENDED] +# step 1: docker-compose exec app ./manage.py migrate --fake registrar 0035_contenttypes_permissions +# step 2: docker-compose exec app ./manage.py migrate registrar 0036_create_groups +# step 3: fake run the latest migration in the migrations list +# [RECOMMENDED] +# Alternatively: +# step 1: duplicate the migration that loads data +# step 2: docker-compose exec app ./manage.py migrate + +from django.db import migrations +from registrar.models import UserGroup +from typing import Any + + +# For linting: RunPython expects a function reference, +# so let's give it one +def create_groups(apps, schema_editor) -> Any: + UserGroup.create_cisa_analyst_group(apps, schema_editor) + UserGroup.create_full_access_group(apps, schema_editor) + + +class Migration(migrations.Migration): + dependencies = [ + ("registrar", "0040_alter_user_domainrole_role"), + ] + + operations = [ + migrations.RunPython( + create_groups, + reverse_code=migrations.RunPython.noop, + atomic=True, + ), + ] From 070631b2160200850be76cd932af996aed677de6 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Fri, 20 Oct 2023 10:19:57 -0700 Subject: [PATCH 57/67] Fix naming for migration --- src/registrar/migrations/0041_create_groups_v03.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/migrations/0041_create_groups_v03.py b/src/registrar/migrations/0041_create_groups_v03.py index 4ddbb651a..d6ec5e433 100644 --- a/src/registrar/migrations/0041_create_groups_v03.py +++ b/src/registrar/migrations/0041_create_groups_v03.py @@ -25,7 +25,7 @@ def create_groups(apps, schema_editor) -> Any: class Migration(migrations.Migration): dependencies = [ - ("registrar", "0040_alter_user_domainrole_role"), + ("registrar", "0040_alter_userdomainrole_role"), ] operations = [ From a0911b46f84e7b16df9c0d38de591799a99c91e0 Mon Sep 17 00:00:00 2001 From: Neil Martinsen-Burrell Date: Fri, 20 Oct 2023 13:28:36 -0500 Subject: [PATCH 58/67] load public_site_url helper --- src/registrar/templates/domain_users.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/domain_users.html b/src/registrar/templates/domain_users.html index 5cb7acffd..f66eef5a6 100644 --- a/src/registrar/templates/domain_users.html +++ b/src/registrar/templates/domain_users.html @@ -1,5 +1,5 @@ {% extends "domain_base.html" %} -{% load static %} +{% load static url_helpers %} {% block title %}Domain managers | {{ domain.name }} | {% endblock %} From 7f153f77ed70147c39e021ab02d85498c1cb4bf2 Mon Sep 17 00:00:00 2001 From: Neil Martinsen-Burrell Date: Fri, 20 Oct 2023 13:46:51 -0500 Subject: [PATCH 59/67] Review feedback: rename tests to match page name --- src/registrar/tests/test_views.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 8ad855433..1262347a1 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -1199,14 +1199,14 @@ class TestDomainOverview(TestWithDomainPermissions, WebTest): self.assertEqual(response.status_code, 403) -class TestDomainUserManagement(TestDomainOverview): - def test_domain_user_management(self): +class TestDomainManagers(TestDomainOverview): + def test_domain_managers(self): response = self.client.get( reverse("domain-users", kwargs={"pk": self.domain.id}) ) self.assertContains(response, "Domain managers") - def test_domain_user_management_add_link(self): + def test_domain_managers_add_link(self): """Button to get to user add page works.""" management_page = self.app.get( reverse("domain-users", kwargs={"pk": self.domain.id}) From 020f2614d0e7c727d0261a4f9bf3da0c22fbff3e Mon Sep 17 00:00:00 2001 From: Cameron Dixon Date: Fri, 20 Oct 2023 15:46:51 -0400 Subject: [PATCH 60/67] Update issue-default.yml --- .github/ISSUE_TEMPLATE/issue-default.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/issue-default.yml b/.github/ISSUE_TEMPLATE/issue-default.yml index 26384ceda..3a34b2943 100644 --- a/.github/ISSUE_TEMPLATE/issue-default.yml +++ b/.github/ISSUE_TEMPLATE/issue-default.yml @@ -21,9 +21,9 @@ body: label: Acceptance criteria description: "If known, share 1-3 statements that would need to be true for this issue to be considered resolved. Use a [task list](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/about-task-lists#creating-task-lists) if appropriate." placeholder: "- [ ]" - - type: textarea - id: additional-context - attributes: + - type: textarea + id: additional-context + attributes: label: Additional context description: "Share any other thoughts, like how this might be implemented or fixed. Screenshots and links to documents/discussions are welcome." - type: textarea From b8b12bad6ea2f1330ef840cdf1318795d7876fae Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Fri, 20 Oct 2023 17:09:50 -0400 Subject: [PATCH 61/67] Clean up migrations --- ...napplication_organization_type_and_more.py | 52 ------------------- .../migrations/0039_merge_20231013_2029.py | 12 ----- .../migrations/0040_merge_20231018_2203.py | 12 ----- 3 files changed, 76 deletions(-) delete mode 100644 src/registrar/migrations/0038_alter_domainapplication_organization_type_and_more.py delete mode 100644 src/registrar/migrations/0039_merge_20231013_2029.py delete mode 100644 src/registrar/migrations/0040_merge_20231018_2203.py diff --git a/src/registrar/migrations/0038_alter_domainapplication_organization_type_and_more.py b/src/registrar/migrations/0038_alter_domainapplication_organization_type_and_more.py deleted file mode 100644 index a06ea0451..000000000 --- a/src/registrar/migrations/0038_alter_domainapplication_organization_type_and_more.py +++ /dev/null @@ -1,52 +0,0 @@ -# Generated by Django 4.2.1 on 2023-10-13 20:26 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("registrar", "0037_create_groups_v01"), - ] - - operations = [ - migrations.AlterField( - model_name="domainapplication", - name="organization_type", - field=models.CharField( - blank=True, - choices=[ - ("federal", "Federal"), - ("interstate", "Interstate"), - ("state_or_territory", "State or territory"), - ("tribal", "Tribal"), - ("county", "County"), - ("city", "City"), - ("special_district", "Special district"), - ("school_district", "School district"), - ], - help_text="Type of organization", - max_length=255, - null=True, - ), - ), - migrations.AlterField( - model_name="domaininformation", - name="organization_type", - field=models.CharField( - blank=True, - choices=[ - ("federal", "Federal"), - ("interstate", "Interstate"), - ("state_or_territory", "State or territory"), - ("tribal", "Tribal"), - ("county", "County"), - ("city", "City"), - ("special_district", "Special district"), - ("school_district", "School district"), - ], - help_text="Type of Organization", - max_length=255, - null=True, - ), - ), - ] diff --git a/src/registrar/migrations/0039_merge_20231013_2029.py b/src/registrar/migrations/0039_merge_20231013_2029.py deleted file mode 100644 index aed231bdc..000000000 --- a/src/registrar/migrations/0039_merge_20231013_2029.py +++ /dev/null @@ -1,12 +0,0 @@ -# Generated by Django 4.2.1 on 2023-10-13 20:29 - -from django.db import migrations - - -class Migration(migrations.Migration): - dependencies = [ - ("registrar", "0038_alter_domainapplication_organization_type_and_more"), - ("registrar", "0038_create_groups_v02"), - ] - - operations = [] diff --git a/src/registrar/migrations/0040_merge_20231018_2203.py b/src/registrar/migrations/0040_merge_20231018_2203.py deleted file mode 100644 index fad679098..000000000 --- a/src/registrar/migrations/0040_merge_20231018_2203.py +++ /dev/null @@ -1,12 +0,0 @@ -# Generated by Django 4.2.1 on 2023-10-18 22:03 - -from django.db import migrations - - -class Migration(migrations.Migration): - dependencies = [ - ("registrar", "0039_alter_transitiondomain_status"), - ("registrar", "0039_merge_20231013_2029"), - ] - - operations = [] From d5b21435ddea84afd8fb821c065a3606a37f1726 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Fri, 20 Oct 2023 17:11:00 -0400 Subject: [PATCH 62/67] re-create org names migration --- ...napplication_organization_type_and_more.py | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 src/registrar/migrations/0041_alter_domainapplication_organization_type_and_more.py diff --git a/src/registrar/migrations/0041_alter_domainapplication_organization_type_and_more.py b/src/registrar/migrations/0041_alter_domainapplication_organization_type_and_more.py new file mode 100644 index 000000000..07cfe0e77 --- /dev/null +++ b/src/registrar/migrations/0041_alter_domainapplication_organization_type_and_more.py @@ -0,0 +1,52 @@ +# Generated by Django 4.2.1 on 2023-10-20 21:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("registrar", "0040_alter_userdomainrole_role"), + ] + + operations = [ + migrations.AlterField( + model_name="domainapplication", + name="organization_type", + field=models.CharField( + blank=True, + choices=[ + ("federal", "Federal"), + ("interstate", "Interstate"), + ("state_or_territory", "State or territory"), + ("tribal", "Tribal"), + ("county", "County"), + ("city", "City"), + ("special_district", "Special district"), + ("school_district", "School district"), + ], + help_text="Type of organization", + max_length=255, + null=True, + ), + ), + migrations.AlterField( + model_name="domaininformation", + name="organization_type", + field=models.CharField( + blank=True, + choices=[ + ("federal", "Federal"), + ("interstate", "Interstate"), + ("state_or_territory", "State or territory"), + ("tribal", "Tribal"), + ("county", "County"), + ("city", "City"), + ("special_district", "Special district"), + ("school_district", "School district"), + ], + help_text="Type of Organization", + max_length=255, + null=True, + ), + ), + ] From 62b6514b0732d3e6713dbc296cd2adb14dce01d1 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Fri, 20 Oct 2023 14:50:02 -0700 Subject: [PATCH 63/67] Remove 0041 so I can match main --- .../migrations/0041_create_groups_v03.py | 37 ------------------- 1 file changed, 37 deletions(-) delete mode 100644 src/registrar/migrations/0041_create_groups_v03.py diff --git a/src/registrar/migrations/0041_create_groups_v03.py b/src/registrar/migrations/0041_create_groups_v03.py deleted file mode 100644 index d6ec5e433..000000000 --- a/src/registrar/migrations/0041_create_groups_v03.py +++ /dev/null @@ -1,37 +0,0 @@ -# This migration creates the create_full_access_group and create_cisa_analyst_group groups -# It is dependent on 0035 (which populates ContentType and Permissions) -# If permissions on the groups need changing, edit CISA_ANALYST_GROUP_PERMISSIONS -# in the user_group model then: -# [NOT RECOMMENDED] -# step 1: docker-compose exec app ./manage.py migrate --fake registrar 0035_contenttypes_permissions -# step 2: docker-compose exec app ./manage.py migrate registrar 0036_create_groups -# step 3: fake run the latest migration in the migrations list -# [RECOMMENDED] -# Alternatively: -# step 1: duplicate the migration that loads data -# step 2: docker-compose exec app ./manage.py migrate - -from django.db import migrations -from registrar.models import UserGroup -from typing import Any - - -# For linting: RunPython expects a function reference, -# so let's give it one -def create_groups(apps, schema_editor) -> Any: - UserGroup.create_cisa_analyst_group(apps, schema_editor) - UserGroup.create_full_access_group(apps, schema_editor) - - -class Migration(migrations.Migration): - dependencies = [ - ("registrar", "0040_alter_userdomainrole_role"), - ] - - operations = [ - migrations.RunPython( - create_groups, - reverse_code=migrations.RunPython.noop, - atomic=True, - ), - ] From 797d896e0a2362f20c221e10dd45e9e041c42ffd Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Fri, 20 Oct 2023 14:51:48 -0700 Subject: [PATCH 64/67] Now updating correct migration after new main additions --- .../migrations/0042_create_groups_v03.py | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 src/registrar/migrations/0042_create_groups_v03.py diff --git a/src/registrar/migrations/0042_create_groups_v03.py b/src/registrar/migrations/0042_create_groups_v03.py new file mode 100644 index 000000000..e7039294b --- /dev/null +++ b/src/registrar/migrations/0042_create_groups_v03.py @@ -0,0 +1,37 @@ +# This migration creates the create_full_access_group and create_cisa_analyst_group groups +# It is dependent on 0035 (which populates ContentType and Permissions) +# If permissions on the groups need changing, edit CISA_ANALYST_GROUP_PERMISSIONS +# in the user_group model then: +# [NOT RECOMMENDED] +# step 1: docker-compose exec app ./manage.py migrate --fake registrar 0035_contenttypes_permissions +# step 2: docker-compose exec app ./manage.py migrate registrar 0036_create_groups +# step 3: fake run the latest migration in the migrations list +# [RECOMMENDED] +# Alternatively: +# step 1: duplicate the migration that loads data +# step 2: docker-compose exec app ./manage.py migrate + +from django.db import migrations +from registrar.models import UserGroup +from typing import Any + + +# For linting: RunPython expects a function reference, +# so let's give it one +def create_groups(apps, schema_editor) -> Any: + UserGroup.create_cisa_analyst_group(apps, schema_editor) + UserGroup.create_full_access_group(apps, schema_editor) + + +class Migration(migrations.Migration): + dependencies = [ + ("registrar", "0041_alter_domainapplication_organization_type_and_more"), + ] + + operations = [ + migrations.RunPython( + create_groups, + reverse_code=migrations.RunPython.noop, + atomic=True, + ), + ] \ No newline at end of file From a25a38f94832036d8189d693b53b120032ebf7af Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Fri, 20 Oct 2023 14:57:04 -0700 Subject: [PATCH 65/67] Linter wins as always --- src/registrar/migrations/0042_create_groups_v03.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/migrations/0042_create_groups_v03.py b/src/registrar/migrations/0042_create_groups_v03.py index e7039294b..01b7985bf 100644 --- a/src/registrar/migrations/0042_create_groups_v03.py +++ b/src/registrar/migrations/0042_create_groups_v03.py @@ -34,4 +34,4 @@ class Migration(migrations.Migration): reverse_code=migrations.RunPython.noop, atomic=True, ), - ] \ No newline at end of file + ] From fb5cc4e5c670235c3f94d9702b6cafaac20e57d1 Mon Sep 17 00:00:00 2001 From: Neil Martinsen-Burrell Date: Mon, 23 Oct 2023 10:32:40 -0500 Subject: [PATCH 66/67] Allow connections from manage.get.gov --- src/registrar/config/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index 7b96af5ee..59f00fe61 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -581,7 +581,7 @@ ALLOWED_HOSTS = [ "getgov-bl.app.cloud.gov", "getgov-rjm.app.cloud.gov", "getgov-dk.app.cloud.gov", - "get.gov", + "manage.get.gov", ] # Extend ALLOWED_HOSTS. From 5319f58191bb0c9a0bdd97f5e2cbe8ff395ffeeb Mon Sep 17 00:00:00 2001 From: Neil Martinsen-Burrell Date: Mon, 23 Oct 2023 10:57:58 -0500 Subject: [PATCH 67/67] Use manage.get.gov as the BASE_URL --- ops/manifests/manifest-stable.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ops/manifests/manifest-stable.yaml b/ops/manifests/manifest-stable.yaml index bc5e933f6..6295fa63b 100644 --- a/ops/manifests/manifest-stable.yaml +++ b/ops/manifests/manifest-stable.yaml @@ -18,7 +18,7 @@ applications: # Tell Django where to find its configuration DJANGO_SETTINGS_MODULE: registrar.config.settings # Tell Django where it is being hosted - DJANGO_BASE_URL: https://getgov-stable.app.cloud.gov + DJANGO_BASE_URL: https://manage.get.gov # Tell Django how much stuff to log DJANGO_LOG_LEVEL: INFO # default public site location