diff --git a/src/registrar/admin.py b/src/registrar/admin.py
index 2d2b90a5f..4b05bbb6d 100644
--- a/src/registrar/admin.py
+++ b/src/registrar/admin.py
@@ -1327,6 +1327,7 @@ class UserPortfolioPermissionAdmin(ListHeaderAdmin):
search_help_text = "Search by first name, last name, email, or portfolio."
change_form_template = "django/admin/user_portfolio_permission_change_form.html"
+ delete_confirmation_template = "django/admin/user_portfolio_permission_delete_confirmation.html"
def get_roles(self, obj):
readable_roles = obj.get_readable_roles()
@@ -1670,6 +1671,7 @@ class PortfolioInvitationAdmin(BaseInvitationAdmin):
autocomplete_fields = ["portfolio"]
change_form_template = "django/admin/portfolio_invitation_change_form.html"
+ delete_confirmation_template = "django/admin/portfolio_invitation_delete_confirmation.html"
# Select portfolio invitations to change -> Portfolio invitations
def changelist_view(self, request, extra_context=None):
@@ -2287,11 +2289,12 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
@admin.display(description=_("Requested Domain"))
def custom_requested_domain(self, obj):
# Example: Show different icons based on `status`
- url = reverse("admin:registrar_domainrequest_changelist") + f"{obj.id}"
text = obj.requested_domain
if obj.portfolio:
- return format_html(' {}', url, text)
- return format_html('{}', url, text)
+ return format_html(
+ f'
{escape(text)}'
+ )
+ return text
custom_requested_domain.admin_order_field = "requested_domain__name" # type: ignore
@@ -3738,11 +3741,13 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
# Using variables to get past the linter
message1 = f"Cannot delete Domain when in state {obj.state}"
message2 = f"This subdomain is being used as a hostname on another domain: {err.note}"
+ message3 = f"Command failed with note: {err.note}"
# Human-readable mappings of ErrorCodes. Can be expanded.
error_messages = {
# noqa on these items as black wants to reformat to an invalid length
ErrorCode.OBJECT_STATUS_PROHIBITS_OPERATION: message1,
ErrorCode.OBJECT_ASSOCIATION_PROHIBITS_OPERATION: message2,
+ ErrorCode.COMMAND_FAILED: message3,
}
message = "Cannot connect to the registry"
diff --git a/src/registrar/assets/src/js/getgov-admin/button-utils.js b/src/registrar/assets/src/js/getgov-admin/button-utils.js
new file mode 100644
index 000000000..e3746d289
--- /dev/null
+++ b/src/registrar/assets/src/js/getgov-admin/button-utils.js
@@ -0,0 +1,15 @@
+/**
+ * Initializes buttons to behave like links by navigating to their data-url attribute
+ * Example usage:
+ */
+export function initButtonLinks() {
+ document.querySelectorAll('button.use-button-as-link').forEach(button => {
+ button.addEventListener('click', function() {
+ // Equivalent to button.getAttribute("data-href")
+ const href = this.dataset.href;
+ if (href) {
+ window.location.href = href;
+ }
+ });
+ });
+}
diff --git a/src/registrar/assets/src/js/getgov-admin/main.js b/src/registrar/assets/src/js/getgov-admin/main.js
index 5c6de20ab..7eb1fc8cd 100644
--- a/src/registrar/assets/src/js/getgov-admin/main.js
+++ b/src/registrar/assets/src/js/getgov-admin/main.js
@@ -16,6 +16,7 @@ import { initDynamicPortfolioFields } from './portfolio-form.js';
import { initDynamicDomainInformationFields } from './domain-information-form.js';
import { initDynamicDomainFields } from './domain-form.js';
import { initAnalyticsDashboard } from './analytics.js';
+import { initButtonLinks } from './button-utils.js';
// General
initModals();
@@ -23,6 +24,7 @@ initCopyToClipboard();
initFilterHorizontalWidget();
initDescriptions();
initSubmitBar();
+initButtonLinks();
// Domain request
initIneligibleModal();
diff --git a/src/registrar/assets/src/js/getgov/helpers.js b/src/registrar/assets/src/js/getgov/helpers.js
index 7d1449bac..08be011c2 100644
--- a/src/registrar/assets/src/js/getgov/helpers.js
+++ b/src/registrar/assets/src/js/getgov/helpers.js
@@ -96,3 +96,14 @@ export function submitForm(form_id) {
console.error("Form '" + form_id + "' not found.");
}
}
+
+/**
+ * Helper function to strip HTML tags
+ * THIS IS NOT SUITABLE FOR SANITIZING DANGEROUS STRINGS
+ */
+export function unsafeStripHtmlTags(input) {
+ const tempDiv = document.createElement("div");
+ // NOTE: THIS IS NOT SUITABLE FOR SANITIZING DANGEROUS STRINGS
+ tempDiv.innerHTML = input;
+ return tempDiv.textContent || tempDiv.innerText || "";
+}
diff --git a/src/registrar/assets/src/js/getgov/portfolio-member-page.js b/src/registrar/assets/src/js/getgov/portfolio-member-page.js
index 95723fc7e..96961e5dc 100644
--- a/src/registrar/assets/src/js/getgov/portfolio-member-page.js
+++ b/src/registrar/assets/src/js/getgov/portfolio-member-page.js
@@ -18,7 +18,7 @@ export function initPortfolioNewMemberPageToggle() {
const unique_id = `${member_type}-${member_id}`;
let cancelInvitationButton = member_type === "invitedmember" ? "Cancel invitation" : "Remove member";
- wrapperDeleteAction.innerHTML = generateKebabHTML('remove-member', unique_id, cancelInvitationButton, `More Options for ${member_name}`);
+ wrapperDeleteAction.innerHTML = generateKebabHTML('remove-member', unique_id, cancelInvitationButton, `More Options for ${member_name}`, "usa-icon--large");
// This easter egg is only for fixtures that dont have names as we are displaying their emails
// All prod users will have emails linked to their account
@@ -100,8 +100,8 @@ export function initAddNewMemberPageListeners() {
const permissionSections = document.querySelectorAll(`#${permission_details_div_id} > h3`);
permissionSections.forEach(section => {
- // Find the
This member is assigned to ${num_domains} domain${num_domains > 1 ? 's' : ''}:
`; - domainsHTML += "This member is assigned to ${num_domains} domain${num_domains > 1 ? 's' : ''}:
`; + if (num_domains > 1) { + domainsHTML += "This member is assigned to 0 domains.
`; } + // If there are more than 6 domains, display a "View assigned domains" link + domainsHTML += ``; + domainsHTML += "Domains: Can view all organization domains. Can manage domains they are assigned to and edit information about the domain (including DNS settings).
`; + domainValue = "Viewer"; } else if (member_permissions.includes(UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS)) { - permissionsHTML += `Domains: Can manage domains they are assigned to and edit information about the domain (including DNS settings).
`; + domainValue = "Viewer, limited"; } - - // Check request-related permissions + + // 3. Request access + let requestValue = "No access"; if (member_permissions.includes(UserPortfolioPermissionChoices.EDIT_REQUESTS)) { - permissionsHTML += `Domain requests: Can view all organization domain requests. Can create domain requests and modify their own requests.
`; + requestValue = "Creator"; } else if (member_permissions.includes(UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS)) { - permissionsHTML += `Domain requests (view-only): Can view all organization domain requests. Can't create or modify any domain requests.
`; + requestValue = "Viewer"; } - - // Check member-related permissions + + // 4. Member access + let memberValue = "No access"; if (member_permissions.includes(UserPortfolioPermissionChoices.EDIT_MEMBERS)) { - permissionsHTML += `Members: Can manage members including inviting new members, removing current members, and assigning domains to members.
`; + memberValue = "Manager"; } else if (member_permissions.includes(UserPortfolioPermissionChoices.VIEW_MEMBERS)) { - permissionsHTML += `Members (view-only): Can view all organizational members. Can't manage any members.
`; + memberValue = "Viewer"; } - - // If no specific permissions are assigned, display a message indicating no additional permissions - if (!permissionsHTML) { - permissionsHTML += `No additional permissions: There are no additional permissions for this member.
`; - } - - // Add a permissions header and wrap the entire output in a container - permissionsHTML = `${label}: ${value}
`; + }; + const permissionsHTML = ` +description beneath each option self.fields["domain_permissions"].descriptions = { - UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS.value: "Can view only the domains they manage", - UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS.value: "Can view all domains for the organization", + UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS.value: get_domains_description_display( + UserPortfolioRoleChoices.ORGANIZATION_MEMBER, None + ), + UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS.value: get_domains_description_display( + UserPortfolioRoleChoices.ORGANIZATION_MEMBER, [UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS] + ), } self.fields["domain_request_permissions"].descriptions = { UserPortfolioPermissionChoices.EDIT_REQUESTS.value: ( - "Can view all domain requests for the organization and create requests" + get_domain_requests_description_display( + UserPortfolioRoleChoices.ORGANIZATION_MEMBER, [UserPortfolioPermissionChoices.EDIT_REQUESTS] + ) ), - UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value: "Can view all domain requests for the organization", - "no_access": "Cannot view or create domain requests", + UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value: ( + get_domain_requests_description_display( + UserPortfolioRoleChoices.ORGANIZATION_MEMBER, [UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS] + ) + ), + "no_access": get_domain_requests_description_display(UserPortfolioRoleChoices.ORGANIZATION_MEMBER, None), } self.fields["member_permissions"].descriptions = { - UserPortfolioPermissionChoices.VIEW_MEMBERS.value: "Can view all members permissions", - "no_access": "Cannot view member permissions", + UserPortfolioPermissionChoices.VIEW_MEMBERS.value: get_members_description_display( + UserPortfolioRoleChoices.ORGANIZATION_MEMBER, [UserPortfolioPermissionChoices.VIEW_MEMBERS] + ), + "no_access": get_members_description_display(UserPortfolioRoleChoices.ORGANIZATION_MEMBER, None), } # Map model instance values to custom form fields @@ -218,6 +262,9 @@ class BasePortfolioMemberForm(forms.ModelForm): cleaned_data = super().clean() role = cleaned_data.get("role") + # handle role + cleaned_data["roles"] = [role] if role else [] + # Get required fields for the selected role. Then validate all required fields for the role. required_fields = self.ROLE_REQUIRED_FIELDS.get(role, []) for field_name in required_fields: @@ -236,9 +283,6 @@ class BasePortfolioMemberForm(forms.ModelForm): if cleaned_data.get("member_permissions") == "no_access": cleaned_data["member_permissions"] = None - # Handle roles - cleaned_data["roles"] = [role] - # Handle additional_permissions valid_fields = self.ROLE_REQUIRED_FIELDS.get(role, []) additional_permissions = {cleaned_data.get(field) for field in valid_fields if cleaned_data.get(field)} diff --git a/src/registrar/management/commands/disclose_security_emails.py b/src/registrar/management/commands/disclose_security_emails.py index 62989e4c0..62456747a 100644 --- a/src/registrar/management/commands/disclose_security_emails.py +++ b/src/registrar/management/commands/disclose_security_emails.py @@ -1,4 +1,4 @@ -"""" +""" " Converts all ready and DNS needed domains with a non-default public contact to disclose their public contact. Created for Issue#1535 to resolve disclose issue of domains with missing security emails. diff --git a/src/registrar/management/commands/master_domain_migrations.py b/src/registrar/management/commands/master_domain_migrations.py index 7f702e047..2907add06 100644 --- a/src/registrar/management/commands/master_domain_migrations.py +++ b/src/registrar/management/commands/master_domain_migrations.py @@ -1,8 +1,8 @@ """Data migration: - 1 - generates a report of data integrity across all - transition domain related tables - 2 - allows users to run all migration scripts for - transition domain data +1 - generates a report of data integrity across all +transition domain related tables +2 - allows users to run all migration scripts for +transition domain data """ import logging diff --git a/src/registrar/management/commands/populate_domain_updated_federal_agency.py b/src/registrar/management/commands/populate_domain_updated_federal_agency.py index dd8ceb3b2..3fbf2792c 100644 --- a/src/registrar/management/commands/populate_domain_updated_federal_agency.py +++ b/src/registrar/management/commands/populate_domain_updated_federal_agency.py @@ -1,4 +1,4 @@ -"""" +""" " Data migration: Renaming deprecated Federal Agencies to their new updated names ie (U.S. Peace Corps to Peace Corps) within Domain Information and Domain Requests diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 42310c3bb..d3c0ed347 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -2,6 +2,7 @@ from itertools import zip_longest import logging import ipaddress import re +import time from datetime import date, timedelta from typing import Optional from django.db import transaction @@ -750,11 +751,7 @@ class Domain(TimeStampedModel, DomainHelper): successTotalNameservers = len(oldNameservers) - deleteCount + addToDomainCount - try: - self._delete_hosts_if_not_used(hostsToDelete=deleted_values) - except Exception as e: - # we don't need this part to succeed in order to continue. - logger.error("Failed to delete nameserver hosts: %s", e) + self._delete_hosts_if_not_used(hostsToDelete=deleted_values) if successTotalNameservers < 2: try: @@ -1038,13 +1035,13 @@ class Domain(TimeStampedModel, DomainHelper): logger.error(f"registry error removing client hold: {err}") raise (err) - def _delete_domain(self): + def _delete_domain(self): # noqa """This domain should be deleted from the registry may raises RegistryError, should be caught or handled correctly by caller""" logger.info("Deleting subdomains for %s", self.name) # check if any subdomains are in use by another domain - hosts = Host.objects.filter(name__regex=r".+{}".format(self.name)) + hosts = Host.objects.filter(name__regex=r".+\.{}".format(self.name)) for host in hosts: if host.domain != self: logger.error("Unable to delete host: %s is in use by another domain: %s", host.name, host.domain) @@ -1052,38 +1049,119 @@ class Domain(TimeStampedModel, DomainHelper): code=ErrorCode.OBJECT_ASSOCIATION_PROHIBITS_OPERATION, note=f"Host {host.name} is in use by {host.domain}", ) + try: + # set hosts to empty list so nameservers are deleted + ( + deleted_values, + updated_values, + new_values, + oldNameservers, + ) = self.getNameserverChanges(hosts=[]) - ( - deleted_values, - updated_values, - new_values, - oldNameservers, - ) = self.getNameserverChanges(hosts=[]) - - _ = self._update_host_values(updated_values, oldNameservers) # returns nothing, just need to be run and errors - addToDomainList, _ = self.createNewHostList(new_values) - deleteHostList, _ = self.createDeleteHostList(deleted_values) - responseCode = self.addAndRemoveHostsFromDomain(hostsToAdd=addToDomainList, hostsToDelete=deleteHostList) - + # update the hosts + _ = self._update_host_values( + updated_values, oldNameservers + ) # returns nothing, just need to be run and errors + addToDomainList, _ = self.createNewHostList(new_values) + deleteHostList, _ = self.createDeleteHostList(deleted_values) + responseCode = self.addAndRemoveHostsFromDomain(hostsToAdd=addToDomainList, hostsToDelete=deleteHostList) + except RegistryError as e: + logger.error(f"Error trying to delete hosts from domain {self}: {e}") + raise e # if unable to update domain raise error and stop if responseCode != ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY: raise NameserverError(code=nsErrorCodes.BAD_DATA) + logger.info("Finished removing nameservers from domain") + # addAndRemoveHostsFromDomain removes the hosts from the domain object, # but we still need to delete the object themselves self._delete_hosts_if_not_used(hostsToDelete=deleted_values) + logger.info("Finished _delete_hosts_if_not_used inside _delete_domain()") + # delete the non-registrant contacts logger.debug("Deleting non-registrant contacts for %s", self.name) contacts = PublicContact.objects.filter(domain=self) - for contact in contacts: - if contact.contact_type != PublicContact.ContactTypeChoices.REGISTRANT: - self._update_domain_with_contact(contact, rem=True) - request = commands.DeleteContact(contact.registry_id) - registry.send(request, cleaned=True) + logger.info(f"retrieved contacts for domain: {contacts}") - logger.info("Deleting domain %s", self.name) + for contact in contacts: + try: + if contact.contact_type != PublicContact.ContactTypeChoices.REGISTRANT: + logger.info(f"Deleting contact: {contact}") + try: + self._update_domain_with_contact(contact, rem=True) + except Exception as e: + logger.error(f"Error while updating domain with contact: {contact}, e: {e}", exc_info=True) + request = commands.DeleteContact(contact.registry_id) + registry.send(request, cleaned=True) + logger.info(f"sent DeleteContact for {contact}") + except RegistryError as e: + logger.error(f"Error deleting contact: {contact}, {e}", exc_info=True) + + logger.info(f"Finished deleting contacts for {self.name}") + + # delete ds data if it exists + if self.dnssecdata: + logger.debug("Deleting ds data for %s", self.name) + try: + # set and unset client hold to be able to change ds data + logger.info("removing client hold") + self._remove_client_hold() + self.dnssecdata = None + logger.info("placing client hold") + self._place_client_hold() + except RegistryError as e: + logger.error("Error deleting ds data for %s: %s", self.name, e) + e.note = "Error deleting ds data for %s" % self.name + raise e + + # check if the domain can be deleted + if not self._domain_can_be_deleted(): + note = "Domain has associated objects that prevent deletion." + raise RegistryError(code=ErrorCode.COMMAND_FAILED, note=note) + + # delete the domain request = commands.DeleteDomain(name=self.name) - registry.send(request, cleaned=True) + try: + registry.send(request, cleaned=True) + logger.info("Domain %s deleted successfully.", self.name) + except RegistryError as e: + logger.error("Error deleting domain %s: %s", self.name, e) + raise e + + def _domain_can_be_deleted(self, max_attempts=5, wait_interval=2) -> bool: + """ + Polls the registry using InfoDomain calls to confirm that the domain can be deleted. + Returns True if the domain can be deleted, False otherwise. Includes a retry mechanism + using wait_interval and max_attempts, which may be necessary if subdomains and other + associated objects were only recently deleted as the registry may not be immediately updated. + """ + logger.info("Polling registry to confirm deletion pre-conditions for %s", self.name) + last_info_error = None + for attempt in range(max_attempts): + try: + info_response = registry.send(commands.InfoDomain(name=self.name), cleaned=True) + domain_info = info_response.res_data[0] + hosts_associated = getattr(domain_info, "hosts", None) + if hosts_associated is None or len(hosts_associated) == 0: + logger.info("InfoDomain reports no associated hosts for %s. Proceeding with deletion.", self.name) + return True + else: + logger.info("Attempt %d: Domain %s still has hosts: %s", attempt + 1, self.name, hosts_associated) + except RegistryError as info_e: + # If the domain is already gone, we can assume deletion already occurred. + if info_e.code == ErrorCode.OBJECT_DOES_NOT_EXIST: + logger.info("InfoDomain check indicates domain %s no longer exists.", self.name) + raise info_e + logger.warning("Attempt %d: Error during InfoDomain check: %s", attempt + 1, info_e) + time.sleep(wait_interval) + else: + logger.error( + "Exceeded max attempts waiting for domain %s to clear associated objects; last error: %s", + self.name, + last_info_error, + ) + return False def __str__(self) -> str: return self.name @@ -1840,8 +1918,6 @@ class Domain(TimeStampedModel, DomainHelper): else: logger.error("Error _delete_hosts_if_not_used, code was %s error was %s" % (e.code, e)) - raise e - def _fix_unknown_state(self, cleaned): """ _fix_unknown_state: Calls _add_missing_contacts_if_unknown diff --git a/src/registrar/models/portfolio_invitation.py b/src/registrar/models/portfolio_invitation.py index 99febc92e..fafa99856 100644 --- a/src/registrar/models/portfolio_invitation.py +++ b/src/registrar/models/portfolio_invitation.py @@ -9,8 +9,11 @@ from .utility.portfolio_helper import ( UserPortfolioPermissionChoices, UserPortfolioRoleChoices, cleanup_after_portfolio_member_deletion, + get_domain_requests_description_display, get_domain_requests_display, + get_domains_description_display, get_domains_display, + get_members_description_display, get_members_display, get_role_display, validate_portfolio_invitation, @@ -115,6 +118,16 @@ class PortfolioInvitation(TimeStampedModel): """ return get_domains_display(self.roles, self.additional_permissions) + @property + def domains_description_display(self): + """ + Returns a string description of the user's domain access level. + + Returns: + str: The display name of the user's domain permissions description. + """ + return get_domains_description_display(self.roles, self.additional_permissions) + @property def domain_requests_display(self): """ @@ -129,6 +142,16 @@ class PortfolioInvitation(TimeStampedModel): """ return get_domain_requests_display(self.roles, self.additional_permissions) + @property + def domain_requests_description_display(self): + """ + Returns a string description of the user's access to domain requests. + + Returns: + str: The display name of the user's domain request permissions description. + """ + return get_domain_requests_description_display(self.roles, self.additional_permissions) + @property def members_display(self): """ @@ -143,6 +166,16 @@ class PortfolioInvitation(TimeStampedModel): """ return get_members_display(self.roles, self.additional_permissions) + @property + def members_description_display(self): + """ + Returns a string description of the user's access to managing members. + + Returns: + str: The display name of the user's member management permissions description. + """ + return get_members_description_display(self.roles, self.additional_permissions) + @transition(field="status", source=PortfolioInvitationStatus.INVITED, target=PortfolioInvitationStatus.RETRIEVED) def retrieve(self): """When an invitation is retrieved, create the corresponding permission. diff --git a/src/registrar/models/user_portfolio_permission.py b/src/registrar/models/user_portfolio_permission.py index e077daa57..0a758ff6a 100644 --- a/src/registrar/models/user_portfolio_permission.py +++ b/src/registrar/models/user_portfolio_permission.py @@ -7,8 +7,11 @@ from registrar.models.utility.portfolio_helper import ( MemberPermissionDisplay, cleanup_after_portfolio_member_deletion, get_domain_requests_display, + get_domain_requests_description_display, get_domains_display, + get_domains_description_display, get_members_display, + get_members_description_display, get_role_display, validate_user_portfolio_permission, ) @@ -211,6 +214,16 @@ class UserPortfolioPermission(TimeStampedModel): """ return get_domains_display(self.roles, self.additional_permissions) + @property + def domains_description_display(self): + """ + Returns a string description of the user's domain access level. + + Returns: + str: The display name of the user's domain permissions description. + """ + return get_domains_description_display(self.roles, self.additional_permissions) + @property def domain_requests_display(self): """ @@ -225,6 +238,16 @@ class UserPortfolioPermission(TimeStampedModel): """ return get_domain_requests_display(self.roles, self.additional_permissions) + @property + def domain_requests_description_display(self): + """ + Returns a string description of the user's access to domain requests. + + Returns: + str: The display name of the user's domain request permissions description. + """ + return get_domain_requests_description_display(self.roles, self.additional_permissions) + @property def members_display(self): """ @@ -239,6 +262,16 @@ class UserPortfolioPermission(TimeStampedModel): """ return get_members_display(self.roles, self.additional_permissions) + @property + def members_description_display(self): + """ + Returns a string description of the user's access to managing members. + + Returns: + str: The display name of the user's member management permissions description. + """ + return get_members_description_display(self.roles, self.additional_permissions) + def clean(self): """Extends clean method to perform additional validation, which can raise errors in django admin.""" super().clean() diff --git a/src/registrar/models/utility/portfolio_helper.py b/src/registrar/models/utility/portfolio_helper.py index 03733237e..e94733fb6 100644 --- a/src/registrar/models/utility/portfolio_helper.py +++ b/src/registrar/models/utility/portfolio_helper.py @@ -123,6 +123,25 @@ def get_domains_display(roles, permissions): return "Viewer, limited" +def get_domains_description_display(roles, permissions): + """ + Determines the display description for a user's domain viewing permissions. + + Args: + roles (list): A list of role strings assigned to the user. + permissions (list): A list of additional permissions assigned to the user. + + Returns: + str: A string representing the user's domain viewing access description. + """ + UserPortfolioPermission = apps.get_model("registrar.UserPortfolioPermission") + all_permissions = UserPortfolioPermission.get_portfolio_permissions(roles, permissions) + if UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS in all_permissions: + return "Can view all domains for the organization" + else: + return "Can view only the domains they manage" + + def get_domain_requests_display(roles, permissions): """ Determines the display name for a user's domain request permissions. @@ -148,6 +167,27 @@ def get_domain_requests_display(roles, permissions): return "No access" +def get_domain_requests_description_display(roles, permissions): + """ + Determines the display description for a user's domain request permissions. + + Args: + roles (list): A list of role strings assigned to the user. + permissions (list): A list of additional permissions assigned to the user. + + Returns: + str: A string representing the user's domain request access level description. + """ + UserPortfolioPermission = apps.get_model("registrar.UserPortfolioPermission") + all_permissions = UserPortfolioPermission.get_portfolio_permissions(roles, permissions) + if UserPortfolioPermissionChoices.EDIT_REQUESTS in all_permissions: + return "Can view all domain requests for the organization and create requests" + elif UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS in all_permissions: + return "Can view all domain requests for the organization" + else: + return "Cannot view or create domain requests" + + def get_members_display(roles, permissions): """ Determines the display name for a user's member management permissions. @@ -173,6 +213,27 @@ def get_members_display(roles, permissions): return "No access" +def get_members_description_display(roles, permissions): + """ + Determines the display description for a user's member management permissions. + + Args: + roles (list): A list of role strings assigned to the user. + permissions (list): A list of additional permissions assigned to the user. + + Returns: + str: A string representing the user's member management access level description. + """ + UserPortfolioPermission = apps.get_model("registrar.UserPortfolioPermission") + all_permissions = UserPortfolioPermission.get_portfolio_permissions(roles, permissions) + if UserPortfolioPermissionChoices.EDIT_MEMBERS in all_permissions: + return "Can view and manage all member permissions" + elif UserPortfolioPermissionChoices.VIEW_MEMBERS in all_permissions: + return "Can view all member permissions" + else: + return "Cannot view member permissions" + + def validate_user_portfolio_permission(user_portfolio_permission): """ Validates a UserPortfolioPermission instance. Located in portfolio_helper to avoid circular imports diff --git a/src/registrar/templates/admin/app_list.html b/src/registrar/templates/admin/app_list.html index aaf3dc423..ecce12a3e 100644 --- a/src/registrar/templates/admin/app_list.html +++ b/src/registrar/templates/admin/app_list.html @@ -4,24 +4,22 @@ {% for app in app_list %}
- {{ app.name }} - | - {% else %} -- {{ app.name }} - | - {% endif %} + {# .gov override: hide headers #} + {% comment %} + {% if show_changelinks %} ++ {{ app.name }} + | + {% else %} ++ {{ app.name }} + | + {% endif %} + {% endcomment %}||||||
---|---|---|---|---|---|---|---|---|---|
Model | @@ -45,16 +43,17 @@ {% endif %} {% if model.add_url %} -{% translate 'Add' %} | + {% comment %} Remove the 's' from the end of the string to avoid text like "Add domain requests" {% endcomment %} +{% translate 'Add' %} | {% else %}{% endif %} {% if model.admin_url and show_changelinks %} {% if model.view_only %} - | {% translate 'View' %} | +{% translate 'View' %} | {% else %} -{% translate 'Change' %} | +{% translate 'Change' %} | {% endif %} {% elif show_changelinks %}@@ -64,9 +63,20 @@ |
Reports | +
---|
Dashboard | +
{% translate 'You don’t have permission to view or edit anything.' %}
diff --git a/src/registrar/templates/admin/change_form_object_tools.html b/src/registrar/templates/admin/change_form_object_tools.html index 2f3d282ea..d2ec555e2 100644 --- a/src/registrar/templates/admin/change_form_object_tools.html +++ b/src/registrar/templates/admin/change_form_object_tools.html @@ -7,10 +7,10 @@ {% if has_absolute_url %} {% else %} @@ -30,18 +30,18 @@ {% endif %}{% url cl.opts|admin_urlname:'add' as add_url %} - + +
{% endif %} {% endblock %} \ No newline at end of file diff --git a/src/registrar/templates/admin/change_list_results.html b/src/registrar/templates/admin/change_list_results.html index 5e4f37711..c5be04133 100644 --- a/src/registrar/templates/admin/change_list_results.html +++ b/src/registrar/templates/admin/change_list_results.html @@ -19,11 +19,11 @@ Load our custom filters to extract info from the django generated markup. {% if results.0|contains_checkbox %} {# .gov - hardcode the select all checkbox #} -+ If you cancel the portfolio invitation here, it won't trigger any emails. It also won't remove the user's + portfolio access if they already logged in. Go to the + + User Portfolio Permissions + + table if you want to remove the user from a portfolio. +
++ If you remove someone from a portfolio here, it will not send any emails when you click "Save". +
+{{ description }}
+{{ description }}
{% endif %} {% endwith %} {% endif %} diff --git a/src/registrar/templates/domain_add_user.html b/src/registrar/templates/domain_add_user.html index abc549a82..8ef167f98 100644 --- a/src/registrar/templates/domain_add_user.html +++ b/src/registrar/templates/domain_add_user.html @@ -42,17 +42,18 @@ {% endblock breadcrumb %}+ Provide an email address for the domain manager you’d like to add. + They’ll need to access the registrar using a Login.gov account that’s associated with this email address. + Domain managers can be a member of only one .gov organization. +
+ {% else %}- You can add another user to help manage your domain. Users can only be a member of one .gov organization, - and they'll need to sign in with their Login.gov account. + Provide an email address for the domain manager you’d like to add. + They’ll need to access the registrar using a Login.gov account that’s associated with this email address.
-{% else %} -- You can add another user to help manage your domain. They will need to sign in to the .gov registrar with - their Login.gov account. -
-{% endif %} + {% endif %}