diff --git a/src/registrar/admin.py b/src/registrar/admin.py index a0c14efff..63bb7ff55 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1376,7 +1376,9 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): "authorizing_official", "other_contacts", "no_other_contacts_rationale", - "cisa_representative", + "cisa_representative_first_name", + "cisa_representative_last_name", + "cisa_representative_email", ] }, ), @@ -1452,8 +1454,10 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): "no_other_contacts_rationale", "anything_else", "is_policy_acknowledged", - "cisa_representative", - ] + "cisa_representative_first_name", + "cisa_representative_last_name", + "cisa_representative_email", +] autocomplete_fields = [ "approved_domain", "requested_domain", diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py index ecb84e62a..9df4a4c1a 100644 --- a/src/registrar/forms/domain_request_wizard.py +++ b/src/registrar/forms/domain_request_wizard.py @@ -647,47 +647,20 @@ class NoOtherContactsForm(BaseDeletableRegistrarForm): class CisaRepresentativeForm(BaseDeletableRegistrarForm): - JOIN = "cisa_representative" - - def to_database(self, obj): - if not self.is_valid(): - return - contact = getattr(obj, "cisa_representative", None) - if contact is not None and not contact.has_more_than_one_join("cisa_representative_domain_requests"): - if self.form_data_marked_for_deletion: - # remove the CISA contact from this domain request - obj.cisa_representative = None - # QUESTION - should we also delete the contact object if it is not joined to other entities? - else: - # update existing contact if it is not joined to other enttities - super().to_database(contact) - else: - # no contact exists OR contact exists which is joined also to other entities; - # in either case, create a new contact and update it - contact = Contact() - super().to_database(contact) - obj.cisa_representative = contact - obj.save() - - @classmethod - def from_database(cls, obj): - contact = getattr(obj, "cisa_representative", None) - return super().from_database(contact) - - first_name = forms.CharField( + cisa_representative_first_name = forms.CharField( label="First name / given name", error_messages={"required": "Enter your first name / given name."}, ) - last_name = forms.CharField( + cisa_representative_last_name = forms.CharField( label="Last name / family name", error_messages={"required": "Enter your last name / family name."}, ) - email = forms.EmailField( - label="Email", + cisa_representative_email = forms.EmailField( + label="Your representative’s email (optional)", max_length=None, required=False, error_messages={ - "invalid": ("Enter your email address in the required format, like name@example.com."), + "invalid": ("Enter your representative’s email address in the required format, like name@example.com."), }, validators=[ MaxLengthValidator( @@ -698,6 +671,7 @@ class CisaRepresentativeForm(BaseDeletableRegistrarForm): ) + class CisaRepresentativeYesNoForm(BaseYesNoForm): """Yes/no toggle for the CISA regions question on additional details""" diff --git a/src/registrar/migrations/0095_domaininformation_cisa_representative_first_name_and_more.py b/src/registrar/migrations/0095_domaininformation_cisa_representative_first_name_and_more.py new file mode 100644 index 000000000..f6f66ab23 --- /dev/null +++ b/src/registrar/migrations/0095_domaininformation_cisa_representative_first_name_and_more.py @@ -0,0 +1,55 @@ +# Generated by Django 4.2.10 on 2024-05-31 21:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("registrar", "0094_create_groups_v12"), + ] + + operations = [ + migrations.AddField( + model_name="domaininformation", + name="cisa_representative_first_name", + field=models.CharField( + blank=True, db_index=True, null=True, verbose_name="CISA regional representative first name" + ), + ), + migrations.AddField( + model_name="domaininformation", + name="cisa_representative_last_name", + field=models.CharField( + blank=True, db_index=True, null=True, verbose_name="CISA regional representative last name" + ), + ), + migrations.AddField( + model_name="domainrequest", + name="cisa_representative_first_name", + field=models.CharField( + blank=True, db_index=True, null=True, verbose_name="CISA regional representative first name" + ), + ), + migrations.AddField( + model_name="domainrequest", + name="cisa_representative_last_name", + field=models.CharField( + blank=True, db_index=True, null=True, verbose_name="CISA regional representative last name" + ), + ), + migrations.AlterField( + model_name="domaininformation", + name="cisa_representative_email", + field=models.EmailField( + blank=True, max_length=320, null=True, verbose_name="CISA regional representative email" + ), + ), + migrations.AlterField( + model_name="domainrequest", + name="cisa_representative_email", + field=models.EmailField( + blank=True, max_length=320, null=True, verbose_name="CISA regional representative email" + ), + ), + ] diff --git a/src/registrar/migrations/0095_remove_domaininformation_cisa_representative_email_and_more.py b/src/registrar/migrations/0095_remove_domaininformation_cisa_representative_email_and_more.py deleted file mode 100644 index 7e7fb5ee6..000000000 --- a/src/registrar/migrations/0095_remove_domaininformation_cisa_representative_email_and_more.py +++ /dev/null @@ -1,46 +0,0 @@ -# Generated by Django 4.2.10 on 2024-05-16 23:08 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ("registrar", "0094_create_groups_v12"), - ] - - operations = [ - migrations.RemoveField( - model_name="domaininformation", - name="cisa_representative_email", - ), - migrations.RemoveField( - model_name="domainrequest", - name="cisa_representative_email", - ), - migrations.AddField( - model_name="domaininformation", - name="cisa_representative", - field=models.ForeignKey( - blank=True, - help_text='Cisa Representative listed under "additional information" in the request form', - null=True, - on_delete=django.db.models.deletion.PROTECT, - related_name="cisa_representative_domain_requests_information", - to="registrar.contact", - ), - ), - migrations.AddField( - model_name="domainrequest", - name="cisa_representative", - field=models.ForeignKey( - blank=True, - help_text='Cisa Representative listed under "additional information" in the request form', - null=True, - on_delete=django.db.models.deletion.PROTECT, - related_name="cisa_representative_domain_requests", - to="registrar.contact", - ), - ), - ] diff --git a/src/registrar/models/domain_information.py b/src/registrar/models/domain_information.py index bf6db1a28..d2300c784 100644 --- a/src/registrar/models/domain_information.py +++ b/src/registrar/models/domain_information.py @@ -206,13 +206,25 @@ class DomainInformation(TimeStampedModel): verbose_name="Additional details", ) - cisa_representative = models.ForeignKey( - "registrar.Contact", + cisa_representative_email = models.EmailField( null=True, blank=True, - related_name="cisa_representative_domain_requests_information", - on_delete=models.PROTECT, - help_text='Cisa Representative listed under "additional information" in the request form', + verbose_name="CISA regional representative email", + max_length=320, + ) + + cisa_representative_first_name = models.CharField( + null=True, + blank=True, + verbose_name="CISA regional representative first name", + db_index=True, + ) + + cisa_representative_last_name = models.CharField( + null=True, + blank=True, + verbose_name="CISA regional representative last name", + db_index=True, ) is_policy_acknowledged = models.BooleanField( diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index 5adc3e91b..f84451543 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -457,13 +457,25 @@ class DomainRequest(TimeStampedModel): help_text="Determines if the user has a anything_else or not", ) - cisa_representative = models.ForeignKey( - "registrar.Contact", + cisa_representative_email = models.EmailField( null=True, blank=True, - related_name="cisa_representative_domain_requests", - on_delete=models.PROTECT, - help_text='Cisa Representative listed under "additional information" in the request form', + verbose_name="CISA regional representative email", + max_length=320, + ) + + cisa_representative_first_name = models.CharField( + null=True, + blank=True, + verbose_name="CISA regional representative first name", + db_index=True, + ) + + cisa_representative_last_name = models.CharField( + null=True, + blank=True, + verbose_name="CISA regional representative last name", + db_index=True, ) # This is a drop-in replacement for an has_cisa_representative() function. @@ -536,17 +548,18 @@ class DomainRequest(TimeStampedModel): We handle that here for def save(). """ - cisa_rep_is_not_none = self.cisa_representative is not None - cisa_first_name = None - # This ensures that if we have prefilled data, the form is prepopulated - if cisa_rep_is_not_none: - cisa_first_name = self.cisa_representative.first_name - self.has_cisa_representative = cisa_first_name is not None and cisa_first_name != "" + # NOTE: this relies on the fact that the first and last names of a CISA representative + # are required fields. Because of this, we can simplify the check to only look at the + # first name to determine whether or not a CISA representative was provided. + if self.cisa_representative_first_name is not None: + self.has_cisa_representative = self.cisa_representative_first_name != "" # This check is required to ensure that the form doesn't start out checked if self.has_cisa_representative is not None: - self.has_cisa_representative = cisa_first_name is not None and cisa_first_name != "" + self.has_cisa_representative = ( + self.cisa_representative_first_name != "" and self.cisa_representative_first_name is not None + ) # This ensures that if we have prefilled data, the form is prepopulated if self.anything_else is not None: diff --git a/src/registrar/templates/domain_request_additional_details.html b/src/registrar/templates/domain_request_additional_details.html index e8bdb5620..96c89d8ad 100644 --- a/src/registrar/templates/domain_request_additional_details.html +++ b/src/registrar/templates/domain_request_additional_details.html @@ -23,10 +23,10 @@ {# forms.0 is a small yes/no form that toggles the visibility of "cisa representative" formset #} -
- {% input_with_errors forms.1.first_name %} - {% input_with_errors forms.1.last_name %} - {% input_with_errors forms.1.email %} +
+ {% input_with_errors forms.1.cisa_representative_first_name %} + {% input_with_errors forms.1.cisa_representative_last_name %} + {% input_with_errors forms.1.cisa_representative_email %} {# forms.1 is a form for inputting the e-mail of a cisa representative #}
diff --git a/src/registrar/templates/domain_request_review.html b/src/registrar/templates/domain_request_review.html index 47b55ec12..bf8cac2d8 100644 --- a/src/registrar/templates/domain_request_review.html +++ b/src/registrar/templates/domain_request_review.html @@ -157,8 +157,16 @@ {% if step == Step.ADDITIONAL_DETAILS %} {% namespaced_url 'domain-request' step as domain_request_url %} - {% with title=form_titles|get_item:step value=domain_request.requested_domain.name|default:"Incomplete" %} - {% include "includes/summary_item.html" with title=title sub_header_text='CISA regional representative' value=domain_request.cisa_representative contact='true' heading_level=heading_level editable=True edit_link=domain_request_url custom_text_for_value_none='No' %} + {% with title=form_titles|get_item:step value=domain_request.requested_domain.has_additional_details|default:"Incomplete" %} + {% include "includes/summary_item.html" with title="Additional Details" value=value heading_level=heading_level editable=True edit_link=domain_request_url %} +

CISA Regional Representative

+ {% endwith %}

Anything else

diff --git a/src/registrar/templates/domain_request_status.html b/src/registrar/templates/domain_request_status.html index 938846714..065019a67 100644 --- a/src/registrar/templates/domain_request_status.html +++ b/src/registrar/templates/domain_request_status.html @@ -118,7 +118,15 @@ {# We always show this field even if None #} {% if DomainRequest %} - {% include "includes/summary_item.html" with title='Additional details' sub_header_text='CISA regional representative' value=DomainRequest.cisa_representative contact='true' custom_text_for_value_none='No' heading_level=heading_level %} +

CISA Regional Representative

+ +

Anything else

{% endif %} - {% endwith %}
diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index 3db6d0e3f..0e5a1a5c0 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -895,12 +895,9 @@ def completed_domain_request( # noqa if has_alternative_gov_domain: domain_request.alternative_domains.add(alt) if has_cisa_representative: - cisa_representative, _ = Contact.objects.get_or_create( - first_name="CISA-first-name", - last_name="CISA-last-name", - email="cisaRep@igorville.gov", - ) - domain_request.cisa_representative = cisa_representative + domain_request.cisa_representative_first_name="CISA-first-name" + domain_request.cisa_representative_last_name="CISA-last-name" + domain_request.cisa_representative_email="cisaRep@igorville.gov" return domain_request diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 06021d8e6..37ef977dc 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -2263,7 +2263,9 @@ class TestDomainRequestAdmin(MockEppLib): "no_other_contacts_rationale", "anything_else", "has_anything_else_text", - "cisa_representative", + "cisa_representative_first_name", + "cisa_representative_last_name," + "cisa_representative_email", "has_cisa_representative", "is_policy_acknowledged", "submission_date", @@ -2296,7 +2298,9 @@ class TestDomainRequestAdmin(MockEppLib): "no_other_contacts_rationale", "anything_else", "is_policy_acknowledged", - "cisa_representative", + "cisa_representative_first_name", + "cisa_representative_last_name," + "cisa_representative_email", ] self.assertEqual(readonly_fields, expected_fields) diff --git a/src/registrar/tests/test_views_request.py b/src/registrar/tests/test_views_request.py index 92ebea36e..92a3ddeb0 100644 --- a/src/registrar/tests/test_views_request.py +++ b/src/registrar/tests/test_views_request.py @@ -379,9 +379,9 @@ class DomainRequestTests(TestWithUser, WebTest): additional_details_result = additional_details_form.submit() # validate that data from this step are being saved domain_request = DomainRequest.objects.get() # there's only one - self.assertEqual(domain_request.cisa_representative.first_name, "CISA-first-name") - self.assertEqual(domain_request.cisa_representative.last_name, "CISA-last-name") - self.assertEqual(domain_request.cisa_representative.email, "FakeEmail@gmail.com") + self.assertEqual(domain_request.cisa_representative_first_name, "CISA-first-name") + self.assertEqual(domain_request.cisa_representative_last_name, "CISA-last-name") + self.assertEqual(domain_request.cisa_representative_email, "FakeEmail@gmail.com") self.assertEqual(domain_request.anything_else, "Nothing else.") # the post request should return a redirect to the next form in # the domain request page @@ -815,7 +815,7 @@ class DomainRequestTests(TestWithUser, WebTest): def test_yes_no_form_inits_yes_for_cisa_representative_and_anything_else(self): """On the Additional Details page, the yes/no form gets initialized with YES selected - for both yes/no radios if the domain request has a value for cisa_representative and + for both yes/no radios if the domain request has a values for cisa_representative_first_name and anything_else""" domain_request = completed_domain_request(user=self.user, has_anything_else=True, has_cisa_representative=True) @@ -905,16 +905,16 @@ class DomainRequestTests(TestWithUser, WebTest): """When a user submits the Additional Details form with no selected for all fields, the domain request's data gets wiped when submitted""" domain_request = completed_domain_request(name="nocisareps.gov", user=self.user) - domain_request.cisa_representative.first_name = "cisa-firstname1" - domain_request.cisa_representative.last_name = "cisa-lastname1" - domain_request.cisa_representative.email = "fake@faketown.gov" + domain_request.cisa_representative_first_name = "cisa-firstname1" + domain_request.cisa_representative_last_name = "cisa-lastname1" + domain_request.cisa_representative_email = "fake@faketown.gov" domain_request.save() # Make sure we have the data we need for the test self.assertEqual(domain_request.anything_else, "There is more") - self.assertEqual(domain_request.cisa_representative.first_name, "cisa-firstname1") - self.assertEqual(domain_request.cisa_representative.last_name, "cisa-lastname1") - self.assertEqual(domain_request.cisa_representative.email, "fake@faketown.gov") + self.assertEqual(domain_request.cisa_representative_first_name, "cisa-firstname1") + self.assertEqual(domain_request.cisa_representative_last_name, "cisa-lastname1") + self.assertEqual(domain_request.cisa_representative_email, "fake@faketown.gov") # prime the form by visiting /edit self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk})) @@ -947,16 +947,20 @@ class DomainRequestTests(TestWithUser, WebTest): self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - # Verify that the anything_else and cisa_representative have been deleted from the DB + # Verify that the anything_else and cisa_representative information have been deleted from the DB domain_request = DomainRequest.objects.get(requested_domain__name="nocisareps.gov") # Check that our data has been cleared self.assertEqual(domain_request.anything_else, None) - self.assertEqual(domain_request.cisa_representative, None) + self.assertEqual(domain_request.cisa_representative_first_name, None) + self.assertEqual(domain_request.cisa_representative_last_name, None) + self.assertEqual(domain_request.cisa_representative_email, None) # Double check the yes/no fields self.assertEqual(domain_request.has_anything_else_text, False) - self.assertEqual(domain_request.has_cisa_representative, False) + self.assertEqual(domain_request.cisa_representative_first_name, None) + self.assertEqual(domain_request.cisa_representative_last_name, None) + self.assertEqual(domain_request.cisa_representative_email, None) def test_submitting_additional_details_populates_cisa_representative_and_anything_else(self): """When a user submits the Additional Details form, @@ -967,7 +971,7 @@ class DomainRequestTests(TestWithUser, WebTest): # Make sure we have the data we need for the test self.assertEqual(domain_request.anything_else, None) - self.assertEqual(domain_request.cisa_representative, None) + self.assertEqual(domain_request.cisa_representative_first_name, None) # These fields should not be selected at all, since we haven't initialized the form yet self.assertEqual(domain_request.has_anything_else_text, None) @@ -1000,13 +1004,13 @@ class DomainRequestTests(TestWithUser, WebTest): self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - # Verify that the anything_else and cisa_representative exist in the db + # Verify that the anything_else and cisa_representative information exist in the db domain_request = DomainRequest.objects.get(requested_domain__name="cisareps.gov") self.assertEqual(domain_request.anything_else, "redandblue") - self.assertEqual(domain_request.cisa_representative.first_name, "cisa-firstname") - self.assertEqual(domain_request.cisa_representative.last_name, "cisa-lastname") - self.assertEqual(domain_request.cisa_representative.email, "test@faketest.gov") + self.assertEqual(domain_request.cisa_representative_first_name, "cisa-firstname") + self.assertEqual(domain_request.cisa_representative_last_name, "cisa-lastname") + self.assertEqual(domain_request.cisa_representative_email, "test@faketest.gov") self.assertEqual(domain_request.has_cisa_representative, True) self.assertEqual(domain_request.has_anything_else_text, True)