From 569ec088f031d650a9244a4e27b6896f2c294078 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Fri, 17 Jan 2025 10:47:54 -0800 Subject: [PATCH 01/40] Link and text changes --- src/registrar/templates/domain_renewal.html | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/registrar/templates/domain_renewal.html b/src/registrar/templates/domain_renewal.html index 32e535ed5..7c01f6718 100644 --- a/src/registrar/templates/domain_renewal.html +++ b/src/registrar/templates/domain_renewal.html @@ -38,11 +38,11 @@ {{ block.super }}

Confirm the following information for accuracy

-

Review these details below. We +

Review the details below. We require that you maintain accurate information for the domain. The details you provide will only be used to support the administration of .gov and won't be made public.

-

If you would like to retire your domain instead, please +

If you would like to retire your domain instead, please contact us.

Required fields are marked with an asterisk (*).

@@ -119,9 +119,8 @@ >
From cb462c1ab2fb8293f9f60ecf2542b0a41780a9f7 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Fri, 17 Jan 2025 10:58:36 -0800 Subject: [PATCH 02/40] Fix tool tip text and adjust the banner for expiring soon --- src/registrar/models/domain.py | 2 +- src/registrar/templates/includes/domains_table.html | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 6bd8278a1..d4c48f6bc 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -1586,7 +1586,7 @@ class Domain(TimeStampedModel, DomainHelper): "This domain has expired, but it is still online. " "To renew this domain, contact help@get.gov." ) elif flag_is_active(request, "domain_renewal") and self.is_expiring(): - help_text = "This domain will expire soon. Contact one of the listed domain managers to renew the domain." + help_text = "This domain will expire soon. Go to “Manage” to renew the domain." else: help_text = Domain.State.get_help_text(self.state) diff --git a/src/registrar/templates/includes/domains_table.html b/src/registrar/templates/includes/domains_table.html index f7e36d330..9b95e840a 100644 --- a/src/registrar/templates/includes/domains_table.html +++ b/src/registrar/templates/includes/domains_table.html @@ -10,9 +10,9 @@ {% if has_domain_renewal_flag and num_expiring_domains > 0 and has_any_domains_portfolio_permission %} -
+
-
+

{% if num_expiring_domains == 1%} One domain will expire soon. Go to "Manage" to renew the domain. Show expiring domain. @@ -76,9 +76,9 @@ {% if has_domain_renewal_flag and num_expiring_domains > 0 and not portfolio %} -

+
-
+

{% if num_expiring_domains == 1%} One domain will expire soon. Go to "Manage" to renew the domain. Show expiring domain. From 61450053639b8b3bd2805f5907f9d61018233737 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Tue, 21 Jan 2025 13:37:51 -0800 Subject: [PATCH 03/40] Only if expired or expiring and domain manager can see /renewal otherwise 403 --- src/registrar/tests/test_views_domain.py | 1 - src/registrar/views/domain.py | 31 +++++++++++++++++++++++- src/registrar/views/utility/mixins.py | 3 ++- 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/src/registrar/tests/test_views_domain.py b/src/registrar/tests/test_views_domain.py index f13490312..2be9e21fa 100644 --- a/src/registrar/tests/test_views_domain.py +++ b/src/registrar/tests/test_views_domain.py @@ -583,7 +583,6 @@ class TestDomainDetailDomainRenewal(TestDomainOverview): reverse("domain", kwargs={"pk": self.domaintorenew.id}), ) - print("puglesss", self.domaintorenew.is_expired) # Make sure we see the link as a domain manager self.assertContains(detail_page, "Renew to maintain access") diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index f82d7005d..d0dd62210 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -311,11 +311,40 @@ class DomainView(DomainBaseView): self._update_session_with_domain() -class DomainRenewalView(DomainView): +class DomainRenewalView(DomainBaseView): """Domain detail overview page.""" template_name = "domain_renewal.html" + def get_context_data(self, **kwargs): + """Grabbing security email information for the renewal form""" + + context = super().get_context_data(**kwargs) + + default_emails = [DefaultEmail.PUBLIC_CONTACT_DEFAULT.value, DefaultEmail.LEGACY_DEFAULT.value] + + context["hidden_security_emails"] = default_emails + + security_email = self.object.get_security_email() + if security_email is None or security_email in default_emails: + context["security_email"] = None + return context + context["security_email"] = security_email + return context + + def in_editable_state(self, pk): + """Override in_editable_state from DomainPermission + Allow renewal form to be accessed""" + + requested_domain = None + if Domain.objects.filter(id=pk).exists(): + requested_domain = Domain.objects.get(id=pk) + + if requested_domain and (requested_domain.is_expiring() or requested_domain.is_expired()): + return True + + return False + def post(self, request, pk): domain = get_object_or_404(Domain, id=pk) diff --git a/src/registrar/views/utility/mixins.py b/src/registrar/views/utility/mixins.py index 11384ca09..954f18b88 100644 --- a/src/registrar/views/utility/mixins.py +++ b/src/registrar/views/utility/mixins.py @@ -192,7 +192,8 @@ class DomainPermission(PermissionsLoginMixin): def can_access_domain_via_portfolio(self, pk): """Most views should not allow permission to portfolio users. If particular views allow access to the domain pages, they will need to override - this function.""" + this function. + """ return False def in_editable_state(self, pk): From 85503160f2d27bbdcb36795d0614bf2d263cc71e Mon Sep 17 00:00:00 2001 From: asaki222 Date: Wed, 22 Jan 2025 09:46:13 -0500 Subject: [PATCH 04/40] added test --- src/registrar/tests/test_views_domain.py | 25 ++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/registrar/tests/test_views_domain.py b/src/registrar/tests/test_views_domain.py index 2be9e21fa..cdc6995d2 100644 --- a/src/registrar/tests/test_views_domain.py +++ b/src/registrar/tests/test_views_domain.py @@ -443,6 +443,15 @@ class TestDomainDetailDomainRenewal(TestDomainOverview): name="domainrenewal.gov", ) + self.domainnotexpiring, _ = Domain.objects.get_or_create( + name="domainnotexpiring.gov", + expiration_date=timezone.now().date() + timedelta(days=65) + ) + + self.domainnodomainmanager, _ = Domain.objects.get_or_create( + name="domainnodomainmanager" + ) + UserDomainRole.objects.get_or_create( user=self.user, domain=self.domaintorenew, role=UserDomainRole.Roles.MANAGER ) @@ -655,7 +664,23 @@ class TestDomainDetailDomainRenewal(TestDomainOverview): edit_page = renewal_page.click(href=edit_button_url, index=1) self.assertEqual(edit_page.status_code, 200) self.assertContains(edit_page, "Domain managers can update all information related to a domain") + + @override_flag("domain_renewal", active=True) + def test_domain_renewal_form_not_expired_or_expiring(self): + with less_console_noise(): + # Start on the Renewal page for the domain + renewal_page = self.client.get(reverse("domain-renewal", kwargs={"pk": self.domainnotexpiring.id})) + self.assertEqual(renewal_page.status_code, 403) + @override_flag("domain_renewal", active=True) + def test_domain_renewal_form_does_not_appear_if_not_domain_manager(self): + with patch.object(Domain, "is_expired", self.custom_is_expired_true), patch.object( + Domain, "is_expired", self.custom_is_expired_true + ): + renewal_page = self.client.get(reverse("domain-renewal", kwargs={"pk": self.domainnodomainmanager.id})) + self.assertEqual(renewal_page.status_code, 403) + + @override_flag("domain_renewal", active=True) def test_ack_checkbox_not_checked(self): From d2323aa9ab900b9fa4e3d4455d244549017399de Mon Sep 17 00:00:00 2001 From: asaki222 Date: Wed, 22 Jan 2025 10:00:59 -0500 Subject: [PATCH 05/40] ran linter --- src/registrar/tests/test_views_domain.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/registrar/tests/test_views_domain.py b/src/registrar/tests/test_views_domain.py index cdc6995d2..58edea32d 100644 --- a/src/registrar/tests/test_views_domain.py +++ b/src/registrar/tests/test_views_domain.py @@ -444,13 +444,10 @@ class TestDomainDetailDomainRenewal(TestDomainOverview): ) self.domainnotexpiring, _ = Domain.objects.get_or_create( - name="domainnotexpiring.gov", - expiration_date=timezone.now().date() + timedelta(days=65) + name="domainnotexpiring.gov", expiration_date=timezone.now().date() + timedelta(days=65) ) - self.domainnodomainmanager, _ = Domain.objects.get_or_create( - name="domainnodomainmanager" - ) + self.domainnodomainmanager, _ = Domain.objects.get_or_create(name="domainnodomainmanager") UserDomainRole.objects.get_or_create( user=self.user, domain=self.domaintorenew, role=UserDomainRole.Roles.MANAGER @@ -664,12 +661,12 @@ class TestDomainDetailDomainRenewal(TestDomainOverview): edit_page = renewal_page.click(href=edit_button_url, index=1) self.assertEqual(edit_page.status_code, 200) self.assertContains(edit_page, "Domain managers can update all information related to a domain") - + @override_flag("domain_renewal", active=True) def test_domain_renewal_form_not_expired_or_expiring(self): with less_console_noise(): - # Start on the Renewal page for the domain - renewal_page = self.client.get(reverse("domain-renewal", kwargs={"pk": self.domainnotexpiring.id})) + # Start on the Renewal page for the domain + renewal_page = self.client.get(reverse("domain-renewal", kwargs={"pk": self.domainnotexpiring.id})) self.assertEqual(renewal_page.status_code, 403) @override_flag("domain_renewal", active=True) @@ -677,10 +674,9 @@ class TestDomainDetailDomainRenewal(TestDomainOverview): with patch.object(Domain, "is_expired", self.custom_is_expired_true), patch.object( Domain, "is_expired", self.custom_is_expired_true ): - renewal_page = self.client.get(reverse("domain-renewal", kwargs={"pk": self.domainnodomainmanager.id})) + renewal_page = self.client.get(reverse("domain-renewal", kwargs={"pk": self.domainnodomainmanager.id})) self.assertEqual(renewal_page.status_code, 403) - @override_flag("domain_renewal", active=True) def test_ack_checkbox_not_checked(self): From 0bf017f6501281850f3b9de2212c55a2b60186f9 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Wed, 22 Jan 2025 15:45:18 -0800 Subject: [PATCH 06/40] Fix link colour for renew and submit button text --- src/registrar/templates/domain_detail.html | 4 ++-- src/registrar/templates/domain_renewal.html | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/registrar/templates/domain_detail.html b/src/registrar/templates/domain_detail.html index 2cd3e5a5c..89685e074 100644 --- a/src/registrar/templates/domain_detail.html +++ b/src/registrar/templates/domain_detail.html @@ -52,11 +52,11 @@ {% if has_domain_renewal_flag and domain.is_expired and is_domain_manager %} This domain has expired, but it is still online. {% url 'domain-renewal' pk=domain.id as url %} - Renew to maintain access. + Renew to maintain access. {% elif has_domain_renewal_flag and domain.is_expiring and is_domain_manager %} This domain will expire soon. {% url 'domain-renewal' pk=domain.id as url %} - Renew to maintain access. + Renew to maintain access. {% elif has_domain_renewal_flag and domain.is_expiring and is_portfolio_user %} This domain will expire soon. Contact one of the listed domain managers to renew the domain. {% elif has_domain_renewal_flag and domain.is_expired and is_portfolio_user %} diff --git a/src/registrar/templates/domain_renewal.html b/src/registrar/templates/domain_renewal.html index 7c01f6718..fa833592c 100644 --- a/src/registrar/templates/domain_renewal.html +++ b/src/registrar/templates/domain_renewal.html @@ -130,7 +130,7 @@ name="submit_button" value="next" class="usa-button margin-top-3" - > Submit + > Submit and renew From 1cae575814066a0df455deca06c715e824b01e9a Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Thu, 23 Jan 2025 16:01:32 -0500 Subject: [PATCH 07/40] initial updates to forms, templates, and javascript --- src/registrar/forms/portfolio.py | 77 ++++++++++--------- .../models/user_portfolio_permission.py | 7 +- .../portfolio_member_permissions.html | 44 +++++------ .../templates/portfolio_members_add_new.html | 42 +++++----- 4 files changed, 89 insertions(+), 81 deletions(-) diff --git a/src/registrar/forms/portfolio.py b/src/registrar/forms/portfolio.py index e57b56c4f..5c7c391e3 100644 --- a/src/registrar/forms/portfolio.py +++ b/src/registrar/forms/portfolio.py @@ -125,38 +125,26 @@ class BasePortfolioMemberForm(forms.ModelForm): }, ) - domain_request_permission_admin = forms.ChoiceField( + domain_permission_member = forms.ChoiceField( label=mark_safe(f"Select permission {required_star}"), # nosec choices=[ - (UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value, "View all requests"), - (UserPortfolioPermissionChoices.EDIT_REQUESTS.value, "View all requests plus create requests"), + (UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS.value, "Viewer, limited"), + (UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS.value, "Viewer, all"), ], widget=forms.RadioSelect, required=False, error_messages={ - "required": "Admin domain request permission is required", - }, - ) - - member_permission_admin = forms.ChoiceField( - label=mark_safe(f"Select permission {required_star}"), # nosec - choices=[ - (UserPortfolioPermissionChoices.VIEW_MEMBERS.value, "View all members"), - (UserPortfolioPermissionChoices.EDIT_MEMBERS.value, "View all members plus manage members"), - ], - widget=forms.RadioSelect, - required=False, - error_messages={ - "required": "Admin member permission is required", + "required": "Member domain permission is required", }, ) domain_request_permission_member = forms.ChoiceField( label=mark_safe(f"Select permission {required_star}"), # nosec choices=[ - (UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value, "View all requests"), - (UserPortfolioPermissionChoices.EDIT_REQUESTS.value, "View all requests plus create requests"), ("no_access", "No access"), + (UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value, "Viewer"), + (UserPortfolioPermissionChoices.EDIT_REQUESTS.value, "Creator"), + ], widget=forms.RadioSelect, required=False, @@ -165,15 +153,28 @@ class BasePortfolioMemberForm(forms.ModelForm): }, ) + member_permission_member = forms.ChoiceField( + label=mark_safe(f"Select permission {required_star}"), # nosec + choices=[ + ("no_access", "No access"), + (UserPortfolioPermissionChoices.VIEW_MEMBERS.value, "Viewer"), + ], + widget=forms.RadioSelect, + required=False, + error_messages={ + "required": "Admin member permission is required", + }, + ) + + # Tracks what form elements are required for a given role choice. # All of the fields included here have "required=False" by default as they are conditionally required. # see def clean() for more details. ROLE_REQUIRED_FIELDS = { - UserPortfolioRoleChoices.ORGANIZATION_ADMIN: [ - "domain_request_permission_admin", - "member_permission_admin", - ], + UserPortfolioRoleChoices.ORGANIZATION_ADMIN: [], UserPortfolioRoleChoices.ORGANIZATION_MEMBER: [ + "domain_permission_member", + "member_permission_member", "domain_request_permission_member", ], } @@ -225,6 +226,10 @@ class BasePortfolioMemberForm(forms.ModelForm): if cleaned_data.get("domain_request_permission_member") == "no_access": cleaned_data["domain_request_permission_member"] = None + # Edgecase: Member uses a special form value for None called "no_access". + if cleaned_data.get("member_permission_member") == "no_access": + cleaned_data["member_permission_member"] = None + # Handle roles cleaned_data["roles"] = [role] @@ -267,12 +272,15 @@ class BasePortfolioMemberForm(forms.ModelForm): UserPortfolioRoleChoices.ORGANIZATION_ADMIN, UserPortfolioRoleChoices.ORGANIZATION_MEMBER, ] - domain_perms = [ + domain_request_perms = [ UserPortfolioPermissionChoices.EDIT_REQUESTS, UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS, ] + domain_perms = [ + UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS, + UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS, + ] member_perms = [ - UserPortfolioPermissionChoices.EDIT_MEMBERS, UserPortfolioPermissionChoices.VIEW_MEMBERS, ] @@ -282,16 +290,15 @@ class BasePortfolioMemberForm(forms.ModelForm): roles = self.instance.roles or [] selected_role = next((role for role in roles if role in roles), None) self.initial["role"] = selected_role - is_admin = selected_role == UserPortfolioRoleChoices.ORGANIZATION_ADMIN - if is_admin: - selected_domain_permission = next((perm for perm in domain_perms if perm in perms), None) - selected_member_permission = next((perm for perm in member_perms if perm in perms), None) - self.initial["domain_request_permission_admin"] = selected_domain_permission - self.initial["member_permission_admin"] = selected_member_permission - else: - # Edgecase: Member uses a special form value for None called "no_access". This ensures a form selection. - selected_domain_permission = next((perm for perm in domain_perms if perm in perms), "no_access") - self.initial["domain_request_permission_member"] = selected_domain_permission + is_member = selected_role == UserPortfolioRoleChoices.ORGANIZATION_MEMBER + if is_member: + # Edgecase: Member and domain request use a special form value for None called "no_access". This ensures a form selection. + selected_domain_permission = next((perm for perm in domain_perms if perm in perms), UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS.value) + selected_domain_request_permission = next((perm for perm in domain_request_perms if perm in perms), "no_access") + selected_member_permission = next((perm for perm in member_perms if perm in perms), "no_access") + self.initial["domain_request_permission_member"] = selected_domain_request_permission + self.initial["domain_permission_member"] = selected_domain_permission + self.initial["member_permission_member"] = selected_member_permission class PortfolioMemberForm(BasePortfolioMemberForm): diff --git a/src/registrar/models/user_portfolio_permission.py b/src/registrar/models/user_portfolio_permission.py index 03a01b80d..8593c3ac9 100644 --- a/src/registrar/models/user_portfolio_permission.py +++ b/src/registrar/models/user_portfolio_permission.py @@ -21,10 +21,11 @@ class UserPortfolioPermission(TimeStampedModel): UserPortfolioRoleChoices.ORGANIZATION_ADMIN: [ UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS, UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS, + UserPortfolioPermissionChoices.EDIT_REQUESTS, UserPortfolioPermissionChoices.VIEW_MEMBERS, + UserPortfolioPermissionChoices.EDIT_MEMBERS, UserPortfolioPermissionChoices.VIEW_PORTFOLIO, UserPortfolioPermissionChoices.EDIT_PORTFOLIO, - # Domain: field specific permissions UserPortfolioPermissionChoices.VIEW_SUBORGANIZATION, UserPortfolioPermissionChoices.EDIT_SUBORGANIZATION, ], @@ -38,9 +39,9 @@ class UserPortfolioPermission(TimeStampedModel): # Used to throw a ValidationError on clean() for UserPortfolioPermission and PortfolioInvitation. FORBIDDEN_PORTFOLIO_ROLE_PERMISSIONS = { UserPortfolioRoleChoices.ORGANIZATION_MEMBER: [ - UserPortfolioPermissionChoices.VIEW_MEMBERS, + UserPortfolioPermissionChoices.EDIT_PORTFOLIO, UserPortfolioPermissionChoices.EDIT_MEMBERS, - UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS, + UserPortfolioPermissionChoices.EDIT_SUBORGANIZATION, ], } diff --git a/src/registrar/templates/portfolio_member_permissions.html b/src/registrar/templates/portfolio_member_permissions.html index 8757d4feb..2b0a7a118 100644 --- a/src/registrar/templates/portfolio_member_permissions.html +++ b/src/registrar/templates/portfolio_member_permissions.html @@ -92,34 +92,34 @@

-

Admin access permissions

-

Member permissions available for admin-level acccess.

- -

Organization domain requests

- {% with group_classes="usa-form-editable usa-form-editable--no-border bg-gray-1 padding-top-0" %} - {% input_with_errors form.domain_request_permission_admin %} - {% endwith %} - -

Organization members

- {% with group_classes="usa-form-editable usa-form-editable--no-border bg-gray-1 padding-top-0" %} - {% input_with_errors form.member_permission_admin %} - {% endwith %}
- -
-

Basic member permissions

-

Member permissions available for basic-level acccess.

+ +
+

Basic member permissions

+

Member permissions available for basic-level acccess.

-

Organization domain requests

+

Domains

+ {% with group_classes="usa-form-editable usa-form-editable--no-border bg-gray-1 padding-top-0" %} + {% input_with_errors form.domain_permission_member %} + {% endwith %} + +

Domain requests

{% with group_classes="usa-form-editable usa-form-editable--no-border bg-gray-1 padding-top-0" %} {% input_with_errors form.domain_request_permission_member %} {% endwith %} + +

Organization members

+ {% with group_classes="usa-form-editable usa-form-editable--no-border bg-gray-1 padding-top-0" %} + {% input_with_errors form.member_permission_member %} + {% endwith %} +
diff --git a/src/registrar/templates/portfolio_members_add_new.html b/src/registrar/templates/portfolio_members_add_new.html index 092a9af31..0587dfdbb 100644 --- a/src/registrar/templates/portfolio_members_add_new.html +++ b/src/registrar/templates/portfolio_members_add_new.html @@ -65,23 +65,6 @@
-

Admin access permissions

-

Member permissions available for admin-level acccess.

- -

Organization domain requests

- {% with group_classes="usa-form-editable usa-form-editable--no-border bg-gray-1 padding-top-0" %} - {% input_with_errors form.domain_request_permission_admin %} - {% endwith %} - -

Organization members

- {% with group_classes="usa-form-editable usa-form-editable--no-border bg-gray-1 padding-top-0" %} - {% input_with_errors form.member_permission_admin %} - {% endwith %}
@@ -89,10 +72,27 @@

Basic member permissions

Member permissions available for basic-level acccess.

-

Organization domain requests

- {% with group_classes="usa-form-editable usa-form-editable--no-border bg-gray-1 padding-top-0" %} - {% input_with_errors form.domain_request_permission_member %} - {% endwith %} +

Domains

+ {% with group_classes="usa-form-editable usa-form-editable--no-border bg-gray-1 padding-top-0" %} + {% input_with_errors form.domain_permission_member %} + {% endwith %} + +

Domain requests

+ {% with group_classes="usa-form-editable usa-form-editable--no-border bg-gray-1 padding-top-0" %} + {% input_with_errors form.domain_request_permission_member %} + {% endwith %} + +

Organization members

+ {% with group_classes="usa-form-editable usa-form-editable--no-border bg-gray-1 padding-top-0" %} + {% input_with_errors form.member_permission_member %} + {% endwith %}
From ca9810d9c63747589c07cf5e3efb4c5ea0e7f2d9 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Thu, 23 Jan 2025 16:46:54 -0500 Subject: [PATCH 08/40] ui wip --- src/registrar/forms/portfolio.py | 54 +++---- .../django/forms/widgets/multiple_input.html | 2 +- .../portfolio_member_permissions.html | 18 +-- .../templates/portfolio_members_add_new.html | 133 +++++++++--------- 4 files changed, 101 insertions(+), 106 deletions(-) diff --git a/src/registrar/forms/portfolio.py b/src/registrar/forms/portfolio.py index 5c7c391e3..f18bb4f5c 100644 --- a/src/registrar/forms/portfolio.py +++ b/src/registrar/forms/portfolio.py @@ -125,8 +125,7 @@ class BasePortfolioMemberForm(forms.ModelForm): }, ) - domain_permission_member = forms.ChoiceField( - label=mark_safe(f"Select permission {required_star}"), # nosec + domain_permissions = forms.ChoiceField( choices=[ (UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS.value, "Viewer, limited"), (UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS.value, "Viewer, all"), @@ -138,8 +137,7 @@ class BasePortfolioMemberForm(forms.ModelForm): }, ) - domain_request_permission_member = forms.ChoiceField( - label=mark_safe(f"Select permission {required_star}"), # nosec + domain_request_permissions = forms.ChoiceField( choices=[ ("no_access", "No access"), (UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value, "Viewer"), @@ -153,8 +151,7 @@ class BasePortfolioMemberForm(forms.ModelForm): }, ) - member_permission_member = forms.ChoiceField( - label=mark_safe(f"Select permission {required_star}"), # nosec + member_permissions = forms.ChoiceField( choices=[ ("no_access", "No access"), (UserPortfolioPermissionChoices.VIEW_MEMBERS.value, "Viewer"), @@ -173,9 +170,9 @@ class BasePortfolioMemberForm(forms.ModelForm): ROLE_REQUIRED_FIELDS = { UserPortfolioRoleChoices.ORGANIZATION_ADMIN: [], UserPortfolioRoleChoices.ORGANIZATION_MEMBER: [ - "domain_permission_member", - "member_permission_member", - "domain_request_permission_member", + "domain_permissions", + "member_permissions", + "domain_request_permissions", ], } @@ -191,15 +188,22 @@ class BasePortfolioMemberForm(forms.ModelForm): Update field descriptions. """ super().__init__(*args, **kwargs) + # Adds a

description beneath each role option - self.fields["role"].descriptions = { - "organization_admin": UserPortfolioRoleChoices.get_role_description( - UserPortfolioRoleChoices.ORGANIZATION_ADMIN - ), - "organization_member": UserPortfolioRoleChoices.get_role_description( - UserPortfolioRoleChoices.ORGANIZATION_MEMBER - ), + # self.fields["role"].descriptions = { + # "organization_admin": UserPortfolioRoleChoices.get_role_description( + # UserPortfolioRoleChoices.ORGANIZATION_ADMIN + # ), + # "organization_member": UserPortfolioRoleChoices.get_role_description( + # UserPortfolioRoleChoices.ORGANIZATION_MEMBER + # ), + # } + + self.fields["domain_permissions"].descriptions = { + UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS.value: "test1", + UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS.value: "test2", } + # Map model instance values to custom form fields if self.instance: self.map_instance_to_initial() @@ -223,12 +227,12 @@ class BasePortfolioMemberForm(forms.ModelForm): self.add_error(field_name, self.fields.get(field_name).error_messages.get("required")) # Edgecase: Member uses a special form value for None called "no_access". - if cleaned_data.get("domain_request_permission_member") == "no_access": - cleaned_data["domain_request_permission_member"] = None + if cleaned_data.get("domain_request_permissions") == "no_access": + cleaned_data["domain_request_permissions"] = None # Edgecase: Member uses a special form value for None called "no_access". - if cleaned_data.get("member_permission_member") == "no_access": - cleaned_data["member_permission_member"] = None + if cleaned_data.get("member_permissions") == "no_access": + cleaned_data["member_permissions"] = None # Handle roles cleaned_data["roles"] = [role] @@ -258,7 +262,7 @@ class BasePortfolioMemberForm(forms.ModelForm): "role": "organization_admin" or "organization_member", "member_permission_admin": permission level if admin, "domain_request_permission_admin": permission level if admin, - "domain_request_permission_member": permission level if member + "domain_request_permissions": permission level if member } """ if self.initial is None: @@ -296,9 +300,9 @@ class BasePortfolioMemberForm(forms.ModelForm): selected_domain_permission = next((perm for perm in domain_perms if perm in perms), UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS.value) selected_domain_request_permission = next((perm for perm in domain_request_perms if perm in perms), "no_access") selected_member_permission = next((perm for perm in member_perms if perm in perms), "no_access") - self.initial["domain_request_permission_member"] = selected_domain_request_permission - self.initial["domain_permission_member"] = selected_domain_permission - self.initial["member_permission_member"] = selected_member_permission + self.initial["domain_request_permissions"] = selected_domain_request_permission + self.initial["domain_permissions"] = selected_domain_permission + self.initial["member_permissions"] = selected_member_permission class PortfolioMemberForm(BasePortfolioMemberForm): @@ -327,7 +331,7 @@ class PortfolioNewMemberForm(BasePortfolioMemberForm): """ email = forms.EmailField( - label="Enter the email of the member you'd like to invite", + label="Email", max_length=None, error_messages={ "invalid": ("Enter an email address in the required format, like name@example.com."), diff --git a/src/registrar/templates/django/forms/widgets/multiple_input.html b/src/registrar/templates/django/forms/widgets/multiple_input.html index cc0e11989..af98e898b 100644 --- a/src/registrar/templates/django/forms/widgets/multiple_input.html +++ b/src/registrar/templates/django/forms/widgets/multiple_input.html @@ -21,7 +21,7 @@ {% if field and field.field and field.field.descriptions %} {% with description=field.field.descriptions|get_dict_value:option.value %} {% if description %} -

{{ description }}

+

{{ description }}

{% endif %} {% endwith %} {% endif %} diff --git a/src/registrar/templates/portfolio_member_permissions.html b/src/registrar/templates/portfolio_member_permissions.html index 2b0a7a118..19f4ad41d 100644 --- a/src/registrar/templates/portfolio_member_permissions.html +++ b/src/registrar/templates/portfolio_member_permissions.html @@ -99,25 +99,19 @@

Basic member permissions

Member permissions available for basic-level acccess.

-

Domains

+

Domains

{% with group_classes="usa-form-editable usa-form-editable--no-border bg-gray-1 padding-top-0" %} - {% input_with_errors form.domain_permission_member %} + {% input_with_errors form.domain_permissions %} {% endwith %} -

Domain requests

+

Domain requests

{% with group_classes="usa-form-editable usa-form-editable--no-border bg-gray-1 padding-top-0" %} - {% input_with_errors form.domain_request_permission_member %} + {% input_with_errors form.domain_request_permissions %} {% endwith %} -

Organization members

+

Organization members

{% with group_classes="usa-form-editable usa-form-editable--no-border bg-gray-1 padding-top-0" %} - {% input_with_errors form.member_permission_member %} + {% input_with_errors form.member_permissions %} {% endwith %}
diff --git a/src/registrar/templates/portfolio_members_add_new.html b/src/registrar/templates/portfolio_members_add_new.html index 0587dfdbb..a0a6839e9 100644 --- a/src/registrar/templates/portfolio_members_add_new.html +++ b/src/registrar/templates/portfolio_members_add_new.html @@ -30,90 +30,87 @@ - {% block new_member_header %}

Add a new member

- {% endblock new_member_header %} + +

After adding a new member, an email invitation will be sent to that user with instructions on how to set up an account. All members must keep their contact information updated and be responsive if contacted by the .gov team.

{% include "includes/required_fields.html" %}
+ {% csrf_token %} -
- -

Email

-
- - {% csrf_token %} +
+ +

Who would you like to add to the organization?

+
+ {% with group_classes="usa-form-editable usa-form-editable--no-border padding-top-0" %} {% input_with_errors form.email %} {% endwith %} -
+
- -
- -

Member Access

-
+ +
+ +

What level of access would you like to grant this member?

+
- Select the level of access for this member. * +

Select one *

- {% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %} - {% input_with_errors form.role %} - {% endwith %} - -
- - -
-
- - -
-

Basic member permissions

-

Member permissions available for basic-level acccess.

- -

Domains

- {% with group_classes="usa-form-editable usa-form-editable--no-border bg-gray-1 padding-top-0" %} - {% input_with_errors form.domain_permission_member %} + {% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %} + {% input_with_errors form.role %} {% endwith %} +
-

Domain requests

- {% with group_classes="usa-form-editable usa-form-editable--no-border bg-gray-1 padding-top-0" %} - {% input_with_errors form.domain_request_permission_member %} - {% endwith %} + +
+
-

Organization members

- {% with group_classes="usa-form-editable usa-form-editable--no-border bg-gray-1 padding-top-0" %} - {% input_with_errors form.member_permission_member %} - {% endwith %} -
+ +
+

What permissions do you want to add?

+

Configure the permissions for this member. Basic members cannot manage member permissions or organization metadata.

+ +

Domains *

+ {% with group_classes="usa-form-editable bg-gray-1 border-top-0 margin-top-0 border-bottom padding-top-0" add_legend_class="usa-sr-only" %} + {% input_with_errors form.domain_permissions %} + {% endwith %} + +

Domain requests *

+ {% with group_classes="usa-form-editable usa-form-editable--no-border bg-gray-1 padding-top-0" add_legend_class="usa-sr-only" %} + {% input_with_errors form.domain_request_permissions %} + {% endwith %} + +

Organization members *

+ {% with group_classes="usa-form-editable usa-form-editable--no-border bg-gray-1 padding-top-0" add_legend_class="usa-sr-only" %} + {% input_with_errors form.member_permissions %} + {% endwith %} +
+ +

Domain management

+ +

After you invite this person to your organization, you can assign domain management permissions on their member profile.

+ + +
+ Cancel + + + +
- -
- Cancel - - - -
Date: Fri, 24 Jan 2025 08:38:03 -0500 Subject: [PATCH 09/40] updated labels, label descriptions, javascript and more --- .../src/js/getgov/portfolio-member-page.js | 97 +++++++++++-------- src/registrar/forms/portfolio.py | 13 ++- .../includes/member_permissions.html | 23 +++-- .../portfolio_member_permissions.html | 16 ++- .../templates/portfolio_members_add_new.html | 6 +- 5 files changed, 92 insertions(+), 63 deletions(-) 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 cfb83badc..e0fe2c561 100644 --- a/src/registrar/assets/src/js/getgov/portfolio-member-page.js +++ b/src/registrar/assets/src/js/getgov/portfolio-member-page.js @@ -87,14 +87,6 @@ export function initAddNewMemberPageListeners() { }); }); - /* - Helper function to capitalize the first letter in a string (for display purposes) - */ - function capitalizeFirstLetter(text) { - if (!text) return ''; // Return empty string if input is falsy - return text.charAt(0).toUpperCase() + text.slice(1); - } - /* Populates contents of the "Add Member" confirmation modal */ @@ -102,10 +94,12 @@ export function initAddNewMemberPageListeners() { const permissionDetailsContainer = document.getElementById("permission_details"); permissionDetailsContainer.innerHTML = ""; // Clear previous content - // Get all permission sections (divs with h3 and radio inputs) - const permissionSections = document.querySelectorAll(`#${permission_details_div_id} > h3`); + if (permission_details_div_id == 'new-member-basic-permissions') { + // for basic users, display values are based on selections in the form + // Get all permission sections (divs with h3 and radio inputs) + const permissionSections = document.querySelectorAll(`#${permission_details_div_id} > h3`); - permissionSections.forEach(section => { + permissionSections.forEach(section => { // Find the

element text const sectionTitle = section.textContent; @@ -113,31 +107,51 @@ export function initAddNewMemberPageListeners() { const fieldset = section.nextElementSibling; if (fieldset && fieldset.tagName.toLowerCase() === 'fieldset') { - // Get the selected radio button within this fieldset - const selectedRadio = fieldset.querySelector('input[type="radio"]:checked'); + // Get the selected radio button within this fieldset + const selectedRadio = fieldset.querySelector('input[type="radio"]:checked'); - // If a radio button is selected, get its label text - let selectedPermission = "No permission selected"; - if (selectedRadio) { - const label = fieldset.querySelector(`label[for="${selectedRadio.id}"]`); - selectedPermission = label ? label.textContent : "No permission selected"; + // If a radio button is selected, get its label text + let selectedPermission = "No permission selected"; + if (selectedRadio) { + const label = fieldset.querySelector(`label[for="${selectedRadio.id}"]`); + if (label) { + // Get only the text node content (excluding subtext in

) + const mainText = Array.from(label.childNodes) + .filter(node => node.nodeType === Node.TEXT_NODE) + .map(node => node.textContent.trim()) + .join(""); // Combine and trim whitespace + selectedPermission = mainText || "No permission selected"; } - // Create new elements for the modal content - const titleElement = document.createElement("h4"); - titleElement.textContent = sectionTitle; - titleElement.classList.add("text-primary"); - titleElement.classList.add("margin-bottom-0"); - const permissionElement = document.createElement("p"); - permissionElement.textContent = selectedPermission; - permissionElement.classList.add("margin-top-0"); - - // Append to the modal content container - permissionDetailsContainer.appendChild(titleElement); - permissionDetailsContainer.appendChild(permissionElement); + // const label = fieldset.querySelector(`label[for="${selectedRadio.id}"]`); + // selectedPermission = label ? label.textContent : "No permission selected"; + } + appendPermissionInContainer(sectionTitle, selectedPermission, permissionDetailsContainer); } - }); + }); + } else { + // for admin users, the permissions are always the same + appendPermissionInContainer('Domains', 'Viewer, all', permissionDetailsContainer); + appendPermissionInContainer('Domain requests', 'Creator', permissionDetailsContainer); + appendPermissionInContainer('Members', 'Manager', permissionDetailsContainer); + } + } + + function appendPermissionInContainer(sectionTitle, permissionDisplay, permissionContainer) { + // Create new elements for the content + const titleElement = document.createElement("h4"); + titleElement.textContent = sectionTitle; + titleElement.classList.add("text-primary"); + titleElement.classList.add("margin-bottom-0"); + + const permissionElement = document.createElement("p"); + permissionElement.textContent = permissionDisplay; + permissionElement.classList.add("margin-top-0"); + + // Append to the content container + permissionContainer.appendChild(titleElement); + permissionContainer.appendChild(permissionElement); } /* @@ -149,16 +163,23 @@ export function initAddNewMemberPageListeners() { let emailValue = document.getElementById('id_email').value; document.getElementById('modalEmail').textContent = emailValue; - // Get selected radio button for access level + // Get selected radio button for member access level let selectedAccess = document.querySelector('input[name="role"]:checked'); - // Set the selected permission text to 'Basic' or 'Admin' (the value of the selected radio button) - // This value does not have the first letter capitalized so let's capitalize it - let accessText = selectedAccess ? capitalizeFirstLetter(selectedAccess.value) : "No access level selected"; + // Map the access level values to user-friendly labels + const accessLevelMapping = { + organization_admin: "Admin", + organization_member: "Basic", + }; + // Determine the access text based on the selected value + let accessText = selectedAccess + ? accessLevelMapping[selectedAccess.value] || "Unknown access level" + : "No access level selected"; + // Update the modal with the appropriate member access level text document.getElementById('modalAccessLevel').textContent = accessText; // Populate permission details based on access level if (selectedAccess && selectedAccess.value === 'organization_admin') { - populatePermissionDetails('new-member-admin-permissions'); + populatePermissionDetails('admin'); } else { populatePermissionDetails('new-member-basic-permissions'); } @@ -181,7 +202,7 @@ export function initPortfolioMemberPageRadio() { hookupRadioTogglerListener( 'role', { - 'organization_admin': 'member-admin-permissions', + 'organization_admin': '', 'organization_member': 'member-basic-permissions' } ); @@ -189,7 +210,7 @@ export function initPortfolioMemberPageRadio() { hookupRadioTogglerListener( 'role', { - 'organization_admin': 'new-member-admin-permissions', + 'organization_admin': '', 'organization_member': 'new-member-basic-permissions' } ); diff --git a/src/registrar/forms/portfolio.py b/src/registrar/forms/portfolio.py index f18bb4f5c..960825cd3 100644 --- a/src/registrar/forms/portfolio.py +++ b/src/registrar/forms/portfolio.py @@ -200,8 +200,17 @@ class BasePortfolioMemberForm(forms.ModelForm): # } self.fields["domain_permissions"].descriptions = { - UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS.value: "test1", - UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS.value: "test2", + UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS.value: "Can view only the domains they manage", + UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS.value: "Can view all domains for the organization", + } + self.fields["domain_request_permissions"].descriptions = { + UserPortfolioPermissionChoices.EDIT_REQUESTS.value: "Can view all domain requests for the organization and create requests", + UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value: "Can view all domain requests for the organization", + "no_access": "Cannot view or create domain requests" + } + self.fields["member_permissions"].descriptions = { + UserPortfolioPermissionChoices.VIEW_MEMBERS.value: "Can view all members permissions", + "no_access": "Cannot view member permissions" } # Map model instance values to custom form fields diff --git a/src/registrar/templates/includes/member_permissions.html b/src/registrar/templates/includes/member_permissions.html index 8cf75cfbf..ba1955a60 100644 --- a/src/registrar/templates/includes/member_permissions.html +++ b/src/registrar/templates/includes/member_permissions.html @@ -1,26 +1,33 @@

Member access

{% if permissions.roles and 'organization_admin' in permissions.roles %} -

Admin access

+

Admin

{% elif permissions.roles and 'organization_member' in permissions.roles %} -

Basic access

+

Basic

{% else %}

{% endif %} -

Organization domain requests

+

Domains

+{% if member_has_any_domains_portfolio_permission %} +

Viewer, all

+{% else %} +

Viewer, limited

+{% endif %} + +

Domain requests

{% if member_has_edit_request_portfolio_permission %} -

View all requests plus create requests

+

Creator

{% elif member_has_view_all_requests_portfolio_permission %} -

View all requests

+

Viewer

{% else %}

No access

{% endif %} -

Organization members

+

Members

{% if member_has_edit_members_portfolio_permission %} -

View all members plus manage members

+

Manager

{% elif member_has_view_members_portfolio_permission %} -

View all members

+

Viewer

{% else %}

No access

{% endif %} \ No newline at end of file diff --git a/src/registrar/templates/portfolio_member_permissions.html b/src/registrar/templates/portfolio_member_permissions.html index 19f4ad41d..47377727d 100644 --- a/src/registrar/templates/portfolio_member_permissions.html +++ b/src/registrar/templates/portfolio_member_permissions.html @@ -90,27 +90,23 @@ - -
-
-

Basic member permissions

Member permissions available for basic-level acccess.

-

Domains

- {% with group_classes="usa-form-editable usa-form-editable--no-border bg-gray-1 padding-top-0" %} +

Domains *

+ {% with group_classes="usa-form-editable usa-form-editable--no-border bg-gray-1 padding-top-0" add_legend_class="usa-sr-only" %} {% input_with_errors form.domain_permissions %} {% endwith %} -

Domain requests

- {% with group_classes="usa-form-editable usa-form-editable--no-border bg-gray-1 padding-top-0" %} +

Domain requests *

+ {% with group_classes="usa-form-editable usa-form-editable--no-border bg-gray-1 padding-top-0" add_legend_class="usa-sr-only" %} {% input_with_errors form.domain_request_permissions %} {% endwith %} -

Organization members

- {% with group_classes="usa-form-editable usa-form-editable--no-border bg-gray-1 padding-top-0" %} +

Members *

+ {% with group_classes="usa-form-editable usa-form-editable--no-border bg-gray-1 padding-top-0" add_legend_class="usa-sr-only" %} {% input_with_errors form.member_permissions %} {% endwith %} diff --git a/src/registrar/templates/portfolio_members_add_new.html b/src/registrar/templates/portfolio_members_add_new.html index a0a6839e9..03155a113 100644 --- a/src/registrar/templates/portfolio_members_add_new.html +++ b/src/registrar/templates/portfolio_members_add_new.html @@ -62,10 +62,6 @@ {% endwith %} - -
-
-

What permissions do you want to add?

@@ -81,7 +77,7 @@ {% input_with_errors form.domain_request_permissions %} {% endwith %} -

Organization members *

+

Members *

{% with group_classes="usa-form-editable usa-form-editable--no-border bg-gray-1 padding-top-0" add_legend_class="usa-sr-only" %} {% input_with_errors form.member_permissions %} {% endwith %} From ef826bf2569e05b70cc7ba58ec094298edbb876d Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Fri, 24 Jan 2025 14:55:38 -0500 Subject: [PATCH 10/40] fixed existing tests, and fixed one broken condition --- src/registrar/forms/portfolio.py | 6 +- .../includes/member_permissions.html | 2 +- src/registrar/tests/test_views_portfolio.py | 89 ++++++++----------- src/registrar/views/portfolios.py | 8 ++ 4 files changed, 51 insertions(+), 54 deletions(-) diff --git a/src/registrar/forms/portfolio.py b/src/registrar/forms/portfolio.py index 960825cd3..33d48f368 100644 --- a/src/registrar/forms/portfolio.py +++ b/src/registrar/forms/portfolio.py @@ -133,7 +133,7 @@ class BasePortfolioMemberForm(forms.ModelForm): widget=forms.RadioSelect, required=False, error_messages={ - "required": "Member domain permission is required", + "required": "Domain permission is required", }, ) @@ -147,7 +147,7 @@ class BasePortfolioMemberForm(forms.ModelForm): widget=forms.RadioSelect, required=False, error_messages={ - "required": "Basic member permission is required", + "required": "Domain request permission is required", }, ) @@ -159,7 +159,7 @@ class BasePortfolioMemberForm(forms.ModelForm): widget=forms.RadioSelect, required=False, error_messages={ - "required": "Admin member permission is required", + "required": "Member permission is required", }, ) diff --git a/src/registrar/templates/includes/member_permissions.html b/src/registrar/templates/includes/member_permissions.html index ba1955a60..65a9b9ea8 100644 --- a/src/registrar/templates/includes/member_permissions.html +++ b/src/registrar/templates/includes/member_permissions.html @@ -8,7 +8,7 @@ {% endif %}

Domains

-{% if member_has_any_domains_portfolio_permission %} +{% if member_has_view_all_domains_portfolio_permission %}

Viewer, all

{% else %}

Viewer, limited

diff --git a/src/registrar/tests/test_views_portfolio.py b/src/registrar/tests/test_views_portfolio.py index 69502d683..87b0d9308 100644 --- a/src/registrar/tests/test_views_portfolio.py +++ b/src/registrar/tests/test_views_portfolio.py @@ -1043,27 +1043,19 @@ class TestPortfolio(WebTest): @override_flag("organization_feature", active=True) @override_flag("organization_members", active=True) def test_can_view_invitedmember_page_when_user_has_edit_members(self): - """Test that user can access the invitedmember page with edit_members permission""" + """Test that user can access the invitedmember page with org admin role""" # Arrange - # give user permissions to view AND manage members + # give user admin role permission_obj, _ = UserPortfolioPermission.objects.get_or_create( user=self.user, portfolio=self.portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN], - additional_permissions=[ - UserPortfolioPermissionChoices.EDIT_REQUESTS, - UserPortfolioPermissionChoices.EDIT_MEMBERS, - ], ) portfolio_invitation, _ = PortfolioInvitation.objects.get_or_create( email="info@example.com", portfolio=self.portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN], - additional_permissions=[ - UserPortfolioPermissionChoices.EDIT_REQUESTS, - UserPortfolioPermissionChoices.EDIT_MEMBERS, - ], ) # Verify the page can be accessed @@ -1074,9 +1066,10 @@ class TestPortfolio(WebTest): # Assert text within the page is correct self.assertContains(response, "Invited") self.assertContains(response, portfolio_invitation.email) - self.assertContains(response, "Admin access") - self.assertContains(response, "View all requests plus create requests") - self.assertContains(response, "View all members plus manage members") + self.assertContains(response, "Admin") + self.assertContains(response, "Viewer, all") + self.assertContains(response, "Creator") + self.assertContains(response, "Manager") self.assertContains( response, 'This member does not manage any domains. To assign this member a domain, click "Manage"' ) @@ -1404,15 +1397,11 @@ class TestPortfolio(WebTest): # In the members_table.html we use data-has-edit-permission as a boolean # to indicate if a user has permission to edit members in the specific portfolio - # 1. User w/ edit permission + # 1. User w/ edit permission. This permission is included in Organization admin role UserPortfolioPermission.objects.get_or_create( user=self.user, portfolio=self.portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN], - additional_permissions=[ - UserPortfolioPermissionChoices.VIEW_MEMBERS, - UserPortfolioPermissionChoices.EDIT_MEMBERS, - ], ) # Create a member under same portfolio @@ -1433,12 +1422,13 @@ class TestPortfolio(WebTest): self.assertContains(response, 'data-has-edit-permission="True"') - # 2. User w/o edit permission (additional permission of EDIT_MEMBERS removed) + # 2. User w/o edit permission. permission = UserPortfolioPermission.objects.get(user=self.user, portfolio=self.portfolio) - # Remove the EDIT_MEMBERS additional permission + # Update to basic member with view members permission + permission.roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER] permission.additional_permissions = [ - perm for perm in permission.additional_permissions if perm != UserPortfolioPermissionChoices.EDIT_MEMBERS + UserPortfolioPermissionChoices.VIEW_MEMBERS, ] # Save the updated permissions list @@ -3123,7 +3113,9 @@ class TestPortfolioInviteNewMemberView(TestWithUser, WebTest): reverse("new-member"), { "role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER.value, - "domain_request_permission_member": UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value, + "domain_request_permissions": UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value, + "domain_permissions": UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS.value, + "member_permissions": "no_access", "email": self.new_member_email, }, ) @@ -3164,7 +3156,9 @@ class TestPortfolioInviteNewMemberView(TestWithUser, WebTest): reverse("new-member"), { "role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER.value, - "domain_request_permission_member": UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value, + "domain_request_permissions": UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value, + "domain_permissions": UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS.value, + "member_permissions": "no_access", "email": self.new_member_email, }, HTTP_X_REQUESTED_WITH="XMLHttpRequest", @@ -3241,7 +3235,9 @@ class TestPortfolioInviteNewMemberView(TestWithUser, WebTest): form_data = { "role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER.value, - "domain_request_permission_member": UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value, + "domain_request_permissions": UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value, + "domain_permissions": UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS.value, + "member_permissions": "no_access", "email": self.new_member_email, } @@ -3280,7 +3276,9 @@ class TestPortfolioInviteNewMemberView(TestWithUser, WebTest): form_data = { "role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER.value, - "domain_request_permission_member": UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value, + "domain_request_permissions": UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value, + "domain_permissions": UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS.value, + "member_permissions": "no_access", "email": self.new_member_email, } @@ -3322,7 +3320,9 @@ class TestPortfolioInviteNewMemberView(TestWithUser, WebTest): form_data = { "role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER.value, - "domain_request_permission_member": UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value, + "domain_request_permissions": UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value, + "domain_permissions": UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS.value, + "member_permissions": "no_access", "email": self.new_member_email, } @@ -3448,7 +3448,9 @@ class TestPortfolioInviteNewMemberView(TestWithUser, WebTest): reverse("new-member"), { "role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER.value, - "domain_request_permission_member": UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value, + "domain_request_permissions": UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value, + "domain_permissions": UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS.value, + "member_permissions": "no_access", "email": "newuser@example.com", }, ) @@ -3532,8 +3534,6 @@ class TestEditPortfolioMemberView(WebTest): reverse("member-permissions", kwargs={"pk": basic_permission.id}), { "role": UserPortfolioRoleChoices.ORGANIZATION_ADMIN, - "domain_request_permission_admin": UserPortfolioPermissionChoices.EDIT_REQUESTS, - "member_permission_admin": UserPortfolioPermissionChoices.EDIT_MEMBERS, }, ) @@ -3543,13 +3543,6 @@ class TestEditPortfolioMemberView(WebTest): # Verify database changes basic_permission.refresh_from_db() self.assertEqual(basic_permission.roles, [UserPortfolioRoleChoices.ORGANIZATION_ADMIN]) - self.assertEqual( - set(basic_permission.additional_permissions), - { - UserPortfolioPermissionChoices.EDIT_REQUESTS, - UserPortfolioPermissionChoices.EDIT_MEMBERS, - }, - ) @less_console_noise_decorator @override_flag("organization_feature", active=True) @@ -3567,18 +3560,21 @@ class TestEditPortfolioMemberView(WebTest): response = self.client.post( reverse("member-permissions", kwargs={"pk": permission.id}), { - "role": UserPortfolioRoleChoices.ORGANIZATION_ADMIN, + "role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER, # Missing required admin fields }, ) self.assertEqual(response.status_code, 200) self.assertEqual( - response.context["form"].errors["domain_request_permission_admin"][0], - "Admin domain request permission is required", + response.context["form"].errors["domain_request_permissions"][0], + "Domain request permission is required", ) self.assertEqual( - response.context["form"].errors["member_permission_admin"][0], "Admin member permission is required" + response.context["form"].errors["member_permissions"][0], "Member permission is required" + ) + self.assertEqual( + response.context["form"].errors["domain_permissions"][0], "Domain permission is required" ) @less_console_noise_decorator @@ -3593,8 +3589,6 @@ class TestEditPortfolioMemberView(WebTest): reverse("invitedmember-permissions", kwargs={"pk": self.invitation.id}), { "role": UserPortfolioRoleChoices.ORGANIZATION_ADMIN, - "domain_request_permission_admin": UserPortfolioPermissionChoices.EDIT_REQUESTS, - "member_permission_admin": UserPortfolioPermissionChoices.EDIT_MEMBERS, }, ) @@ -3603,13 +3597,6 @@ class TestEditPortfolioMemberView(WebTest): # Verify invitation was updated updated_invitation = PortfolioInvitation.objects.get(pk=self.invitation.id) self.assertEqual(updated_invitation.roles, [UserPortfolioRoleChoices.ORGANIZATION_ADMIN]) - self.assertEqual( - set(updated_invitation.additional_permissions), - { - UserPortfolioPermissionChoices.EDIT_REQUESTS, - UserPortfolioPermissionChoices.EDIT_MEMBERS, - }, - ) @less_console_noise_decorator @override_flag("organization_feature", active=True) @@ -3631,7 +3618,9 @@ class TestEditPortfolioMemberView(WebTest): reverse("member-permissions", kwargs={"pk": admin_permission.id}), { "role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER, - "domain_request_permission_member": UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS, + "domain_permissions": UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS, + "member_permissions": "no_access", + "domain_request_permissions": "no_access", }, ) diff --git a/src/registrar/views/portfolios.py b/src/registrar/views/portfolios.py index c4f60ca35..a25b2094b 100644 --- a/src/registrar/views/portfolios.py +++ b/src/registrar/views/portfolios.py @@ -82,6 +82,9 @@ class PortfolioMemberView(PortfolioMemberPermissionView, View): member_has_edit_members_portfolio_permission = member.has_edit_members_portfolio_permission( portfolio_permission.portfolio ) + member_has_view_all_domains_portfolio_permission = member.has_view_all_domains_portfolio_permission( + portfolio_permission.portfolio + ) return render( request, @@ -95,6 +98,7 @@ class PortfolioMemberView(PortfolioMemberPermissionView, View): "member_has_edit_request_portfolio_permission": member_has_edit_request_portfolio_permission, "member_has_view_members_portfolio_permission": member_has_view_members_portfolio_permission, "member_has_edit_members_portfolio_permission": member_has_edit_members_portfolio_permission, + "member_has_view_all_domains_portfolio_permission": member_has_view_all_domains_portfolio_permission, }, ) @@ -346,6 +350,9 @@ class PortfolioInvitedMemberView(PortfolioMemberPermissionView, View): member_has_edit_members_portfolio_permission = ( UserPortfolioPermissionChoices.EDIT_MEMBERS in portfolio_invitation.get_portfolio_permissions() ) + member_has_view_all_domains_portfolio_permission = ( + UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS in portfolio_invitation.get_portfolio_permissions() + ) return render( request, @@ -358,6 +365,7 @@ class PortfolioInvitedMemberView(PortfolioMemberPermissionView, View): "member_has_edit_request_portfolio_permission": member_has_edit_request_portfolio_permission, "member_has_view_members_portfolio_permission": member_has_view_members_portfolio_permission, "member_has_edit_members_portfolio_permission": member_has_edit_members_portfolio_permission, + "member_has_view_all_domains_portfolio_permission": member_has_view_all_domains_portfolio_permission, }, ) From d3a8e018b6b3ed30e568bd521ed1a4c84ffa6c87 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Fri, 24 Jan 2025 14:57:26 -0500 Subject: [PATCH 11/40] ui wip --- .../src/js/getgov/portfolio-member-page.js | 14 +- .../assets/src/sass/_theme/_accordions.scss | 26 ++++ .../assets/src/sass/_theme/_tables.scss | 12 ++ .../assets/src/sass/_theme/_typography.scss | 4 + src/registrar/forms/portfolio.py | 3 + .../templates/portfolio_members_add_new.html | 135 +++++++++++++++++- 6 files changed, 181 insertions(+), 13 deletions(-) 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 e0fe2c561..26f3020fb 100644 --- a/src/registrar/assets/src/js/getgov/portfolio-member-page.js +++ b/src/registrar/assets/src/js/getgov/portfolio-member-page.js @@ -94,7 +94,7 @@ export function initAddNewMemberPageListeners() { const permissionDetailsContainer = document.getElementById("permission_details"); permissionDetailsContainer.innerHTML = ""; // Clear previous content - if (permission_details_div_id == 'new-member-basic-permissions') { + if (permission_details_div_id == 'member-basic-permissions') { // for basic users, display values are based on selections in the form // Get all permission sections (divs with h3 and radio inputs) const permissionSections = document.querySelectorAll(`#${permission_details_div_id} > h3`); @@ -181,7 +181,7 @@ export function initAddNewMemberPageListeners() { if (selectedAccess && selectedAccess.value === 'organization_admin') { populatePermissionDetails('admin'); } else { - populatePermissionDetails('new-member-basic-permissions'); + populatePermissionDetails('member-basic-permissions'); } //------- Show the modal @@ -198,7 +198,7 @@ export function initPortfolioMemberPageRadio() { document.addEventListener("DOMContentLoaded", () => { let memberForm = document.getElementById("member_form"); let newMemberForm = document.getElementById("add_member_form") - if (memberForm) { + if (memberForm || newMemberForm) { hookupRadioTogglerListener( 'role', { @@ -206,14 +206,6 @@ export function initPortfolioMemberPageRadio() { 'organization_member': 'member-basic-permissions' } ); - }else if (newMemberForm){ - hookupRadioTogglerListener( - 'role', - { - 'organization_admin': '', - 'organization_member': 'new-member-basic-permissions' - } - ); } }); } diff --git a/src/registrar/assets/src/sass/_theme/_accordions.scss b/src/registrar/assets/src/sass/_theme/_accordions.scss index 762618415..d9a669794 100644 --- a/src/registrar/assets/src/sass/_theme/_accordions.scss +++ b/src/registrar/assets/src/sass/_theme/_accordions.scss @@ -49,3 +49,29 @@ tr:last-of-type .usa-accordion--more-actions .usa-accordion__content { bottom: -10px; right: 30px; } + +.usa-accordion--show-more { + width: auto; + .usa-accordion__button[aria-expanded=false], + .usa-accordion__button[aria-expanded=false]:hover, + .usa-accordion__button[aria-expanded=true], + .usa-accordion__button[aria-expanded=true]:hover { + background-image: none; + background-color: transparent; + padding-right: 0; + padding-left: 0; + font-weight: normal; + } + .usa-accordion__button[aria-expanded=true] .expand-more { + display: inline-block; + } + .usa-accordion__button[aria-expanded=true] .expand-less { + display: none; + } + .usa-accordion__button[aria-expanded=false] .expand-more { + display: none; + } + .usa-accordion__button[aria-expanded=false] .expand-less { + display: inline-block; + } +} diff --git a/src/registrar/assets/src/sass/_theme/_tables.scss b/src/registrar/assets/src/sass/_theme/_tables.scss index ea160396e..3c8f15d70 100644 --- a/src/registrar/assets/src/sass/_theme/_tables.scss +++ b/src/registrar/assets/src/sass/_theme/_tables.scss @@ -99,3 +99,15 @@ th { } } } + +.dotgov-table--padding-left { + td, th { + padding: units(2) units(4) units(2) units(2); + } +} + +.usa-table--bg-transparent { + td, thead th { + background-color: transparent; + } +} diff --git a/src/registrar/assets/src/sass/_theme/_typography.scss b/src/registrar/assets/src/sass/_theme/_typography.scss index db19a595b..ff41d2509 100644 --- a/src/registrar/assets/src/sass/_theme/_typography.scss +++ b/src/registrar/assets/src/sass/_theme/_typography.scss @@ -51,3 +51,7 @@ h2 { padding-left: units(1); border-left: 2px solid color('base-lighter'); } + +.font-body-1 { + font-size: size('body', 1); +} diff --git a/src/registrar/forms/portfolio.py b/src/registrar/forms/portfolio.py index 960825cd3..d0a0712cf 100644 --- a/src/registrar/forms/portfolio.py +++ b/src/registrar/forms/portfolio.py @@ -132,6 +132,7 @@ class BasePortfolioMemberForm(forms.ModelForm): ], widget=forms.RadioSelect, required=False, + initial=UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS.value, error_messages={ "required": "Member domain permission is required", }, @@ -146,6 +147,7 @@ class BasePortfolioMemberForm(forms.ModelForm): ], widget=forms.RadioSelect, required=False, + initial="no_access", error_messages={ "required": "Basic member permission is required", }, @@ -158,6 +160,7 @@ class BasePortfolioMemberForm(forms.ModelForm): ], widget=forms.RadioSelect, required=False, + initial="no_access", error_messages={ "required": "Admin member permission is required", }, diff --git a/src/registrar/templates/portfolio_members_add_new.html b/src/registrar/templates/portfolio_members_add_new.html index 03155a113..663cbb17f 100644 --- a/src/registrar/templates/portfolio_members_add_new.html +++ b/src/registrar/templates/portfolio_members_add_new.html @@ -62,8 +62,139 @@ {% endwith %} +
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Member actions availableAdminBasic
+ View domains they manage + + + + + + + +
View all domains for the organization + + + Optional +
View all domain requests + + + Optional +
Create domain requests + + + Optional +
View all member permissions + + + Optional +
Manage member permissions + + + +
Manage organization metadata (address) + + + +
+
+ -
+

What permissions do you want to add?

Configure the permissions for this member. Basic members cannot manage member permissions or organization metadata.

@@ -85,7 +216,7 @@

Domain management

-

After you invite this person to your organization, you can assign domain management permissions on their member profile.

+

After you invite this person to your organization, you can assign domain management permissions on their member profile.

From 7b5b4ae2cdd1d6ab8b6711991650a423f0040a00 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Fri, 24 Jan 2025 15:13:44 -0500 Subject: [PATCH 12/40] pull out common code into includes --- .../assets/src/sass/_theme/_accordions.scss | 1 + .../includes/member_basic_permissions.html | 20 +++ .../includes/member_permissions_matrix.html | 131 +++++++++++++++ ...s.html => member_permissions_summary.html} | 0 .../templates/includes/summary_item.html | 2 +- .../portfolio_member_permissions.html | 23 +-- .../templates/portfolio_members_add_new.html | 153 +----------------- 7 files changed, 159 insertions(+), 171 deletions(-) create mode 100644 src/registrar/templates/includes/member_basic_permissions.html create mode 100644 src/registrar/templates/includes/member_permissions_matrix.html rename src/registrar/templates/includes/{member_permissions.html => member_permissions_summary.html} (100%) diff --git a/src/registrar/assets/src/sass/_theme/_accordions.scss b/src/registrar/assets/src/sass/_theme/_accordions.scss index d9a669794..ca9990ca9 100644 --- a/src/registrar/assets/src/sass/_theme/_accordions.scss +++ b/src/registrar/assets/src/sass/_theme/_accordions.scss @@ -50,6 +50,7 @@ tr:last-of-type .usa-accordion--more-actions .usa-accordion__content { right: 30px; } +// A CSS only show-more/show-less based on usa-accordion .usa-accordion--show-more { width: auto; .usa-accordion__button[aria-expanded=false], diff --git a/src/registrar/templates/includes/member_basic_permissions.html b/src/registrar/templates/includes/member_basic_permissions.html new file mode 100644 index 000000000..ebb4d0508 --- /dev/null +++ b/src/registrar/templates/includes/member_basic_permissions.html @@ -0,0 +1,20 @@ +{% load field_helpers %} +
+

What permissions do you want to add?

+

Configure the permissions for this member. Basic members cannot manage member permissions or organization metadata.

+ +

Domains *

+ {% with group_classes="usa-form-editable bg-gray-1 border-top-0 margin-top-0 border-bottom padding-top-0" add_legend_class="usa-sr-only" %} + {% input_with_errors form.domain_permissions %} + {% endwith %} + +

Domain requests *

+ {% with group_classes="usa-form-editable usa-form-editable--no-border bg-gray-1 padding-top-0" add_legend_class="usa-sr-only" %} + {% input_with_errors form.domain_request_permissions %} + {% endwith %} + +

Members *

+ {% with group_classes="usa-form-editable usa-form-editable--no-border bg-gray-1 padding-top-0" add_legend_class="usa-sr-only" %} + {% input_with_errors form.member_permissions %} + {% endwith %} +
diff --git a/src/registrar/templates/includes/member_permissions_matrix.html b/src/registrar/templates/includes/member_permissions_matrix.html new file mode 100644 index 000000000..9b3c20ac5 --- /dev/null +++ b/src/registrar/templates/includes/member_permissions_matrix.html @@ -0,0 +1,131 @@ +
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Member actions availableAdminBasic
+ View domains they manage + + + + + + + +
View all domains for the organization + + + Optional +
View all domain requests + + + Optional +
Create domain requests + + + Optional +
View all member permissions + + + Optional +
Manage member permissions + + + +
Manage organization metadata (address) + + + +
+
+
diff --git a/src/registrar/templates/includes/member_permissions.html b/src/registrar/templates/includes/member_permissions_summary.html similarity index 100% rename from src/registrar/templates/includes/member_permissions.html rename to src/registrar/templates/includes/member_permissions_summary.html diff --git a/src/registrar/templates/includes/summary_item.html b/src/registrar/templates/includes/summary_item.html index 25387ecbd..319c74822 100644 --- a/src/registrar/templates/includes/summary_item.html +++ b/src/registrar/templates/includes/summary_item.html @@ -25,7 +25,7 @@

{{ sub_header_text }}

{% endif %} {% if permissions %} - {% include "includes/member_permissions.html" with permissions=value %} + {% include "includes/member_permissions_summary.html" with permissions=value %} {% elif domain_mgmt %} {% include "includes/member_domain_management.html" with domain_count=value %} {% elif address %} diff --git a/src/registrar/templates/portfolio_member_permissions.html b/src/registrar/templates/portfolio_member_permissions.html index 47377727d..906fd3a9a 100644 --- a/src/registrar/templates/portfolio_member_permissions.html +++ b/src/registrar/templates/portfolio_member_permissions.html @@ -90,27 +90,10 @@ + {% include "includes/member_permissions_matrix.html" %} + -
-

Basic member permissions

-

Member permissions available for basic-level acccess.

- -

Domains *

- {% with group_classes="usa-form-editable usa-form-editable--no-border bg-gray-1 padding-top-0" add_legend_class="usa-sr-only" %} - {% input_with_errors form.domain_permissions %} - {% endwith %} - -

Domain requests *

- {% with group_classes="usa-form-editable usa-form-editable--no-border bg-gray-1 padding-top-0" add_legend_class="usa-sr-only" %} - {% input_with_errors form.domain_request_permissions %} - {% endwith %} - -

Members *

- {% with group_classes="usa-form-editable usa-form-editable--no-border bg-gray-1 padding-top-0" add_legend_class="usa-sr-only" %} - {% input_with_errors form.member_permissions %} - {% endwith %} - -
+ {% include "includes/member_basic_permissions.html" %}
diff --git a/src/registrar/templates/portfolio_members_add_new.html b/src/registrar/templates/portfolio_members_add_new.html index 663cbb17f..76f8947f9 100644 --- a/src/registrar/templates/portfolio_members_add_new.html +++ b/src/registrar/templates/portfolio_members_add_new.html @@ -62,158 +62,11 @@ {% endwith %} -
-

- -

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Member actions availableAdminBasic
- View domains they manage - - - - - - - -
View all domains for the organization - - - Optional -
View all domain requests - - - Optional -
Create domain requests - - - Optional -
View all member permissions - - - Optional -
Manage member permissions - - - -
Manage organization metadata (address) - - - -
-
+ {% include "includes/member_permissions_matrix.html" %} -
-

What permissions do you want to add?

-

Configure the permissions for this member. Basic members cannot manage member permissions or organization metadata.

- -

Domains *

- {% with group_classes="usa-form-editable bg-gray-1 border-top-0 margin-top-0 border-bottom padding-top-0" add_legend_class="usa-sr-only" %} - {% input_with_errors form.domain_permissions %} - {% endwith %} - -

Domain requests *

- {% with group_classes="usa-form-editable usa-form-editable--no-border bg-gray-1 padding-top-0" add_legend_class="usa-sr-only" %} - {% input_with_errors form.domain_request_permissions %} - {% endwith %} - -

Members *

- {% with group_classes="usa-form-editable usa-form-editable--no-border bg-gray-1 padding-top-0" add_legend_class="usa-sr-only" %} - {% input_with_errors form.member_permissions %} - {% endwith %} -
- + {% include "includes/member_basic_permissions.html" %} +

Domain management

After you invite this person to your organization, you can assign domain management permissions on their member profile.

From 05a6a20bfb48d85a879ddb6360948b70a2c09276 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Fri, 24 Jan 2025 15:56:37 -0500 Subject: [PATCH 13/40] fix broken tests --- src/registrar/tests/test_forms.py | 158 +++++++++++++++++++++--------- 1 file changed, 111 insertions(+), 47 deletions(-) diff --git a/src/registrar/tests/test_forms.py b/src/registrar/tests/test_forms.py index a2960deac..4279ca068 100644 --- a/src/registrar/tests/test_forms.py +++ b/src/registrar/tests/test_forms.py @@ -3,6 +3,7 @@ import json from django.test import TestCase, RequestFactory from api.views import available +from api.tests.common import less_console_noise_decorator from registrar.forms.domain_request_wizard import ( AlternativeDomainForm, @@ -39,6 +40,7 @@ class TestFormValidation(MockEppLib): self.user = get_user_model().objects.create(username="username") self.factory = RequestFactory() + @less_console_noise_decorator def test_org_contact_zip_invalid(self): form = OrganizationContactForm(data={"zipcode": "nah"}) self.assertEqual( @@ -46,11 +48,13 @@ class TestFormValidation(MockEppLib): ["Enter a 5-digit or 9-digit zip code, like 12345 or 12345-6789."], ) + @less_console_noise_decorator def test_org_contact_zip_valid(self): for zipcode in ["12345", "12345-6789"]: form = OrganizationContactForm(data={"zipcode": zipcode}) self.assertNotIn("zipcode", form.errors) + @less_console_noise_decorator def test_website_invalid(self): form = CurrentSitesForm(data={"website": "nah"}) self.assertEqual( @@ -58,33 +62,39 @@ class TestFormValidation(MockEppLib): ["Enter your organization's current website in the required format, like example.com."], ) + @less_console_noise_decorator def test_website_valid(self): form = CurrentSitesForm(data={"website": "hyphens-rule.gov.uk"}) self.assertEqual(len(form.errors), 0) + @less_console_noise_decorator def test_website_scheme_valid(self): form = CurrentSitesForm(data={"website": "http://hyphens-rule.gov.uk"}) self.assertEqual(len(form.errors), 0) form = CurrentSitesForm(data={"website": "https://hyphens-rule.gov.uk"}) self.assertEqual(len(form.errors), 0) + @less_console_noise_decorator def test_requested_domain_valid(self): """Just a valid domain name with no .gov at the end.""" form = DotGovDomainForm(data={"requested_domain": "top-level-agency"}) self.assertEqual(len(form.errors), 0) + @less_console_noise_decorator def test_requested_domain_starting_www(self): """Test a valid domain name with .www at the beginning.""" form = DotGovDomainForm(data={"requested_domain": "www.top-level-agency"}) self.assertEqual(len(form.errors), 0) self.assertEqual(form.cleaned_data["requested_domain"], "top-level-agency") + @less_console_noise_decorator def test_requested_domain_ending_dotgov(self): """Just a valid domain name with .gov at the end.""" form = DotGovDomainForm(data={"requested_domain": "top-level-agency.gov"}) self.assertEqual(len(form.errors), 0) self.assertEqual(form.cleaned_data["requested_domain"], "top-level-agency") + @less_console_noise_decorator def test_requested_domain_ending_dotcom_invalid(self): """don't accept domains ending other than .gov.""" form = DotGovDomainForm(data={"requested_domain": "top-level-agency.com"}) @@ -93,6 +103,7 @@ class TestFormValidation(MockEppLib): ["Enter the .gov domain you want without any periods."], ) + @less_console_noise_decorator def test_requested_domain_errors_consistent(self): """Tests if the errors on submit and with the check availability buttons are consistent for requested_domains @@ -150,6 +161,7 @@ class TestFormValidation(MockEppLib): # for good measure, test if the two objects are equal anyway self.assertEqual([json_error], form_error) + @less_console_noise_decorator def test_alternate_domain_errors_consistent(self): """Tests if the errors on submit and with the check availability buttons are consistent for alternative_domains @@ -200,6 +212,7 @@ class TestFormValidation(MockEppLib): # for good measure, test if the two objects are equal anyway self.assertEqual([json_error], form_error) + @less_console_noise_decorator def test_requested_domain_two_dots_invalid(self): """don't accept domains that are subdomains""" form = DotGovDomainForm(data={"requested_domain": "sub.top-level-agency.gov"}) @@ -218,6 +231,7 @@ class TestFormValidation(MockEppLib): ["Enter the .gov domain you want without any periods."], ) + @less_console_noise_decorator def test_requested_domain_invalid_characters(self): """must be a valid .gov domain name.""" form = DotGovDomainForm(data={"requested_domain": "underscores_forever"}) @@ -226,6 +240,7 @@ class TestFormValidation(MockEppLib): ["Enter a domain using only letters, numbers, or hyphens (though we don't recommend using hyphens)."], ) + @less_console_noise_decorator def test_senior_official_email_invalid(self): """must be a valid email address.""" form = SeniorOfficialForm(data={"email": "boss@boss"}) @@ -234,6 +249,7 @@ class TestFormValidation(MockEppLib): ["Enter an email address in the required format, like name@example.com."], ) + @less_console_noise_decorator def test_purpose_form_character_count_invalid(self): """Response must be less than 2000 characters.""" form = PurposeForm( @@ -281,6 +297,7 @@ class TestFormValidation(MockEppLib): ["Response must be less than 2000 characters."], ) + @less_console_noise_decorator def test_anything_else_form_about_your_organization_character_count_invalid(self): """Response must be less than 2000 characters.""" form = AnythingElseForm( @@ -327,6 +344,7 @@ class TestFormValidation(MockEppLib): ["Response must be less than 2000 characters."], ) + @less_console_noise_decorator def test_anything_else_form_character_count_invalid(self): """Response must be less than 2000 characters.""" form = AboutYourOrganizationForm( @@ -375,6 +393,7 @@ class TestFormValidation(MockEppLib): ["Response must be less than 2000 characters."], ) + @less_console_noise_decorator def test_other_contact_email_invalid(self): """must be a valid email address.""" form = OtherContactsForm(data={"email": "splendid@boss"}) @@ -383,11 +402,13 @@ class TestFormValidation(MockEppLib): ["Enter an email address in the required format, like name@example.com."], ) + @less_console_noise_decorator def test_other_contact_phone_invalid(self): """Must be a valid phone number.""" form = OtherContactsForm(data={"phone": "super@boss"}) self.assertTrue(form.errors["phone"][0].startswith("Enter a valid 10-digit phone number.")) + @less_console_noise_decorator def test_requirements_form_blank(self): """Requirements box unchecked is an error.""" form = RequirementsForm(data={}) @@ -396,6 +417,7 @@ class TestFormValidation(MockEppLib): ["Check the box if you read and agree to the requirements for operating a .gov domain."], ) + @less_console_noise_decorator def test_requirements_form_unchecked(self): """Requirements box unchecked is an error.""" form = RequirementsForm(data={"is_policy_acknowledged": False}) @@ -404,6 +426,7 @@ class TestFormValidation(MockEppLib): ["Check the box if you read and agree to the requirements for operating a .gov domain."], ) + @less_console_noise_decorator def test_tribal_government_unrecognized(self): """Not state or federally recognized is an error.""" form = TribalGovernmentForm(data={"state_recognized": False, "federally_recognized": False}) @@ -411,10 +434,12 @@ class TestFormValidation(MockEppLib): class TestContactForm(TestCase): + @less_console_noise_decorator def test_contact_form_email_invalid(self): form = ContactForm(data={"email": "example.net"}) self.assertEqual(form.errors["email"], ["Enter a valid email address."]) + @less_console_noise_decorator def test_contact_form_email_invalid2(self): form = ContactForm(data={"email": "@"}) self.assertEqual(form.errors["email"], ["Enter a valid email address."]) @@ -442,7 +467,6 @@ class TestBasePortfolioMemberForms(TestCase): if instance is not None: form = form_class(data=data, instance=instance) else: - print("no instance") form = form_class(data=data) self.assertTrue(form.is_valid(), f"Form {form_class.__name__} failed validation with data: {data}") return form @@ -465,38 +489,30 @@ class TestBasePortfolioMemberForms(TestCase): for permission in expected_permissions: self.assertIn(permission, cleaned_data["additional_permissions"]) - def test_required_field_for_admin(self): - """Test that required fields are validated for an admin role.""" - data = { - "role": UserPortfolioRoleChoices.ORGANIZATION_ADMIN.value, - "domain_request_permission_admin": "", # Simulate missing field - "member_permission_admin": "", # Simulate missing field - } - - # Check required fields for all forms - self._assert_form_has_error(PortfolioMemberForm, data, "domain_request_permission_admin") - self._assert_form_has_error(PortfolioMemberForm, data, "member_permission_admin") - - self._assert_form_has_error(PortfolioInvitedMemberForm, data, "domain_request_permission_admin") - self._assert_form_has_error(PortfolioInvitedMemberForm, data, "member_permission_admin") - - self._assert_form_has_error(PortfolioNewMemberForm, data, "domain_request_permission_admin") - self._assert_form_has_error(PortfolioNewMemberForm, data, "member_permission_admin") - + @less_console_noise_decorator def test_required_field_for_member(self): """Test that required fields are validated for a member role.""" data = { "role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER.value, - "domain_request_permission_member": "", # Simulate missing field + "domain_request_permissions": "", # Simulate missing field + "domain_permissions": "", # Simulate missing field + "member_permissions": "", # Simulate missing field } # Check required fields for all forms - self._assert_form_has_error(PortfolioMemberForm, data, "domain_request_permission_member") - self._assert_form_has_error(PortfolioInvitedMemberForm, data, "domain_request_permission_member") - self._assert_form_has_error(PortfolioNewMemberForm, data, "domain_request_permission_member") + self._assert_form_has_error(PortfolioMemberForm, data, "domain_request_permissions") + self._assert_form_has_error(PortfolioMemberForm, data, "domain_permissions") + self._assert_form_has_error(PortfolioMemberForm, data, "member_permissions") + self._assert_form_has_error(PortfolioInvitedMemberForm, data, "domain_request_permissions") + self._assert_form_has_error(PortfolioInvitedMemberForm, data, "domain_permissions") + self._assert_form_has_error(PortfolioInvitedMemberForm, data, "member_permissions") + self._assert_form_has_error(PortfolioNewMemberForm, data, "domain_request_permissions") + self._assert_form_has_error(PortfolioNewMemberForm, data, "domain_permissions") + self._assert_form_has_error(PortfolioNewMemberForm, data, "member_permissions") - def test_clean_validates_required_fields_for_role(self): - """Test that the `clean` method validates the correct fields for each role. + @less_console_noise_decorator + def test_clean_validates_required_fields_for_admin_role(self): + """Test that the `clean` method validates the correct fields for admin role. For PortfolioMemberForm and PortfolioInvitedMemberForm, we pass an object as the instance to the form. For UserPortfolioPermissionChoices, we add a portfolio and an email to the POST data. @@ -510,34 +526,80 @@ class TestBasePortfolioMemberForms(TestCase): data = { "role": UserPortfolioRoleChoices.ORGANIZATION_ADMIN.value, - "domain_request_permission_admin": UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value, - "member_permission_admin": UserPortfolioPermissionChoices.EDIT_MEMBERS.value, } # Check form validity for all forms form = self._assert_form_is_valid(PortfolioMemberForm, data, user_portfolio_permission) cleaned_data = form.cleaned_data self.assertEqual(cleaned_data["roles"], [UserPortfolioRoleChoices.ORGANIZATION_ADMIN.value]) - self.assertEqual(cleaned_data["additional_permissions"], [UserPortfolioPermissionChoices.EDIT_MEMBERS]) form = self._assert_form_is_valid(PortfolioInvitedMemberForm, data, portfolio_invitation) cleaned_data = form.cleaned_data self.assertEqual(cleaned_data["roles"], [UserPortfolioRoleChoices.ORGANIZATION_ADMIN.value]) - self.assertEqual(cleaned_data["additional_permissions"], [UserPortfolioPermissionChoices.EDIT_MEMBERS]) data = { "email": "hi@ho.com", "portfolio": self.portfolio.id, "role": UserPortfolioRoleChoices.ORGANIZATION_ADMIN.value, - "domain_request_permission_admin": UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value, - "member_permission_admin": UserPortfolioPermissionChoices.EDIT_MEMBERS.value, } form = self._assert_form_is_valid(PortfolioNewMemberForm, data) cleaned_data = form.cleaned_data self.assertEqual(cleaned_data["roles"], [UserPortfolioRoleChoices.ORGANIZATION_ADMIN.value]) - self.assertEqual(cleaned_data["additional_permissions"], [UserPortfolioPermissionChoices.EDIT_MEMBERS]) + @less_console_noise_decorator + def test_clean_validates_required_fields_for_basic_role(self): + """Test that the `clean` method validates the correct fields for basic role. + + For PortfolioMemberForm and PortfolioInvitedMemberForm, we pass an object as the instance to the form. + For UserPortfolioPermissionChoices, we add a portfolio and an email to the POST data. + + These things are handled in the views.""" + + user_portfolio_permission, _ = UserPortfolioPermission.objects.get_or_create( + portfolio=self.portfolio, user=self.user + ) + portfolio_invitation, _ = PortfolioInvitation.objects.get_or_create(portfolio=self.portfolio, email="hi@ho") + + data = { + "role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER.value, + "domain_request_permissions": UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value, + "domain_permissions": UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS.value, + "member_permissions": UserPortfolioPermissionChoices.VIEW_MEMBERS.value, + } + + # Check form validity for all forms + form = self._assert_form_is_valid(PortfolioMemberForm, data, user_portfolio_permission) + cleaned_data = form.cleaned_data + self.assertEqual(cleaned_data["roles"], [UserPortfolioRoleChoices.ORGANIZATION_MEMBER.value]) + self.assertEqual(cleaned_data["domain_request_permissions"], UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value) + self.assertEqual(cleaned_data["domain_permissions"], UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS.value) + self.assertEqual(cleaned_data["member_permissions"], UserPortfolioPermissionChoices.VIEW_MEMBERS.value) + + form = self._assert_form_is_valid(PortfolioInvitedMemberForm, data, portfolio_invitation) + cleaned_data = form.cleaned_data + self.assertEqual(cleaned_data["roles"], [UserPortfolioRoleChoices.ORGANIZATION_MEMBER.value]) + self.assertEqual(cleaned_data["domain_request_permissions"], UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value) + self.assertEqual(cleaned_data["domain_permissions"], UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS.value) + self.assertEqual(cleaned_data["member_permissions"], UserPortfolioPermissionChoices.VIEW_MEMBERS.value) + + data = { + "email": "hi@ho.com", + "portfolio": self.portfolio.id, + "role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER.value, + "domain_request_permissions": UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value, + "domain_permissions": UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS.value, + "member_permissions": UserPortfolioPermissionChoices.VIEW_MEMBERS.value, + } + + form = self._assert_form_is_valid(PortfolioNewMemberForm, data) + cleaned_data = form.cleaned_data + self.assertEqual(cleaned_data["roles"], [UserPortfolioRoleChoices.ORGANIZATION_MEMBER.value]) + self.assertEqual(cleaned_data["domain_request_permissions"], UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value) + self.assertEqual(cleaned_data["domain_permissions"], UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS.value) + self.assertEqual(cleaned_data["member_permissions"], UserPortfolioPermissionChoices.VIEW_MEMBERS.value) + + @less_console_noise_decorator def test_clean_member_permission_edgecase(self): """Test that the clean method correctly handles the special "no_access" value for members. We'll need to add a portfolio, which in the app is handled by the view post.""" @@ -549,38 +611,38 @@ class TestBasePortfolioMemberForms(TestCase): data = { "role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER.value, - "domain_request_permission_member": "no_access", # Simulate no access permission + "domain_request_permissions": "no_access", # Simulate no access permission + "domain_permissions": UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS.value, + "member_permissions": UserPortfolioPermissionChoices.VIEW_MEMBERS.value, } form = self._assert_form_is_valid(PortfolioMemberForm, data, user_portfolio_permission) cleaned_data = form.cleaned_data - self.assertEqual(cleaned_data["domain_request_permission_member"], None) + self.assertEqual(cleaned_data["domain_request_permissions"], None) form = self._assert_form_is_valid(PortfolioInvitedMemberForm, data, portfolio_invitation) cleaned_data = form.cleaned_data - self.assertEqual(cleaned_data["domain_request_permission_member"], None) + self.assertEqual(cleaned_data["domain_request_permissions"], None) + @less_console_noise_decorator def test_map_instance_to_initial_admin_role(self): """Test that instance data is correctly mapped to the initial form values for an admin role.""" user_portfolio_permission = UserPortfolioPermission( roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN], - additional_permissions=[UserPortfolioPermissionChoices.VIEW_MEMBERS], ) portfolio_invitation, _ = PortfolioInvitation.objects.get_or_create( portfolio=self.portfolio, email="hi@ho", roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN], - additional_permissions=[UserPortfolioPermissionChoices.VIEW_MEMBERS], ) expected_initial_data = { "role": UserPortfolioRoleChoices.ORGANIZATION_ADMIN, - "domain_request_permission_admin": UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS, - "member_permission_admin": UserPortfolioPermissionChoices.VIEW_MEMBERS, } self._assert_initial_data(PortfolioMemberForm, user_portfolio_permission, expected_initial_data) self._assert_initial_data(PortfolioInvitedMemberForm, portfolio_invitation, expected_initial_data) + @less_console_noise_decorator def test_map_instance_to_initial_member_role(self): """Test that instance data is correctly mapped to the initial form values for a member role.""" user_portfolio_permission = UserPortfolioPermission( @@ -595,19 +657,21 @@ class TestBasePortfolioMemberForms(TestCase): ) expected_initial_data = { "role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER, - "domain_request_permission_member": UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS, + "domain_request_permissions": UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS, } self._assert_initial_data(PortfolioMemberForm, user_portfolio_permission, expected_initial_data) self._assert_initial_data(PortfolioInvitedMemberForm, portfolio_invitation, expected_initial_data) - def test_invalid_data_for_admin(self): - """Test invalid form submission for an admin role with missing permissions.""" + @less_console_noise_decorator + def test_invalid_data_for_member(self): + """Test invalid form submission for a member role with missing permissions.""" data = { "email": "hi@ho.com", "portfolio": self.portfolio.id, - "role": UserPortfolioRoleChoices.ORGANIZATION_ADMIN.value, - "domain_request_permission_admin": "", # Missing field - "member_permission_admin": "", # Missing field + "role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER.value, + "domain_request_permissions": "", # Missing field + "member_permissions": "", # Missing field + "domain_permissions": "", # Missing field } - self._assert_form_has_error(PortfolioMemberForm, data, "domain_request_permission_admin") - self._assert_form_has_error(PortfolioInvitedMemberForm, data, "member_permission_admin") + self._assert_form_has_error(PortfolioMemberForm, data, "domain_request_permissions") + self._assert_form_has_error(PortfolioInvitedMemberForm, data, "member_permissions") From 6856da38f129b0d86bbce148b5ab0c6f3a4e82cc Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Fri, 24 Jan 2025 15:57:36 -0500 Subject: [PATCH 14/40] fix broken tests --- src/registrar/tests/test_views_portfolio.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/registrar/tests/test_views_portfolio.py b/src/registrar/tests/test_views_portfolio.py index 87b0d9308..0300610a7 100644 --- a/src/registrar/tests/test_views_portfolio.py +++ b/src/registrar/tests/test_views_portfolio.py @@ -915,9 +915,9 @@ class TestPortfolio(WebTest): # Assert text within the page is correct self.assertContains(response, "First Last") self.assertContains(response, self.user.email) - self.assertContains(response, "Basic access") + self.assertContains(response, "Basic") self.assertContains(response, "No access") - self.assertContains(response, "View all members") + self.assertContains(response, "Viewer") self.assertContains(response, "This member does not manage any domains.") # Assert buttons and links within the page are correct @@ -933,15 +933,11 @@ class TestPortfolio(WebTest): """Test that user can access the member page with edit_members permission""" # Arrange - # give user permissions to view AND manage members + # give user admin role, which includes edit_members permission_obj, _ = UserPortfolioPermission.objects.get_or_create( user=self.user, portfolio=self.portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN], - additional_permissions=[ - UserPortfolioPermissionChoices.EDIT_REQUESTS, - UserPortfolioPermissionChoices.EDIT_MEMBERS, - ], ) # Verify the page can be accessed @@ -952,9 +948,9 @@ class TestPortfolio(WebTest): # Assert text within the page is correct self.assertContains(response, "First Last") self.assertContains(response, self.user.email) - self.assertContains(response, "Admin access") - self.assertContains(response, "View all requests plus create requests") - self.assertContains(response, "View all members plus manage members") + self.assertContains(response, "Admin") + self.assertContains(response, "Creator") + self.assertContains(response, "Manager") self.assertContains( response, 'This member does not manage any domains. To assign this member a domain, click "Manage"' ) @@ -1028,9 +1024,9 @@ class TestPortfolio(WebTest): # Assert text within the page is correct self.assertContains(response, "Invited") self.assertContains(response, portfolio_invitation.email) - self.assertContains(response, "Basic access") + self.assertContains(response, "Basic") self.assertContains(response, "No access") - self.assertContains(response, "View all members") + self.assertContains(response, "Viewer") self.assertContains(response, "This member does not manage any domains.") # Assert buttons and links within the page are correct From bfb4a52b6014ffe58befba3d4ca9c33f4739df57 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Fri, 24 Jan 2025 16:29:58 -0500 Subject: [PATCH 15/40] ui done --- .../assets/src/sass/_theme/_tables.scss | 14 ++++++++++-- .../includes/member_basic_permissions.html | 6 ++--- .../includes/member_permissions_matrix.html | 22 +++++++++---------- 3 files changed, 26 insertions(+), 16 deletions(-) diff --git a/src/registrar/assets/src/sass/_theme/_tables.scss b/src/registrar/assets/src/sass/_theme/_tables.scss index 3c8f15d70..8322a2fde 100644 --- a/src/registrar/assets/src/sass/_theme/_tables.scss +++ b/src/registrar/assets/src/sass/_theme/_tables.scss @@ -100,14 +100,24 @@ th { } } -.dotgov-table--padding-left { +.dotgov-table--padding-2 { td, th { - padding: units(2) units(4) units(2) units(2); + padding: units(2); } } +.usa-table--striped tbody tr:nth-child(odd) th, +.usa-table--striped tbody tr:nth-child(odd) td { + background-color: color('primary-lightest'); +} + .usa-table--bg-transparent { td, thead th { background-color: transparent; } } + +.usa-table--full-borderless td, +.usa-table--full-borderless th { + border: none !important; +} diff --git a/src/registrar/templates/includes/member_basic_permissions.html b/src/registrar/templates/includes/member_basic_permissions.html index ebb4d0508..c84f3de09 100644 --- a/src/registrar/templates/includes/member_basic_permissions.html +++ b/src/registrar/templates/includes/member_basic_permissions.html @@ -4,17 +4,17 @@

Configure the permissions for this member. Basic members cannot manage member permissions or organization metadata.

Domains *

- {% with group_classes="usa-form-editable bg-gray-1 border-top-0 margin-top-0 border-bottom padding-top-0" add_legend_class="usa-sr-only" %} + {% with group_classes="bg-gray-1 border-bottom-2px border-base-lighter padding-bottom-2 margin-top-0" add_legend_class="usa-sr-only" %} {% input_with_errors form.domain_permissions %} {% endwith %}

Domain requests *

- {% with group_classes="usa-form-editable usa-form-editable--no-border bg-gray-1 padding-top-0" add_legend_class="usa-sr-only" %} + {% with group_classes="bg-gray-1 border-bottom-2px border-base-lighter padding-bottom-2 margin-top-0" add_legend_class="usa-sr-only" %} {% input_with_errors form.domain_request_permissions %} {% endwith %}

Members *

- {% with group_classes="usa-form-editable usa-form-editable--no-border bg-gray-1 padding-top-0" add_legend_class="usa-sr-only" %} + {% with group_classes="bg-gray-1 border-bottom-2px border-base-lighter padding-bottom-2 margin-top-0" add_legend_class="usa-sr-only" %} {% input_with_errors form.member_permissions %} {% endwith %}
diff --git a/src/registrar/templates/includes/member_permissions_matrix.html b/src/registrar/templates/includes/member_permissions_matrix.html index 9b3c20ac5..f4b207472 100644 --- a/src/registrar/templates/includes/member_permissions_matrix.html +++ b/src/registrar/templates/includes/member_permissions_matrix.html @@ -20,21 +20,21 @@ -
- +
+
- + - - + - + - + - + - + - +
Member actions availableMember actions available Admin Basic
+ View domains they manage
View all domains for the organizationView all domains for the organization
View all domain requestsView all domain requests
Create domain requestsCreate domain requests
View all member permissionsView all member permissions
Manage member permissionsManage member permissions
Manage organization metadata (address)Manage organization metadata (address)

description beneath each role option - # self.fields["role"].descriptions = { - # "organization_admin": UserPortfolioRoleChoices.get_role_description( - # UserPortfolioRoleChoices.ORGANIZATION_ADMIN - # ), - # "organization_member": UserPortfolioRoleChoices.get_role_description( - # UserPortfolioRoleChoices.ORGANIZATION_MEMBER - # ), - # } - + # Adds a

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", From 6d9b0119c24be9b733cf574aa949b2fd911eef28 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Fri, 24 Jan 2025 16:47:54 -0500 Subject: [PATCH 17/40] ui tweaks: border top on table --- .../templates/includes/member_permissions_matrix.html | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/registrar/templates/includes/member_permissions_matrix.html b/src/registrar/templates/includes/member_permissions_matrix.html index f4b207472..24a0ac614 100644 --- a/src/registrar/templates/includes/member_permissions_matrix.html +++ b/src/registrar/templates/includes/member_permissions_matrix.html @@ -21,7 +21,7 @@

- +
@@ -62,7 +62,7 @@ - @@ -73,7 +73,7 @@ - @@ -84,7 +84,7 @@ - @@ -95,7 +95,7 @@ - From 535a41c13e3f830caa6fb7e18c50c96b6882bd68 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Fri, 24 Jan 2025 17:14:18 -0500 Subject: [PATCH 18/40] lint and test --- src/registrar/forms/portfolio.py | 25 ++++++++++++--------- src/registrar/tests/test_forms.py | 12 +++++++--- src/registrar/tests/test_reports.py | 4 ++-- src/registrar/tests/test_views_portfolio.py | 12 ++++------ 4 files changed, 30 insertions(+), 23 deletions(-) diff --git a/src/registrar/forms/portfolio.py b/src/registrar/forms/portfolio.py index a8853d339..c9ef280b0 100644 --- a/src/registrar/forms/portfolio.py +++ b/src/registrar/forms/portfolio.py @@ -4,7 +4,6 @@ import logging from django import forms from django.core.validators import RegexValidator from django.core.validators import MaxLengthValidator -from django.utils.safestring import mark_safe from registrar.forms.utility.combobox import ComboboxWidget from registrar.models import ( @@ -143,7 +142,6 @@ class BasePortfolioMemberForm(forms.ModelForm): ("no_access", "No access"), (UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value, "Viewer"), (UserPortfolioPermissionChoices.EDIT_REQUESTS.value, "Creator"), - ], widget=forms.RadioSelect, required=False, @@ -166,7 +164,6 @@ class BasePortfolioMemberForm(forms.ModelForm): }, ) - # Tracks what form elements are required for a given role choice. # All of the fields included here have "required=False" by default as they are conditionally required. # see def clean() for more details. @@ -191,20 +188,22 @@ class BasePortfolioMemberForm(forms.ModelForm): Update field descriptions. """ super().__init__(*args, **kwargs) - + # Adds a

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", } self.fields["domain_request_permissions"].descriptions = { - UserPortfolioPermissionChoices.EDIT_REQUESTS.value: "Can view all domain requests for the organization and create requests", + UserPortfolioPermissionChoices.EDIT_REQUESTS.value: ( + "Can view all domain requests for the organization and create requests" + ), UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value: "Can view all domain requests for the organization", - "no_access": "Cannot view or create domain requests" + "no_access": "Cannot view or create domain requests", } self.fields["member_permissions"].descriptions = { UserPortfolioPermissionChoices.VIEW_MEMBERS.value: "Can view all members permissions", - "no_access": "Cannot view member permissions" + "no_access": "Cannot view member permissions", } # Map model instance values to custom form fields @@ -299,9 +298,15 @@ class BasePortfolioMemberForm(forms.ModelForm): self.initial["role"] = selected_role is_member = selected_role == UserPortfolioRoleChoices.ORGANIZATION_MEMBER if is_member: - # Edgecase: Member and domain request use a special form value for None called "no_access". This ensures a form selection. - selected_domain_permission = next((perm for perm in domain_perms if perm in perms), UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS.value) - selected_domain_request_permission = next((perm for perm in domain_request_perms if perm in perms), "no_access") + # Edgecase: Member and domain request use a special form value for None called "no_access". + # This ensures a form selection. + selected_domain_permission = next( + (perm for perm in domain_perms if perm in perms), + UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS.value, + ) + selected_domain_request_permission = next( + (perm for perm in domain_request_perms if perm in perms), "no_access" + ) selected_member_permission = next((perm for perm in member_perms if perm in perms), "no_access") self.initial["domain_request_permissions"] = selected_domain_request_permission self.initial["domain_permissions"] = selected_domain_permission diff --git a/src/registrar/tests/test_forms.py b/src/registrar/tests/test_forms.py index 4279ca068..82e3b40bb 100644 --- a/src/registrar/tests/test_forms.py +++ b/src/registrar/tests/test_forms.py @@ -572,14 +572,18 @@ class TestBasePortfolioMemberForms(TestCase): form = self._assert_form_is_valid(PortfolioMemberForm, data, user_portfolio_permission) cleaned_data = form.cleaned_data self.assertEqual(cleaned_data["roles"], [UserPortfolioRoleChoices.ORGANIZATION_MEMBER.value]) - self.assertEqual(cleaned_data["domain_request_permissions"], UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value) + self.assertEqual( + cleaned_data["domain_request_permissions"], UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value + ) self.assertEqual(cleaned_data["domain_permissions"], UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS.value) self.assertEqual(cleaned_data["member_permissions"], UserPortfolioPermissionChoices.VIEW_MEMBERS.value) form = self._assert_form_is_valid(PortfolioInvitedMemberForm, data, portfolio_invitation) cleaned_data = form.cleaned_data self.assertEqual(cleaned_data["roles"], [UserPortfolioRoleChoices.ORGANIZATION_MEMBER.value]) - self.assertEqual(cleaned_data["domain_request_permissions"], UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value) + self.assertEqual( + cleaned_data["domain_request_permissions"], UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value + ) self.assertEqual(cleaned_data["domain_permissions"], UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS.value) self.assertEqual(cleaned_data["member_permissions"], UserPortfolioPermissionChoices.VIEW_MEMBERS.value) @@ -595,7 +599,9 @@ class TestBasePortfolioMemberForms(TestCase): form = self._assert_form_is_valid(PortfolioNewMemberForm, data) cleaned_data = form.cleaned_data self.assertEqual(cleaned_data["roles"], [UserPortfolioRoleChoices.ORGANIZATION_MEMBER.value]) - self.assertEqual(cleaned_data["domain_request_permissions"], UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value) + self.assertEqual( + cleaned_data["domain_request_permissions"], UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value + ) self.assertEqual(cleaned_data["domain_permissions"], UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS.value) self.assertEqual(cleaned_data["member_permissions"], UserPortfolioPermissionChoices.VIEW_MEMBERS.value) diff --git a/src/registrar/tests/test_reports.py b/src/registrar/tests/test_reports.py index 9d410e430..8e558d887 100644 --- a/src/registrar/tests/test_reports.py +++ b/src/registrar/tests/test_reports.py @@ -885,7 +885,7 @@ class MemberExportTest(MockDbForIndividualTests, MockEppLib): "big_lebowski@dude.co,False,help@get.gov,2022-04-01,Invalid date,None," "Viewer,True,1,cdomain1.gov\n" "cozy_staffuser@igorville.gov,True,help@get.gov,2022-04-01,2024-02-01," - "Viewer,Viewer,False,0,\n" + "Viewer Requester,Manager,False,0,\n" "icy_superuser@igorville.gov,True,help@get.gov,2022-04-01,2024-02-01," "Viewer Requester,Manager,False,0,\n" "meoward@rocks.com,False,big_lebowski@dude.co,2022-04-01,Invalid date,None," @@ -899,7 +899,7 @@ class MemberExportTest(MockDbForIndividualTests, MockEppLib): "nonexistentmember_4@igorville.gov,True,help@get.gov,Unretrieved,Invited," "Viewer Requester,Manager,False,0,\n" "nonexistentmember_5@igorville.gov,True,help@get.gov,Unretrieved,Invited," - "Viewer,Viewer,False,0,\n" + "Viewer Requester,Manager,False,0,\n" "tired_sleepy@igorville.gov,False,System,2022-04-01,Invalid date,Viewer," "None,False,0,\n" ) diff --git a/src/registrar/tests/test_views_portfolio.py b/src/registrar/tests/test_views_portfolio.py index 3ba228228..e6af20771 100644 --- a/src/registrar/tests/test_views_portfolio.py +++ b/src/registrar/tests/test_views_portfolio.py @@ -1422,7 +1422,7 @@ class TestPortfolio(WebTest): permission = UserPortfolioPermission.objects.get(user=self.user, portfolio=self.portfolio) # Update to basic member with view members permission - permission.roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER] + permission.roles = [UserPortfolioRoleChoices.ORGANIZATION_MEMBER] permission.additional_permissions = [ UserPortfolioPermissionChoices.VIEW_MEMBERS, ] @@ -3564,14 +3564,10 @@ class TestEditPortfolioMemberView(WebTest): self.assertEqual(response.status_code, 200) self.assertEqual( response.context["form"].errors["domain_request_permissions"][0], - "Domain request permission is required", - ) - self.assertEqual( - response.context["form"].errors["member_permissions"][0], "Member permission is required" - ) - self.assertEqual( - response.context["form"].errors["domain_permissions"][0], "Domain permission is required" + "Domain request permission is required.", ) + self.assertEqual(response.context["form"].errors["member_permissions"][0], "Member permission is required.") + self.assertEqual(response.context["form"].errors["domain_permissions"][0], "Domain permission is required.") @less_console_noise_decorator @override_flag("organization_feature", active=True) From 06f9abae6b44600f7399149180f81da6ccdd8951 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Mon, 27 Jan 2025 08:39:25 -0500 Subject: [PATCH 19/40] added VIEW_SUBORGANIZATION to member perms --- src/registrar/models/user_portfolio_permission.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/registrar/models/user_portfolio_permission.py b/src/registrar/models/user_portfolio_permission.py index 8593c3ac9..c4be90a9b 100644 --- a/src/registrar/models/user_portfolio_permission.py +++ b/src/registrar/models/user_portfolio_permission.py @@ -32,6 +32,7 @@ class UserPortfolioPermission(TimeStampedModel): # NOTE: Check FORBIDDEN_PORTFOLIO_ROLE_PERMISSIONS before adding roles here. UserPortfolioRoleChoices.ORGANIZATION_MEMBER: [ UserPortfolioPermissionChoices.VIEW_PORTFOLIO, + UserPortfolioPermissionChoices.VIEW_SUBORGANIZATION, ], } From d36ba58f77da7803b62e4d28ea8a5ac67f935b35 Mon Sep 17 00:00:00 2001 From: asaki222 Date: Mon, 27 Jan 2025 10:19:29 -0500 Subject: [PATCH 20/40] script thus far --- .../reset-db-without-sb-takedown.yaml | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 .github/workflows/reset-db-without-sb-takedown.yaml diff --git a/.github/workflows/reset-db-without-sb-takedown.yaml b/.github/workflows/reset-db-without-sb-takedown.yaml new file mode 100644 index 000000000..53e371cdb --- /dev/null +++ b/.github/workflows/reset-db-without-sb-takedown.yaml @@ -0,0 +1,88 @@ +# This workflow can be run from the CLI +# gh workflow run reset-db.yaml -f environment=ENVIRONMENT +# OR +# cf run-task getgov-ENVIRONMENT --command 'python manage.py flush' --name flush +# cf run-task getgov-ENVIRONMENT --command 'python manage.py load' --name loaddata + +name: Reset database +run-name: Reset database for ${{ github.event.inputs.environment }} without taking down the sandbox + +on: + workflow_dispatch: + inputs: + environment: + type: choice + description: Which environment should we flush and re-load data for? + options: + - staging + - development + - el + - ad + - ms + - ag + - litterbox + - hotgov + - cb + - bob + - meoward + - backup + - ky + - es + - nl + - rh + - za + - gd + - rb + - ko + - ab + - rjm + - dk + +jobs: + reset-db: + runs-on: ubuntu-latest + env: + CF_USERNAME: CF_${{ github.event.inputs.environment }}_USERNAME + CF_PASSWORD: CF_${{ github.event.inputs.environment }}_PASSWORD + steps: + - name: Unbind the database service + uses: cloud-gov/cg-cli-tools@main + with: + cf_username: ${{ secrets[env.CF_USERNAME] }} + cf_password: ${{ secrets[env.CF_PASSWORD] }} + cf_org: cisa-dotgov + cf_space: ${{ github.event.inputs.environment }} + cf_command: "cf unbind-service getgov-${{ github.event.inputs.environment }} getgov-${{ github.event.inputs.environment }}-database" + - name: Delete the service + uses: cloud-gov/cg-cli-tools@main + with: + cf_username: ${{ secrets[env.CF_USERNAME] }} + cf_password: ${{ secrets[env.CF_PASSWORD] }} + cf_org: cisa-dotgov + cf_space: ${{ github.event.inputs.environment }} + cf_command: "cf unbind-service getgov-${{ github.event.inputs.environment }} getgov-${{ github.event.inputs.environment }}-database" + -name: Recreate the service + uses: cloud-gov/cg-cli-tools@main + with: + cf_username: ${{ secrets[env.CF_USERNAME] }} + cf_password: ${{ secrets[env.CF_PASSWORD] }} + cf_org: cisa-dotgov + cf_space: ${{ github.event.inputs.environment }} + cf_command: "cf create-service aws-rds micro-psql getgov-${{ github.event.inputs.environment }} getgov-${{ github.event.inputs.environment }}-database" + -name: Recreate the service + uses: cloud-gov/cg-cli-tools@main + with: + cf_username: ${{ secrets[env.CF_USERNAME] }} + cf_password: ${{ secrets[env.CF_PASSWORD] }} + cf_org: cisa-dotgov + cf_space: ${{ github.event.inputs.environment }} + cf_command: "cf create-service aws-rds micro-psql getgov-${{ github.event.inputs.environment }} getgov-${{ github.event.inputs.environment }}-database" + -name: Rebind the service + uses: cloud-gov/cg-cli-tools@main + with: + cf_username: ${{ secrets[env.CF_USERNAME] }} + cf_password: ${{ secrets[env.CF_PASSWORD] }} + cf_org: cisa-dotgov + cf_space: ${{ github.event.inputs.environment }} + cf_command: "cf bind-service getgov-env getgov-${{ github.event.inputs.environment }}" + \ No newline at end of file From 78afb9524e7e5b306c16e84f16d0a469ed9041b1 Mon Sep 17 00:00:00 2001 From: asaki222 Date: Mon, 27 Jan 2025 10:40:13 -0500 Subject: [PATCH 21/40] removed the .gov, and converted variables to snake case --- src/registrar/tests/test_views_domain.py | 52 ++++++++++++------------ 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/src/registrar/tests/test_views_domain.py b/src/registrar/tests/test_views_domain.py index 58edea32d..76e2a5402 100644 --- a/src/registrar/tests/test_views_domain.py +++ b/src/registrar/tests/test_views_domain.py @@ -439,21 +439,21 @@ class TestDomainDetailDomainRenewal(TestDomainOverview): username="usertest", ) - self.domaintorenew, _ = Domain.objects.get_or_create( + self.domain_to_renew, _ = Domain.objects.get_or_create( name="domainrenewal.gov", ) - self.domainnotexpiring, _ = Domain.objects.get_or_create( + self.domain_not_expiring, _ = Domain.objects.get_or_create( name="domainnotexpiring.gov", expiration_date=timezone.now().date() + timedelta(days=65) ) - self.domainnodomainmanager, _ = Domain.objects.get_or_create(name="domainnodomainmanager") + self.domain_no_domain_manager, _ = Domain.objects.get_or_create(name="domainnodomainmanager.gov") UserDomainRole.objects.get_or_create( - user=self.user, domain=self.domaintorenew, role=UserDomainRole.Roles.MANAGER + user=self.user, domain=self.domain_to_renew, role=UserDomainRole.Roles.MANAGER ) - DomainInformation.objects.get_or_create(creator=self.user, domain=self.domaintorenew) + DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_to_renew) self.portfolio, _ = Portfolio.objects.get_or_create(organization_name="Test org", creator=self.user) @@ -483,9 +483,9 @@ class TestDomainDetailDomainRenewal(TestDomainOverview): with patch.object(Domain, "is_expiring", self.custom_is_expiring), patch.object( Domain, "is_expired", self.custom_is_expired_false ): - self.assertEquals(self.domaintorenew.state, Domain.State.UNKNOWN) + self.assertEquals(self.domain_to_renew.state, Domain.State.UNKNOWN) detail_page = self.client.get( - reverse("domain", kwargs={"pk": self.domaintorenew.id}), + reverse("domain", kwargs={"pk": self.domain_to_renew.id}), ) self.assertContains(detail_page, "Expiring soon") @@ -516,9 +516,9 @@ class TestDomainDetailDomainRenewal(TestDomainOverview): UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS, ], ) - domaintorenew2, _ = Domain.objects.get_or_create(name="bogusdomain2.gov") + domain_to_renew2, _ = Domain.objects.get_or_create(name="bogusdomain2.gov") DomainInformation.objects.get_or_create( - creator=non_dom_manage_user, domain=domaintorenew2, portfolio=self.portfolio + creator=non_dom_manage_user, domain=domain_to_renew2, portfolio=self.portfolio ) non_dom_manage_user.refresh_from_db() self.client.force_login(non_dom_manage_user) @@ -526,7 +526,7 @@ class TestDomainDetailDomainRenewal(TestDomainOverview): Domain, "is_expired", self.custom_is_expired_false ): detail_page = self.client.get( - reverse("domain", kwargs={"pk": domaintorenew2.id}), + reverse("domain", kwargs={"pk": domain_to_renew2.id}), ) self.assertContains(detail_page, "Contact one of the listed domain managers to renew the domain.") @@ -535,17 +535,17 @@ class TestDomainDetailDomainRenewal(TestDomainOverview): def test_expiring_domain_on_detail_page_in_org_model_as_a_domain_manager(self): portfolio, _ = Portfolio.objects.get_or_create(organization_name="Test org2", creator=self.user) - domaintorenew3, _ = Domain.objects.get_or_create(name="bogusdomain3.gov") + domain_to_renew3, _ = Domain.objects.get_or_create(name="bogusdomain3.gov") - UserDomainRole.objects.get_or_create(user=self.user, domain=domaintorenew3, role=UserDomainRole.Roles.MANAGER) - DomainInformation.objects.get_or_create(creator=self.user, domain=domaintorenew3, portfolio=portfolio) + UserDomainRole.objects.get_or_create(user=self.user, domain=domain_to_renew3, role=UserDomainRole.Roles.MANAGER) + DomainInformation.objects.get_or_create(creator=self.user, domain=domain_to_renew3, portfolio=portfolio) self.user.refresh_from_db() self.client.force_login(self.user) with patch.object(Domain, "is_expiring", self.custom_is_expiring), patch.object( Domain, "is_expired", self.custom_is_expired_false ): detail_page = self.client.get( - reverse("domain", kwargs={"pk": domaintorenew3.id}), + reverse("domain", kwargs={"pk": domain_to_renew3.id}), ) self.assertContains(detail_page, "Renew to maintain access") @@ -557,7 +557,7 @@ class TestDomainDetailDomainRenewal(TestDomainOverview): ): # Grab the detail page detail_page = self.client.get( - reverse("domain", kwargs={"pk": self.domaintorenew.id}), + reverse("domain", kwargs={"pk": self.domain_to_renew.id}), ) # Make sure we see the link as a domain manager @@ -567,14 +567,14 @@ class TestDomainDetailDomainRenewal(TestDomainOverview): self.assertContains(detail_page, "Renewal form") # Grab link to the renewal page - renewal_form_url = reverse("domain-renewal", kwargs={"pk": self.domaintorenew.id}) + renewal_form_url = reverse("domain-renewal", kwargs={"pk": self.domain_to_renew.id}) self.assertContains(detail_page, f'href="{renewal_form_url}"') # Simulate clicking the link response = self.client.get(renewal_form_url) self.assertEqual(response.status_code, 200) - self.assertContains(response, f"Renew {self.domaintorenew.name}") + self.assertContains(response, f"Renew {self.domain_to_renew.name}") @override_flag("domain_renewal", active=True) def test_domain_renewal_form_and_sidebar_expired(self): @@ -586,7 +586,7 @@ class TestDomainDetailDomainRenewal(TestDomainOverview): ): # Grab the detail page detail_page = self.client.get( - reverse("domain", kwargs={"pk": self.domaintorenew.id}), + reverse("domain", kwargs={"pk": self.domain_to_renew.id}), ) # Make sure we see the link as a domain manager @@ -596,14 +596,14 @@ class TestDomainDetailDomainRenewal(TestDomainOverview): self.assertContains(detail_page, "Renewal form") # Grab link to the renewal page - renewal_form_url = reverse("domain-renewal", kwargs={"pk": self.domaintorenew.id}) + renewal_form_url = reverse("domain-renewal", kwargs={"pk": self.domain_to_renew.id}) self.assertContains(detail_page, f'href="{renewal_form_url}"') # Simulate clicking the link response = self.client.get(renewal_form_url) self.assertEqual(response.status_code, 200) - self.assertContains(response, f"Renew {self.domaintorenew.name}") + self.assertContains(response, f"Renew {self.domain_to_renew.name}") @override_flag("domain_renewal", active=True) def test_domain_renewal_form_your_contact_info_edit(self): @@ -666,7 +666,7 @@ class TestDomainDetailDomainRenewal(TestDomainOverview): def test_domain_renewal_form_not_expired_or_expiring(self): with less_console_noise(): # Start on the Renewal page for the domain - renewal_page = self.client.get(reverse("domain-renewal", kwargs={"pk": self.domainnotexpiring.id})) + renewal_page = self.client.get(reverse("domain-renewal", kwargs={"pk": self.domain_not_expiring.id})) self.assertEqual(renewal_page.status_code, 403) @override_flag("domain_renewal", active=True) @@ -674,7 +674,7 @@ class TestDomainDetailDomainRenewal(TestDomainOverview): with patch.object(Domain, "is_expired", self.custom_is_expired_true), patch.object( Domain, "is_expired", self.custom_is_expired_true ): - renewal_page = self.client.get(reverse("domain-renewal", kwargs={"pk": self.domainnodomainmanager.id})) + renewal_page = self.client.get(reverse("domain-renewal", kwargs={"pk": self.domain_no_domain_manager.id})) self.assertEqual(renewal_page.status_code, 403) @override_flag("domain_renewal", active=True) @@ -2825,11 +2825,11 @@ class TestDomainRenewal(TestWithUser): name="igorville.gov", expiration_date=expiring_date ) self.domain_with_expired_date, _ = Domain.objects.get_or_create( - name="domainwithexpireddate.com", expiration_date=expired_date + name="domainwithexpireddate.gov", expiration_date=expired_date ) self.domain_with_current_date, _ = Domain.objects.get_or_create( - name="domainwithfarexpireddate.com", expiration_date=expiring_date_current + name="domainwithfarexpireddate.gov", expiration_date=expiring_date_current ) UserDomainRole.objects.get_or_create( @@ -2875,7 +2875,7 @@ class TestDomainRenewal(TestWithUser): today = datetime.now() expiring_date = (today + timedelta(days=30)).strftime("%Y-%m-%d") self.domain_with_another_expiring, _ = Domain.objects.get_or_create( - name="domainwithanotherexpiringdate.com", expiration_date=expiring_date + name="domainwithanotherexpiringdate.gov", expiration_date=expiring_date ) UserDomainRole.objects.get_or_create( @@ -2911,7 +2911,7 @@ class TestDomainRenewal(TestWithUser): today = datetime.now() expiring_date = (today + timedelta(days=31)).strftime("%Y-%m-%d") self.domain_with_another_expiring_org_model, _ = Domain.objects.get_or_create( - name="domainwithanotherexpiringdate_orgmodel.com", expiration_date=expiring_date + name="domainwithanotherexpiringdate_orgmodel.gov", expiration_date=expiring_date ) UserDomainRole.objects.get_or_create( From 3dab617ff1b869f96b142c5ce52ec290d4cc5c16 Mon Sep 17 00:00:00 2001 From: asaki222 Date: Mon, 27 Jan 2025 12:15:14 -0500 Subject: [PATCH 22/40] updated changes --- src/registrar/views/domain.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index d0dd62210..b9f6675ad 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -317,7 +317,8 @@ class DomainRenewalView(DomainBaseView): template_name = "domain_renewal.html" def get_context_data(self, **kwargs): - """Grabbing security email information for the renewal form""" + """Grabs the security email information and adds security_email to the renewal form context + sets it to None if it uses a default email""" context = super().get_context_data(**kwargs) @@ -334,16 +335,13 @@ class DomainRenewalView(DomainBaseView): def in_editable_state(self, pk): """Override in_editable_state from DomainPermission - Allow renewal form to be accessed""" - + Allow renewal form to be accessed + returns boolean""" requested_domain = None if Domain.objects.filter(id=pk).exists(): requested_domain = Domain.objects.get(id=pk) - if requested_domain and (requested_domain.is_expiring() or requested_domain.is_expired()): - return True - - return False + return requested_domain and requested_domain.is_editable() and (requested_domain.is_expiring() or requested_domain.is_expired()) def post(self, request, pk): From 33c4f6497f667a5411578a423a5be329769b8361 Mon Sep 17 00:00:00 2001 From: asaki222 Date: Mon, 27 Jan 2025 13:00:16 -0500 Subject: [PATCH 23/40] ran linter --- src/registrar/views/domain.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index b9f6675ad..2d7d88cd6 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -341,7 +341,11 @@ class DomainRenewalView(DomainBaseView): if Domain.objects.filter(id=pk).exists(): requested_domain = Domain.objects.get(id=pk) - return requested_domain and requested_domain.is_editable() and (requested_domain.is_expiring() or requested_domain.is_expired()) + return ( + requested_domain + and requested_domain.is_editable() + and (requested_domain.is_expiring() or requested_domain.is_expired()) + ) def post(self, request, pk): From 94c63215292d6a5a600254e557d12890062e96b7 Mon Sep 17 00:00:00 2001 From: asaki222 Date: Mon, 27 Jan 2025 13:29:34 -0500 Subject: [PATCH 24/40] delete yaml script --- .../reset-db-without-sb-takedown.yaml | 88 ------------------- 1 file changed, 88 deletions(-) delete mode 100644 .github/workflows/reset-db-without-sb-takedown.yaml diff --git a/.github/workflows/reset-db-without-sb-takedown.yaml b/.github/workflows/reset-db-without-sb-takedown.yaml deleted file mode 100644 index 53e371cdb..000000000 --- a/.github/workflows/reset-db-without-sb-takedown.yaml +++ /dev/null @@ -1,88 +0,0 @@ -# This workflow can be run from the CLI -# gh workflow run reset-db.yaml -f environment=ENVIRONMENT -# OR -# cf run-task getgov-ENVIRONMENT --command 'python manage.py flush' --name flush -# cf run-task getgov-ENVIRONMENT --command 'python manage.py load' --name loaddata - -name: Reset database -run-name: Reset database for ${{ github.event.inputs.environment }} without taking down the sandbox - -on: - workflow_dispatch: - inputs: - environment: - type: choice - description: Which environment should we flush and re-load data for? - options: - - staging - - development - - el - - ad - - ms - - ag - - litterbox - - hotgov - - cb - - bob - - meoward - - backup - - ky - - es - - nl - - rh - - za - - gd - - rb - - ko - - ab - - rjm - - dk - -jobs: - reset-db: - runs-on: ubuntu-latest - env: - CF_USERNAME: CF_${{ github.event.inputs.environment }}_USERNAME - CF_PASSWORD: CF_${{ github.event.inputs.environment }}_PASSWORD - steps: - - name: Unbind the database service - uses: cloud-gov/cg-cli-tools@main - with: - cf_username: ${{ secrets[env.CF_USERNAME] }} - cf_password: ${{ secrets[env.CF_PASSWORD] }} - cf_org: cisa-dotgov - cf_space: ${{ github.event.inputs.environment }} - cf_command: "cf unbind-service getgov-${{ github.event.inputs.environment }} getgov-${{ github.event.inputs.environment }}-database" - - name: Delete the service - uses: cloud-gov/cg-cli-tools@main - with: - cf_username: ${{ secrets[env.CF_USERNAME] }} - cf_password: ${{ secrets[env.CF_PASSWORD] }} - cf_org: cisa-dotgov - cf_space: ${{ github.event.inputs.environment }} - cf_command: "cf unbind-service getgov-${{ github.event.inputs.environment }} getgov-${{ github.event.inputs.environment }}-database" - -name: Recreate the service - uses: cloud-gov/cg-cli-tools@main - with: - cf_username: ${{ secrets[env.CF_USERNAME] }} - cf_password: ${{ secrets[env.CF_PASSWORD] }} - cf_org: cisa-dotgov - cf_space: ${{ github.event.inputs.environment }} - cf_command: "cf create-service aws-rds micro-psql getgov-${{ github.event.inputs.environment }} getgov-${{ github.event.inputs.environment }}-database" - -name: Recreate the service - uses: cloud-gov/cg-cli-tools@main - with: - cf_username: ${{ secrets[env.CF_USERNAME] }} - cf_password: ${{ secrets[env.CF_PASSWORD] }} - cf_org: cisa-dotgov - cf_space: ${{ github.event.inputs.environment }} - cf_command: "cf create-service aws-rds micro-psql getgov-${{ github.event.inputs.environment }} getgov-${{ github.event.inputs.environment }}-database" - -name: Rebind the service - uses: cloud-gov/cg-cli-tools@main - with: - cf_username: ${{ secrets[env.CF_USERNAME] }} - cf_password: ${{ secrets[env.CF_PASSWORD] }} - cf_org: cisa-dotgov - cf_space: ${{ github.event.inputs.environment }} - cf_command: "cf bind-service getgov-env getgov-${{ github.event.inputs.environment }}" - \ No newline at end of file From 08616b3dd5a531a225742f89dda1b7a0417779c3 Mon Sep 17 00:00:00 2001 From: asaki222 Date: Tue, 28 Jan 2025 12:51:14 -0500 Subject: [PATCH 25/40] updated the num_of_domains --- src/registrar/models/user.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py index 2b5b56a78..1d508f88f 100644 --- a/src/registrar/models/user.py +++ b/src/registrar/models/user.py @@ -171,11 +171,14 @@ class User(AbstractUser): now = timezone.now().date() expiration_window = 60 threshold_date = now + timedelta(days=expiration_window) + acceptable_statuses = [Domain.State.UNKNOWN, Domain.State.DNS_NEEDED, Domain.State.READY] + num_of_expiring_domains = Domain.objects.filter( id__in=domain_ids, expiration_date__isnull=False, expiration_date__lte=threshold_date, expiration_date__gt=now, + state__in=acceptable_statuses, ).count() return num_of_expiring_domains From a3d454194425d9b92dabb30fbb9ea63c92a2178a Mon Sep 17 00:00:00 2001 From: asaki222 Date: Tue, 28 Jan 2025 13:51:46 -0500 Subject: [PATCH 26/40] pr changes --- src/registrar/views/domain.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 2d7d88cd6..aff32e825 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -327,9 +327,6 @@ class DomainRenewalView(DomainBaseView): context["hidden_security_emails"] = default_emails security_email = self.object.get_security_email() - if security_email is None or security_email in default_emails: - context["security_email"] = None - return context context["security_email"] = security_email return context From 0c1def30aa8f3cbcca1ef43ac7a431f346362a66 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Tue, 28 Jan 2025 19:06:26 -0500 Subject: [PATCH 27/40] 3041 - export as csv buttons --- src/registrar/templates/includes/domain_requests_table.html | 2 +- src/registrar/templates/includes/domains_table.html | 2 +- src/registrar/templates/includes/members_table.html | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/registrar/templates/includes/domain_requests_table.html b/src/registrar/templates/includes/domain_requests_table.html index b026a7a6b..562ef12bf 100644 --- a/src/registrar/templates/includes/domain_requests_table.html +++ b/src/registrar/templates/includes/domain_requests_table.html @@ -57,7 +57,7 @@ -

Member actions available + Optional
+ Optional
+ Optional
+ Optional