diff --git a/src/registrar/templates/domain_users.html b/src/registrar/templates/domain_users.html index a2eb3e604..7fe97233f 100644 --- a/src/registrar/templates/domain_users.html +++ b/src/registrar/templates/domain_users.html @@ -6,21 +6,30 @@ {% block domain_content %}

Domain managers

+ {% comment %}Copy below differs depending on whether view is in portfolio mode.{% endcomment %} + {% if not portfolio %} +

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

+ {% else %}

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

+ {% endif %} - {% if domain.permissions %} + {% if domain_manager_roles %}

Domain managers

@@ -28,17 +37,18 @@

Domain managers

- + {% if not portfolio %}{% endif %} - {% for permission in domain.permissions.all %} + {% for item in domain_manager_roles %} - - + {% if not portfolio %}{% endif %}
EmailRoleRoleAction
- {{ permission.user.email }} + + {{ item.permission.user.email }} + {% if item.has_admin_flag %}Admin{% endif %} {{ permission.role|title }}{{ item.permission.role|title }} {% if can_delete_users %} Domain managers Remove {# Display a custom message if the user is trying to delete themselves #} - {% if permission.user.email == current_user_email %} + {% if item.permission.user.email == current_user_email %}
Domain managers aria-describedby="You will be removed from this domain" data-force-action > -
+ {% with domain_name=domain.name|force_escape %} {% include 'includes/modal.html' with modal_heading="Are you sure you want to remove yourself as a domain manager?" modal_description="You will no longer be able to manage the domain "|add:domain_name|add:"."|safe modal_button=modal_button_self|safe %} {% endwith %} @@ -71,11 +81,11 @@

Domain managers

class="usa-modal" id="toggle-user-alert-{{ forloop.counter }}" aria-labelledby="Are you sure you want to continue?" - aria-describedby="{{ permission.user.email }} will be removed" + aria-describedby="{{ item.permission.user.email }} will be removed" data-force-action > - - {% with email=permission.user.email|default:permission.user|force_escape domain_name=domain.name|force_escape %} + + {% with email=item.permission.user.email|default:item.permission.user|force_escape domain_name=domain.name|force_escape %} {% include 'includes/modal.html' with modal_heading="Are you sure you want to remove " heading_value=email|add:"?" modal_description=""|add:email|add:" will no longer be able to manage the domain "|add:domain_name|add:"."|safe modal_button=modal_button|safe %} {% endwith %}
@@ -111,7 +121,7 @@

Domain managers

- {% if domain.invitations.exists %} + {% if invitations %}

Invitations

@@ -120,21 +130,22 @@

Invitations

- + {% if not portfolio %}{% endif %} - {% for invitation in domain.invitations.all %} + {% for invitation in invitations %} - - - + + {% if not portfolio %}{% endif %}
Email Date createdStatusStatusAction
- {{ invitation.email }} + + {{ invitation.domain_invitation.email }} + {% if invitation.has_admin_flag %}Admin{% endif %} {{ invitation.created_at|date }} {{ invitation.status|title }}{{ invitation.domain_invitation.created_at|date }} {{ invitation.domain_invitation.status|title }} - {% if invitation.status == invitation.DomainInvitationStatus.INVITED %} -
+ {% if invitation.domain_invitation.status == invitation.domain_invitation.DomainInvitationStatus.INVITED %} + {% csrf_token %}
{% endif %} diff --git a/src/registrar/tests/test_views_domain.py b/src/registrar/tests/test_views_domain.py index a375493be..1b7731222 100644 --- a/src/registrar/tests/test_views_domain.py +++ b/src/registrar/tests/test_views_domain.py @@ -370,6 +370,17 @@ def setUpClass(cls): ] AllowedEmail.objects.bulk_create(allowed_emails) + def setUp(self): + super().setUp() + # Add portfolio in order to test portfolio view + self.portfolio = Portfolio.objects.create(creator=self.user, organization_name="Ice Cream") + # Add the portfolio to the domain_information object + self.domain_information.portfolio = self.portfolio + # Add portfolio perms to the user object + self.portfolio_permission, _ = UserPortfolioPermission.objects.get_or_create( + user=self.user, portfolio=self.portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN] + ) + @classmethod def tearDownClass(cls): super().tearDownClass() @@ -383,13 +394,22 @@ def tearDown(self): def test_domain_managers(self): response = self.client.get(reverse("domain-users", kwargs={"pk": self.domain.id})) self.assertContains(response, "Domain managers") + self.assertContains(response, "Add a domain manager") + # assert that the non-portfolio view contains Role column and doesn't contain Admin + self.assertContains(response, "Role") + self.assertNotContains(response, "Admin") + self.assertContains(response, "This domain has one manager. Adding more can prevent issues.") @less_console_noise_decorator - def test_domain_managers_add_link(self): - """Button to get to user add page works.""" - management_page = self.app.get(reverse("domain-users", kwargs={"pk": self.domain.id})) - add_page = management_page.click("Add a domain manager") - self.assertContains(add_page, "Add a domain manager") + @override_flag("organization_feature", active=True) + def test_domain_managers_portfolio_view(self): + response = self.client.get(reverse("domain-users", kwargs={"pk": self.domain.id})) + self.assertContains(response, "Domain managers") + self.assertContains(response, "Add a domain manager") + # assert that the portfolio view doesn't contain Role column and does contain Admin + self.assertNotContains(response, "Role") + self.assertContains(response, "Admin") + self.assertContains(response, "This domain has one manager. Adding more can prevent issues.") @less_console_noise_decorator def test_domain_user_add(self): diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 6e85c9ffb..c378feeb9 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -28,6 +28,7 @@ UserPortfolioPermission, PublicContact, ) +from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices from registrar.utility.enums import DefaultEmail from registrar.utility.errors import ( GenericError, @@ -841,11 +842,86 @@ def get_context_data(self, **kwargs): # Add modal buttons to the context (such as for delete) context = self._add_modal_buttons_to_context(context) + # Get portfolio from session (if set) + portfolio = self.request.session.get("portfolio") + + # Add domain manager roles separately in order to also pass admin status + context = self._add_domain_manager_roles_to_context(context, portfolio) + + # Add domain invitations separately in order to also pass admin status + context = self._add_invitations_to_context(context, portfolio) + # Get the email of the current user context["current_user_email"] = self.request.user.email return context + def get(self, request, *args, **kwargs): + """Get method for DomainUsersView.""" + # Call the parent class's `get` method to get the response and context + response = super().get(request, *args, **kwargs) + + # Ensure context is available after the parent call + context = response.context_data if hasattr(response, "context_data") else {} + + # Check if context contains `domain_managers_roles` and its length is 1 + if context.get("domain_manager_roles") and len(context["domain_manager_roles"]) == 1: + # Add an info message + messages.info(request, "This domain has one manager. Adding more can prevent issues.") + + return response + + def _add_domain_manager_roles_to_context(self, context, portfolio): + """Add domain_manager_roles to context separately, as roles need admin indicator.""" + + # Prepare a list to store roles with an admin flag + domain_manager_roles = [] + + for permission in self.object.permissions.all(): + # Determine if the user has the ORGANIZATION_ADMIN role + has_admin_flag = any( + UserPortfolioRoleChoices.ORGANIZATION_ADMIN in portfolio_permission.roles + and portfolio == portfolio_permission.portfolio + for portfolio_permission in permission.user.portfolio_permissions.all() + ) + + # Add the role along with the computed flag to the list + domain_manager_roles.append({"permission": permission, "has_admin_flag": has_admin_flag}) + + # Pass roles_with_flags to the context + context["domain_manager_roles"] = domain_manager_roles + + return context + + def _add_invitations_to_context(self, context, portfolio): + """Add invitations to context separately as invitations needs admin indicator.""" + + # Prepare a list to store invitations with an admin flag + invitations = [] + + for domain_invitation in self.object.invitations.all(): + # Check if there are any PortfolioInvitations linked to the same portfolio with the ORGANIZATION_ADMIN role + has_admin_flag = False + + # Query PortfolioInvitations linked to the same portfolio and check roles + portfolio_invitations = PortfolioInvitation.objects.filter( + portfolio=portfolio, email=domain_invitation.email + ) + + # If any of the PortfolioInvitations have the ORGANIZATION_ADMIN role, set the flag to True + for portfolio_invitation in portfolio_invitations: + if UserPortfolioRoleChoices.ORGANIZATION_ADMIN in portfolio_invitation.roles: + has_admin_flag = True + break # Once we find one match, no need to check further + + # Add the role along with the computed flag to the list + invitations.append({"domain_invitation": domain_invitation, "has_admin_flag": has_admin_flag}) + + # Pass roles_with_flags to the context + context["invitations"] = invitations + + return context + def _add_booleans_to_context(self, context): # Determine if the current user can delete managers domain_pk = None