diff --git a/src/registrar/assets/sass/_theme/_forms.scss b/src/registrar/assets/sass/_theme/_forms.scss index 08e35b19f..9158de174 100644 --- a/src/registrar/assets/sass/_theme/_forms.scss +++ b/src/registrar/assets/sass/_theme/_forms.scss @@ -1,5 +1,6 @@ @use "uswds-core" as *; @use "cisa_colors" as *; +@use "typography" as *; .usa-form .usa-button { margin-top: units(3); @@ -69,9 +70,9 @@ legend.float-left-tablet + button.float-right-tablet { } .read-only-label { - font-size: size('body', 'sm'); + @extend .h4--sm-05; + font-weight: bold; color: color('primary-dark'); - margin-bottom: units(0.5); } .read-only-value { diff --git a/src/registrar/assets/sass/_theme/_typography.scss b/src/registrar/assets/sass/_theme/_typography.scss index d815ef6dd..466b6f975 100644 --- a/src/registrar/assets/sass/_theme/_typography.scss +++ b/src/registrar/assets/sass/_theme/_typography.scss @@ -23,6 +23,13 @@ h2 { color: color('primary-darker'); } +.h4--sm-05 { + font-size: size('body', 'sm'); + font-weight: normal; + color: color('primary'); + margin-bottom: units(0.5); +} + // Normalize typography in forms .usa-form, .usa-form fieldset { diff --git a/src/registrar/context_processors.py b/src/registrar/context_processors.py index ae35a8865..c1547ad88 100644 --- a/src/registrar/context_processors.py +++ b/src/registrar/context_processors.py @@ -68,6 +68,7 @@ def portfolio_permissions(request): "has_organization_feature_flag": False, "has_organization_requests_flag": False, "has_organization_members_flag": False, + "is_portfolio_admin": False, } try: portfolio = request.session.get("portfolio") @@ -88,6 +89,7 @@ def portfolio_permissions(request): "has_organization_feature_flag": True, "has_organization_requests_flag": request.user.has_organization_requests_flag(), "has_organization_members_flag": request.user.has_organization_members_flag(), + "is_portfolio_admin": request.user.is_portfolio_admin(portfolio), } return portfolio_context diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py index 2d65aa02e..6c9c37c92 100644 --- a/src/registrar/models/user.py +++ b/src/registrar/models/user.py @@ -258,6 +258,9 @@ class User(AbstractUser): def has_edit_suborganization_portfolio_permission(self, portfolio): return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.EDIT_SUBORGANIZATION) + def is_portfolio_admin(self, portfolio): + return "Admin" in self.portfolio_role_summary(portfolio) + def get_first_portfolio(self): permission = self.portfolio_permissions.first() if permission: diff --git a/src/registrar/templates/domain_add_user.html b/src/registrar/templates/domain_add_user.html index fa3f8e821..1429127e6 100644 --- a/src/registrar/templates/domain_add_user.html +++ b/src/registrar/templates/domain_add_user.html @@ -5,6 +5,25 @@ {% block domain_content %} {% block breadcrumb %} + {% if portfolio %} + + + {% else %} {% url 'domain-users' pk=domain.id as url %} + {% endif %} {% endblock breadcrumb %}

Add a domain manager

{% if has_organization_feature_flag %} diff --git a/src/registrar/templates/domain_detail.html b/src/registrar/templates/domain_detail.html index 96ec4c5b6..add7ca725 100644 --- a/src/registrar/templates/domain_detail.html +++ b/src/registrar/templates/domain_detail.html @@ -3,6 +3,22 @@ {% load custom_filters %} {% block domain_content %} + {% block breadcrumb %} + {% if portfolio %} + + + {% endif %} + {% endblock breadcrumb %} + {{ block.super }}

{{ domain.name }}

@@ -74,13 +90,17 @@ {% include "includes/summary_item.html" with title='DNSSEC' value='Not Enabled' edit_link=url editable=is_editable %} {% endif %} - {% if portfolio and has_any_domains_portfolio_permission and has_view_suborganization_portfolio_permission %} - {% url 'domain-suborganization' pk=domain.id as url %} - {% include "includes/summary_item.html" with title='Suborganization' value=domain.domain_info.sub_organization edit_link=url editable=is_editable|and:has_edit_suborganization_portfolio_permission %} + {% if portfolio %} + {% if has_any_domains_portfolio_permission and has_edit_suborganization_portfolio_permission %} + {% url 'domain-suborganization' pk=domain.id as url %} + {% include "includes/summary_item.html" with title='Suborganization' value=domain.domain_info.sub_organization edit_link=url editable=is_editable|and:has_edit_suborganization_portfolio_permission %} + {% elif has_any_domains_portfolio_permission and has_view_suborganization_portfolio_permission %} + {% url 'domain-suborganization' pk=domain.id as url %} + {% include "includes/summary_item.html" with title='Suborganization' value=domain.domain_info.sub_organization edit_link=url editable=is_editable|and:has_view_suborganization_portfolio_permission view_button=True %} + {% endif %} {% else %} {% url 'domain-org-name-address' pk=domain.id as url %} {% include "includes/summary_item.html" with title='Organization' value=domain.domain_info address='true' edit_link=url editable=is_editable %} - {% url 'domain-senior-official' pk=domain.id as url %} {% include "includes/summary_item.html" with title='Senior official' value=domain.domain_info.senior_official contact='true' edit_link=url editable=is_editable %} {% endif %} @@ -92,7 +112,11 @@ {% include "includes/summary_item.html" with title='Security email' value='None provided' edit_link=url editable=is_editable %} {% endif %} {% url 'domain-users' pk=domain.id as url %} - {% include "includes/summary_item.html" with title='Domain managers' users='true' list=True value=domain.permissions.all edit_link=url editable=is_editable %} + {% if portfolio %} + {% include "includes/summary_item.html" with title='Domain managers' domain_permissions=True value=domain edit_link=url editable=is_editable %} + {% else %} + {% include "includes/summary_item.html" with title='Domain managers' list=True users=True value=domain.permissions.all edit_link=url editable=is_editable %} + {% endif %}
{% endblock %} {# domain_content #} diff --git a/src/registrar/templates/domain_dns.html b/src/registrar/templates/domain_dns.html index 291319a59..9a2070c64 100644 --- a/src/registrar/templates/domain_dns.html +++ b/src/registrar/templates/domain_dns.html @@ -4,6 +4,24 @@ {% block title %}DNS | {{ domain.name }} | {% endblock %} {% block domain_content %} + {% block breadcrumb %} + {% if portfolio %} + + + {% endif %} + {% endblock breadcrumb %}

DNS

diff --git a/src/registrar/templates/domain_dnssec.html b/src/registrar/templates/domain_dnssec.html index 7742a329b..cfec053c2 100644 --- a/src/registrar/templates/domain_dnssec.html +++ b/src/registrar/templates/domain_dnssec.html @@ -5,6 +5,28 @@ {% block domain_content %} + {% block breadcrumb %} + {% if portfolio %} + + + {% endif %} + {% endblock breadcrumb %} +

DNSSEC

DNSSEC, or DNS Security Extensions, is an additional security layer to protect your website. Enabling DNSSEC ensures that when someone visits your domain, they can be certain that it’s connecting to the correct server, preventing potential hijacking or tampering with your domain's records.

diff --git a/src/registrar/templates/domain_dsdata.html b/src/registrar/templates/domain_dsdata.html index ba742ab09..0f60235e1 100644 --- a/src/registrar/templates/domain_dsdata.html +++ b/src/registrar/templates/domain_dsdata.html @@ -4,6 +4,32 @@ {% block title %}DS data | {{ domain.name }} | {% endblock %} {% block domain_content %} + + {% block breadcrumb %} + {% if portfolio %} + + + {% endif %} + {% endblock breadcrumb %} + {% if domain.dnssecdata is None %}
diff --git a/src/registrar/templates/domain_nameservers.html b/src/registrar/templates/domain_nameservers.html index cc1fc0164..a5fd171a2 100644 --- a/src/registrar/templates/domain_nameservers.html +++ b/src/registrar/templates/domain_nameservers.html @@ -4,6 +4,28 @@ {% block title %}DNS name servers | {{ domain.name }} | {% endblock %} {% block domain_content %} + {% block breadcrumb %} + {% if portfolio %} + + + {% endif %} + {% endblock breadcrumb %} + {# this is right after the messages block in the parent template #} {% for form in formset %} {% include "includes/form_errors.html" with form=form %} diff --git a/src/registrar/templates/domain_security_email.html b/src/registrar/templates/domain_security_email.html index e1755f85e..f5a58eb5d 100644 --- a/src/registrar/templates/domain_security_email.html +++ b/src/registrar/templates/domain_security_email.html @@ -4,6 +4,25 @@ {% block title %}Security email | {{ domain.name }} | {% endblock %} {% block domain_content %} + {% block breadcrumb %} + {% if portfolio %} + + + {% endif %} + {% endblock breadcrumb %} + {% include "includes/form_errors.html" with form=form %}

Security email

diff --git a/src/registrar/templates/domain_suborganization.html b/src/registrar/templates/domain_suborganization.html index 67726e9d5..648563d58 100644 --- a/src/registrar/templates/domain_suborganization.html +++ b/src/registrar/templates/domain_suborganization.html @@ -4,9 +4,30 @@ {% block title %}Suborganization{% if suborganization_name %} | suborganization_name{% endif %} | {% endblock %} {% block domain_content %} + + {% block breadcrumb %} + {% if portfolio %} + + + {% endif %} + {% endblock breadcrumb %} + {# this is right after the messages block in the parent template #} {% include "includes/form_errors.html" with form=form %} +

Suborganization

diff --git a/src/registrar/templates/domain_users.html b/src/registrar/templates/domain_users.html index b8a622455..af292d9d5 100644 --- a/src/registrar/templates/domain_users.html +++ b/src/registrar/templates/domain_users.html @@ -4,6 +4,25 @@ {% block title %}Domain managers | {{ domain.name }} | {% endblock %} {% block domain_content %} + {% block breadcrumb %} + {% if portfolio %} + +

+ {% endif %} + {% endblock breadcrumb %} +

Domain managers

{% comment %}Copy below differs depending on whether view is in portfolio mode.{% endcomment %} diff --git a/src/registrar/templates/includes/summary_item.html b/src/registrar/templates/includes/summary_item.html index 0600d7ea7..15cc0f67f 100644 --- a/src/registrar/templates/includes/summary_item.html +++ b/src/registrar/templates/includes/summary_item.html @@ -106,6 +106,26 @@ {% endfor %} {% endif %} + {% elif domain_permissions %} + {% if value.permissions.all %} + {% if value.permissions|length == 1 %} +

{{ value.permissions.0.user.email }}

+ {% else %} + + {% endif %} + {% endif %} + {% if value.invitations.all %} +

Invited domain managers

+ + {% endif %} {% else %}

{% if value %} diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py index 0c1bdec2a..46604a44a 100644 --- a/src/registrar/tests/test_models.py +++ b/src/registrar/tests/test_models.py @@ -824,6 +824,15 @@ class TestUser(TestCase): cm.exception.message, "When portfolio roles or additional permissions are assigned, portfolio is required." ) + @less_console_noise_decorator + def test_user_with_admin_portfolio_role(self): + portfolio, _ = Portfolio.objects.get_or_create(creator=self.user, organization_name="Hotel California") + self.assertFalse(self.user.is_portfolio_admin(portfolio)) + UserPortfolioPermission.objects.get_or_create( + portfolio=portfolio, user=self.user, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN] + ) + self.assertTrue(self.user.is_portfolio_admin(portfolio)) + @less_console_noise_decorator def test_get_active_requests_count_in_portfolio_returns_zero_if_no_portfolio(self): # There is no portfolio referenced in session so should return 0 diff --git a/src/registrar/tests/test_views_domain.py b/src/registrar/tests/test_views_domain.py index 678d5be82..25e8b0fb6 100644 --- a/src/registrar/tests/test_views_domain.py +++ b/src/registrar/tests/test_views_domain.py @@ -6,7 +6,7 @@ from django.urls import reverse from django.contrib.auth import get_user_model from waffle.testutils import override_flag from api.tests.common import less_console_noise_decorator -from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices +from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices from .common import MockEppLib, MockSESClient, create_user # type: ignore from django_webtest import WebTest # type: ignore import boto3_mocking # type: ignore @@ -142,6 +142,7 @@ class TestWithDomainPermissions(TestWithUser): def tearDown(self): try: UserDomainRole.objects.all().delete() + DomainInvitation.objects.all().delete() if hasattr(self.domain, "contacts"): self.domain.contacts.all().delete() DomainRequest.objects.all().delete() @@ -341,7 +342,7 @@ class TestDomainDetail(TestDomainOverview): detail_page = self.client.get(reverse("domain", kwargs={"pk": self.domain.id})) self.assertNotContains( - detail_page, "To manage information for this domain, you must add yourself as a domain manager." + detail_page, "If you need to make updates, contact one of the listed domain managers." ) @less_console_noise_decorator @@ -363,7 +364,12 @@ class TestDomainDetail(TestDomainOverview): DomainInformation.objects.get_or_create(creator=user, domain=domain, portfolio=portfolio) UserPortfolioPermission.objects.get_or_create( - user=user, portfolio=portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN] + user=user, + portfolio=portfolio, + roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER], + additional_permissions=[ + UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS, + ], ) user.refresh_from_db() self.client.force_login(user) @@ -377,6 +383,45 @@ class TestDomainDetail(TestDomainOverview): ) # Check that user does not have option to Edit domain self.assertNotContains(detail_page, "Edit") + # Check that invited domain manager section not displayed when no invited domain managers + self.assertNotContains(detail_page, "Invited domain managers") + + @less_console_noise_decorator + @override_flag("organization_feature", active=True) + def test_domain_readonly_on_detail_page_for_org_admin_not_manager(self): + """Test that a domain, which is part of a portfolio, but for which the user is not a domain manager, + properly displays read only""" + + portfolio, _ = Portfolio.objects.get_or_create(organization_name="Test org", creator=self.user) + # need to create a different user than self.user because the user needs permission assignments + user = get_user_model().objects.create( + first_name="Test", + last_name="User", + email="bogus@example.gov", + phone="8003111234", + title="test title", + ) + domain, _ = Domain.objects.get_or_create(name="bogusdomain.gov") + DomainInformation.objects.get_or_create(creator=user, domain=domain, portfolio=portfolio) + + UserPortfolioPermission.objects.get_or_create( + user=user, portfolio=portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN] + ) + # add a domain invitation + DomainInvitation.objects.get_or_create(email="invited@example.com", domain=domain) + user.refresh_from_db() + self.client.force_login(user) + detail_page = self.client.get(f"/domain/{domain.id}") + # Check that alert message displays properly + self.assertContains( + detail_page, + "If you need to make updates, contact one of the listed domain managers.", + ) + # Check that user does not have option to Edit domain + self.assertNotContains(detail_page, "Edit") + # Check that invited domain manager is displayed + self.assertContains(detail_page, "Invited domain managers") + self.assertContains(detail_page, "invited@example.com") class TestDomainManagers(TestDomainOverview):