+
{% include "django/forms/widgets/select.html" %}
diff --git a/src/registrar/tests/test_views_domain.py b/src/registrar/tests/test_views_domain.py
index 16b6690c0..2bf88a8b8 100644
--- a/src/registrar/tests/test_views_domain.py
+++ b/src/registrar/tests/test_views_domain.py
@@ -1577,6 +1577,52 @@ class TestDomainOrganization(TestDomainOverview):
class TestDomainSuborganization(TestDomainOverview):
"""Tests the Suborganization page for portfolio users"""
+ @less_console_noise_decorator
+ @override_flag("organization_feature", active=True)
+ def test_edit_suborganization_field(self):
+
+ # Create a portfolio and two suborgs
+ portfolio = Portfolio.objects.create(creator=self.user, organization_name="Ice Cream")
+ suborg = Suborganization.objects.create(portfolio=portfolio, name="Vanilla")
+ suborg_2 = Suborganization.objects.create(portfolio=portfolio, name="Chocolate")
+
+ # Create an unrelated portfolio
+ unrelated_portfolio = Portfolio.objects.create(creator=self.user, organization_name="Fruit")
+ unrelated_suborg = Suborganization.objects.create(portfolio=portfolio, name="Apple")
+
+ # Add the portfolio to the domain_information object
+ self.domain_information.portfolio = portfolio
+
+ # Add a organization_name to test if the old value still displays
+ self.domain_information.organization_name = "Broccoli"
+ self.domain_information.save()
+ self.domain_information.refresh_from_db()
+
+ # Add portfolio perms to the user object
+ self.user.portfolio = portfolio
+ self.user.portfolio_roles = [UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
+ self.user.save()
+ self.user.refresh_from_db()
+
+ # Navigate to the suborganization page
+ page = self.app.get(reverse("domain-suborganization", kwargs={"pk": self.domain.id}))
+
+ print(page)
+ # The page should contain the choices Vanilla and Chocolate
+ self.assertContains(page, "Vanilla")
+ self.assertContains("Chocolate")
+ self.assertNotContains("Apple")
+
+ # Try changing the suborg
+ session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
+ page.form["suborganization"] = suborg_2
+ self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+ page.form.submit()
+
+ self.assertContains(page, "The suborganization name for this domain has been updated.")
+ self.assertNotContains(page, "Vanilla")
+ self.assertContains("Chocolate")
+
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
def test_has_suborganization_field_on_overview_with_flag(self):
From f38cd72d4c222153a2867891befdb654ca1d0e83 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Mon, 5 Aug 2024 11:49:35 -0600
Subject: [PATCH 09/61] Add unit test
---
src/registrar/tests/test_views_domain.py | 32 ++++++++++++++++--------
1 file changed, 22 insertions(+), 10 deletions(-)
diff --git a/src/registrar/tests/test_views_domain.py b/src/registrar/tests/test_views_domain.py
index 2bf88a8b8..f58ed4cd0 100644
--- a/src/registrar/tests/test_views_domain.py
+++ b/src/registrar/tests/test_views_domain.py
@@ -1580,7 +1580,7 @@ class TestDomainSuborganization(TestDomainOverview):
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
def test_edit_suborganization_field(self):
-
+
# Create a portfolio and two suborgs
portfolio = Portfolio.objects.create(creator=self.user, organization_name="Ice Cream")
suborg = Suborganization.objects.create(portfolio=portfolio, name="Vanilla")
@@ -1588,10 +1588,11 @@ class TestDomainSuborganization(TestDomainOverview):
# Create an unrelated portfolio
unrelated_portfolio = Portfolio.objects.create(creator=self.user, organization_name="Fruit")
- unrelated_suborg = Suborganization.objects.create(portfolio=portfolio, name="Apple")
+ unrelated_suborg = Suborganization.objects.create(portfolio=unrelated_portfolio, name="Apple")
# Add the portfolio to the domain_information object
self.domain_information.portfolio = portfolio
+ self.domain_information.sub_organization = suborg
# Add a organization_name to test if the old value still displays
self.domain_information.organization_name = "Broccoli"
@@ -1604,24 +1605,35 @@ class TestDomainSuborganization(TestDomainOverview):
self.user.save()
self.user.refresh_from_db()
+ self.assertEqual(self.domain_information.sub_organization, suborg)
+
# Navigate to the suborganization page
page = self.app.get(reverse("domain-suborganization", kwargs={"pk": self.domain.id}))
- print(page)
# The page should contain the choices Vanilla and Chocolate
self.assertContains(page, "Vanilla")
- self.assertContains("Chocolate")
- self.assertNotContains("Apple")
+ self.assertContains(page, "Chocolate")
+ self.assertNotContains(page, unrelated_suborg.name)
+
+ # Assert that the right option is selected. This component uses data-default-value.
+ self.assertContains(page, f'data-default-value="{suborg.id}"')
# Try changing the suborg
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
- page.form["suborganization"] = suborg_2
+ page.form["sub_organization"] = suborg_2.id
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
- page.form.submit()
+ page = page.form.submit().follow()
- self.assertContains(page, "The suborganization name for this domain has been updated.")
- self.assertNotContains(page, "Vanilla")
- self.assertContains("Chocolate")
+ # The page should contain the choices Vanilla and Chocolate
+ self.assertContains(page, "Vanilla")
+ self.assertContains(page, "Chocolate")
+ self.assertNotContains(page, unrelated_suborg.name)
+
+ # Assert that the right option is selected
+ self.assertContains(page, f'data-default-value="{suborg_2.id}"')
+
+ self.domain_information.refresh_from_db()
+ self.assertEqual(self.domain_information.sub_organization, suborg_2)
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
From 7b8da895db2adf90beffff830738a156682d6fcb Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Mon, 5 Aug 2024 12:10:30 -0600
Subject: [PATCH 10/61] Cleanup
---
src/registrar/forms/domain.py | 19 +++++--------------
.../templates/domain_suborganization.html | 3 +--
src/registrar/templatetags/custom_filters.py | 2 +-
3 files changed, 7 insertions(+), 17 deletions(-)
diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py
index 074c4add4..ec29f4032 100644
--- a/src/registrar/forms/domain.py
+++ b/src/registrar/forms/domain.py
@@ -174,21 +174,12 @@ class DomainSuborganizationForm(forms.ModelForm):
self.request = kwargs.pop("request", None)
super().__init__(*args, **kwargs)
- portfolio = None
- if self.instance and self.instance.portfolio:
- # Get suborgs under the portfolio that this is associated with first
- portfolio = self.instance.portfolio
- elif self.request and self.request.user and self.request.user.portfolio:
- # Question: If no portfolio is associated with this record,
- # should we default to the user one?
- # portfolio = self.request.user.portfolio
- logger.warning(f"No portfolio was found for {self.instance} on user {self.request.user}.")
-
+ portfolio = self.instance.portfolio if self.instance else None
self.fields["sub_organization"].queryset = Suborganization.objects.filter(portfolio=portfolio)
# Set initial value
if self.instance and self.instance.sub_organization:
- self.fields['sub_organization'].initial = self.instance.sub_organization
+ self.fields["sub_organization"].initial = self.instance.sub_organization
# Set custom form label
self.fields["sub_organization"].label = "Suborganization name"
@@ -197,12 +188,12 @@ class DomainSuborganizationForm(forms.ModelForm):
self.fields["sub_organization"].widget.template_name = "django/forms/widgets/combobox.html"
# Set data-default-value attribute
- self.fields['sub_organization'].widget.attrs['data-default-value'] = self.instance.sub_organization.pk if self.instance and self.instance.sub_organization else ''
+ if self.instance and self.instance.sub_organization:
+ self.fields["sub_organization"].widget.attrs["data-default-value"] = self.instance.sub_organization.pk
-
def get_suborganization_name(self):
"""Returns the suborganization name for the readonly view"""
- return self.instance.sub_organization if self.instance else None
+ return self.instance.sub_organization.name if self.instance else None
class BaseNameserverFormset(forms.BaseFormSet):
diff --git a/src/registrar/templates/domain_suborganization.html b/src/registrar/templates/domain_suborganization.html
index cea889352..d1fdeac2f 100644
--- a/src/registrar/templates/domain_suborganization.html
+++ b/src/registrar/templates/domain_suborganization.html
@@ -26,5 +26,4 @@
{% include "includes/input_read_only.html" with field=form.sub_organization value=instance.get_suborganization_name label_description=description%}
{% endwith %}
{% endif %}
-
-{% endblock %}
\ No newline at end of file
+{% endblock %}
diff --git a/src/registrar/templatetags/custom_filters.py b/src/registrar/templatetags/custom_filters.py
index 5dcdecef6..7ad63bd15 100644
--- a/src/registrar/templatetags/custom_filters.py
+++ b/src/registrar/templatetags/custom_filters.py
@@ -158,4 +158,4 @@ def and_filter(value, arg):
Implements logical AND operation in templates.
Usage: {{ value|and:arg }}
"""
- return bool(value and arg)
\ No newline at end of file
+ return bool(value and arg)
From c109d383c09042b5a614d943739d775ce5f5b078 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Mon, 5 Aug 2024 12:19:59 -0600
Subject: [PATCH 11/61] Add unit test for view
---
src/registrar/tests/test_views_domain.py | 47 +++++++++++++++++++++++-
1 file changed, 46 insertions(+), 1 deletion(-)
diff --git a/src/registrar/tests/test_views_domain.py b/src/registrar/tests/test_views_domain.py
index f58ed4cd0..1ec1f004a 100644
--- a/src/registrar/tests/test_views_domain.py
+++ b/src/registrar/tests/test_views_domain.py
@@ -1580,7 +1580,7 @@ class TestDomainSuborganization(TestDomainOverview):
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
def test_edit_suborganization_field(self):
-
+ """Ensure that org admins can edit the suborganization field"""
# Create a portfolio and two suborgs
portfolio = Portfolio.objects.create(creator=self.user, organization_name="Ice Cream")
suborg = Suborganization.objects.create(portfolio=portfolio, name="Vanilla")
@@ -1635,6 +1635,51 @@ class TestDomainSuborganization(TestDomainOverview):
self.domain_information.refresh_from_db()
self.assertEqual(self.domain_information.sub_organization, suborg_2)
+ @less_console_noise_decorator
+ @override_flag("organization_feature", active=True)
+ def test_view_suborganization_field(self):
+ """Only org admins can edit the suborg field, ensure that others cannot"""
+
+ # Create a portfolio and two suborgs
+ portfolio = Portfolio.objects.create(creator=self.user, organization_name="Ice Cream")
+ suborg = Suborganization.objects.create(portfolio=portfolio, name="Vanilla")
+ suborg_2 = Suborganization.objects.create(portfolio=portfolio, name="Chocolate")
+
+ # Create an unrelated portfolio
+ unrelated_portfolio = Portfolio.objects.create(creator=self.user, organization_name="Fruit")
+ unrelated_suborg = Suborganization.objects.create(portfolio=unrelated_portfolio, name="Apple")
+
+ # Add the portfolio to the domain_information object
+ self.domain_information.portfolio = portfolio
+ self.domain_information.sub_organization = suborg
+
+ # Add a organization_name to test if the old value still displays
+ self.domain_information.organization_name = "Broccoli"
+ self.domain_information.save()
+ self.domain_information.refresh_from_db()
+
+ # Add portfolio perms to the user object
+ self.user.portfolio = portfolio
+ self.user.portfolio_roles = [UserPortfolioRoleChoices.ORGANIZATION_ADMIN_READ_ONLY]
+ self.user.save()
+ self.user.refresh_from_db()
+
+ self.assertEqual(self.domain_information.sub_organization, suborg)
+
+ # Navigate to the suborganization page
+ page = self.app.get(reverse("domain-suborganization", kwargs={"pk": self.domain.id}))
+
+ # The page shouldn't contain these choices
+ self.assertNotContains(page, "Vanilla")
+ self.assertNotContains(page, "Chocolate")
+ self.assertNotContains(page, unrelated_suborg.name)
+ self.assertNotContains(page, "Save")
+
+ self.assertContains(
+ page,
+ "The suborganization for this domain can only be updated by a organization administrator."
+ )
+
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
def test_has_suborganization_field_on_overview_with_flag(self):
From 9c800e1f1d5a1a3b645438d17ea35754d923e39e Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Mon, 5 Aug 2024 12:23:49 -0600
Subject: [PATCH 12/61] Linting cleanup
---
src/registrar/models/user.py | 2 +-
src/registrar/templatetags/custom_filters.py | 2 +-
src/registrar/tests/test_views_domain.py | 5 ++---
3 files changed, 4 insertions(+), 5 deletions(-)
diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py
index 451a52213..e3e59409a 100644
--- a/src/registrar/models/user.py
+++ b/src/registrar/models/user.py
@@ -273,7 +273,7 @@ class User(AbstractUser):
# Field specific permission checks
def has_view_suborganization(self):
return self._has_portfolio_permission(UserPortfolioPermissionChoices.VIEW_SUBORGANIZATION)
-
+
def has_edit_suborganization(self):
return self._has_portfolio_permission(UserPortfolioPermissionChoices.EDIT_SUBORGANIZATION)
diff --git a/src/registrar/templatetags/custom_filters.py b/src/registrar/templatetags/custom_filters.py
index 7ad63bd15..e90b3b17f 100644
--- a/src/registrar/templatetags/custom_filters.py
+++ b/src/registrar/templatetags/custom_filters.py
@@ -152,7 +152,7 @@ def in_path(url, path):
return url in path
-@register.filter(name='and')
+@register.filter(name="and")
def and_filter(value, arg):
"""
Implements logical AND operation in templates.
diff --git a/src/registrar/tests/test_views_domain.py b/src/registrar/tests/test_views_domain.py
index 1ec1f004a..d6fe95371 100644
--- a/src/registrar/tests/test_views_domain.py
+++ b/src/registrar/tests/test_views_domain.py
@@ -1643,7 +1643,7 @@ class TestDomainSuborganization(TestDomainOverview):
# Create a portfolio and two suborgs
portfolio = Portfolio.objects.create(creator=self.user, organization_name="Ice Cream")
suborg = Suborganization.objects.create(portfolio=portfolio, name="Vanilla")
- suborg_2 = Suborganization.objects.create(portfolio=portfolio, name="Chocolate")
+ Suborganization.objects.create(portfolio=portfolio, name="Chocolate")
# Create an unrelated portfolio
unrelated_portfolio = Portfolio.objects.create(creator=self.user, organization_name="Fruit")
@@ -1676,8 +1676,7 @@ class TestDomainSuborganization(TestDomainOverview):
self.assertNotContains(page, "Save")
self.assertContains(
- page,
- "The suborganization for this domain can only be updated by a organization administrator."
+ page, "The suborganization for this domain can only be updated by a organization administrator."
)
@less_console_noise_decorator
From bd143af6f1de484c52c79aa14389b7eac9d8d669 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Mon, 5 Aug 2024 12:29:45 -0600
Subject: [PATCH 13/61] Fix test
---
src/registrar/tests/test_views_domain.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/registrar/tests/test_views_domain.py b/src/registrar/tests/test_views_domain.py
index d6fe95371..de9cd5c0a 100644
--- a/src/registrar/tests/test_views_domain.py
+++ b/src/registrar/tests/test_views_domain.py
@@ -1161,7 +1161,7 @@ class TestDomainSeniorOfficial(TestDomainOverview):
# Add portfolio perms to the user object
self.user.portfolio = portfolio
- self.user.portfolio_additional_permissions = [UserPortfolioPermissionChoices.VIEW_PORTFOLIO]
+ self.user.portfolio_roles = [UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
self.user.save()
self.user.refresh_from_db()
From cde0d527ce7251e9073a66b053034d9657937ecb Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Mon, 5 Aug 2024 12:59:44 -0600
Subject: [PATCH 14/61] PR cleanup
---
src/registrar/context_processors.py | 2 --
src/registrar/forms/domain.py | 2 --
src/registrar/models/utility/portfolio_helper.py | 1 -
src/registrar/templates/django/forms/widgets/combobox.html | 7 +++++++
src/registrar/views/domain.py | 5 +----
src/registrar/views/portfolios.py | 2 --
6 files changed, 8 insertions(+), 11 deletions(-)
diff --git a/src/registrar/context_processors.py b/src/registrar/context_processors.py
index 36706edc9..ee5f8aee1 100644
--- a/src/registrar/context_processors.py
+++ b/src/registrar/context_processors.py
@@ -66,7 +66,6 @@ def portfolio_permissions(request):
"has_base_portfolio_permission": False,
"has_domains_portfolio_permission": False,
"has_domain_requests_portfolio_permission": False,
- "has_edit_org_portfolio_permission": False,
"portfolio": None,
"has_organization_feature_flag": False,
}
@@ -74,7 +73,6 @@ def portfolio_permissions(request):
"has_base_portfolio_permission": request.user.has_base_portfolio_permission(),
"has_domains_portfolio_permission": request.user.has_domains_portfolio_permission(),
"has_domain_requests_portfolio_permission": request.user.has_domain_requests_portfolio_permission(),
- "has_edit_org_portfolio_permission": request.user.has_edit_org_portfolio_permission(),
"portfolio": request.user.portfolio,
"has_organization_feature_flag": True,
}
diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py
index ec29f4032..ec370f268 100644
--- a/src/registrar/forms/domain.py
+++ b/src/registrar/forms/domain.py
@@ -170,8 +170,6 @@ class DomainSuborganizationForm(forms.ModelForm):
]
def __init__(self, *args, **kwargs):
- # Get the incoming request object
- self.request = kwargs.pop("request", None)
super().__init__(*args, **kwargs)
portfolio = self.instance.portfolio if self.instance else None
diff --git a/src/registrar/models/utility/portfolio_helper.py b/src/registrar/models/utility/portfolio_helper.py
index 2edca3422..86aaa5e16 100644
--- a/src/registrar/models/utility/portfolio_helper.py
+++ b/src/registrar/models/utility/portfolio_helper.py
@@ -27,7 +27,6 @@ class UserPortfolioPermissionChoices(models.TextChoices):
VIEW_PORTFOLIO = "view_portfolio", "View organization"
EDIT_PORTFOLIO = "edit_portfolio", "Edit organization"
- # TODO - think of other solutions
# Domain: field specific permissions
VIEW_SUBORGANIZATION = "view_suborganization", "View suborganization"
EDIT_SUBORGANIZATION = "edit_suborganization", "Edit suborganization"
diff --git a/src/registrar/templates/django/forms/widgets/combobox.html b/src/registrar/templates/django/forms/widgets/combobox.html
index c53fdc816..107c2e14e 100644
--- a/src/registrar/templates/django/forms/widgets/combobox.html
+++ b/src/registrar/templates/django/forms/widgets/combobox.html
@@ -1,3 +1,10 @@
+{% comment %}
+This is a custom widget for USWDS's comboboxes.
+USWDS comboboxes are basically just selects with a "usa-combo-box" div wrapper.
+We can further customize these by applying attributes to this parent element,
+for now we just carry the attribute to both the parent element and the select.
+{% endcomment %}
+
Date: Wed, 7 Aug 2024 10:12:18 -0600
Subject: [PATCH 15/61] Reset search logic
---
src/registrar/assets/js/get-gov.js | 62 +++++++++++++++++++
src/registrar/assets/sass/_theme/_base.scss | 6 ++
.../templates/domain_suborganization.html | 2 +
3 files changed, 70 insertions(+)
diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js
index 0712da0f7..b44f13062 100644
--- a/src/registrar/assets/js/get-gov.js
+++ b/src/registrar/assets/js/get-gov.js
@@ -1985,3 +1985,65 @@ document.addEventListener('DOMContentLoaded', function() {
showInputOnErrorFields();
})();
+
+
+/**
+ * An IIFE that changes the default clear behavior on comboboxes to the input field.
+ * We want the search bar to act soley as a search bar.
+ */
+(function loadInitialValuesForComboBoxes() {
+ document.addEventListener('DOMContentLoaded', (event) => {
+ // The file location for the #undo svg
+ const undoIcon = document.querySelector("#uswds-undo-icon-url");
+ const undoIconUrl = undoIcon ? undoIcon.getAttribute("data-undo-icon-url") : null;
+ if (undoIconUrl) {
+ handleAllComboBoxElements(undoIconUrl);
+ }
+ });
+
+ function handleAllComboBoxElements(undoIconUrl) {
+ const comboBoxElements = document.querySelectorAll(".usa-combo-box");
+ comboBoxElements.forEach(comboBox => {
+ const input = comboBox.querySelector('input');
+ if (!input || !undoIconUrl) {
+ console.warn("No input element found");
+ return;
+ }
+
+ let clearInputButton = comboBox.querySelector(".usa-combo-box__clear-input");
+ if (!clearInputButton) {
+ console.warn("No clear element found");
+ return;
+ }
+
+ let resetSearchButton = clearInputButton.cloneNode(true);
+ resetSearchButton.classList.add('usa-combo-box__reset-search');
+ resetSearchButton.style.display = 'none';
+ // Change the icon to the "undo" icon. Due to the nature of how this element is styled, we have to do this as so.
+ resetSearchButton.style.backgroundImage = `url("${undoIconUrl}"), linear-gradient(transparent, transparent)`;
+ clearInputButton.insertAdjacentElement('afterend', resetSearchButton);
+
+ // Show the reset search button when typing
+ input.addEventListener('input', () => {
+ resetSearchButton.style.display = 'inline-block';
+ });
+
+ // Hide the reset search button when input loses focus
+ input.addEventListener('blur', () => {
+ resetSearchButton.style.display = 'none';
+ });
+
+ handleMouseDownOnButton(resetSearchButton, input)
+ });
+ }
+
+ function handleMouseDownOnButton(button, inputToTarget) {
+ // Reset the input value when the reset search button is clicked
+ button.addEventListener('mousedown', (event) => {
+ // Simulate focus and blur to trigger the built in "resetSelection" and "hideList" functions
+ inputToTarget.focus();
+ inputToTarget.blur();
+ button.style.display = 'none';
+ });
+ }
+})();
\ No newline at end of file
diff --git a/src/registrar/assets/sass/_theme/_base.scss b/src/registrar/assets/sass/_theme/_base.scss
index 9f8a0cbb6..27dac777d 100644
--- a/src/registrar/assets/sass/_theme/_base.scss
+++ b/src/registrar/assets/sass/_theme/_base.scss
@@ -192,3 +192,9 @@ abbr[title] {
max-width: 50ch;
}
}
+
+.usa-combo-box__clear-input {
+ &.usa-combo-box__clear-input--reset {
+
+ }
+}
\ No newline at end of file
diff --git a/src/registrar/templates/domain_suborganization.html b/src/registrar/templates/domain_suborganization.html
index d1fdeac2f..c2e3431ad 100644
--- a/src/registrar/templates/domain_suborganization.html
+++ b/src/registrar/templates/domain_suborganization.html
@@ -16,6 +16,8 @@
{% if has_domains_portfolio_permission and request.user.has_edit_suborganization %}
+ {% comment %} Store the undo icon for reference in js - since this is dynamic {% endcomment %}
+
{% endblock %} {# domain_content #}
diff --git a/src/registrar/templates/includes/readonly_senior_official.html b/src/registrar/templates/includes/readonly_senior_official.html
new file mode 100644
index 000000000..73e7bfd77
--- /dev/null
+++ b/src/registrar/templates/includes/readonly_senior_official.html
@@ -0,0 +1,11 @@
+{% if form.full_name.value is not None %}
+ {% include "includes/input_read_only.html" with field=form.full_name %}
+{% endif %}
+
+{% if form.title.value is not None %}
+ {% include "includes/input_read_only.html" with field=form.title %}
+{% endif %}
+
+{% if form.email.value is not None %}
+ {% include "includes/input_read_only.html" with field=form.email %}
+{% endif %}
\ No newline at end of file
diff --git a/src/registrar/templates/portfolio_senior_official.html b/src/registrar/templates/portfolio_senior_official.html
index 12ddab908..976cf9639 100644
--- a/src/registrar/templates/portfolio_senior_official.html
+++ b/src/registrar/templates/portfolio_senior_official.html
@@ -28,15 +28,7 @@
{% if not senior_official %}
No senior official was found for this portfolio.
{% else %}
- {% if form.full_name.value is not None %}
- {% include "includes/input_read_only.html" with field=form.full_name %}
- {% endif %}
- {% if form.title.value is not None %}
- {% include "includes/input_read_only.html" with field=form.title %}
- {% endif %}
- {% if form.email.value is not None %}
- {% include "includes/input_read_only.html" with field=form.email %}
- {% endif %}
+ {% include "includes/readonly_senior_official.html" with form=form %}
{% endif %}
diff --git a/src/registrar/views/portfolios.py b/src/registrar/views/portfolios.py
index 8879f020c..a7c7d1356 100644
--- a/src/registrar/views/portfolios.py
+++ b/src/registrar/views/portfolios.py
@@ -48,7 +48,7 @@ class PortfolioOrganizationView(PortfolioBasePermissionView, FormMixin):
def get_context_data(self, **kwargs):
"""Add additional context data to the template."""
context = super().get_context_data(**kwargs)
- # context["has_edit_org_portfolio_permission"]
+ context["has_edit_org_portfolio_permission"] = self.request.user.has_edit_org_portfolio_permission()
return context
def get_object(self, queryset=None):
From cdcd90bb89706776399384e6430b2e5f7bc89a73 Mon Sep 17 00:00:00 2001
From: matthewswspence
Date: Fri, 9 Aug 2024 14:37:49 -0500
Subject: [PATCH 27/61] add dnssec summary item to overview page
---
src/registrar/templates/domain_detail.html | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/src/registrar/templates/domain_detail.html b/src/registrar/templates/domain_detail.html
index 0b6f47481..ad187aa4e 100644
--- a/src/registrar/templates/domain_detail.html
+++ b/src/registrar/templates/domain_detail.html
@@ -64,6 +64,13 @@
{% endif %}
{% endif %}
+ {% url 'domain-dns-dnssec' pk=domain.id as url %}
+ {% if domain.dnssecdata is not None %}
+ {% include "includes/summary_item.html" with title='DNSSEC' value='Enabled' edit_link=url editable=is_editable %}
+ {% else %}
+ {% include "includes/summary_item.html" with title='DNSSEC' value='Disabled' edit_link=url editable=is_editable %}
+ {% endif %}
+
{% if portfolio %}
{% comment %} TODO - uncomment in #2352 and add to edit_link
{% url 'domain-suborganization' pk=domain.id as url %}
From 2863e29d96892dd076ffa572ada9eae8162821d0 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Fri, 9 Aug 2024 13:40:16 -0600
Subject: [PATCH 28/61] Fix bug with senior_official in django admin, add
readonly
---
src/registrar/admin.py | 2 --
src/registrar/forms/domain.py | 5 ++++-
src/registrar/forms/portfolio.py | 2 +-
3 files changed, 5 insertions(+), 4 deletions(-)
diff --git a/src/registrar/admin.py b/src/registrar/admin.py
index ca4038d51..138183082 100644
--- a/src/registrar/admin.py
+++ b/src/registrar/admin.py
@@ -504,8 +504,6 @@ class AdminSortFields:
# == Contact == #
"other_contacts": (Contact, _name_sort),
"submitter": (Contact, _name_sort),
- # == Senior Official == #
- "senior_official": (SeniorOfficial, _name_sort),
# == User == #
"creator": (User, _name_sort),
"user": (User, _name_sort),
diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py
index 02a0724d1..53f340aee 100644
--- a/src/registrar/forms/domain.py
+++ b/src/registrar/forms/domain.py
@@ -321,9 +321,12 @@ class SeniorOfficialContactForm(ContactForm):
"""Form for updating senior official contacts."""
JOIN = "senior_official"
-
+ full_name = forms.CharField(label="Full name", required=False)
def __init__(self, disable_fields=False, *args, **kwargs):
super().__init__(*args, **kwargs)
+
+ if self.instance:
+ self.fields["full_name"].initial = self.instance.get_formatted_name()
# Overriding bc phone not required in this form
self.fields["phone"] = forms.IntegerField(required=False)
diff --git a/src/registrar/forms/portfolio.py b/src/registrar/forms/portfolio.py
index ab9459588..3a9e65b7f 100644
--- a/src/registrar/forms/portfolio.py
+++ b/src/registrar/forms/portfolio.py
@@ -76,7 +76,7 @@ class PortfolioSeniorOfficialForm(forms.ModelForm):
"""
JOIN = "senior_official"
- full_name = forms.CharField(label="Full name")
+ full_name = forms.CharField(label="Full name", required=False)
class Meta:
model = SeniorOfficial
fields = [
From 4be1e3e29e2140ebecfd3c08885de00ad7a95d7e Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Fri, 9 Aug 2024 13:53:54 -0600
Subject: [PATCH 29/61] Linting and fix existing unit test
---
src/registrar/admin.py | 2 +-
src/registrar/forms/domain.py | 3 +-
src/registrar/forms/portfolio.py | 1 +
.../includes/readonly_senior_official.html | 2 +-
src/registrar/tests/test_views_domain.py | 45 +++----------------
5 files changed, 11 insertions(+), 42 deletions(-)
diff --git a/src/registrar/admin.py b/src/registrar/admin.py
index 138183082..ae06a8d96 100644
--- a/src/registrar/admin.py
+++ b/src/registrar/admin.py
@@ -22,7 +22,7 @@ from epplibwrapper.errors import ErrorCode, RegistryError
from registrar.models.user_domain_role import UserDomainRole
from waffle.admin import FlagAdmin
from waffle.models import Sample, Switch
-from registrar.models import Contact, Domain, DomainRequest, DraftDomain, User, Website, SeniorOfficial
+from registrar.models import Contact, Domain, DomainRequest, DraftDomain, User, Website
from registrar.utility.errors import FSMDomainRequestError, FSMErrorCodes
from registrar.views.utility.mixins import OrderableFieldsMixin
from django.contrib.admin.views.main import ORDER_VAR
diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py
index 53f340aee..4a1e95431 100644
--- a/src/registrar/forms/domain.py
+++ b/src/registrar/forms/domain.py
@@ -322,9 +322,10 @@ class SeniorOfficialContactForm(ContactForm):
JOIN = "senior_official"
full_name = forms.CharField(label="Full name", required=False)
+
def __init__(self, disable_fields=False, *args, **kwargs):
super().__init__(*args, **kwargs)
-
+
if self.instance:
self.fields["full_name"].initial = self.instance.get_formatted_name()
diff --git a/src/registrar/forms/portfolio.py b/src/registrar/forms/portfolio.py
index 3a9e65b7f..44d195dd6 100644
--- a/src/registrar/forms/portfolio.py
+++ b/src/registrar/forms/portfolio.py
@@ -77,6 +77,7 @@ class PortfolioSeniorOfficialForm(forms.ModelForm):
JOIN = "senior_official"
full_name = forms.CharField(label="Full name", required=False)
+
class Meta:
model = SeniorOfficial
fields = [
diff --git a/src/registrar/templates/includes/readonly_senior_official.html b/src/registrar/templates/includes/readonly_senior_official.html
index 73e7bfd77..7a7908726 100644
--- a/src/registrar/templates/includes/readonly_senior_official.html
+++ b/src/registrar/templates/includes/readonly_senior_official.html
@@ -8,4 +8,4 @@
{% if form.email.value is not None %}
{% include "includes/input_read_only.html" with field=form.email %}
-{% endif %}
\ No newline at end of file
+{% endif %}
diff --git a/src/registrar/tests/test_views_domain.py b/src/registrar/tests/test_views_domain.py
index 31ca776e2..efbc1296c 100644
--- a/src/registrar/tests/test_views_domain.py
+++ b/src/registrar/tests/test_views_domain.py
@@ -1192,14 +1192,14 @@ class TestDomainSeniorOfficial(TestDomainOverview):
self.assertTrue("disabled" in form[field_name].attrs)
@less_console_noise_decorator
- def test_domain_edit_senior_official_federal(self):
+ def test_domain_cannot_edit_senior_official_when_federal(self):
"""Tests that no edit can occur when the underlying domain is federal"""
# Set the org type to federal
self.domain_information.generic_org_type = DomainInformation.OrganizationChoices.FEDERAL
self.domain_information.save()
- # Add an SO. We can do this at the model level, just not the form level.
+ # Add an SO
self.domain_information.senior_official = Contact(
first_name="Apple", last_name="Tester", title="CIO", email="nobody@igorville.gov"
)
@@ -1210,43 +1210,10 @@ class TestDomainSeniorOfficial(TestDomainOverview):
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
- # Test if the form is populating data correctly
- so_form = so_page.forms[0]
-
- test_cases = [
- ("first_name", "Apple"),
- ("last_name", "Tester"),
- ("title", "CIO"),
- ("email", "nobody@igorville.gov"),
- ]
- self.assert_all_form_fields_have_expected_values(so_form, test_cases, test_for_disabled=True)
-
- # Attempt to change data on each field. Because this domain is federal,
- # this should not succeed.
- so_form["first_name"] = "Orange"
- so_form["last_name"] = "Smoothie"
- so_form["title"] = "Cat"
- so_form["email"] = "somebody@igorville.gov"
-
- submission = so_form.submit()
-
- # A 302 indicates this page underwent a redirect.
- self.assertEqual(submission.status_code, 302)
-
- followed_submission = submission.follow()
-
- # Test the returned form for data accuracy. These values should be unchanged.
- new_form = followed_submission.forms[0]
- self.assert_all_form_fields_have_expected_values(new_form, test_cases, test_for_disabled=True)
-
- # refresh domain information. Test these values in the DB.
- self.domain_information.refresh_from_db()
-
- # All values should be unchanged. These are defined manually for code clarity.
- self.assertEqual("Apple", self.domain_information.senior_official.first_name)
- self.assertEqual("Tester", self.domain_information.senior_official.last_name)
- self.assertEqual("CIO", self.domain_information.senior_official.title)
- self.assertEqual("nobody@igorville.gov", self.domain_information.senior_official.email)
+ self.assertContains(so_page, "Apple Tester")
+ self.assertContains(so_page, "CIO")
+ self.assertContains(so_page, "nobody@igorville.gov")
+ self.assertNotContains(so_page, "Save")
@less_console_noise_decorator
def test_domain_edit_senior_official_tribal(self):
From 3005158b82fc563f3e94dc7e94e1351772971241 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Fri, 9 Aug 2024 14:05:11 -0600
Subject: [PATCH 30/61] Add unit test
---
src/registrar/tests/test_views_portfolio.py | 31 ++++++++++++++++++++-
1 file changed, 30 insertions(+), 1 deletion(-)
diff --git a/src/registrar/tests/test_views_portfolio.py b/src/registrar/tests/test_views_portfolio.py
index f72130d94..d725c95ad 100644
--- a/src/registrar/tests/test_views_portfolio.py
+++ b/src/registrar/tests/test_views_portfolio.py
@@ -1,7 +1,7 @@
from django.urls import reverse
from api.tests.common import less_console_noise_decorator
from registrar.config import settings
-from registrar.models.portfolio import Portfolio
+from registrar.models import Portfolio, SeniorOfficial
from django_webtest import WebTest # type: ignore
from registrar.models import (
DomainRequest,
@@ -38,6 +38,35 @@ class TestPortfolio(WebTest):
User.objects.all().delete()
super().tearDown()
+ @less_console_noise_decorator
+ @override_flag("organization_feature", active=True)
+ def test_portfolio_senior_official(self):
+ """Tests the senior official page on portfolio"""
+ self.app.set_user(self.user.username)
+
+ so = SeniorOfficial.objects.create(
+ first_name="Saturn", last_name="Enceladus", title="Planet/Moon", email="spacedivision@igorville.com"
+ )
+
+ self.portfolio.senior_official = so
+ self.portfolio.save()
+ self.portfolio.refresh_from_db()
+
+ self.user.portfolio = self.portfolio
+ self.user.portfolio_additional_permissions = [UserPortfolioPermissionChoices.VIEW_PORTFOLIO]
+ self.user.save()
+ self.user.refresh_from_db()
+
+ so_portfolio_page = self.app.get(reverse("senior-official"))
+ # Assert that we're on the right page
+ self.assertContains(so_portfolio_page, "Senior official")
+ self.assertContains(so_portfolio_page, "Saturn Enceladus")
+ self.assertContains(so_portfolio_page, "Planet/Moon")
+ self.assertContains(so_portfolio_page, "spacedivision@igorville.com")
+
+ self.portfolio.delete()
+ so.delete()
+
@less_console_noise_decorator
def test_middleware_does_not_redirect_if_no_permission(self):
"""Test that user with no portfolio permission is not redirected when attempting to access home"""
From 38e1c79e751d0d8266c3531b41383d8fce6379d0 Mon Sep 17 00:00:00 2001
From: matthewswspence
Date: Fri, 9 Aug 2024 15:10:47 -0500
Subject: [PATCH 31/61] Additional required changes
---
src/registrar/templates/domain_base.html | 2 +-
src/registrar/templates/domain_detail.html | 8 ++++----
src/registrar/templates/includes/summary_item.html | 4 ++--
3 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/src/registrar/templates/domain_base.html b/src/registrar/templates/domain_base.html
index 9a869ef42..b99b12740 100644
--- a/src/registrar/templates/domain_base.html
+++ b/src/registrar/templates/domain_base.html
@@ -54,7 +54,7 @@
{% block domain_content %}
- {{ domain.name }}
+ Domain Overview
{% endblock %} {# domain_content #}
{% endif %}
diff --git a/src/registrar/templates/domain_detail.html b/src/registrar/templates/domain_detail.html
index ad187aa4e..312c35f40 100644
--- a/src/registrar/templates/domain_detail.html
+++ b/src/registrar/templates/domain_detail.html
@@ -4,7 +4,7 @@
{% block domain_content %}
{{ block.super }}
-
+
{{ domain.name }}
DNS name servers
+
DNS name servers
No DNS name servers have been added yet. Before your domain can be used we’ll need information about your domain name servers.
Add DNS name servers
{% else %}
{% include "includes/summary_item.html" with title='DNS name servers' domains='true' value='' edit_link=url editable=is_editable %}
{% endif %}
{% endif %}
-
+
{% url 'domain-dns-dnssec' pk=domain.id as url %}
{% if domain.dnssecdata is not None %}
{% include "includes/summary_item.html" with title='DNSSEC' value='Enabled' edit_link=url editable=is_editable %}
{% else %}
- {% include "includes/summary_item.html" with title='DNSSEC' value='Disabled' edit_link=url editable=is_editable %}
+ {% include "includes/summary_item.html" with title='DNSSEC' value='Not Enabled' edit_link=url editable=is_editable %}
{% endif %}
{% if portfolio %}
diff --git a/src/registrar/templates/includes/summary_item.html b/src/registrar/templates/includes/summary_item.html
index 6ec69b770..ff42c2eaf 100644
--- a/src/registrar/templates/includes/summary_item.html
+++ b/src/registrar/templates/includes/summary_item.html
@@ -7,7 +7,7 @@
{% if heading_level %}
<{{ heading_level }}
{% else %}
-
-
-
Senior Official
-
-
Your senior official is a person within your organization who can authorize domain requests.
-
-
The senior official for your organization can’t be updated here. To suggest an update, email help@get.gov
-
- {% if not senior_official %}
-
No senior official was found for this portfolio.
- {% else %}
- {% include "includes/readonly_senior_official.html" with form=form %}
- {% endif %}
-
+ {% include "includes/senior_official.html" with can_edit=False %}
{% endblock %}
diff --git a/src/registrar/views/portfolios.py b/src/registrar/views/portfolios.py
index b9042d617..8a5321cc9 100644
--- a/src/registrar/views/portfolios.py
+++ b/src/registrar/views/portfolios.py
@@ -110,12 +110,6 @@ class PortfolioSeniorOfficialView(PortfolioBasePermissionView, FormMixin):
form_class = PortfolioSeniorOfficialForm
context_object_name = "portfolio"
- def get_context_data(self, **kwargs):
- """Add additional context data to the template."""
- context = super().get_context_data(**kwargs)
- context["senior_official"] = self.get_object().senior_official
- return context
-
def get_object(self, queryset=None):
"""Get the portfolio object based on the request user."""
portfolio = self.request.user.portfolio
From de50972ff1e29c835ee81cfe3f93100a5edd7baa Mon Sep 17 00:00:00 2001
From: Cameron Dixon
Date: Mon, 12 Aug 2024 11:40:31 -0400
Subject: [PATCH 34/61] Update domain_invitation.txt
---
src/registrar/templates/emails/domain_invitation.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/registrar/templates/emails/domain_invitation.txt b/src/registrar/templates/emails/domain_invitation.txt
index e426ae6ef..068040205 100644
--- a/src/registrar/templates/emails/domain_invitation.txt
+++ b/src/registrar/templates/emails/domain_invitation.txt
@@ -38,5 +38,5 @@ The .gov team
Contact us:
Learn about .gov
-The .gov registry is a part of the Cybersecurity and Infrastructure Security Agency (CISA)
+The .gov registry is a part of the Cybersecurity and Infrastructure Security Agency (CISA)
{% endautoescape %}
From d24c7f64604c03493aa76204da11de743efdf4f2 Mon Sep 17 00:00:00 2001
From: Cameron Dixon
Date: Mon, 12 Aug 2024 11:42:11 -0400
Subject: [PATCH 35/61] Update transition_domain_invitation.txt
---
src/registrar/templates/emails/transition_domain_invitation.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/registrar/templates/emails/transition_domain_invitation.txt b/src/registrar/templates/emails/transition_domain_invitation.txt
index bdce7d107..b6773d9e9 100644
--- a/src/registrar/templates/emails/transition_domain_invitation.txt
+++ b/src/registrar/templates/emails/transition_domain_invitation.txt
@@ -60,5 +60,5 @@ The .gov team
Domain management
Get.gov
-The .gov registry is a part of the Cybersecurity and Infrastructure Security Agency (CISA)
+The .gov registry is a part of the Cybersecurity and Infrastructure Security Agency (CISA)
{% endautoescape %}
From 5cd0900fe1e2dea415bb79b0e340c14270351706 Mon Sep 17 00:00:00 2001
From: Cameron Dixon
Date: Mon, 12 Aug 2024 11:42:32 -0400
Subject: [PATCH 36/61] Update questionable_senior_official.txt
---
.../action_needed_reasons/questionable_senior_official.txt | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/registrar/templates/emails/action_needed_reasons/questionable_senior_official.txt b/src/registrar/templates/emails/action_needed_reasons/questionable_senior_official.txt
index 94e15b78c..e20e4cb60 100644
--- a/src/registrar/templates/emails/action_needed_reasons/questionable_senior_official.txt
+++ b/src/registrar/templates/emails/action_needed_reasons/questionable_senior_official.txt
@@ -32,5 +32,5 @@ 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
+The .gov registry is a part of the Cybersecurity and Infrastructure Security Agency (CISA)
+{% endautoescape %}
From 411b1b655739d1226d78278fc49b5dc717e366e4 Mon Sep 17 00:00:00 2001
From: Cameron Dixon
Date: Mon, 12 Aug 2024 11:44:58 -0400
Subject: [PATCH 37/61] Update status_change_rejected.txt
---
src/registrar/templates/emails/status_change_rejected.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/registrar/templates/emails/status_change_rejected.txt b/src/registrar/templates/emails/status_change_rejected.txt
index 12693deb9..2fcbb1d83 100644
--- a/src/registrar/templates/emails/status_change_rejected.txt
+++ b/src/registrar/templates/emails/status_change_rejected.txt
@@ -77,5 +77,5 @@ The .gov team
Contact us:
Learn about .gov
-The .gov registry is a part of the Cybersecurity and Infrastructure Security Agency (CISA)
+The .gov registry is a part of the Cybersecurity and Infrastructure Security Agency (CISA)
{% endautoescape %}
From 5ce926f84badebeb3af5efe562ee42a215b1ec2c Mon Sep 17 00:00:00 2001
From: Cameron Dixon
Date: Mon, 12 Aug 2024 11:45:56 -0400
Subject: [PATCH 38/61] Update eligibility_unclear.txt
---
.../emails/action_needed_reasons/eligibility_unclear.txt | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
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 280321045..d3a986183 100644
--- a/src/registrar/templates/emails/action_needed_reasons/eligibility_unclear.txt
+++ b/src/registrar/templates/emails/action_needed_reasons/eligibility_unclear.txt
@@ -31,5 +31,5 @@ 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
+The .gov registry is a part of the Cybersecurity and Infrastructure Security Agency (CISA)
+{% endautoescape %}
From 77499cdc384fe6cc3292ec901e6aa798da5f577d Mon Sep 17 00:00:00 2001
From: Cameron Dixon
Date: Mon, 12 Aug 2024 11:47:53 -0400
Subject: [PATCH 39/61] Update already_has_domains.txt
---
.../emails/action_needed_reasons/already_has_domains.txt | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
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 264fe265b..b1b3b0a1c 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
@@ -47,5 +47,5 @@ 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
+The .gov registry is a part of the Cybersecurity and Infrastructure Security Agency (CISA)
+{% endautoescape %}
From f8c20946d0b5b6f86da5db1e16f3f22754f99eab Mon Sep 17 00:00:00 2001
From: Cameron Dixon
Date: Mon, 12 Aug 2024 11:48:41 -0400
Subject: [PATCH 40/61] Update submission_confirmation.txt
---
src/registrar/templates/emails/submission_confirmation.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/registrar/templates/emails/submission_confirmation.txt b/src/registrar/templates/emails/submission_confirmation.txt
index 2dd4387a4..740e6f393 100644
--- a/src/registrar/templates/emails/submission_confirmation.txt
+++ b/src/registrar/templates/emails/submission_confirmation.txt
@@ -38,5 +38,5 @@ The .gov team
Contact us:
Learn about .gov
-The .gov registry is a part of the Cybersecurity and Infrastructure Security Agency (CISA)
+The .gov registry is a part of the Cybersecurity and Infrastructure Security Agency (CISA)
{% endautoescape %}
From c1c89f1c57b37b28982073c0361318f3b79d9881 Mon Sep 17 00:00:00 2001
From: Cameron Dixon
Date: Mon, 12 Aug 2024 11:49:03 -0400
Subject: [PATCH 41/61] Update bad_name.txt
---
.../templates/emails/action_needed_reasons/bad_name.txt | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
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 95967639c..7d088aa4e 100644
--- a/src/registrar/templates/emails/action_needed_reasons/bad_name.txt
+++ b/src/registrar/templates/emails/action_needed_reasons/bad_name.txt
@@ -30,5 +30,5 @@ 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
+The .gov registry is a part of the Cybersecurity and Infrastructure Security Agency (CISA)
+{% endautoescape %}
From 6c73175c802e382b63f71ac5af1ee761b06390a4 Mon Sep 17 00:00:00 2001
From: Cameron Dixon
Date: Mon, 12 Aug 2024 11:49:22 -0400
Subject: [PATCH 42/61] Update status_change_approved.txt
---
src/registrar/templates/emails/status_change_approved.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/registrar/templates/emails/status_change_approved.txt b/src/registrar/templates/emails/status_change_approved.txt
index bbef8c81a..70f813599 100644
--- a/src/registrar/templates/emails/status_change_approved.txt
+++ b/src/registrar/templates/emails/status_change_approved.txt
@@ -49,5 +49,5 @@ The .gov team
Contact us:
Learn about .gov
-The .gov registry is a part of the Cybersecurity and Infrastructure Security Agency (CISA)
+The .gov registry is a part of the Cybersecurity and Infrastructure Security Agency (CISA)
{% endautoescape %}
From 7463e356f948fe7aa72813b467224787b408e4f1 Mon Sep 17 00:00:00 2001
From: Cameron Dixon
Date: Mon, 12 Aug 2024 11:54:13 -0400
Subject: [PATCH 43/61] Update domain_request_withdrawn.txt
---
src/registrar/templates/emails/domain_request_withdrawn.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/registrar/templates/emails/domain_request_withdrawn.txt b/src/registrar/templates/emails/domain_request_withdrawn.txt
index 0c061c53c..6efa92d64 100644
--- a/src/registrar/templates/emails/domain_request_withdrawn.txt
+++ b/src/registrar/templates/emails/domain_request_withdrawn.txt
@@ -26,5 +26,5 @@ The .gov team
Contact us:
Learn about .gov
-The .gov registry is a part of the Cybersecurity and Infrastructure Security Agency (CISA)
+The .gov registry is a part of the Cybersecurity and Infrastructure Security Agency (CISA)
{% endautoescape %}
From ad674e646b7e31d8538f28e46c105e6f10a131ea Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Mon, 12 Aug 2024 09:59:10 -0600
Subject: [PATCH 44/61] Fix pre-existing bug with sortfields
The AdminSortFields helper is incorrectly sorting the senior_official contact object.
Added a workaround as this ultimately needs a refactor
---
src/registrar/admin.py | 42 ++++++++++++++++---
src/registrar/forms/domain.py | 2 +-
src/registrar/forms/portfolio.py | 2 +-
.../templates/includes/senior_official.html | 2 +-
src/registrar/tests/test_views_portfolio.py | 3 +-
5 files changed, 41 insertions(+), 10 deletions(-)
diff --git a/src/registrar/admin.py b/src/registrar/admin.py
index ae06a8d96..31cbdc38d 100644
--- a/src/registrar/admin.py
+++ b/src/registrar/admin.py
@@ -22,7 +22,7 @@ from epplibwrapper.errors import ErrorCode, RegistryError
from registrar.models.user_domain_role import UserDomainRole
from waffle.admin import FlagAdmin
from waffle.models import Sample, Switch
-from registrar.models import Contact, Domain, DomainRequest, DraftDomain, User, Website
+from registrar.models import Contact, Domain, DomainRequest, DraftDomain, User, Website, SeniorOfficial
from registrar.utility.errors import FSMDomainRequestError, FSMErrorCodes
from registrar.views.utility.mixins import OrderableFieldsMixin
from django.contrib.admin.views.main import ORDER_VAR
@@ -493,6 +493,8 @@ class CustomLogEntryAdmin(LogEntryAdmin):
# return super().change_view(request, object_id, form_url, extra_context=extra_context)
+# TODO - this should be refactored. This is shared among every class that inherits this,
+# and it breaks the senior_official field because it exists both as model "Contact" and "SeniorOfficial".
class AdminSortFields:
_name_sort = ["first_name", "last_name", "email"]
@@ -504,6 +506,8 @@ class AdminSortFields:
# == Contact == #
"other_contacts": (Contact, _name_sort),
"submitter": (Contact, _name_sort),
+ # == Senior Official == #
+ "senior_official": (SeniorOfficial, _name_sort),
# == User == #
"creator": (User, _name_sort),
"user": (User, _name_sort),
@@ -553,15 +557,16 @@ class AuditedAdmin(admin.ModelAdmin):
)
)
- def formfield_for_manytomany(self, db_field, request, **kwargs):
+ def formfield_for_manytomany(self, db_field, request, use_admin_sort_fields=True, **kwargs):
"""customize the behavior of formfields with manytomany relationships. the customized
behavior includes sorting of objects in lists as well as customizing helper text"""
# Define a queryset. Note that in the super of this,
# a new queryset will only be generated if one does not exist.
# Thus, the order in which we define queryset matters.
+
queryset = AdminSortFields.get_queryset(db_field)
- if queryset:
+ if queryset and use_admin_sort_fields:
kwargs["queryset"] = queryset
formfield = super().formfield_for_manytomany(db_field, request, **kwargs)
@@ -572,7 +577,7 @@ class AuditedAdmin(admin.ModelAdmin):
)
return formfield
- def formfield_for_foreignkey(self, db_field, request, **kwargs):
+ def formfield_for_foreignkey(self, db_field, request, use_admin_sort_fields=True, **kwargs):
"""Customize the behavior of formfields with foreign key relationships. This will customize
the behavior of selects. Customized behavior includes sorting of objects in list."""
@@ -580,7 +585,7 @@ class AuditedAdmin(admin.ModelAdmin):
# a new queryset will only be generated if one does not exist.
# Thus, the order in which we define queryset matters.
queryset = AdminSortFields.get_queryset(db_field)
- if queryset:
+ if queryset and use_admin_sort_fields:
kwargs["queryset"] = queryset
return super().formfield_for_foreignkey(db_field, request, **kwargs)
@@ -1542,6 +1547,16 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
# Get the filtered values
return super().changelist_view(request, extra_context=extra_context)
+ def formfield_for_foreignkey(self, db_field, request, **kwargs):
+ """Customize the behavior of formfields with foreign key relationships. This will customize
+ the behavior of selects. Customized behavior includes sorting of objects in list."""
+ # Remove this check on senior_official if this underlying model changes from
+ # "Contact" to "SeniorOfficial" or if we refactor AdminSortFields.
+ # Removing this will cause the list on django admin to return SeniorOffical
+ # objects rather than Contact objects.
+ use_sort = db_field.name != "senior_official"
+ return super().formfield_for_foreignkey(db_field, request, use_admin_sort_fields=use_sort, **kwargs)
+
class DomainRequestResource(FsmModelResource):
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
@@ -2209,6 +2224,16 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
return None
+ def formfield_for_foreignkey(self, db_field, request, **kwargs):
+ """Customize the behavior of formfields with foreign key relationships. This will customize
+ the behavior of selects. Customized behavior includes sorting of objects in list."""
+ # Remove this check on senior_official if this underlying model changes from
+ # "Contact" to "SeniorOfficial" or if we refactor AdminSortFields.
+ # Removing this will cause the list on django admin to return SeniorOffical
+ # objects rather than Contact objects.
+ use_sort = db_field.name != "senior_official"
+ return super().formfield_for_foreignkey(db_field, request, use_admin_sort_fields=use_sort, **kwargs)
+
class TransitionDomainAdmin(ListHeaderAdmin):
"""Custom transition domain admin class."""
@@ -2258,6 +2283,7 @@ class DomainInformationInline(admin.StackedInline):
def formfield_for_manytomany(self, db_field, request, **kwargs):
"""customize the behavior of formfields with manytomany relationships. the customized
behavior includes sorting of objects in lists as well as customizing helper text"""
+
queryset = AdminSortFields.get_queryset(db_field)
if queryset:
kwargs["queryset"] = queryset
@@ -2272,8 +2298,12 @@ class DomainInformationInline(admin.StackedInline):
def formfield_for_foreignkey(self, db_field, request, **kwargs):
"""Customize the behavior of formfields with foreign key relationships. This will customize
the behavior of selects. Customized behavior includes sorting of objects in list."""
+ # Remove this check on senior_official if this underlying model changes from
+ # "Contact" to "SeniorOfficial" or if we refactor AdminSortFields.
+ # Removing this will cause the list on django admin to return SeniorOffical
+ # objects rather than Contact objects.
queryset = AdminSortFields.get_queryset(db_field)
- if queryset:
+ if queryset and db_field.name != "senior_official":
kwargs["queryset"] = queryset
return super().formfield_for_foreignkey(db_field, request, **kwargs)
diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py
index 4a1e95431..a46a4d3e8 100644
--- a/src/registrar/forms/domain.py
+++ b/src/registrar/forms/domain.py
@@ -326,7 +326,7 @@ class SeniorOfficialContactForm(ContactForm):
def __init__(self, disable_fields=False, *args, **kwargs):
super().__init__(*args, **kwargs)
- if self.instance:
+ if self.instance and self.instance.id:
self.fields["full_name"].initial = self.instance.get_formatted_name()
# Overriding bc phone not required in this form
diff --git a/src/registrar/forms/portfolio.py b/src/registrar/forms/portfolio.py
index 44d195dd6..67ccf2464 100644
--- a/src/registrar/forms/portfolio.py
+++ b/src/registrar/forms/portfolio.py
@@ -87,5 +87,5 @@ class PortfolioSeniorOfficialForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
- if self.instance:
+ if self.instance and self.instance.id:
self.fields["full_name"].initial = self.instance.get_formatted_name()
diff --git a/src/registrar/templates/includes/senior_official.html b/src/registrar/templates/includes/senior_official.html
index c63493ae8..b080127f4 100644
--- a/src/registrar/templates/includes/senior_official.html
+++ b/src/registrar/templates/includes/senior_official.html
@@ -44,4 +44,4 @@
{% if form.email.value is not None %}
{% include "includes/input_read_only.html" with field=form.email %}
{% endif %}
-{% endif %}
\ No newline at end of file
+{% endif %}
diff --git a/src/registrar/tests/test_views_portfolio.py b/src/registrar/tests/test_views_portfolio.py
index d725c95ad..60764cf1c 100644
--- a/src/registrar/tests/test_views_portfolio.py
+++ b/src/registrar/tests/test_views_portfolio.py
@@ -41,7 +41,7 @@ class TestPortfolio(WebTest):
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
def test_portfolio_senior_official(self):
- """Tests the senior official page on portfolio"""
+ """Tests that the senior official page on portfolio contains the content we expect"""
self.app.set_user(self.user.username)
so = SeniorOfficial.objects.create(
@@ -63,6 +63,7 @@ class TestPortfolio(WebTest):
self.assertContains(so_portfolio_page, "Saturn Enceladus")
self.assertContains(so_portfolio_page, "Planet/Moon")
self.assertContains(so_portfolio_page, "spacedivision@igorville.com")
+ self.assertNotContains(so_portfolio_page, "Save")
self.portfolio.delete()
so.delete()
From 6c4e10cab14a9896ffbdda55dfddddabdd06ba07 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Mon, 12 Aug 2024 10:10:38 -0600
Subject: [PATCH 45/61] Fix unit tests
---
src/registrar/forms/domain.py | 6 +++
src/registrar/forms/portfolio.py | 6 +++
src/registrar/tests/test_views_domain.py | 51 +++---------------------
3 files changed, 18 insertions(+), 45 deletions(-)
diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py
index a46a4d3e8..c724ce383 100644
--- a/src/registrar/forms/domain.py
+++ b/src/registrar/forms/domain.py
@@ -351,6 +351,12 @@ class SeniorOfficialContactForm(ContactForm):
if disable_fields:
DomainHelper.mass_disable_fields(fields=self.fields, disable_required=True, disable_maxlength=True)
+ def clean(self):
+ """Clean override to remove unused fields"""
+ cleaned_data = super().clean()
+ cleaned_data.pop("full_name", None)
+ return cleaned_data
+
def save(self, commit=True):
"""
Override the save() method of the BaseModelForm.
diff --git a/src/registrar/forms/portfolio.py b/src/registrar/forms/portfolio.py
index 67ccf2464..cfd23c630 100644
--- a/src/registrar/forms/portfolio.py
+++ b/src/registrar/forms/portfolio.py
@@ -89,3 +89,9 @@ class PortfolioSeniorOfficialForm(forms.ModelForm):
super().__init__(*args, **kwargs)
if self.instance and self.instance.id:
self.fields["full_name"].initial = self.instance.get_formatted_name()
+
+ def clean(self):
+ """Clean override to remove unused fields"""
+ cleaned_data = super().clean()
+ cleaned_data.pop("full_name", None)
+ return cleaned_data
diff --git a/src/registrar/tests/test_views_domain.py b/src/registrar/tests/test_views_domain.py
index efbc1296c..8c79c92aa 100644
--- a/src/registrar/tests/test_views_domain.py
+++ b/src/registrar/tests/test_views_domain.py
@@ -1128,7 +1128,7 @@ class TestDomainSeniorOfficial(TestDomainOverview):
def test_domain_senior_official(self):
"""Can load domain's senior official page."""
page = self.client.get(reverse("domain-senior-official", kwargs={"pk": self.domain.id}))
- self.assertContains(page, "Senior official", count=14)
+ self.assertContains(page, "Senior official", count=3)
@less_console_noise_decorator
def test_domain_senior_official_content(self):
@@ -1207,16 +1207,13 @@ class TestDomainSeniorOfficial(TestDomainOverview):
self.domain_information.save()
so_page = self.app.get(reverse("domain-senior-official", kwargs={"pk": self.domain.id}))
- session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
- self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
-
self.assertContains(so_page, "Apple Tester")
self.assertContains(so_page, "CIO")
self.assertContains(so_page, "nobody@igorville.gov")
self.assertNotContains(so_page, "Save")
@less_console_noise_decorator
- def test_domain_edit_senior_official_tribal(self):
+ def test_domain_cannot_edit_senior_official_tribal(self):
"""Tests that no edit can occur when the underlying domain is tribal"""
# Set the org type to federal
@@ -1231,46 +1228,10 @@ class TestDomainSeniorOfficial(TestDomainOverview):
self.domain_information.save()
so_page = self.app.get(reverse("domain-senior-official", kwargs={"pk": self.domain.id}))
- session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
- self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
-
- # Test if the form is populating data correctly
- so_form = so_page.forms[0]
-
- test_cases = [
- ("first_name", "Apple"),
- ("last_name", "Tester"),
- ("title", "CIO"),
- ("email", "nobody@igorville.gov"),
- ]
- self.assert_all_form_fields_have_expected_values(so_form, test_cases, test_for_disabled=True)
-
- # Attempt to change data on each field. Because this domain is federal,
- # this should not succeed.
- so_form["first_name"] = "Orange"
- so_form["last_name"] = "Smoothie"
- so_form["title"] = "Cat"
- so_form["email"] = "somebody@igorville.gov"
-
- submission = so_form.submit()
-
- # A 302 indicates this page underwent a redirect.
- self.assertEqual(submission.status_code, 302)
-
- followed_submission = submission.follow()
-
- # Test the returned form for data accuracy. These values should be unchanged.
- new_form = followed_submission.forms[0]
- self.assert_all_form_fields_have_expected_values(new_form, test_cases, test_for_disabled=True)
-
- # refresh domain information. Test these values in the DB.
- self.domain_information.refresh_from_db()
-
- # All values should be unchanged. These are defined manually for code clarity.
- self.assertEqual("Apple", self.domain_information.senior_official.first_name)
- self.assertEqual("Tester", self.domain_information.senior_official.last_name)
- self.assertEqual("CIO", self.domain_information.senior_official.title)
- self.assertEqual("nobody@igorville.gov", self.domain_information.senior_official.email)
+ self.assertContains(so_page, "Apple Tester")
+ self.assertContains(so_page, "CIO")
+ self.assertContains(so_page, "nobody@igorville.gov")
+ self.assertNotContains(so_page, "Save")
@less_console_noise_decorator
def test_domain_edit_senior_official_creates_new(self):
From 60c4f54e63c3ab703bc02dbcb7a33c520084479d Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Mon, 12 Aug 2024 10:24:36 -0600
Subject: [PATCH 46/61] Fix suborg issue
---
src/registrar/forms/domain.py | 4 ----
src/registrar/templates/domain_suborganization.html | 2 +-
src/registrar/views/domain.py | 7 +++++++
3 files changed, 8 insertions(+), 5 deletions(-)
diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py
index ec370f268..55b6619d9 100644
--- a/src/registrar/forms/domain.py
+++ b/src/registrar/forms/domain.py
@@ -189,10 +189,6 @@ class DomainSuborganizationForm(forms.ModelForm):
if self.instance and self.instance.sub_organization:
self.fields["sub_organization"].widget.attrs["data-default-value"] = self.instance.sub_organization.pk
- def get_suborganization_name(self):
- """Returns the suborganization name for the readonly view"""
- return self.instance.sub_organization.name if self.instance else None
-
class BaseNameserverFormset(forms.BaseFormSet):
def clean(self):
diff --git a/src/registrar/templates/domain_suborganization.html b/src/registrar/templates/domain_suborganization.html
index c2e3431ad..a3b2231a7 100644
--- a/src/registrar/templates/domain_suborganization.html
+++ b/src/registrar/templates/domain_suborganization.html
@@ -25,7 +25,7 @@
{% else %}
{% with description="The suborganization for this domain can only be updated by a organization administrator."%}
- {% include "includes/input_read_only.html" with field=form.sub_organization value=instance.get_suborganization_name label_description=description%}
+ {% include "includes/input_read_only.html" with field=form.sub_organization value=suborganization_name label_description=description%}
{% endwith %}
{% endif %}
{% endblock %}
diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py
index 949d580bd..9b5f2e1c7 100644
--- a/src/registrar/views/domain.py
+++ b/src/registrar/views/domain.py
@@ -250,6 +250,13 @@ class DomainSubOrganizationView(DomainFormBaseView):
context_object_name = "domain"
form_class = DomainSuborganizationForm
+ def get_context_data(self, **kwargs):
+ """Adds custom context."""
+ context = super().get_context_data(**kwargs)
+ if self.object and self.object.domain_info and self.object.domain_info.sub_organization:
+ context["suborganization_name"] = self.object.domain_info.sub_organization.name
+ return context
+
def get_form_kwargs(self, *args, **kwargs):
"""Add domain_info.organization_name instance to make a bound form."""
form_kwargs = super().get_form_kwargs(*args, **kwargs)
From b59a6243916c5105489a6f5185d59985d5dcd873 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Mon, 12 Aug 2024 10:42:52 -0600
Subject: [PATCH 47/61] Code cleanup
---
src/registrar/assets/js/get-gov.js | 22 +++++++------------
.../templates/domain_suborganization.html | 2 --
src/registrar/views/domain.py | 10 +++++++++
3 files changed, 18 insertions(+), 16 deletions(-)
diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js
index 0bebb4d9b..1d9caafde 100644
--- a/src/registrar/assets/js/get-gov.js
+++ b/src/registrar/assets/js/get-gov.js
@@ -1996,26 +1996,20 @@ document.addEventListener('DOMContentLoaded', function() {
var isTyping = false;
document.addEventListener('DOMContentLoaded', (event) => {
- // The file location for the #undo svg
- const undoIcon = document.querySelector("#uswds-undo-icon-url");
- const undoIconUrl = undoIcon ? undoIcon.getAttribute("data-undo-icon-url") : null;
- if (undoIconUrl) {
- handleAllComboBoxElements(undoIconUrl);
- }
+ handleAllComboBoxElements();
});
- function handleAllComboBoxElements(undoIconUrl) {
+ function handleAllComboBoxElements() {
const comboBoxElements = document.querySelectorAll(".usa-combo-box");
comboBoxElements.forEach(comboBox => {
- const input = comboBox.querySelector('input');
+ const input = comboBox.querySelector("input");
const select = comboBox.querySelector("select");
- if (!input || !undoIconUrl || !select) {
+ if (!input || !select) {
console.warn("No combobox element found");
return;
}
// Set the initial value of the combobox
let initialValue = select.getAttribute("data-default-value");
-
let clearInputButton = comboBox.querySelector(".usa-combo-box__clear-input");
if (!clearInputButton) {
console.warn("No clear element found");
@@ -2041,13 +2035,13 @@ document.addEventListener('DOMContentLoaded', function() {
const config = { childList: true, subtree: true };
observer.observe(dropdownList, config);
- // Add input event listener to detect typing
- input.addEventListener('input', () => {
+ // Input event listener to detect typing
+ input.addEventListener("input", () => {
isTyping = true;
});
- // Add blur event listener to reset typing state
- input.addEventListener('blur', () => {
+ // Blur event listener to reset typing state
+ input.addEventListener("blur", () => {
isTyping = false;
});
diff --git a/src/registrar/templates/domain_suborganization.html b/src/registrar/templates/domain_suborganization.html
index a3b2231a7..42bb766a3 100644
--- a/src/registrar/templates/domain_suborganization.html
+++ b/src/registrar/templates/domain_suborganization.html
@@ -16,8 +16,6 @@
{% if has_domains_portfolio_permission and request.user.has_edit_suborganization %}
- {% comment %} Store the undo icon for reference in js - since this is dynamic {% endcomment %}
-