From a789b34bc001f467dff389e1f87d1b9a65d98e09 Mon Sep 17 00:00:00 2001
From: CocoByte
Date: Fri, 17 May 2024 12:19:54 -0600
Subject: [PATCH 001/200] Updates to Cisa Rep fields in Additional Details.
Refactored Cisa Rep to utilize ContactsModel
---
src/registrar/forms/domain_request_wizard.py | 68 ++++++++++++++++---
...tion_cisa_representative_email_and_more.py | 46 +++++++++++++
src/registrar/models/domain_information.py | 8 ++-
src/registrar/models/domain_request.py | 20 +++---
.../domain_request_additional_details.html | 9 +--
5 files changed, 126 insertions(+), 25 deletions(-)
create mode 100644 src/registrar/migrations/0095_remove_domaininformation_cisa_representative_email_and_more.py
diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py
index 0e9e87f9d..96ebbfebf 100644
--- a/src/registrar/forms/domain_request_wizard.py
+++ b/src/registrar/forms/domain_request_wizard.py
@@ -646,21 +646,72 @@ class NoOtherContactsForm(BaseDeletableRegistrarForm):
)
-class CisaRepresentativeForm(BaseDeletableRegistrarForm):
- cisa_representative_email = forms.EmailField(
- required=True,
+# class CisaRepresentativeForm(BaseDeletableRegistrarForm):
+# cisa_representative_email = forms.EmailField(
+# required=False,
+# max_length=None,
+# label="Your representative’s email",
+# validators=[
+# MaxLengthValidator(
+# 320,
+# message="Response must be less than 320 characters.",
+# )
+# ],
+# error_messages={
+# "invalid": ("Enter your email address in the required format, like name@example.com."),
+# "required": ("Enter the email address of your CISA regional representative."),
+# },
+# )
+
+class CisaRepresentativeForm(RegistrarForm):
+ JOIN = "cisa_representative"
+
+ logger.debug("GETTING CISA REP")
+
+ def to_database(self, obj):
+ logger.debug("SAVING CISA REP")
+ if not self.is_valid():
+ return
+ contact = getattr(obj, "cisa_representative", None)
+ logger.debug("EXISTING REP: %s" % contact)
+ if contact is not None and not contact.has_more_than_one_join("cisa_representative_domain_requests"):
+ # if contact exists in the database and is not joined to other entities
+ 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)
+ logger.debug("NEW REP: %s" % 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(
+ label="First name / given name",
+ error_messages={"required": "Enter your first name / given name."},
+ )
+ last_name = forms.CharField(
+ label="Last name / family name",
+ error_messages={"required": "Enter your last name / family name."},
+ )
+ email = forms.EmailField(
+ label="Email",
max_length=None,
- label="Your representative’s email",
+ error_messages={
+ "invalid": ("Enter your email address in the required format, like name@example.com."),
+ "required": ("Enter the email address of your CISA regional representative."),
+ },
validators=[
MaxLengthValidator(
320,
message="Response must be less than 320 characters.",
)
],
- error_messages={
- "invalid": ("Enter your email address in the required format, like name@example.com."),
- "required": ("Enter the email address of your CISA regional representative."),
- },
)
@@ -668,6 +719,7 @@ class CisaRepresentativeYesNoForm(BaseYesNoForm):
"""Yes/no toggle for the CISA regions question on additional details"""
form_is_checked = property(lambda self: self.domain_request.has_cisa_representative) # type: ignore
+ logger.debug("CHECKING FOR YES/NO CHECK -- %s" % form_is_checked)
field_name = "has_cisa_representative"
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
new file mode 100644
index 000000000..7e7fb5ee6
--- /dev/null
+++ b/src/registrar/migrations/0095_remove_domaininformation_cisa_representative_email_and_more.py
@@ -0,0 +1,46 @@
+# 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 264e322b8..bf6db1a28 100644
--- a/src/registrar/models/domain_information.py
+++ b/src/registrar/models/domain_information.py
@@ -206,11 +206,13 @@ class DomainInformation(TimeStampedModel):
verbose_name="Additional details",
)
- cisa_representative_email = models.EmailField(
+ cisa_representative = models.ForeignKey(
+ "registrar.Contact",
null=True,
blank=True,
- verbose_name="CISA regional representative",
- max_length=320,
+ related_name="cisa_representative_domain_requests_information",
+ on_delete=models.PROTECT,
+ help_text='Cisa Representative listed under "additional information" in the request form',
)
is_policy_acknowledged = models.BooleanField(
diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py
index 2501cdc87..a1668a6c8 100644
--- a/src/registrar/models/domain_request.py
+++ b/src/registrar/models/domain_request.py
@@ -457,11 +457,14 @@ class DomainRequest(TimeStampedModel):
help_text="Determines if the user has a anything_else or not",
)
- cisa_representative_email = models.EmailField(
+
+ cisa_representative = models.ForeignKey(
+ "registrar.Contact",
null=True,
blank=True,
- verbose_name="CISA regional representative",
- max_length=320,
+ related_name="cisa_representative_domain_requests",
+ on_delete=models.PROTECT,
+ help_text='Cisa Representative listed under "additional information" in the request form',
)
# This is a drop-in replacement for an has_cisa_representative() function.
@@ -534,15 +537,16 @@ class DomainRequest(TimeStampedModel):
We handle that here for def save().
"""
+ cisa_rep_is_not_none = self.cisa_representative is not None
+ logger.debug("CISA REPRESENTATIVE IS %s" % cisa_rep_is_not_none)
+
# This ensures that if we have prefilled data, the form is prepopulated
- if self.cisa_representative_email is not None:
- self.has_cisa_representative = self.cisa_representative_email != ""
+ if cisa_rep_is_not_none:
+ self.has_cisa_representative = True
# 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 = (
- self.cisa_representative_email != "" and self.cisa_representative_email is not None
- )
+ self.has_cisa_representative = cisa_rep_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 e13d3c7ee..e8bdb5620 100644
--- a/src/registrar/templates/domain_request_additional_details.html
+++ b/src/registrar/templates/domain_request_additional_details.html
@@ -9,7 +9,6 @@
{# commented out so it does not appear at this point on this page #}
{% endblock %}
-
{% block form_fields %}
- {% input_with_errors forms.1.cisa_representative_email %}
+ {% input_with_errors forms.1.first_name %}
+ {% input_with_errors forms.1.last_name %}
+ {% input_with_errors forms.1.email %}
{# forms.1 is a form for inputting the e-mail of a cisa representative #}
-
@@ -42,7 +41,6 @@
{% input_with_errors forms.2.has_anything_else_text %}
{% endwith %}
{# forms.2 is a small yes/no form that toggles the visibility of "cisa representative" formset #}
-
@@ -50,6 +48,5 @@
{% input_with_errors forms.3.anything_else %}
{% endwith %}
{# forms.3 is a form for inputting the e-mail of a cisa representative #}
-
{% endblock %}
From 321590ec5c22b21d7ba01b67708cbabb7583beab Mon Sep 17 00:00:00 2001
From: CocoByte
Date: Fri, 17 May 2024 12:28:44 -0600
Subject: [PATCH 002/200] updated summary
---
src/registrar/forms/domain_request_wizard.py | 17 -----------------
.../templates/domain_request_review.html | 2 +-
.../templates/domain_request_status.html | 2 +-
3 files changed, 2 insertions(+), 19 deletions(-)
diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py
index 96ebbfebf..9a2f206fd 100644
--- a/src/registrar/forms/domain_request_wizard.py
+++ b/src/registrar/forms/domain_request_wizard.py
@@ -646,23 +646,6 @@ class NoOtherContactsForm(BaseDeletableRegistrarForm):
)
-# class CisaRepresentativeForm(BaseDeletableRegistrarForm):
-# cisa_representative_email = forms.EmailField(
-# required=False,
-# max_length=None,
-# label="Your representative’s email",
-# validators=[
-# MaxLengthValidator(
-# 320,
-# message="Response must be less than 320 characters.",
-# )
-# ],
-# error_messages={
-# "invalid": ("Enter your email address in the required format, like name@example.com."),
-# "required": ("Enter the email address of your CISA regional representative."),
-# },
-# )
-
class CisaRepresentativeForm(RegistrarForm):
JOIN = "cisa_representative"
diff --git a/src/registrar/templates/domain_request_review.html b/src/registrar/templates/domain_request_review.html
index 5f359e95f..47b55ec12 100644
--- a/src/registrar/templates/domain_request_review.html
+++ b/src/registrar/templates/domain_request_review.html
@@ -158,7 +158,7 @@
{% 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_email heading_level=heading_level editable=True edit_link=domain_request_url custom_text_for_value_none='No' %}
+ {% 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' %}
{% endwith %}
Anything else
diff --git a/src/registrar/templates/domain_request_status.html b/src/registrar/templates/domain_request_status.html
index 0ea16e3a3..938846714 100644
--- a/src/registrar/templates/domain_request_status.html
+++ b/src/registrar/templates/domain_request_status.html
@@ -118,7 +118,7 @@
{# 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_email custom_text_for_value_none='No' heading_level=heading_level %}
+ {% 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 %}
Anything else
{% if DomainRequest.anything_else %}
From 377cb30818dd793f30d13d3b3b74fd650e2c2372 Mon Sep 17 00:00:00 2001
From: CocoByte
Date: Fri, 17 May 2024 12:32:07 -0600
Subject: [PATCH 003/200] fix deletable-ness of form
---
src/registrar/forms/domain_request_wizard.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py
index 9a2f206fd..4992703d3 100644
--- a/src/registrar/forms/domain_request_wizard.py
+++ b/src/registrar/forms/domain_request_wizard.py
@@ -646,7 +646,7 @@ class NoOtherContactsForm(BaseDeletableRegistrarForm):
)
-class CisaRepresentativeForm(RegistrarForm):
+class CisaRepresentativeForm(BaseDeletableRegistrarForm):
JOIN = "cisa_representative"
logger.debug("GETTING CISA REP")
From c5b841f6fb2938bad86cfec6d37558822733905c Mon Sep 17 00:00:00 2001
From: CocoByte
Date: Sun, 19 May 2024 15:39:00 -0600
Subject: [PATCH 004/200] Fixed form state check
---
src/registrar/forms/domain_request_wizard.py | 2 +-
src/registrar/models/domain_request.py | 7 ++++---
2 files changed, 5 insertions(+), 4 deletions(-)
diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py
index 4992703d3..19e994c90 100644
--- a/src/registrar/forms/domain_request_wizard.py
+++ b/src/registrar/forms/domain_request_wizard.py
@@ -685,9 +685,9 @@ class CisaRepresentativeForm(BaseDeletableRegistrarForm):
email = forms.EmailField(
label="Email",
max_length=None,
+ required=False,
error_messages={
"invalid": ("Enter your email address in the required format, like name@example.com."),
- "required": ("Enter the email address of your CISA regional representative."),
},
validators=[
MaxLengthValidator(
diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py
index a1668a6c8..cad599439 100644
--- a/src/registrar/models/domain_request.py
+++ b/src/registrar/models/domain_request.py
@@ -538,15 +538,16 @@ class DomainRequest(TimeStampedModel):
"""
cisa_rep_is_not_none = self.cisa_representative is not None
- logger.debug("CISA REPRESENTATIVE IS %s" % cisa_rep_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:
- self.has_cisa_representative = True
+ cisa_first_name = self.cisa_representative.first_name
+ self.has_cisa_representative = cisa_first_name is not None and cisa_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_rep_is_not_none
+ self.has_cisa_representative = cisa_first_name is not None and cisa_first_name != ""
# This ensures that if we have prefilled data, the form is prepopulated
if self.anything_else is not None:
From d9cc947f6ef8e07348774177ca1eb30c471555fb Mon Sep 17 00:00:00 2001
From: CocoByte
Date: Sun, 19 May 2024 16:02:34 -0600
Subject: [PATCH 005/200] linted
---
src/registrar/forms/domain_request_wizard.py | 1 -
src/registrar/models/domain_request.py | 1 -
2 files changed, 2 deletions(-)
diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py
index 19e994c90..8e0e2887f 100644
--- a/src/registrar/forms/domain_request_wizard.py
+++ b/src/registrar/forms/domain_request_wizard.py
@@ -702,7 +702,6 @@ class CisaRepresentativeYesNoForm(BaseYesNoForm):
"""Yes/no toggle for the CISA regions question on additional details"""
form_is_checked = property(lambda self: self.domain_request.has_cisa_representative) # type: ignore
- logger.debug("CHECKING FOR YES/NO CHECK -- %s" % form_is_checked)
field_name = "has_cisa_representative"
diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py
index cad599439..5adc3e91b 100644
--- a/src/registrar/models/domain_request.py
+++ b/src/registrar/models/domain_request.py
@@ -457,7 +457,6 @@ class DomainRequest(TimeStampedModel):
help_text="Determines if the user has a anything_else or not",
)
-
cisa_representative = models.ForeignKey(
"registrar.Contact",
null=True,
From 7dc9dba8335dfe89aefabb307619041e4f07657e Mon Sep 17 00:00:00 2001
From: CocoByte
Date: Mon, 20 May 2024 13:30:51 -0600
Subject: [PATCH 006/200] fixed unit tests (still need to rethink how to handle
deletion of CISA rep contacts)
---
src/registrar/admin.py | 4 +-
src/registrar/forms/domain_request_wizard.py | 5 ---
src/registrar/tests/common.py | 8 ++++
src/registrar/tests/test_admin.py | 4 +-
src/registrar/tests/test_views_request.py | 45 ++++++++++++--------
src/registrar/views/domain_request.py | 2 +-
6 files changed, 41 insertions(+), 27 deletions(-)
diff --git a/src/registrar/admin.py b/src/registrar/admin.py
index 9905cf340..a0c14efff 100644
--- a/src/registrar/admin.py
+++ b/src/registrar/admin.py
@@ -1376,7 +1376,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
"authorizing_official",
"other_contacts",
"no_other_contacts_rationale",
- "cisa_representative_email",
+ "cisa_representative",
]
},
),
@@ -1452,7 +1452,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
"no_other_contacts_rationale",
"anything_else",
"is_policy_acknowledged",
- "cisa_representative_email",
+ "cisa_representative",
]
autocomplete_fields = [
"approved_domain",
diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py
index 8e0e2887f..14df50f76 100644
--- a/src/registrar/forms/domain_request_wizard.py
+++ b/src/registrar/forms/domain_request_wizard.py
@@ -649,14 +649,10 @@ class NoOtherContactsForm(BaseDeletableRegistrarForm):
class CisaRepresentativeForm(BaseDeletableRegistrarForm):
JOIN = "cisa_representative"
- logger.debug("GETTING CISA REP")
-
def to_database(self, obj):
- logger.debug("SAVING CISA REP")
if not self.is_valid():
return
contact = getattr(obj, "cisa_representative", None)
- logger.debug("EXISTING REP: %s" % contact)
if contact is not None and not contact.has_more_than_one_join("cisa_representative_domain_requests"):
# if contact exists in the database and is not joined to other entities
super().to_database(contact)
@@ -665,7 +661,6 @@ class CisaRepresentativeForm(BaseDeletableRegistrarForm):
# in either case, create a new contact and update it
contact = Contact()
super().to_database(contact)
- logger.debug("NEW REP: %s" % contact)
obj.cisa_representative = contact
obj.save()
diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py
index be7065403..5978523f5 100644
--- a/src/registrar/tests/common.py
+++ b/src/registrar/tests/common.py
@@ -812,6 +812,7 @@ def completed_domain_request(
has_alternative_gov_domain=True,
has_about_your_organization=True,
has_anything_else=True,
+ has_cisa_representative=True,
status=DomainRequest.DomainRequestStatus.STARTED,
user=False,
submitter=False,
@@ -893,6 +894,13 @@ def completed_domain_request(
domain_request.current_websites.add(current)
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
return domain_request
diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py
index 4a6e76e3d..06021d8e6 100644
--- a/src/registrar/tests/test_admin.py
+++ b/src/registrar/tests/test_admin.py
@@ -2263,7 +2263,7 @@ class TestDomainRequestAdmin(MockEppLib):
"no_other_contacts_rationale",
"anything_else",
"has_anything_else_text",
- "cisa_representative_email",
+ "cisa_representative",
"has_cisa_representative",
"is_policy_acknowledged",
"submission_date",
@@ -2296,7 +2296,7 @@ class TestDomainRequestAdmin(MockEppLib):
"no_other_contacts_rationale",
"anything_else",
"is_policy_acknowledged",
- "cisa_representative_email",
+ "cisa_representative",
]
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 2b577b41a..4856eeef7 100644
--- a/src/registrar/tests/test_views_request.py
+++ b/src/registrar/tests/test_views_request.py
@@ -369,7 +369,9 @@ class DomainRequestTests(TestWithUser, WebTest):
additional_details_form["additional_details-has_cisa_representative"] = "True"
additional_details_form["additional_details-has_anything_else_text"] = "True"
- additional_details_form["additional_details-cisa_representative_email"] = "FakeEmail@gmail.com"
+ additional_details_form["additional_details-first_name"] = "CISA-first-name"
+ additional_details_form["additional_details-last_name"] = "CISA-last-name"
+ additional_details_form["additional_details-email"] = "FakeEmail@gmail.com"
additional_details_form["additional_details-anything_else"] = "Nothing else."
# test next button
@@ -377,7 +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_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
@@ -814,8 +818,7 @@ class DomainRequestTests(TestWithUser, WebTest):
for both yes/no radios if the domain request has a value for cisa_representative and
anything_else"""
- domain_request = completed_domain_request(user=self.user, has_anything_else=True)
- domain_request.cisa_representative_email = "test@igorville.gov"
+ domain_request = completed_domain_request(user=self.user, has_anything_else=True, has_cisa_representative=True)
domain_request.anything_else = "1234"
domain_request.save()
@@ -867,12 +870,11 @@ class DomainRequestTests(TestWithUser, WebTest):
"""On the Additional details page, the form preselects "no" when has_cisa_representative
and anything_else is no"""
- domain_request = completed_domain_request(user=self.user, has_anything_else=False)
+ domain_request = completed_domain_request(user=self.user, has_anything_else=False, has_cisa_representative=False)
# Unlike the other contacts form, the no button is tracked with these boolean fields.
# This means that we should expect this to correlate with the no button.
domain_request.has_anything_else_text = False
- domain_request.has_cisa_representative = False
domain_request.save()
# prime the form by visiting /edit
@@ -891,7 +893,7 @@ class DomainRequestTests(TestWithUser, WebTest):
# Check the cisa representative yes/no field
yes_no_cisa = additional_details_form["additional_details-has_cisa_representative"].value
- self.assertEquals(yes_no_cisa, "False")
+ self.assertEquals(yes_no_cisa, None)
# Check the anything else yes/no field
yes_no_anything_else = additional_details_form["additional_details-has_anything_else_text"].value
@@ -901,12 +903,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_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_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}))
@@ -944,7 +950,7 @@ class DomainRequestTests(TestWithUser, WebTest):
# Check that our data has been cleared
self.assertEqual(domain_request.anything_else, None)
- self.assertEqual(domain_request.cisa_representative_email, None)
+ self.assertEqual(domain_request.cisa_representative, None)
# Double check the yes/no fields
self.assertEqual(domain_request.has_anything_else_text, False)
@@ -953,11 +959,11 @@ class DomainRequestTests(TestWithUser, WebTest):
def test_submitting_additional_details_populates_cisa_representative_and_anything_else(self):
"""When a user submits the Additional Details form,
the domain request's data gets submitted"""
- domain_request = completed_domain_request(name="cisareps.gov", user=self.user, has_anything_else=False)
+ domain_request = completed_domain_request(name="cisareps.gov", user=self.user, has_anything_else=False, has_cisa_representative=False)
# Make sure we have the data we need for the test
self.assertEqual(domain_request.anything_else, None)
- self.assertEqual(domain_request.cisa_representative_email, None)
+ self.assertEqual(domain_request.cisa_representative, 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)
@@ -980,7 +986,9 @@ class DomainRequestTests(TestWithUser, WebTest):
# Set fields to true, and set data on those fields
additional_details_form["additional_details-has_cisa_representative"] = "True"
additional_details_form["additional_details-has_anything_else_text"] = "True"
- additional_details_form["additional_details-cisa_representative_email"] = "test@faketest.gov"
+ additional_details_form["additional_details-first_name"] = "cisa-firstname"
+ additional_details_form["additional_details-last_name"] = "cisa-lastname"
+ additional_details_form["additional_details-email"] = "test@faketest.gov"
additional_details_form["additional_details-anything_else"] = "redandblue"
# Submit the form
@@ -992,14 +1000,16 @@ class DomainRequestTests(TestWithUser, WebTest):
domain_request = DomainRequest.objects.get(requested_domain__name="cisareps.gov")
self.assertEqual(domain_request.anything_else, "redandblue")
- 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)
def test_if_cisa_representative_yes_no_form_is_yes_then_field_is_required(self):
"""Applicants with a cisa representative must provide a value"""
- domain_request = completed_domain_request(name="cisareps.gov", user=self.user, has_anything_else=False)
+ domain_request = completed_domain_request(name="cisareps.gov", user=self.user, has_anything_else=False, has_cisa_representative=False)
# prime the form by visiting /edit
self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk}))
@@ -1024,7 +1034,8 @@ class DomainRequestTests(TestWithUser, WebTest):
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
- self.assertContains(response, "Enter the email address of your CISA regional representative.")
+ self.assertContains(response, "Enter your first name / given name.")
+ self.assertContains(response, "Enter your last name / family name.")
def test_if_anything_else_yes_no_form_is_yes_then_field_is_required(self):
"""Applicants with a anything else must provide a value"""
@@ -1059,7 +1070,7 @@ class DomainRequestTests(TestWithUser, WebTest):
def test_additional_details_form_fields_required(self):
"""When a user submits the Additional Details form without checking the
has_cisa_representative and has_anything_else_text fields, the form should deny this action"""
- domain_request = completed_domain_request(name="cisareps.gov", user=self.user, has_anything_else=False)
+ domain_request = completed_domain_request(name="cisareps.gov", user=self.user, has_anything_else=False, has_cisa_representative=False)
self.assertEqual(domain_request.has_anything_else_text, None)
self.assertEqual(domain_request.has_cisa_representative, None)
diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py
index f93976138..d6ae99538 100644
--- a/src/registrar/views/domain_request.py
+++ b/src/registrar/views/domain_request.py
@@ -366,7 +366,7 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
or self.domain_request.no_other_contacts_rationale is not None
),
"additional_details": (
- (self.domain_request.anything_else is not None and self.domain_request.cisa_representative_email)
+ (self.domain_request.anything_else is not None and self.domain_request.has_cisa_representative)
or self.domain_request.is_policy_acknowledged is not None
),
"requirements": self.domain_request.is_policy_acknowledged is not None,
From 6ecd9e59fc654be8461c2d4cba6510748a7770b0 Mon Sep 17 00:00:00 2001
From: CocoByte
Date: Mon, 20 May 2024 14:17:38 -0600
Subject: [PATCH 007/200] updated logic for deletion of CISA contact (deleting
association only...)
---
src/registrar/forms/domain_request_wizard.py | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py
index 14df50f76..74b1f9924 100644
--- a/src/registrar/forms/domain_request_wizard.py
+++ b/src/registrar/forms/domain_request_wizard.py
@@ -654,8 +654,13 @@ class CisaRepresentativeForm(BaseDeletableRegistrarForm):
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 contact exists in the database and is not joined to other entities
- super().to_database(contact)
+ 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
From 9fecf1155895846f73f2604b49be845f3fb68a11 Mon Sep 17 00:00:00 2001
From: CocoByte
Date: Tue, 21 May 2024 14:07:04 -0600
Subject: [PATCH 008/200] linted
---
src/registrar/forms/domain_request_wizard.py | 2 +-
src/registrar/tests/common.py | 2 +-
src/registrar/tests/test_views_request.py | 16 ++++++++++++----
3 files changed, 14 insertions(+), 6 deletions(-)
diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py
index 74b1f9924..ecb84e62a 100644
--- a/src/registrar/forms/domain_request_wizard.py
+++ b/src/registrar/forms/domain_request_wizard.py
@@ -657,7 +657,7 @@ class CisaRepresentativeForm(BaseDeletableRegistrarForm):
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?
+ # 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)
diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py
index 5978523f5..3db6d0e3f 100644
--- a/src/registrar/tests/common.py
+++ b/src/registrar/tests/common.py
@@ -806,7 +806,7 @@ def create_ready_domain():
# TODO in 1793: Remove the federal agency/updated federal agency fields
-def completed_domain_request(
+def completed_domain_request( # noqa
has_other_contacts=True,
has_current_website=True,
has_alternative_gov_domain=True,
diff --git a/src/registrar/tests/test_views_request.py b/src/registrar/tests/test_views_request.py
index 4856eeef7..92ebea36e 100644
--- a/src/registrar/tests/test_views_request.py
+++ b/src/registrar/tests/test_views_request.py
@@ -870,7 +870,9 @@ class DomainRequestTests(TestWithUser, WebTest):
"""On the Additional details page, the form preselects "no" when has_cisa_representative
and anything_else is no"""
- domain_request = completed_domain_request(user=self.user, has_anything_else=False, has_cisa_representative=False)
+ domain_request = completed_domain_request(
+ user=self.user, has_anything_else=False, has_cisa_representative=False
+ )
# Unlike the other contacts form, the no button is tracked with these boolean fields.
# This means that we should expect this to correlate with the no button.
@@ -959,7 +961,9 @@ class DomainRequestTests(TestWithUser, WebTest):
def test_submitting_additional_details_populates_cisa_representative_and_anything_else(self):
"""When a user submits the Additional Details form,
the domain request's data gets submitted"""
- domain_request = completed_domain_request(name="cisareps.gov", user=self.user, has_anything_else=False, has_cisa_representative=False)
+ domain_request = completed_domain_request(
+ name="cisareps.gov", user=self.user, has_anything_else=False, has_cisa_representative=False
+ )
# Make sure we have the data we need for the test
self.assertEqual(domain_request.anything_else, None)
@@ -1009,7 +1013,9 @@ class DomainRequestTests(TestWithUser, WebTest):
def test_if_cisa_representative_yes_no_form_is_yes_then_field_is_required(self):
"""Applicants with a cisa representative must provide a value"""
- domain_request = completed_domain_request(name="cisareps.gov", user=self.user, has_anything_else=False, has_cisa_representative=False)
+ domain_request = completed_domain_request(
+ name="cisareps.gov", user=self.user, has_anything_else=False, has_cisa_representative=False
+ )
# prime the form by visiting /edit
self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk}))
@@ -1070,7 +1076,9 @@ class DomainRequestTests(TestWithUser, WebTest):
def test_additional_details_form_fields_required(self):
"""When a user submits the Additional Details form without checking the
has_cisa_representative and has_anything_else_text fields, the form should deny this action"""
- domain_request = completed_domain_request(name="cisareps.gov", user=self.user, has_anything_else=False, has_cisa_representative=False)
+ domain_request = completed_domain_request(
+ name="cisareps.gov", user=self.user, has_anything_else=False, has_cisa_representative=False
+ )
self.assertEqual(domain_request.has_anything_else_text, None)
self.assertEqual(domain_request.has_cisa_representative, None)
From daeabaf83395635532081d6a91aad413c1f9cbcd Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Fri, 31 May 2024 14:34:02 -0600
Subject: [PATCH 009/200] add action needed
---
src/registrar/admin.py | 21 ++++++++++++++
...0096_domainrequest_action_needed_reason.py | 28 +++++++++++++++++++
src/registrar/models/domain_request.py | 14 ++++++++++
src/registrar/utility/errors.py | 3 ++
4 files changed, 66 insertions(+)
create mode 100644 src/registrar/migrations/0096_domainrequest_action_needed_reason.py
diff --git a/src/registrar/admin.py b/src/registrar/admin.py
index 2d6559570..7bb3fb229 100644
--- a/src/registrar/admin.py
+++ b/src/registrar/admin.py
@@ -216,6 +216,7 @@ class DomainRequestAdminForm(forms.ModelForm):
status = cleaned_data.get("status")
investigator = cleaned_data.get("investigator")
rejection_reason = cleaned_data.get("rejection_reason")
+ action_needed_reason = cleaned_data.get("action_needed_reason")
# Get the old status
initial_status = self.initial.get("status", None)
@@ -239,6 +240,8 @@ class DomainRequestAdminForm(forms.ModelForm):
# If the status is rejected, a rejection reason must exist
if status == DomainRequest.DomainRequestStatus.REJECTED:
self._check_for_valid_rejection_reason(rejection_reason)
+ elif status == DomainRequest.DomainRequestStatus.IN_REVIEW:
+ self._check_for_valid_action_needed_reason(action_needed_reason)
return cleaned_data
@@ -262,6 +265,23 @@ class DomainRequestAdminForm(forms.ModelForm):
return is_valid
+ def _check_for_valid_action_needed_reason(self, action_needed_reason) -> bool:
+ """
+ Checks if the action_needed_reason field is not none.
+ Adds form errors on failure.
+ """
+ is_valid = False
+ error_message = None
+ if action_needed_reason is None or action_needed_reason == "":
+ error_message = FSMDomainRequestError.get_error_message(FSMErrorCodes.NO_ACTION_NEEDED_REASON)
+ else:
+ is_valid = True
+
+ if error_message is not None:
+ self.add_error("action_needed_reason", error_message)
+
+ return is_valid
+
def _check_for_valid_investigator(self, investigator) -> bool:
"""
Checks if the investigator field is not none, and is staff.
@@ -1361,6 +1381,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
"fields": [
"status",
"rejection_reason",
+ "action_needed_reason",
"investigator",
"creator",
"submitter",
diff --git a/src/registrar/migrations/0096_domainrequest_action_needed_reason.py b/src/registrar/migrations/0096_domainrequest_action_needed_reason.py
new file mode 100644
index 000000000..c8f729f69
--- /dev/null
+++ b/src/registrar/migrations/0096_domainrequest_action_needed_reason.py
@@ -0,0 +1,28 @@
+# Generated by Django 4.2.10 on 2024-05-31 20:32
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("registrar", "0095_user_middle_name_user_title"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="domainrequest",
+ name="action_needed_reason",
+ field=models.TextField(
+ blank=True,
+ choices=[
+ ("eligibility_unclear", "Unclear organization eligibility"),
+ ("questionable_authorizing_official", "Questionable authorizing official"),
+ ("ALREADY_HAS_DOMAINS", "Already has domains"),
+ ("bad_name", "Doesn’t meet naming requirements"),
+ ("other", "Other (no auto-email sent)"),
+ ],
+ null=True,
+ ),
+ ),
+ ]
diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py
index 2501cdc87..4a970fb06 100644
--- a/src/registrar/models/domain_request.py
+++ b/src/registrar/models/domain_request.py
@@ -244,6 +244,14 @@ class DomainRequest(TimeStampedModel):
ORGANIZATION_ELIGIBILITY = "org_not_eligible", "Org not eligible for a .gov domain"
NAMING_REQUIREMENTS = "naming_not_met", "Naming requirements not met"
OTHER = "other", "Other/Unspecified"
+
+ class ActionNeededReasons(models.TextChoices):
+ """Defines common"""
+ ELIGIBILITY_UNCLEAR = ("eligibility_unclear", "Unclear organization eligibility")
+ QUESTIONABLE_AUTHORIZING_OFFICIAL = ("questionable_authorizing_official" , "Questionable authorizing official")
+ ALREADY_HAS_DOMAINS = ("ALREADY_HAS_DOMAINS", "Already has domains")
+ BAD_NAME = ("bad_name", "Doesn’t meet naming requirements")
+ OTHER = ("other", "Other (no auto-email sent)")
# #### Internal fields about the domain request #####
status = FSMField(
@@ -258,6 +266,12 @@ class DomainRequest(TimeStampedModel):
blank=True,
)
+ action_needed_reason = models.TextField(
+ choices=ActionNeededReasons.choices,
+ null=True,
+ blank=True,
+ )
+
federal_agency = models.ForeignKey(
"registrar.FederalAgency",
on_delete=models.PROTECT,
diff --git a/src/registrar/utility/errors.py b/src/registrar/utility/errors.py
index f00c59bd0..740651c04 100644
--- a/src/registrar/utility/errors.py
+++ b/src/registrar/utility/errors.py
@@ -79,6 +79,7 @@ class FSMErrorCodes(IntEnum):
- 3 INVESTIGATOR_NOT_STAFF Investigator is a non-staff user
- 4 INVESTIGATOR_NOT_SUBMITTER The form submitter is not the investigator
- 5 NO_REJECTION_REASON No rejection reason is specified
+ - 6 NO_ACTION_NEEDED_REASON No action needed reason is specified
"""
APPROVE_DOMAIN_IN_USE = 1
@@ -86,6 +87,7 @@ class FSMErrorCodes(IntEnum):
INVESTIGATOR_NOT_STAFF = 3
INVESTIGATOR_NOT_SUBMITTER = 4
NO_REJECTION_REASON = 5
+ NO_ACTION_NEEDED_REASON = 6
class FSMDomainRequestError(Exception):
@@ -100,6 +102,7 @@ class FSMDomainRequestError(Exception):
FSMErrorCodes.INVESTIGATOR_NOT_STAFF: ("Investigator is not a staff user."),
FSMErrorCodes.INVESTIGATOR_NOT_SUBMITTER: ("Only the assigned investigator can make this change."),
FSMErrorCodes.NO_REJECTION_REASON: ("A rejection reason is required."),
+ FSMErrorCodes.NO_ACTION_NEEDED_REASON: ("A action needed reason is required."),
}
def __init__(self, *args, code=None, **kwargs):
From c772c71707712559303123b4db331eabdf018704 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Fri, 31 May 2024 15:19:28 -0600
Subject: [PATCH 010/200] Initial logic
Generalize our javascript handler for the hide rejection reason field
---
src/registrar/admin.py | 2 +
src/registrar/assets/js/get-gov-admin.js | 92 +++++++++++++++++-------
2 files changed, 68 insertions(+), 26 deletions(-)
diff --git a/src/registrar/admin.py b/src/registrar/admin.py
index 7bb3fb229..1147d34bf 100644
--- a/src/registrar/admin.py
+++ b/src/registrar/admin.py
@@ -1584,6 +1584,8 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
# The opposite of this condition is acceptable (rejected -> other status and rejection_reason)
# because we clean up the rejection reason in the transition in the model.
error_message = FSMDomainRequestError.get_error_message(FSMErrorCodes.NO_REJECTION_REASON)
+ elif obj.status == models.DomainRequest.DomainRequestStatus.ACTION_NEEDED and not obj.action_needed_reason:
+ error_message = FSMDomainRequestError.get_error_message(FSMErrorCodes.NO_ACTION_NEEDED_REASON)
else:
# This is an fsm in model which will throw an error if the
# transition condition is violated, so we roll back the
diff --git a/src/registrar/assets/js/get-gov-admin.js b/src/registrar/assets/js/get-gov-admin.js
index 702364cba..dc347bd1f 100644
--- a/src/registrar/assets/js/get-gov-admin.js
+++ b/src/registrar/assets/js/get-gov-admin.js
@@ -296,28 +296,46 @@ function initializeWidgetOnList(list, parentId) {
}
/** An IIFE for admin in DjangoAdmin to listen to changes on the domain request
- * status select and to show/hide the rejection reason
+ * status select and to show/hide fields like rejection reason or action needed reason
*/
(function (){
- let rejectionReasonFormGroup = document.querySelector('.field-rejection_reason')
+ // Hides or shows a given field based off of the current value of the given status selector,
+ // and stores its state in the session.
+ function showHideFieldsOnStatusChange(elementToHide, statusToShowOn, sessionObjectName) {
+ if (elementToHide) {
+ let statusSelect = document.getElementById('id_status')
+
+ let shouldHide = statusSelect.value != statusToShowOn
+ // Initial handling of parentFormGroup display
+ hideOrShowDomObject(elementToHide, hideObject=shouldHide)
+
+ // Listen to change events and handle rejectionReasonFormGroup display, then save status to session storage
+ statusSelect.addEventListener('change', function() {
+ // Hide the object only if we're in an invalid state
+ shouldHide = statusSelect.value != statusToShowOn
- if (rejectionReasonFormGroup) {
- let statusSelect = document.getElementById('id_status')
+ // Hide the action needed field if we're on a different status type
+ hideOrShowDomObject(elementToHide, hideObject=shouldHide)
- // Initial handling of rejectionReasonFormGroup display
- if (statusSelect.value != 'rejected')
- rejectionReasonFormGroup.style.display = 'none';
+ // Add a key to our session storage to track if we should hide the object automatically
+ // (to catch the edge case where you click the back button)
+ if (!shouldHide){
+ sessionStorage.removeItem(sessionObjectName);
+ }else {
+ sessionStorage.setItem(sessionObjectName, 'true');
+ }
+ });
+ }
+ }
- // Listen to change events and handle rejectionReasonFormGroup display, then save status to session storage
- statusSelect.addEventListener('change', function() {
- if (statusSelect.value == 'rejected') {
- rejectionReasonFormGroup.style.display = 'block';
- sessionStorage.removeItem('hideRejectionReason');
- } else {
- rejectionReasonFormGroup.style.display = 'none';
- sessionStorage.setItem('hideRejectionReason', 'true');
+ function hideOrShowDomObject(object, hideObject){
+ if (object){
+ if (hideObject){
+ object.classList.add("display-none");
+ }else {
+ object.classList.remove("display-none");
}
- });
+ }
}
// Listen to Back/Forward button navigation and handle rejectionReasonFormGroup display based on session storage
@@ -325,17 +343,39 @@ function initializeWidgetOnList(list, parentId) {
// When you navigate using forward/back after changing status but not saving, when you land back on the DA page the
// status select will say (for example) Rejected but the selected option can be something else. To manage the show/hide
// accurately for this edge case, we use cache and test for the back/forward navigation.
- const observer = new PerformanceObserver((list) => {
- list.getEntries().forEach((entry) => {
- if (entry.type === "back_forward") {
- if (sessionStorage.getItem('hideRejectionReason'))
- document.querySelector('.field-rejection_reason').style.display = 'none';
- else
- document.querySelector('.field-rejection_reason').style.display = 'block';
- }
+ function handleBackButtonObserver(fieldsToObserve) {
+ const observer = new PerformanceObserver((list) => {
+ list.getEntries().forEach((entry) => {
+ if (entry.type === "back_forward") {
+ fieldsToObserve.forEach((fieldName) => {
+ fieldClass = `.field-${fieldName}`
+ field = document.querySelector(fieldClass)
+ if (field) {
+ shouldHideField = sessionStorage.getItem(`hide_${fieldName}`)
+ hideOrShowDomObject(field, hideObject=shouldHideField)
+ }else {
+ console.error(`Could not find field with class ${fieldClass}`)
+ }
+ });
+ }
+ });
});
- });
- observer.observe({ type: "navigation" });
+ observer.observe({ type: "navigation" });
+ }
+
+ function handleStatusChanges() {
+ // Show/hide the rejection reason
+ let rejectionReasonFormGroup = document.querySelector('.field-rejection_reason')
+ showHideFieldsOnStatusChange(rejectionReasonFormGroup, "rejected", "hide_rejection_reason");
+
+ // Show/hude the action needed reason
+ let actionNeededReasonFormGroup = document.querySelector('.field-action_needed_reason');
+ showHideFieldsOnStatusChange(actionNeededReasonFormGroup, "action_needed", "hide_action_needed_reason");
+ }
+ handleStatusChanges();
+
+ fieldsToObserve = ["rejection_reason", "action_needed_reason"]
+ handleBackButtonObserver(fieldsToObserve);
})();
/** An IIFE for toggling the submit bar on domain request forms
From 8af8a8c670bb234922a6c98a55bcc2a5bc492dff Mon Sep 17 00:00:00 2001
From: CocoByte
Date: Fri, 31 May 2024 15:39:40 -0600
Subject: [PATCH 011/200] Refactored storage of CISA rep info to NOT use
Contacts model
---
src/registrar/admin.py | 10 +++-
src/registrar/forms/domain_request_wizard.py | 38 ++-----------
...cisa_representative_first_name_and_more.py | 55 +++++++++++++++++++
...tion_cisa_representative_email_and_more.py | 46 ----------------
src/registrar/models/domain_information.py | 22 ++++++--
src/registrar/models/domain_request.py | 37 +++++++++----
.../domain_request_additional_details.html | 8 +--
.../templates/domain_request_review.html | 12 +++-
.../templates/domain_request_status.html | 11 +++-
src/registrar/tests/common.py | 9 +--
src/registrar/tests/test_admin.py | 8 ++-
src/registrar/tests/test_views_request.py | 40 ++++++++------
12 files changed, 164 insertions(+), 132 deletions(-)
create mode 100644 src/registrar/migrations/0095_domaininformation_cisa_representative_first_name_and_more.py
delete mode 100644 src/registrar/migrations/0095_remove_domaininformation_cisa_representative_email_and_more.py
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.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
+
+ {% if domain_request.cisa_representative_first_name %}
+ {{domain_request.cisa_representative_first_name}} {{domain_request.cisa_representative_last_name}}
+ {% else %}
+ No
+ {% endif %}
+
{% 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
+
+ {% if domain_request.cisa_representative_first_name %}
+ {{domain_request.cisa_representative_first_name}} {{domain_request.cisa_representative_last_name}}
+ {% else %}
+ No
+ {% endif %}
+
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)
From b6195356735485d173eb24868f9d1f524a54fc6e Mon Sep 17 00:00:00 2001
From: CocoByte
Date: Fri, 31 May 2024 15:46:58 -0600
Subject: [PATCH 012/200] fixed migrations
---
...aininformation_cisa_representative_first_name_and_more.py} | 4 ++--
src/registrar/templates/domain_request_review.html | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
rename src/registrar/migrations/{0095_domaininformation_cisa_representative_first_name_and_more.py => 0096_domaininformation_cisa_representative_first_name_and_more.py} (94%)
diff --git a/src/registrar/migrations/0095_domaininformation_cisa_representative_first_name_and_more.py b/src/registrar/migrations/0096_domaininformation_cisa_representative_first_name_and_more.py
similarity index 94%
rename from src/registrar/migrations/0095_domaininformation_cisa_representative_first_name_and_more.py
rename to src/registrar/migrations/0096_domaininformation_cisa_representative_first_name_and_more.py
index f6f66ab23..ec85c9732 100644
--- a/src/registrar/migrations/0095_domaininformation_cisa_representative_first_name_and_more.py
+++ b/src/registrar/migrations/0096_domaininformation_cisa_representative_first_name_and_more.py
@@ -1,4 +1,4 @@
-# Generated by Django 4.2.10 on 2024-05-31 21:21
+# Generated by Django 4.2.10 on 2024-05-31 21:46
from django.db import migrations, models
@@ -6,7 +6,7 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
- ("registrar", "0094_create_groups_v12"),
+ ("registrar", "0095_user_middle_name_user_title"),
]
operations = [
diff --git a/src/registrar/templates/domain_request_review.html b/src/registrar/templates/domain_request_review.html
index bf8cac2d8..604a9b689 100644
--- a/src/registrar/templates/domain_request_review.html
+++ b/src/registrar/templates/domain_request_review.html
@@ -157,7 +157,7 @@
{% 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.has_additional_details|default:"Incomplete" %}
+ {% with title=form_titles|get_item:step value=domain_request.requested_domain.has_additional_details %}
{% 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
From 6a7da08dbcc9417f2b32862828e0e4056a58389c Mon Sep 17 00:00:00 2001
From: CocoByte
Date: Fri, 31 May 2024 15:51:55 -0600
Subject: [PATCH 013/200] linted
---
src/registrar/admin.py | 2 +-
src/registrar/forms/domain_request_wizard.py | 1 -
src/registrar/models/domain_information.py | 2 +-
src/registrar/models/domain_request.py | 2 +-
src/registrar/tests/common.py | 6 +++---
src/registrar/tests/test_admin.py | 6 ++----
6 files changed, 8 insertions(+), 11 deletions(-)
diff --git a/src/registrar/admin.py b/src/registrar/admin.py
index 72025db45..f3e8c81cd 100644
--- a/src/registrar/admin.py
+++ b/src/registrar/admin.py
@@ -1457,7 +1457,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
"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 9df4a4c1a..bb34ac56e 100644
--- a/src/registrar/forms/domain_request_wizard.py
+++ b/src/registrar/forms/domain_request_wizard.py
@@ -671,7 +671,6 @@ class CisaRepresentativeForm(BaseDeletableRegistrarForm):
)
-
class CisaRepresentativeYesNoForm(BaseYesNoForm):
"""Yes/no toggle for the CISA regions question on additional details"""
diff --git a/src/registrar/models/domain_information.py b/src/registrar/models/domain_information.py
index d2300c784..95af94010 100644
--- a/src/registrar/models/domain_information.py
+++ b/src/registrar/models/domain_information.py
@@ -219,7 +219,7 @@ class DomainInformation(TimeStampedModel):
verbose_name="CISA regional representative first name",
db_index=True,
)
-
+
cisa_representative_last_name = models.CharField(
null=True,
blank=True,
diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py
index f84451543..457558f9c 100644
--- a/src/registrar/models/domain_request.py
+++ b/src/registrar/models/domain_request.py
@@ -470,7 +470,7 @@ class DomainRequest(TimeStampedModel):
verbose_name="CISA regional representative first name",
db_index=True,
)
-
+
cisa_representative_last_name = models.CharField(
null=True,
blank=True,
diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py
index 0e5a1a5c0..2fdc64d44 100644
--- a/src/registrar/tests/common.py
+++ b/src/registrar/tests/common.py
@@ -895,9 +895,9 @@ def completed_domain_request( # noqa
if has_alternative_gov_domain:
domain_request.alternative_domains.add(alt)
if has_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"
+ 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 e4c748e8c..6ef9f9cdf 100644
--- a/src/registrar/tests/test_admin.py
+++ b/src/registrar/tests/test_admin.py
@@ -2264,8 +2264,7 @@ class TestDomainRequestAdmin(MockEppLib):
"anything_else",
"has_anything_else_text",
"cisa_representative_first_name",
- "cisa_representative_last_name,"
- "cisa_representative_email",
+ "cisa_representative_last_name," "cisa_representative_email",
"has_cisa_representative",
"is_policy_acknowledged",
"submission_date",
@@ -2299,8 +2298,7 @@ class TestDomainRequestAdmin(MockEppLib):
"anything_else",
"is_policy_acknowledged",
"cisa_representative_first_name",
- "cisa_representative_last_name,"
- "cisa_representative_email",
+ "cisa_representative_last_name," "cisa_representative_email",
]
self.assertEqual(readonly_fields, expected_fields)
From 882d7d59d449d6f4796f8e89a82b2665a0f3a92a Mon Sep 17 00:00:00 2001
From: CocoByte
Date: Fri, 31 May 2024 15:53:13 -0600
Subject: [PATCH 014/200] updated error messages
---
src/registrar/forms/domain_request_wizard.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py
index bb34ac56e..4ef8f2943 100644
--- a/src/registrar/forms/domain_request_wizard.py
+++ b/src/registrar/forms/domain_request_wizard.py
@@ -649,11 +649,11 @@ class NoOtherContactsForm(BaseDeletableRegistrarForm):
class CisaRepresentativeForm(BaseDeletableRegistrarForm):
cisa_representative_first_name = forms.CharField(
label="First name / given name",
- error_messages={"required": "Enter your first name / given name."},
+ error_messages={"required": "Enter the first name / given name of the CISA regional representative."},
)
cisa_representative_last_name = forms.CharField(
label="Last name / family name",
- error_messages={"required": "Enter your last name / family name."},
+ error_messages={"required": "Enter the last name / family name of the CISA regional representative."},
)
cisa_representative_email = forms.EmailField(
label="Your representative’s email (optional)",
From 8876e47a02d0f64e1637ebb48752974779180707 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Mon, 3 Jun 2024 14:23:15 -0600
Subject: [PATCH 015/200] Fix errs
---
src/registrar/admin.py | 2 +-
src/registrar/assets/js/get-gov-admin.js | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/registrar/admin.py b/src/registrar/admin.py
index 1147d34bf..565bdb011 100644
--- a/src/registrar/admin.py
+++ b/src/registrar/admin.py
@@ -240,7 +240,7 @@ class DomainRequestAdminForm(forms.ModelForm):
# If the status is rejected, a rejection reason must exist
if status == DomainRequest.DomainRequestStatus.REJECTED:
self._check_for_valid_rejection_reason(rejection_reason)
- elif status == DomainRequest.DomainRequestStatus.IN_REVIEW:
+ elif status == DomainRequest.DomainRequestStatus.ACTION_NEEDED:
self._check_for_valid_action_needed_reason(action_needed_reason)
return cleaned_data
diff --git a/src/registrar/assets/js/get-gov-admin.js b/src/registrar/assets/js/get-gov-admin.js
index dc347bd1f..a12feb70d 100644
--- a/src/registrar/assets/js/get-gov-admin.js
+++ b/src/registrar/assets/js/get-gov-admin.js
@@ -370,7 +370,7 @@ function initializeWidgetOnList(list, parentId) {
// Show/hude the action needed reason
let actionNeededReasonFormGroup = document.querySelector('.field-action_needed_reason');
- showHideFieldsOnStatusChange(actionNeededReasonFormGroup, "action_needed", "hide_action_needed_reason");
+ showHideFieldsOnStatusChange(actionNeededReasonFormGroup, "action needed", "hide_action_needed_reason");
}
handleStatusChanges();
From ca2149d3edf5b5c3f0b14366b96b0a8071a240d1 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Mon, 3 Jun 2024 14:29:47 -0600
Subject: [PATCH 016/200] Cleanup
---
src/registrar/models/domain_request.py | 10 ++++++----
1 file changed, 6 insertions(+), 4 deletions(-)
diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py
index 4a970fb06..eb67fd9cc 100644
--- a/src/registrar/models/domain_request.py
+++ b/src/registrar/models/domain_request.py
@@ -704,9 +704,10 @@ class DomainRequest(TimeStampedModel):
if self.status == self.DomainRequestStatus.APPROVED:
self.delete_and_clean_up_domain("in_review")
-
- if self.status == self.DomainRequestStatus.REJECTED:
+ elif self.status == self.DomainRequestStatus.REJECTED:
self.rejection_reason = None
+ elif self.status == self.DomainRequestStatus.ACTION_NEEDED:
+ self.action_needed_reason = None
literal = DomainRequest.DomainRequestStatus.IN_REVIEW
# Check if the tuple exists, then grab its value
@@ -736,8 +737,7 @@ class DomainRequest(TimeStampedModel):
if self.status == self.DomainRequestStatus.APPROVED:
self.delete_and_clean_up_domain("reject_with_prejudice")
-
- if self.status == self.DomainRequestStatus.REJECTED:
+ elif self.status == self.DomainRequestStatus.REJECTED:
self.rejection_reason = None
literal = DomainRequest.DomainRequestStatus.ACTION_NEEDED
@@ -793,6 +793,8 @@ class DomainRequest(TimeStampedModel):
if self.status == self.DomainRequestStatus.REJECTED:
self.rejection_reason = None
+ elif self.status == self.DomainRequestStatus.ACTION_NEEDED:
+ self.action_needed_reason = None
# == Send out an email == #
self._send_status_update_email(
From e4285998e7b54e826ca53c35c4bb2dd16b50c305 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Mon, 3 Jun 2024 14:32:43 -0600
Subject: [PATCH 017/200] add let
---
src/registrar/assets/js/get-gov-admin.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/registrar/assets/js/get-gov-admin.js b/src/registrar/assets/js/get-gov-admin.js
index a12feb70d..e5a3bfb62 100644
--- a/src/registrar/assets/js/get-gov-admin.js
+++ b/src/registrar/assets/js/get-gov-admin.js
@@ -374,7 +374,7 @@ function initializeWidgetOnList(list, parentId) {
}
handleStatusChanges();
- fieldsToObserve = ["rejection_reason", "action_needed_reason"]
+ let fieldsToObserve = ["rejection_reason", "action_needed_reason"]
handleBackButtonObserver(fieldsToObserve);
})();
From 21c44a4110011af11d8938c0358d9f22aba88e6e Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Mon, 3 Jun 2024 15:35:18 -0600
Subject: [PATCH 018/200] Add emails (no content)
---
...0096_domainrequest_action_needed_reason.py | 2 +-
src/registrar/models/domain_request.py | 37 +++++++++++++++++--
.../already_has_domains.txt | 0
.../already_has_domains_subject.txt | 0
.../emails/action_needed_reasons/bad_name.txt | 0
.../bad_name_subject.txt | 0
.../eligibility_unclear.txt | 0
.../eligibility_unclear_subject.txt | 0
.../questionable_authorizing_official.txt | 0
...stionable_authorizing_official_subject.txt | 0
10 files changed, 35 insertions(+), 4 deletions(-)
create mode 100644 src/registrar/templates/emails/action_needed_reasons/already_has_domains.txt
create mode 100644 src/registrar/templates/emails/action_needed_reasons/already_has_domains_subject.txt
create mode 100644 src/registrar/templates/emails/action_needed_reasons/bad_name.txt
create mode 100644 src/registrar/templates/emails/action_needed_reasons/bad_name_subject.txt
create mode 100644 src/registrar/templates/emails/action_needed_reasons/eligibility_unclear.txt
create mode 100644 src/registrar/templates/emails/action_needed_reasons/eligibility_unclear_subject.txt
create mode 100644 src/registrar/templates/emails/action_needed_reasons/questionable_authorizing_official.txt
create mode 100644 src/registrar/templates/emails/action_needed_reasons/questionable_authorizing_official_subject.txt
diff --git a/src/registrar/migrations/0096_domainrequest_action_needed_reason.py b/src/registrar/migrations/0096_domainrequest_action_needed_reason.py
index c8f729f69..61fac3606 100644
--- a/src/registrar/migrations/0096_domainrequest_action_needed_reason.py
+++ b/src/registrar/migrations/0096_domainrequest_action_needed_reason.py
@@ -18,7 +18,7 @@ class Migration(migrations.Migration):
choices=[
("eligibility_unclear", "Unclear organization eligibility"),
("questionable_authorizing_official", "Questionable authorizing official"),
- ("ALREADY_HAS_DOMAINS", "Already has domains"),
+ ("already_has_domains", "Already has domains"),
("bad_name", "Doesn’t meet naming requirements"),
("other", "Other (no auto-email sent)"),
],
diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py
index eb67fd9cc..659526ecc 100644
--- a/src/registrar/models/domain_request.py
+++ b/src/registrar/models/domain_request.py
@@ -1,6 +1,6 @@
from __future__ import annotations
from typing import Union
-
+import os
import logging
from django.apps import apps
@@ -249,7 +249,7 @@ class DomainRequest(TimeStampedModel):
"""Defines common"""
ELIGIBILITY_UNCLEAR = ("eligibility_unclear", "Unclear organization eligibility")
QUESTIONABLE_AUTHORIZING_OFFICIAL = ("questionable_authorizing_official" , "Questionable authorizing official")
- ALREADY_HAS_DOMAINS = ("ALREADY_HAS_DOMAINS", "Already has domains")
+ ALREADY_HAS_DOMAINS = ("already_has_domains", "Already has domains")
BAD_NAME = ("bad_name", "Doesn’t meet naming requirements")
OTHER = ("other", "Other (no auto-email sent)")
@@ -725,7 +725,7 @@ class DomainRequest(TimeStampedModel):
target=DomainRequestStatus.ACTION_NEEDED,
conditions=[domain_is_not_active, investigator_exists_and_is_staff],
)
- def action_needed(self):
+ def action_needed(self, send_email=True):
"""Send back an domain request that is under investigation or rejected.
This action is logged.
@@ -745,6 +745,37 @@ class DomainRequest(TimeStampedModel):
action_needed = literal if literal is not None else "Action Needed"
logger.info(f"A status change occurred. {self} was changed to '{action_needed}'")
+ # Send out an email if an action needed reason exists
+ if self.action_needed_reason and self.action_needed_reason != self.ActionNeededReasons.OTHER:
+ self._send_action_needed_reason_email(send_email)
+
+ def _send_action_needed_reason_email(self, send_email=True):
+ """Sends out an automatic email for each valid action needed reason provided"""
+
+ email_template_name: str = ""
+ email_template_subject_name: str = ""
+ can_send_email = True
+ match self.action_needed_reason:
+ # Add to this match if you need to pass in a custom filename for these templates.
+ case self.ActionNeededReasons.OTHER, _:
+ # Unknown and other are default cases - do nothing
+ can_send_email = False
+
+ if can_send_email:
+ # Assumes that the template name matches the action needed reason if nothing is specified.
+ # This is so you can override if you need, or have this taken care of for you.
+ if not email_template_name and not email_template_subject_name:
+ reason = self.action_needed_reason.value
+ email_template_name = f"{reason}.txt"
+ email_template_subject_name = f"{reason}_subject.txt"
+
+ self._send_status_update_email(
+ new_status="action needed",
+ email_template=f"emails/action_needed_reasons/{email_template_name}",
+ email_template_subject=f"emails/action_needed_reasons/{email_template_subject_name}",
+ send_email=send_email,
+ )
+
@transition(
field="status",
source=[
diff --git a/src/registrar/templates/emails/action_needed_reasons/already_has_domains.txt b/src/registrar/templates/emails/action_needed_reasons/already_has_domains.txt
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/registrar/templates/emails/action_needed_reasons/already_has_domains_subject.txt b/src/registrar/templates/emails/action_needed_reasons/already_has_domains_subject.txt
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/registrar/templates/emails/action_needed_reasons/bad_name.txt b/src/registrar/templates/emails/action_needed_reasons/bad_name.txt
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/registrar/templates/emails/action_needed_reasons/bad_name_subject.txt b/src/registrar/templates/emails/action_needed_reasons/bad_name_subject.txt
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/registrar/templates/emails/action_needed_reasons/eligibility_unclear.txt b/src/registrar/templates/emails/action_needed_reasons/eligibility_unclear.txt
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/registrar/templates/emails/action_needed_reasons/eligibility_unclear_subject.txt b/src/registrar/templates/emails/action_needed_reasons/eligibility_unclear_subject.txt
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/registrar/templates/emails/action_needed_reasons/questionable_authorizing_official.txt b/src/registrar/templates/emails/action_needed_reasons/questionable_authorizing_official.txt
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/registrar/templates/emails/action_needed_reasons/questionable_authorizing_official_subject.txt b/src/registrar/templates/emails/action_needed_reasons/questionable_authorizing_official_subject.txt
new file mode 100644
index 000000000..e69de29bb
From 65cb2dfb412b9c24d74f8cf850ce8dda88e73b6e Mon Sep 17 00:00:00 2001
From: CocoByte
Date: Mon, 3 Jun 2024 17:42:13 -0600
Subject: [PATCH 019/200] Fixing tests
---
src/registrar/tests/test_admin.py | 6 ++++--
src/registrar/tests/test_views_request.py | 12 ++++++------
src/registrar/views/domain_request.py | 1 -
3 files changed, 10 insertions(+), 9 deletions(-)
diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py
index 6ef9f9cdf..c65d6a16b 100644
--- a/src/registrar/tests/test_admin.py
+++ b/src/registrar/tests/test_admin.py
@@ -2264,7 +2264,8 @@ class TestDomainRequestAdmin(MockEppLib):
"anything_else",
"has_anything_else_text",
"cisa_representative_first_name",
- "cisa_representative_last_name," "cisa_representative_email",
+ "cisa_representative_last_name",
+ "cisa_representative_email",
"has_cisa_representative",
"is_policy_acknowledged",
"submission_date",
@@ -2298,7 +2299,8 @@ class TestDomainRequestAdmin(MockEppLib):
"anything_else",
"is_policy_acknowledged",
"cisa_representative_first_name",
- "cisa_representative_last_name," "cisa_representative_email",
+ "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 1dd718e55..1c5683424 100644
--- a/src/registrar/tests/test_views_request.py
+++ b/src/registrar/tests/test_views_request.py
@@ -366,9 +366,9 @@ class DomainRequestTests(TestWithUser, WebTest):
additional_details_form["additional_details-has_cisa_representative"] = "True"
additional_details_form["additional_details-has_anything_else_text"] = "True"
- additional_details_form["additional_details-first_name"] = "CISA-first-name"
- additional_details_form["additional_details-last_name"] = "CISA-last-name"
- additional_details_form["additional_details-email"] = "FakeEmail@gmail.com"
+ additional_details_form["additional_details-cisa_representative_first_name"] = "CISA-first-name"
+ additional_details_form["additional_details-cisa_representative_last_name"] = "CISA-last-name"
+ additional_details_form["additional_details-cisa_representative_email"] = "FakeEmail@gmail.com"
additional_details_form["additional_details-anything_else"] = "Nothing else."
# test next button
@@ -991,9 +991,9 @@ class DomainRequestTests(TestWithUser, WebTest):
# Set fields to true, and set data on those fields
additional_details_form["additional_details-has_cisa_representative"] = "True"
additional_details_form["additional_details-has_anything_else_text"] = "True"
- additional_details_form["additional_details-first_name"] = "cisa-firstname"
- additional_details_form["additional_details-last_name"] = "cisa-lastname"
- additional_details_form["additional_details-email"] = "test@faketest.gov"
+ additional_details_form["additional_details-cisa_representative_first_name"] = "cisa-firstname"
+ additional_details_form["additional_details-cisa_representative_last_name"] = "cisa-lastname"
+ additional_details_form["additional_details-cisa_representative_email"] = "test@faketest.gov"
additional_details_form["additional_details-anything_else"] = "redandblue"
# Submit the form
diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py
index 901a817e4..6213f95f4 100644
--- a/src/registrar/views/domain_request.py
+++ b/src/registrar/views/domain_request.py
@@ -388,7 +388,6 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
modal_heading = "You are about to submit an incomplete request"
has_profile_flag = flag_is_active(self.request, "profile_feature")
- logger.debug("PROFILE FLAG is %s" % has_profile_flag)
context = {
"form_titles": self.TITLES,
From 73826c306d3fd585edfbbee6a8316ad7dead3ee5 Mon Sep 17 00:00:00 2001
From: CocoByte
Date: Mon, 3 Jun 2024 18:38:40 -0600
Subject: [PATCH 020/200] More unit test fixes
---
src/registrar/tests/test_admin.py | 2 +-
src/registrar/tests/test_views_request.py | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py
index c65d6a16b..990365692 100644
--- a/src/registrar/tests/test_admin.py
+++ b/src/registrar/tests/test_admin.py
@@ -2263,9 +2263,9 @@ class TestDomainRequestAdmin(MockEppLib):
"no_other_contacts_rationale",
"anything_else",
"has_anything_else_text",
+ "cisa_representative_email",
"cisa_representative_first_name",
"cisa_representative_last_name",
- "cisa_representative_email",
"has_cisa_representative",
"is_policy_acknowledged",
"submission_date",
diff --git a/src/registrar/tests/test_views_request.py b/src/registrar/tests/test_views_request.py
index 1c5683424..36e365e85 100644
--- a/src/registrar/tests/test_views_request.py
+++ b/src/registrar/tests/test_views_request.py
@@ -1041,8 +1041,8 @@ class DomainRequestTests(TestWithUser, WebTest):
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
- self.assertContains(response, "Enter your first name / given name.")
- self.assertContains(response, "Enter your last name / family name.")
+ self.assertContains(response, "Enter the first name / given name of the CISA regional representative.")
+ self.assertContains(response, "Enter the last name / family name of the CISA regional representative.")
def test_if_anything_else_yes_no_form_is_yes_then_field_is_required(self):
"""Applicants with a anything else must provide a value"""
From 9e1c2553910ee2f6539ba04613ef4b722e729db4 Mon Sep 17 00:00:00 2001
From: CocoByte
Date: Mon, 3 Jun 2024 23:48:49 -0600
Subject: [PATCH 021/200] more fixes
---
src/registrar/templates/domain_request_review.html | 3 +++
1 file changed, 3 insertions(+)
diff --git a/src/registrar/templates/domain_request_review.html b/src/registrar/templates/domain_request_review.html
index 604a9b689..80980ac27 100644
--- a/src/registrar/templates/domain_request_review.html
+++ b/src/registrar/templates/domain_request_review.html
@@ -163,6 +163,9 @@
{% if domain_request.cisa_representative_first_name %}
{{domain_request.cisa_representative_first_name}} {{domain_request.cisa_representative_last_name}}
+ {% if domain_request.cisa_representative_email %}
+ {{domain_request.cisa_representative_email}}
+ {% endif %}
{% else %}
No
{% endif %}
From 245150ad32bb344e8ae81dfe4c962564af1d3da4 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Tue, 4 Jun 2024 12:20:35 -0600
Subject: [PATCH 022/200] Add email content
---
.../already_has_domains.txt | 44 +++++++++++++++++++
.../already_has_domains_subject.txt | 1 +
.../emails/action_needed_reasons/bad_name.txt | 29 ++++++++++++
.../bad_name_subject.txt | 1 +
.../eligibility_unclear.txt | 32 ++++++++++++++
.../eligibility_unclear_subject.txt | 1 +
.../questionable_authorizing_official.txt | 33 ++++++++++++++
...stionable_authorizing_official_subject.txt | 1 +
8 files changed, 142 insertions(+)
diff --git a/src/registrar/templates/emails/action_needed_reasons/already_has_domains.txt b/src/registrar/templates/emails/action_needed_reasons/already_has_domains.txt
index e69de29bb..2740fda69 100644
--- a/src/registrar/templates/emails/action_needed_reasons/already_has_domains.txt
+++ b/src/registrar/templates/emails/action_needed_reasons/already_has_domains.txt
@@ -0,0 +1,44 @@
+{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
+Hi, {{ domain_request.submitter.first_name }}.
+We've identified an action that you’ll need to complete before we continue reviewing your .gov domain request.
+DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
+REQUEST RECEIVED ON: {{ domain_request.submission_date|date }}
+STATUS: Action needed
+
+
+ORGANIZATION ALREADY HAS A .GOV DOMAIN
+We've reviewed your domain request, but your organization already has at least one other .gov domain. We need more information about your rationale for registering another .gov domain.
+In general, there are two reasons we will approve an additional domain:
+You determine a current .gov domain name will be replaced
+We determine an additional domain name is appropriate
+
+
+WE LIMIT ADDITIONAL DOMAIN NAMES
+Our practice is to only approve one domain per online service per government organization, evaluating additional requests on a case-by-case basis.
+There are two core reasons we limit additional domains:
+We want to minimize your operational and security load, which increases with each additional domain.
+Fewer domains allow us to take protective, namespace-wide security actions faster and without undue dependencies.
+If you’re attempting to claim an additional domain to prevent others from obtaining it, that’s not necessary. .Gov domains are only available to U.S.-based government organizations, and we don’t operate on a first come, first served basis. We'll only assign a domain to the organization whose real name or services actually correspond to the domain name.
+
+
+CONSIDER USING A SUBDOMAIN
+Using a subdomain of an existing domain (e.g., service.domain.gov) is a common approach to logically divide your namespace while still maintaining an association with your existing domain name. Subdomains can also be delegated to allow an affiliated entity to manage their own DNS settings.
+
+
+ACTION NEEDED
+FOR A REPLACEMENT DOMAIN: If you’re requesting a new domain that will replace your current domain name, we can allow for a transition period where both are registered to your organization. Afterwards, we will reclaim and retire the legacy name.
+Reply to this email. Tell us how many months your organization needs to maintain your current .gov domain and conduct a transition to a new one. Detail why that period of time is needed.
+FOR AN ADDITIONAL DOMAIN: If you’re requesting an additional domain and not replacing your existing one, we’ll need more information to support that request.
+Reply to this email. Detail why you believe another domain is necessary for your organization, and why a subdomain won’t meet your needs.
+
+
+If you have questions or comments, include those in your reply.
+
+----------------------------------------------------------------
+
+The .gov team
+Contact us:
+Learn about .gov
+
+The .gov registry is a part of the Cybersecurity and Infrastructure Security Agency (CISA)
+{% endautoescape %}
\ No newline at end of file
diff --git a/src/registrar/templates/emails/action_needed_reasons/already_has_domains_subject.txt b/src/registrar/templates/emails/action_needed_reasons/already_has_domains_subject.txt
index e69de29bb..7ca332ddd 100644
--- a/src/registrar/templates/emails/action_needed_reasons/already_has_domains_subject.txt
+++ b/src/registrar/templates/emails/action_needed_reasons/already_has_domains_subject.txt
@@ -0,0 +1 @@
+Update on your .gov request: {{ domain_request.requested_domain.name }}
\ No newline at end of file
diff --git a/src/registrar/templates/emails/action_needed_reasons/bad_name.txt b/src/registrar/templates/emails/action_needed_reasons/bad_name.txt
index e69de29bb..e8003e83d 100644
--- a/src/registrar/templates/emails/action_needed_reasons/bad_name.txt
+++ b/src/registrar/templates/emails/action_needed_reasons/bad_name.txt
@@ -0,0 +1,29 @@
+{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
+Hi, {{ domain_request.submitter.first_name }}.
+We've identified an action that you’ll need to complete before we continue reviewing your .gov domain request.
+DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
+REQUEST RECEIVED ON: {{ domain_request.submission_date|date }}
+STATUS: Action needed
+
+
+DOMAIN NAME DOES NOT MEET .GOV REQUIREMENTS
+We've reviewed your domain request and, unfortunately, it does not meet our naming requirements.
+Domains should uniquely identify a government organization and be clear to the general public. Read more about naming requirements for your type of organization .
+
+
+ACTION NEEDED
+First, we need you to identify a new domain name that meets our naming requirements for your type of organization. Then, log in to the registrar and update the name in your domain request. Once you submit your updated request, we’ll resume the adjudication process.
+If you have questions or want to discuss potential domain names, reply to this email.
+
+
+THANK YOU
+.Gov helps the public identify official, trusted information. Thank you for requesting a .gov domain.
+
+----------------------------------------------------------------
+
+The .gov team
+Contact us:
+Learn about .gov
+
+The .gov registry is a part of the Cybersecurity and Infrastructure Security Agency (CISA)
+{% endautoescape %}
\ No newline at end of file
diff --git a/src/registrar/templates/emails/action_needed_reasons/bad_name_subject.txt b/src/registrar/templates/emails/action_needed_reasons/bad_name_subject.txt
index e69de29bb..7ca332ddd 100644
--- a/src/registrar/templates/emails/action_needed_reasons/bad_name_subject.txt
+++ b/src/registrar/templates/emails/action_needed_reasons/bad_name_subject.txt
@@ -0,0 +1 @@
+Update on your .gov request: {{ domain_request.requested_domain.name }}
\ No newline at end of file
diff --git a/src/registrar/templates/emails/action_needed_reasons/eligibility_unclear.txt b/src/registrar/templates/emails/action_needed_reasons/eligibility_unclear.txt
index e69de29bb..c3f6d14de 100644
--- a/src/registrar/templates/emails/action_needed_reasons/eligibility_unclear.txt
+++ b/src/registrar/templates/emails/action_needed_reasons/eligibility_unclear.txt
@@ -0,0 +1,32 @@
+{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
+Hi, {{ domain_request.submitter.first_name }}.
+We've identified an action that you’ll need to complete before we continue reviewing your .gov domain request.
+DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
+REQUEST RECEIVED ON: {{ domain_request.submission_date|date }}
+STATUS: Action needed
+
+
+ORGANIZATION MAY NOT MEET ELIGIBILITY REQUIREMENTS
+We've reviewed your domain request, but we need more information about the organization you represent:
+
+{{ domain_request.organization_name }}
+
+.Gov domains are only available to official US-based government organizations, not simply those that provide a public benefit. We lack clear documentation that demonstrates your organization is eligible for a .gov domain.
+
+
+ACTION NEEDED
+Reply to this email with links to (or copies of) your authorizing legislation, your founding charter or bylaws, recent election results, or other similar documentation. Without this, we can’t continue our review and your request will likely be rejected.
+If you have questions or comments, include those in your reply.
+
+
+THANK YOU
+.Gov helps the public identify official, trusted information. Thank you for requesting a .gov domain.
+
+----------------------------------------------------------------
+
+The .gov team
+Contact us:
+Learn about .gov
+
+The .gov registry is a part of the Cybersecurity and Infrastructure Security Agency (CISA)
+{% endautoescape %}
\ No newline at end of file
diff --git a/src/registrar/templates/emails/action_needed_reasons/eligibility_unclear_subject.txt b/src/registrar/templates/emails/action_needed_reasons/eligibility_unclear_subject.txt
index e69de29bb..7ca332ddd 100644
--- a/src/registrar/templates/emails/action_needed_reasons/eligibility_unclear_subject.txt
+++ b/src/registrar/templates/emails/action_needed_reasons/eligibility_unclear_subject.txt
@@ -0,0 +1 @@
+Update on your .gov request: {{ domain_request.requested_domain.name }}
\ No newline at end of file
diff --git a/src/registrar/templates/emails/action_needed_reasons/questionable_authorizing_official.txt b/src/registrar/templates/emails/action_needed_reasons/questionable_authorizing_official.txt
index e69de29bb..1531520cd 100644
--- a/src/registrar/templates/emails/action_needed_reasons/questionable_authorizing_official.txt
+++ b/src/registrar/templates/emails/action_needed_reasons/questionable_authorizing_official.txt
@@ -0,0 +1,33 @@
+{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
+Hi, {{ domain_request.submitter.first_name }}.
+We've identified an action that you’ll need to complete before we continue reviewing your .gov domain request.
+DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
+REQUEST RECEIVED ON: {{ domain_request.submission_date|date }}
+STATUS: Action needed
+
+
+AUTHORIZING OFFICIAL DOES NOT MEET ELIGIBILITY REQUIREMENTS
+We've reviewed your domain request, but we need more information about the authorizing official listed on the request:
+{{ domain_request.authorizing_official.get_formatted_name }}
+{{ domain_request.authorizing_official.title }}
+
+We expect an authorizing official to be someone in a role of significant, executive responsibility within the organization. Our guidelines are open-ended to accommodate the wide variety of government organizations that are eligible for .gov domains, but the person you listed does not meet our expectations for your type of organization. Read more about our guidelines for authorizing officials.
+
+
+ACTION NEEDED
+Reply to this email with a justification for naming {{ domain_request.authorizing_official.get_formatted_name }} as the authorizing official. If you have questions or comments, include those in your reply.
+
+Alternatively, you can log in to the registrar and enter a different authorizing official for this domain request. Once you submit your updated request, we’ll resume the adjudication process.
+
+
+THANK YOU
+.Gov helps the public identify official, trusted information. Thank you for requesting a .gov domain.
+
+----------------------------------------------------------------
+
+The .gov team
+Contact us:
+Learn about .gov
+
+The .gov registry is a part of the Cybersecurity and Infrastructure Security Agency (CISA)
+{% endautoescape %}
\ No newline at end of file
diff --git a/src/registrar/templates/emails/action_needed_reasons/questionable_authorizing_official_subject.txt b/src/registrar/templates/emails/action_needed_reasons/questionable_authorizing_official_subject.txt
index e69de29bb..7ca332ddd 100644
--- a/src/registrar/templates/emails/action_needed_reasons/questionable_authorizing_official_subject.txt
+++ b/src/registrar/templates/emails/action_needed_reasons/questionable_authorizing_official_subject.txt
@@ -0,0 +1 @@
+Update on your .gov request: {{ domain_request.requested_domain.name }}
\ No newline at end of file
From d120f31581576ecf31fe91f12260ddfab2e61f71 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Tue, 4 Jun 2024 12:56:47 -0600
Subject: [PATCH 023/200] Fix bug
---
src/registrar/models/domain_request.py | 2 +-
.../emails/action_needed_reasons/already_has_domains.txt | 5 +++--
.../emails/action_needed_reasons/eligibility_unclear.txt | 3 +--
.../questionable_authorizing_official.txt | 4 ++--
4 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py
index 659526ecc..26527ba98 100644
--- a/src/registrar/models/domain_request.py
+++ b/src/registrar/models/domain_request.py
@@ -765,7 +765,7 @@ class DomainRequest(TimeStampedModel):
# Assumes that the template name matches the action needed reason if nothing is specified.
# This is so you can override if you need, or have this taken care of for you.
if not email_template_name and not email_template_subject_name:
- reason = self.action_needed_reason.value
+ reason = self.action_needed_reason
email_template_name = f"{reason}.txt"
email_template_subject_name = f"{reason}_subject.txt"
diff --git a/src/registrar/templates/emails/action_needed_reasons/already_has_domains.txt b/src/registrar/templates/emails/action_needed_reasons/already_has_domains.txt
index 2740fda69..660f2e5c3 100644
--- a/src/registrar/templates/emails/action_needed_reasons/already_has_domains.txt
+++ b/src/registrar/templates/emails/action_needed_reasons/already_has_domains.txt
@@ -9,8 +9,8 @@ STATUS: Action needed
ORGANIZATION ALREADY HAS A .GOV DOMAIN
We've reviewed your domain request, but your organization already has at least one other .gov domain. We need more information about your rationale for registering another .gov domain.
In general, there are two reasons we will approve an additional domain:
-You determine a current .gov domain name will be replaced
-We determine an additional domain name is appropriate
+- You determine a current .gov domain name will be replaced
+- We determine an additional domain name is appropriate
WE LIMIT ADDITIONAL DOMAIN NAMES
@@ -28,6 +28,7 @@ Using a subdomain of an existing domain (e.g., service.domain.gov) is a common a
ACTION NEEDED
FOR A REPLACEMENT DOMAIN: If you’re requesting a new domain that will replace your current domain name, we can allow for a transition period where both are registered to your organization. Afterwards, we will reclaim and retire the legacy name.
Reply to this email. Tell us how many months your organization needs to maintain your current .gov domain and conduct a transition to a new one. Detail why that period of time is needed.
+
FOR AN ADDITIONAL DOMAIN: If you’re requesting an additional domain and not replacing your existing one, we’ll need more information to support that request.
Reply to this email. Detail why you believe another domain is necessary for your organization, and why a subdomain won’t meet your needs.
diff --git a/src/registrar/templates/emails/action_needed_reasons/eligibility_unclear.txt b/src/registrar/templates/emails/action_needed_reasons/eligibility_unclear.txt
index c3f6d14de..47f958c67 100644
--- a/src/registrar/templates/emails/action_needed_reasons/eligibility_unclear.txt
+++ b/src/registrar/templates/emails/action_needed_reasons/eligibility_unclear.txt
@@ -8,8 +8,7 @@ STATUS: Action needed
ORGANIZATION MAY NOT MEET ELIGIBILITY REQUIREMENTS
We've reviewed your domain request, but we need more information about the organization you represent:
-
-{{ domain_request.organization_name }}
+- {{ domain_request.organization_name }}
.Gov domains are only available to official US-based government organizations, not simply those that provide a public benefit. We lack clear documentation that demonstrates your organization is eligible for a .gov domain.
diff --git a/src/registrar/templates/emails/action_needed_reasons/questionable_authorizing_official.txt b/src/registrar/templates/emails/action_needed_reasons/questionable_authorizing_official.txt
index 1531520cd..e4a0d0e89 100644
--- a/src/registrar/templates/emails/action_needed_reasons/questionable_authorizing_official.txt
+++ b/src/registrar/templates/emails/action_needed_reasons/questionable_authorizing_official.txt
@@ -8,8 +8,8 @@ STATUS: Action needed
AUTHORIZING OFFICIAL DOES NOT MEET ELIGIBILITY REQUIREMENTS
We've reviewed your domain request, but we need more information about the authorizing official listed on the request:
-{{ domain_request.authorizing_official.get_formatted_name }}
-{{ domain_request.authorizing_official.title }}
+- {{ domain_request.authorizing_official.get_formatted_name }}
+- {{ domain_request.authorizing_official.title }}
We expect an authorizing official to be someone in a role of significant, executive responsibility within the organization. Our guidelines are open-ended to accommodate the wide variety of government organizations that are eligible for .gov domains, but the person you listed does not meet our expectations for your type of organization. Read more about our guidelines for authorizing officials.
From 805b7ca6c73a1657cd865a1d7e4a9c1e3fcff033 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Tue, 4 Jun 2024 14:44:19 -0600
Subject: [PATCH 024/200] Add unit tests
---
src/registrar/admin.py | 9 +--
src/registrar/assets/js/get-gov-admin.js | 19 +++++-
src/registrar/models/domain_request.py | 27 ++++----
src/registrar/tests/test_admin.py | 79 +++++++++++++++++++++---
4 files changed, 106 insertions(+), 28 deletions(-)
diff --git a/src/registrar/admin.py b/src/registrar/admin.py
index 565bdb011..283281ad2 100644
--- a/src/registrar/admin.py
+++ b/src/registrar/admin.py
@@ -270,14 +270,9 @@ class DomainRequestAdminForm(forms.ModelForm):
Checks if the action_needed_reason field is not none.
Adds form errors on failure.
"""
- is_valid = False
- error_message = None
- if action_needed_reason is None or action_needed_reason == "":
+ is_valid = action_needed_reason is not None and action_needed_reason != ""
+ if not is_valid:
error_message = FSMDomainRequestError.get_error_message(FSMErrorCodes.NO_ACTION_NEEDED_REASON)
- else:
- is_valid = True
-
- if error_message is not None:
self.add_error("action_needed_reason", error_message)
return is_valid
diff --git a/src/registrar/assets/js/get-gov-admin.js b/src/registrar/assets/js/get-gov-admin.js
index e5a3bfb62..9f9e0f06a 100644
--- a/src/registrar/assets/js/get-gov-admin.js
+++ b/src/registrar/assets/js/get-gov-admin.js
@@ -328,6 +328,7 @@ function initializeWidgetOnList(list, parentId) {
}
}
+ // Adds or removes the display-none class to object depending on the value of boolean hideObject
function hideOrShowDomObject(object, hideObject){
if (object){
if (hideObject){
@@ -346,10 +347,13 @@ function initializeWidgetOnList(list, parentId) {
function handleBackButtonObserver(fieldsToObserve) {
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
+ // This currently only handles the navigation buttons
if (entry.type === "back_forward") {
+ // For each field we specify...
fieldsToObserve.forEach((fieldName) => {
fieldClass = `.field-${fieldName}`
field = document.querySelector(fieldClass)
+ // ...Grab its related session object to determine if it should be visible or not
if (field) {
shouldHideField = sessionStorage.getItem(`hide_${fieldName}`)
hideOrShowDomObject(field, hideObject=shouldHideField)
@@ -363,17 +367,30 @@ function initializeWidgetOnList(list, parentId) {
observer.observe({ type: "navigation" });
}
+ // Links the given field we want to show/hide with a given value of the status selector,
+ // and maintains this state with a given session object.
+ // For now, we assume that the session object follows this pattern: `hide_${field_name}`
function handleStatusChanges() {
// Show/hide the rejection reason
let rejectionReasonFormGroup = document.querySelector('.field-rejection_reason')
+
+ // element to hide, statusToShowOn, sessionObjectName
showHideFieldsOnStatusChange(rejectionReasonFormGroup, "rejected", "hide_rejection_reason");
- // Show/hude the action needed reason
+ // Show/hide the action needed reason
let actionNeededReasonFormGroup = document.querySelector('.field-action_needed_reason');
+
+ // element to hide, statusToShowOn, sessionObjectName
showHideFieldsOnStatusChange(actionNeededReasonFormGroup, "action needed", "hide_action_needed_reason");
}
+
+ // Hookup the fields that we want to programatically show/hide depending on the current value of the status field.
+ // Add your field name to this function if you are adding another dynamic field.
handleStatusChanges();
+ // Add an observer to each field to track when the back button is pressed. This is so
+ // our current state doesn't get wiped by browser events.
+ // Add a field name to this array if you are adding another dynamic field.
let fieldsToObserve = ["rejection_reason", "action_needed_reason"]
handleBackButtonObserver(fieldsToObserve);
})();
diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py
index 26527ba98..8a1d6a93e 100644
--- a/src/registrar/models/domain_request.py
+++ b/src/registrar/models/domain_request.py
@@ -1,6 +1,5 @@
from __future__ import annotations
from typing import Union
-import os
import logging
from django.apps import apps
@@ -244,11 +243,12 @@ class DomainRequest(TimeStampedModel):
ORGANIZATION_ELIGIBILITY = "org_not_eligible", "Org not eligible for a .gov domain"
NAMING_REQUIREMENTS = "naming_not_met", "Naming requirements not met"
OTHER = "other", "Other/Unspecified"
-
+
class ActionNeededReasons(models.TextChoices):
- """Defines common"""
+ """Defines common action needed reasons for domain requests"""
+
ELIGIBILITY_UNCLEAR = ("eligibility_unclear", "Unclear organization eligibility")
- QUESTIONABLE_AUTHORIZING_OFFICIAL = ("questionable_authorizing_official" , "Questionable authorizing official")
+ QUESTIONABLE_AUTHORIZING_OFFICIAL = ("questionable_authorizing_official", "Questionable authorizing official")
ALREADY_HAS_DOMAINS = ("already_has_domains", "Already has domains")
BAD_NAME = ("bad_name", "Doesn’t meet naming requirements")
OTHER = ("other", "Other (no auto-email sent)")
@@ -752,23 +752,26 @@ class DomainRequest(TimeStampedModel):
def _send_action_needed_reason_email(self, send_email=True):
"""Sends out an automatic email for each valid action needed reason provided"""
+ # Store the filenames of the template and template subject
email_template_name: str = ""
email_template_subject_name: str = ""
+
+ # Check for the "type" of action needed reason.
can_send_email = True
match self.action_needed_reason:
# Add to this match if you need to pass in a custom filename for these templates.
case self.ActionNeededReasons.OTHER, _:
# Unknown and other are default cases - do nothing
can_send_email = False
-
- if can_send_email:
- # Assumes that the template name matches the action needed reason if nothing is specified.
- # This is so you can override if you need, or have this taken care of for you.
- if not email_template_name and not email_template_subject_name:
- reason = self.action_needed_reason
- email_template_name = f"{reason}.txt"
- email_template_subject_name = f"{reason}_subject.txt"
+ # Assumes that the template name matches the action needed reason if nothing is specified.
+ # This is so you can override if you need, or have this taken care of for you.
+ if not email_template_name and not email_template_subject_name:
+ email_template_name = f"{self.action_needed_reason}.txt"
+ email_template_subject_name = f"{self.action_needed_reason}_subject.txt"
+
+ # If we can, try to send out an email as long as send_email=True
+ if can_send_email:
self._send_status_update_email(
new_status="action needed",
email_template=f"emails/action_needed_reasons/{email_template_name}",
diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py
index e2d390471..fa571331c 100644
--- a/src/registrar/tests/test_admin.py
+++ b/src/registrar/tests/test_admin.py
@@ -1444,20 +1444,25 @@ class TestDomainRequestAdmin(MockEppLib):
# The results are filtered by "status in [submitted,in review,action needed]"
self.assertContains(response, "status in [submitted,in review,action needed]", count=1)
- def transition_state_and_send_email(self, domain_request, status, rejection_reason=None):
+ @less_console_noise_decorator
+ def transition_state_and_send_email(self, domain_request, status, rejection_reason=None, action_needed_reason=None):
"""Helper method for the email test cases."""
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
- with less_console_noise():
- # Create a mock request
- request = self.factory.post("/admin/registrar/domainrequest/{}/change/".format(domain_request.pk))
+ # Create a mock request
+ request = self.factory.post("/admin/registrar/domainrequest/{}/change/".format(domain_request.pk))
- # Modify the domain request's properties
- domain_request.status = status
+ # Modify the domain request's properties
+ domain_request.status = status
+
+ if rejection_reason:
domain_request.rejection_reason = rejection_reason
- # Use the model admin's save_model method
- self.admin.save_model(request, domain_request, form=None, change=True)
+ if action_needed_reason:
+ domain_request.action_needed_reason = action_needed_reason
+
+ # Use the model admin's save_model method
+ self.admin.save_model(request, domain_request, form=None, change=True)
def assert_email_is_accurate(
self, expected_string, email_index, email_address, test_that_no_bcc=False, bcc_email_address=""
@@ -1492,6 +1497,64 @@ class TestDomainRequestAdmin(MockEppLib):
bcc_email = kwargs["Destination"]["BccAddresses"][0]
self.assertEqual(bcc_email, bcc_email_address)
+ def test_action_needed_sends_reason_email(self):
+ """When an action needed reason is set, an email is sent out."""
+ # Ensure there is no user with this email
+ EMAIL = "mayor@igorville.gov"
+ User.objects.filter(email=EMAIL).delete()
+ in_review = DomainRequest.DomainRequestStatus.IN_REVIEW
+ action_needed = DomainRequest.DomainRequestStatus.ACTION_NEEDED
+
+ # Create a sample domain request
+ domain_request = completed_domain_request(status=in_review)
+
+ # Test the email sent out for already_has_domains
+ already_has_domains = DomainRequest.ActionNeededReasons.ALREADY_HAS_DOMAINS
+ self.transition_state_and_send_email(domain_request, action_needed, action_needed_reason=already_has_domains)
+ self.assert_email_is_accurate("ORGANIZATION ALREADY HAS A .GOV DOMAIN", 0, EMAIL, True)
+ self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
+
+ # Revert back to in review to reset for the next assert
+ domain_request.status = DomainRequest.DomainRequestStatus.IN_REVIEW
+ domain_request.save()
+
+ # Test the email sent out for bad_name
+ bad_name = DomainRequest.ActionNeededReasons.BAD_NAME
+ self.transition_state_and_send_email(domain_request, action_needed, action_needed_reason=bad_name)
+ self.assert_email_is_accurate("DOMAIN NAME DOES NOT MEET .GOV REQUIREMENTS", 1, EMAIL, True)
+ self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
+
+ # Revert back to in review to reset for the next assert
+ domain_request.status = DomainRequest.DomainRequestStatus.IN_REVIEW
+ domain_request.save()
+
+ # Test the email sent out for eligibility_unclear
+ eligibility_unclear = DomainRequest.ActionNeededReasons.ELIGIBILITY_UNCLEAR
+ self.transition_state_and_send_email(domain_request, action_needed, action_needed_reason=eligibility_unclear)
+ self.assert_email_is_accurate("ORGANIZATION MAY NOT MEET ELIGIBILITY REQUIREMENTS", 2, EMAIL, True)
+ self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
+
+ # Revert back to in review to reset for the next assert
+ domain_request.status = DomainRequest.DomainRequestStatus.IN_REVIEW
+ domain_request.save()
+
+ # Test the email sent out for questionable_ao
+ questionable_ao = DomainRequest.ActionNeededReasons.QUESTIONABLE_AUTHORIZING_OFFICIAL
+ self.transition_state_and_send_email(domain_request, action_needed, action_needed_reason=questionable_ao)
+ self.assert_email_is_accurate("AUTHORIZING OFFICIAL DOES NOT MEET ELIGIBILITY REQUIREMENTS", 3, EMAIL, True)
+ self.assertEqual(len(self.mock_client.EMAILS_SENT), 4)
+
+ # Revert back to in review to reset for the next assert
+ domain_request.status = DomainRequest.DomainRequestStatus.IN_REVIEW
+ domain_request.save()
+
+ # Assert that no other emails are sent on OTHER
+ other = DomainRequest.ActionNeededReasons.OTHER
+ self.transition_state_and_send_email(domain_request, action_needed, action_needed_reason=other)
+
+ # Should be unchanged from before
+ self.assertEqual(len(self.mock_client.EMAILS_SENT), 4)
+
def test_save_model_sends_submitted_email(self):
"""When transitioning to submitted from started or withdrawn on a domain request,
an email is sent out.
From 501af8e017064c52f86d7fb1f963f729fd78f36f Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Tue, 4 Jun 2024 15:02:47 -0600
Subject: [PATCH 025/200] Fix existing unit tests
---
src/registrar/tests/test_admin.py | 17 +++++++++++++----
1 file changed, 13 insertions(+), 4 deletions(-)
diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py
index fa571331c..3a7bc7529 100644
--- a/src/registrar/tests/test_admin.py
+++ b/src/registrar/tests/test_admin.py
@@ -1590,7 +1590,9 @@ class TestDomainRequestAdmin(MockEppLib):
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
# Move it to IN_REVIEW
- self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.IN_REVIEW)
+ other = DomainRequest.ActionNeededReasons.OTHER
+ in_review = DomainRequest.DomainRequestStatus.IN_REVIEW
+ self.transition_state_and_send_email(domain_request, in_review, action_needed_reason=other)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
# Test Submitted Status Again from in IN_REVIEW, no new email should be sent
@@ -1598,7 +1600,7 @@ class TestDomainRequestAdmin(MockEppLib):
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
# Move it to IN_REVIEW
- self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.IN_REVIEW)
+ self.transition_state_and_send_email(domain_request, in_review, action_needed_reason=other)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
# Move it to ACTION_NEEDED
@@ -1648,7 +1650,9 @@ class TestDomainRequestAdmin(MockEppLib):
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
# Move it to IN_REVIEW
- self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.IN_REVIEW)
+ other = domain_request.ActionNeededReasons.OTHER
+ in_review = DomainRequest.DomainRequestStatus.IN_REVIEW
+ self.transition_state_and_send_email(domain_request, in_review, action_needed_reason=other)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
# Test Submitted Status Again from in IN_REVIEW, no new email should be sent
@@ -1656,7 +1660,7 @@ class TestDomainRequestAdmin(MockEppLib):
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
# Move it to IN_REVIEW
- self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.IN_REVIEW)
+ self.transition_state_and_send_email(domain_request, in_review, action_needed_reason=other)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
# Move it to ACTION_NEEDED
@@ -2300,6 +2304,7 @@ class TestDomainRequestAdmin(MockEppLib):
"updated_at",
"status",
"rejection_reason",
+ "action_needed_reason",
"federal_agency",
"creator",
"investigator",
@@ -2457,6 +2462,10 @@ class TestDomainRequestAdmin(MockEppLib):
stack.enter_context(patch.object(messages, "error"))
domain_request.status = another_state
+
+ if another_state == DomainRequest.DomainRequestStatus.ACTION_NEEDED:
+ domain_request.action_needed_reason = domain_request.ActionNeededReasons.BAD_NAME
+
domain_request.rejection_reason = rejection_reason
self.admin.save_model(request, domain_request, None, True)
From 03ea9e3a70b486cb0fb085a3f6bac6a66f01d3c3 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Tue, 4 Jun 2024 15:03:50 -0600
Subject: [PATCH 026/200] Update test_admin.py
---
src/registrar/tests/test_admin.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py
index 3a7bc7529..046608b0b 100644
--- a/src/registrar/tests/test_admin.py
+++ b/src/registrar/tests/test_admin.py
@@ -2464,7 +2464,7 @@ class TestDomainRequestAdmin(MockEppLib):
domain_request.status = another_state
if another_state == DomainRequest.DomainRequestStatus.ACTION_NEEDED:
- domain_request.action_needed_reason = domain_request.ActionNeededReasons.BAD_NAME
+ domain_request.action_needed_reason = domain_request.ActionNeededReasons.OTHER
domain_request.rejection_reason = rejection_reason
From 5e6f405b95f38d4be26011c41d7ba8a88f481397 Mon Sep 17 00:00:00 2001
From: Rachid Mrad
Date: Wed, 5 Jun 2024 01:19:47 -0400
Subject: [PATCH 027/200] implement seach on domains and requests
---
src/registrar/assets/js/get-gov.js | 134 +++++++++++++-----
src/registrar/assets/js/uswds-edited.js | 8 +-
src/registrar/assets/sass/_theme/_tables.scss | 4 +-
src/registrar/templates/home.html | 80 +++++++++--
src/registrar/views/domain_requests_json.py | 11 ++
src/registrar/views/domains_json.py | 9 ++
6 files changed, 197 insertions(+), 49 deletions(-)
diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js
index 0d594b315..cc24f7df7 100644
--- a/src/registrar/assets/js/get-gov.js
+++ b/src/registrar/assets/js/get-gov.js
@@ -918,8 +918,9 @@ function ScrollToElement(attributeName, attributeValue) {
* @param {boolean} hasPrevious - Whether there is a page before the current page.
* @param {boolean} hasNext - Whether there is a page after the current page.
* @param {number} totalItems - The total number of items.
+ * @param {string} searchTerm - The search term
*/
-function updatePagination(itemName, paginationSelector, counterSelector, headerAnchor, loadPageFunction, currentPage, numPages, hasPrevious, hasNext, totalItems) {
+function updatePagination(itemName, paginationSelector, counterSelector, headerAnchor, loadPageFunction, currentPage, numPages, hasPrevious, hasNext, totalItems, searchTerm) {
const paginationContainer = document.querySelector(paginationSelector);
const paginationCounter = document.querySelector(counterSelector);
const paginationButtons = document.querySelector(`${paginationSelector} .usa-pagination__list`);
@@ -932,7 +933,7 @@ function updatePagination(itemName, paginationSelector, counterSelector, headerA
// Counter should only be displayed if there is more than 1 item
paginationContainer.classList.toggle('display-none', totalItems < 1);
- paginationCounter.innerHTML = `${totalItems} ${itemName}${totalItems > 1 ? 's' : ''}`;
+ paginationCounter.innerHTML = `${totalItems} ${itemName}${totalItems > 1 ? 's' : ''}${searchTerm ? ' for ' + searchTerm : ''}`;
if (hasPrevious) {
const prevPageItem = document.createElement('li');
@@ -1018,6 +1019,28 @@ function updatePagination(itemName, paginationSelector, counterSelector, headerA
}
}
+const updateDisplay = (data, dataWrapper, noDataWrapper, noSearchResultsWrapper) => {
+ const { unfiltered_total, total } = data;
+
+ const showElement = (element) => element.classList.remove('display-none');
+ const hideElement = (element) => element.classList.add('display-none');
+
+ if (unfiltered_total) {
+ if (total) {
+ showElement(dataWrapper);
+ hideElement(noSearchResultsWrapper);
+ hideElement(noDataWrapper);
+ } else {
+ hideElement(dataWrapper);
+ showElement(noSearchResultsWrapper);
+ hideElement(noDataWrapper);
+ }
+ } else {
+ hideElement(dataWrapper);
+ hideElement(noSearchResultsWrapper);
+ showElement(noDataWrapper);
+ }
+};
/**
* An IIFE that listens for DOM Content to be loaded, then executes. This function
@@ -1025,13 +1048,19 @@ function updatePagination(itemName, paginationSelector, counterSelector, headerA
*
*/
document.addEventListener('DOMContentLoaded', function() {
- let domainsWrapper = document.querySelector('.domains-wrapper');
+ let domainsWrapper = document.querySelector('.domains__table-wrapper');
if (domainsWrapper) {
let currentSortBy = 'id';
let currentOrder = 'asc';
- let noDomainsWrapper = document.querySelector('.no-domains-wrapper');
+ let noDomainsWrapper = document.querySelector('.domains__no-data');
+ let noSearchResultsWrapper = document.querySelector('.domains__no-search-results');
let hasLoaded = false;
+ let currentSearchTerm = ''
+ let domainsSearchInput = document.getElementById('domains__search-field');
+ let domainsSearchSubmit = document.getElementById('domains__search-field-submit');
+ let tableHeaders = document.querySelectorAll('.domains__table th[data-sortable]');
+ let tableAnnouncementRegion = document.querySelector('.domains__table-wrapper .usa-table__announcement-region')
/**
* Loads rows in the domains list, as well as updates pagination around the domains list
@@ -1040,10 +1069,11 @@ document.addEventListener('DOMContentLoaded', function() {
* @param {*} sortBy - the sort column option
* @param {*} order - the sort order {asc, desc}
* @param {*} loaded - control for the scrollToElement functionality
+ * @param {*} searchTerm - the search term
*/
- function loadDomains(page, sortBy = currentSortBy, order = currentOrder, loaded = hasLoaded) {
+ function loadDomains(page, sortBy = currentSortBy, order = currentOrder, loaded = hasLoaded, searchTerm = currentSearchTerm) {
//fetch json of page of domains, given page # and sort
- fetch(`/get-domains-json/?page=${page}&sort_by=${sortBy}&order=${order}`)
+ fetch(`/get-domains-json/?page=${page}&sort_by=${sortBy}&order=${order}&search_term=${searchTerm}`)
.then(response => response.json())
.then(data => {
if (data.error) {
@@ -1051,17 +1081,11 @@ document.addEventListener('DOMContentLoaded', function() {
return;
}
- // handle the display of proper messaging in the event that no domains exist in the list
- if (data.domains.length) {
- domainsWrapper.classList.remove('display-none');
- noDomainsWrapper.classList.add('display-none');
- } else {
- domainsWrapper.classList.add('display-none');
- noDomainsWrapper.classList.remove('display-none');
- }
+ // handle the display of proper messaging in the event that no domains exist in the list or search returns no results
+ updateDisplay(data, domainsWrapper, noDomainsWrapper, noSearchResultsWrapper);
// identify the DOM element where the domain list will be inserted into the DOM
- const domainList = document.querySelector('.dotgov-table__registered-domains tbody');
+ const domainList = document.querySelector('.domains__table tbody');
domainList.innerHTML = '';
data.domains.forEach(domain => {
@@ -1122,7 +1146,8 @@ document.addEventListener('DOMContentLoaded', function() {
data.num_pages,
data.has_previous,
data.has_next,
- data.total
+ data.total,
+ currentSearchTerm
);
currentSortBy = sortBy;
currentOrder = order;
@@ -1133,7 +1158,7 @@ document.addEventListener('DOMContentLoaded', function() {
// Add event listeners to table headers for sorting
- document.querySelectorAll('.dotgov-table__registered-domains th[data-sortable]').forEach(header => {
+ tableHeaders.forEach(header => {
header.addEventListener('click', function() {
const sortBy = this.getAttribute('data-sortable');
let order = 'asc';
@@ -1147,6 +1172,23 @@ document.addEventListener('DOMContentLoaded', function() {
});
});
+ domainsSearchSubmit.addEventListener('click', function(e) {
+ e.preventDefault();
+ currentSearchTerm = domainsSearchInput.value;
+ loadDomains(1, 'id', 'asc');
+ resetheaders();
+ })
+
+ // Reset UI and accessibility
+ function resetheaders() {
+ tableHeaders.forEach(header => {
+ // unset sort UI in headers
+ window.table.unsetHeader(header);
+ });
+ // Reset the announcement region
+ tableAnnouncementRegion.innerHTML = '';
+ }
+
// Load the first page initially
loadDomains(1);
}
@@ -1157,10 +1199,13 @@ const utcDateString = (dateString) => {
const utcYear = date.getUTCFullYear();
const utcMonth = date.toLocaleString('en-US', { month: 'short', timeZone: 'UTC' });
const utcDay = date.getUTCDate().toString().padStart(2, '0');
- const utcHours = date.getUTCHours().toString().padStart(2, '0');
+ let utcHours = date.getUTCHours();
const utcMinutes = date.getUTCMinutes().toString().padStart(2, '0');
-
- return `${utcMonth} ${utcDay}, ${utcYear}, ${utcHours}:${utcMinutes} UTC`;
+
+ const ampm = utcHours >= 12 ? 'PM' : 'AM';
+ utcHours = utcHours % 12 || 12; // Convert to 12-hour format, '0' hours should be '12'
+
+ return `${utcMonth} ${utcDay}, ${utcYear}, ${utcHours}:${utcMinutes} ${ampm} UTC`;
};
/**
@@ -1169,13 +1214,19 @@ const utcDateString = (dateString) => {
*
*/
document.addEventListener('DOMContentLoaded', function() {
- let domainRequestsWrapper = document.querySelector('.domain-requests-wrapper');
+ let domainRequestsWrapper = document.querySelector('.domain-requests__table-wrapper');
if (domainRequestsWrapper) {
let currentSortBy = 'id';
let currentOrder = 'asc';
- let noDomainRequestsWrapper = document.querySelector('.no-domain-requests-wrapper');
+ let noDomainRequestsWrapper = document.querySelector('.domain-requests__no-data');
+ let noSearchResultsWrapper = document.querySelector('.domain-requests__no-search-results');
let hasLoaded = false;
+ let currentSearchTerm = ''
+ let domainRequestsSearchInput = document.getElementById('domain-requests__search-field');
+ let domainRequestsSearchSubmit = document.getElementById('domain-requests__search-field-submit');
+ let tableHeaders = document.querySelectorAll('.domain-requests__table th[data-sortable]');
+ let tableAnnouncementRegion = document.querySelector('.domain-requests__table-wrapper .usa-table__announcement-region')
/**
* Loads rows in the domain requests list, as well as updates pagination around the domain requests list
@@ -1184,10 +1235,11 @@ document.addEventListener('DOMContentLoaded', function() {
* @param {*} sortBy - the sort column option
* @param {*} order - the sort order {asc, desc}
* @param {*} loaded - control for the scrollToElement functionality
+ * @param {*} searchTerm - the search term
*/
- function loadDomainRequests(page, sortBy = currentSortBy, order = currentOrder, loaded = hasLoaded) {
+ function loadDomainRequests(page, sortBy = currentSortBy, order = currentOrder, loaded = hasLoaded, searchTerm = currentSearchTerm) {
//fetch json of page of domain requests, given page # and sort
- fetch(`/get-domain-requests-json/?page=${page}&sort_by=${sortBy}&order=${order}`)
+ fetch(`/get-domain-requests-json/?page=${page}&sort_by=${sortBy}&order=${order}&search_term=${searchTerm}`)
.then(response => response.json())
.then(data => {
if (data.error) {
@@ -1195,17 +1247,11 @@ document.addEventListener('DOMContentLoaded', function() {
return;
}
- // handle the display of proper messaging in the event that no domain requests exist in the list
- if (data.domain_requests.length) {
- domainRequestsWrapper.classList.remove('display-none');
- noDomainRequestsWrapper.classList.add('display-none');
- } else {
- domainRequestsWrapper.classList.add('display-none');
- noDomainRequestsWrapper.classList.remove('display-none');
- }
+ // handle the display of proper messaging in the event that no requests exist in the list or search returns no results
+ updateDisplay(data, domainRequestsWrapper, noDomainRequestsWrapper, noSearchResultsWrapper);
// identify the DOM element where the domain request list will be inserted into the DOM
- const tbody = document.querySelector('.dotgov-table__domain-requests tbody');
+ const tbody = document.querySelector('.domain-requests__table tbody');
tbody.innerHTML = '';
// remove any existing modal elements from the DOM so they can be properly re-initialized
@@ -1272,7 +1318,8 @@ document.addEventListener('DOMContentLoaded', function() {
data.num_pages,
data.has_previous,
data.has_next,
- data.total
+ data.total,
+ currentSearchTerm
);
currentSortBy = sortBy;
currentOrder = order;
@@ -1281,7 +1328,7 @@ document.addEventListener('DOMContentLoaded', function() {
}
// Add event listeners to table headers for sorting
- document.querySelectorAll('.dotgov-table__domain-requests th[data-sortable]').forEach(header => {
+ tableHeaders.forEach(header => {
header.addEventListener('click', function() {
const sortBy = this.getAttribute('data-sortable');
let order = 'asc';
@@ -1294,6 +1341,23 @@ document.addEventListener('DOMContentLoaded', function() {
});
});
+ domainRequestsSearchSubmit.addEventListener('click', function(e) {
+ e.preventDefault();
+ currentSearchTerm = domainRequestsSearchInput.value;
+ loadDomainRequests(1, 'id', 'asc');
+ resetheaders();
+ })
+
+ // Reset UI and accessibility
+ function resetheaders() {
+ tableHeaders.forEach(header => {
+ // unset sort UI in headers
+ window.table.unsetHeader(header);
+ });
+ // Reset the announcement region
+ tableAnnouncementRegion.innerHTML = '';
+ }
+
// Load the first page initially
loadDomainRequests(1);
}
diff --git a/src/registrar/assets/js/uswds-edited.js b/src/registrar/assets/js/uswds-edited.js
index e73f3b6c0..556488554 100644
--- a/src/registrar/assets/js/uswds-edited.js
+++ b/src/registrar/assets/js/uswds-edited.js
@@ -5709,9 +5709,15 @@ const table = behavior({
},
TABLE,
SORTABLE_HEADER,
- SORT_BUTTON
+ SORT_BUTTON,
+ // DOTGOV: Export unsetSort
+ unsetHeader(header) {
+ unsetSort(header);
+ }
});
module.exports = table;
+// DOTGOV: modified uswds.js to add table module to window so that it is accessible to other js
+window.table = table;
},{"../../uswds-core/src/js/config":35,"../../uswds-core/src/js/events":36,"../../uswds-core/src/js/utils/behavior":45,"../../uswds-core/src/js/utils/sanitizer":50,"../../uswds-core/src/js/utils/select":53}],32:[function(require,module,exports){
"use strict";
diff --git a/src/registrar/assets/sass/_theme/_tables.scss b/src/registrar/assets/sass/_theme/_tables.scss
index 26d90d291..a5eb5a4e0 100644
--- a/src/registrar/assets/sass/_theme/_tables.scss
+++ b/src/registrar/assets/sass/_theme/_tables.scss
@@ -98,7 +98,7 @@
}
}
@media (min-width: 1040px){
- .dotgov-table__domain-requests {
+ .domain-requests__table {
th:nth-of-type(1) {
width: 200px;
}
@@ -122,7 +122,7 @@
}
@media (min-width: 1040px){
- .dotgov-table__registered-domains {
+ .domains__table {
th:nth-of-type(1) {
width: 200px;
}
diff --git a/src/registrar/templates/home.html b/src/registrar/templates/home.html
index fd54769a8..c3877ca3f 100644
--- a/src/registrar/templates/home.html
+++ b/src/registrar/templates/home.html
@@ -23,10 +23,35 @@