From 99fde890175608b272f9304d5bfd258d78f10b83 Mon Sep 17 00:00:00 2001 From: Cameron Dixon Date: Thu, 20 Jun 2024 17:16:40 -0400 Subject: [PATCH 01/17] Update domain_invitation_description.html added more detail to give better direction to analysts --- .../includes/descriptions/domain_invitation_description.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/templates/django/admin/includes/descriptions/domain_invitation_description.html b/src/registrar/templates/django/admin/includes/descriptions/domain_invitation_description.html index 7765b9203..3a5609ee8 100644 --- a/src/registrar/templates/django/admin/includes/descriptions/domain_invitation_description.html +++ b/src/registrar/templates/django/admin/includes/descriptions/domain_invitation_description.html @@ -5,8 +5,8 @@ accept and become a domain manager.

-An “invited” status indicates that the recipient has not logged in to the registrar since the invitation was sent. -A “received” status indicates that the recipient has logged in. +An “invited” status indicates that the recipient has not logged in to the registrar since the invitation was sent. Deleting an invitation with a "invited" status will prevent the user from signing in. +A “received” status indicates that the recipient has logged in. Deleting an invitation with a "received" status will stop a user's email address from showing in the domain managers list, but it will not revoke their access from the domain. To remove a user who has already signed in, go to User domain roles and delete the role for the correct domain/manager combination.

From 9d116df83fd29f7f056504b877c599d7296c2d73 Mon Sep 17 00:00:00 2001 From: Cameron Dixon Date: Fri, 21 Jun 2024 21:46:31 -0400 Subject: [PATCH 02/17] Update src/registrar/templates/django/admin/includes/descriptions/domain_invitation_description.html Co-authored-by: zandercymatics <141044360+zandercymatics@users.noreply.github.com> --- .../includes/descriptions/domain_invitation_description.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/django/admin/includes/descriptions/domain_invitation_description.html b/src/registrar/templates/django/admin/includes/descriptions/domain_invitation_description.html index 3a5609ee8..03d0cd99a 100644 --- a/src/registrar/templates/django/admin/includes/descriptions/domain_invitation_description.html +++ b/src/registrar/templates/django/admin/includes/descriptions/domain_invitation_description.html @@ -6,7 +6,7 @@ accept and become a domain manager.

An “invited” status indicates that the recipient has not logged in to the registrar since the invitation was sent. Deleting an invitation with a "invited" status will prevent the user from signing in. -A “received” status indicates that the recipient has logged in. Deleting an invitation with a "received" status will stop a user's email address from showing in the domain managers list, but it will not revoke their access from the domain. To remove a user who has already signed in, go to User domain roles and delete the role for the correct domain/manager combination. +A “received” status indicates that the recipient has logged in. Deleting an invitation with a "received" status will stop a user's email address from showing in the domain managers list, but it will not revoke their access from the domain. To remove a user who has already signed in, go to User domain roles and delete the role for the correct domain/manager combination.

From 64933e02c60c1ffffdf7f09d7c12430450076870 Mon Sep 17 00:00:00 2001 From: Cameron Dixon Date: Tue, 25 Jun 2024 09:32:46 -0400 Subject: [PATCH 03/17] Update test_admin.py adding another "invited" --- src/registrar/tests/test_admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 802974b6e..798dfaad8 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -2820,7 +2820,7 @@ class TestDomainInvitationAdmin(TestCase): ) # Assert that the filters are added - self.assertContains(response, "invited", count=4) + self.assertContains(response, "invited", count=5) self.assertContains(response, "Invited", count=2) self.assertContains(response, "retrieved", count=2) self.assertContains(response, "Retrieved", count=2) From 2e28a0ffe5c22a059bb136bcd64ba16e4f5b0dcb Mon Sep 17 00:00:00 2001 From: Cameron Dixon Date: Tue, 25 Jun 2024 12:53:08 -0400 Subject: [PATCH 04/17] Update src/registrar/templates/django/admin/includes/descriptions/domain_invitation_description.html Co-authored-by: Katherine-Osos <119689946+Katherine-Osos@users.noreply.github.com> --- .../includes/descriptions/domain_invitation_description.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/django/admin/includes/descriptions/domain_invitation_description.html b/src/registrar/templates/django/admin/includes/descriptions/domain_invitation_description.html index 03d0cd99a..23c617f1c 100644 --- a/src/registrar/templates/django/admin/includes/descriptions/domain_invitation_description.html +++ b/src/registrar/templates/django/admin/includes/descriptions/domain_invitation_description.html @@ -5,7 +5,7 @@ accept and become a domain manager.

-An “invited” status indicates that the recipient has not logged in to the registrar since the invitation was sent. Deleting an invitation with a "invited" status will prevent the user from signing in. +An “invited” status indicates that the recipient has not logged in to the registrar since the invitation was sent. Deleting an invitation with an "invited" status will prevent the user from signing in. A “received” status indicates that the recipient has logged in. Deleting an invitation with a "received" status will stop a user's email address from showing in the domain managers list, but it will not revoke their access from the domain. To remove a user who has already signed in, go to User domain roles and delete the role for the correct domain/manager combination.

From 658e7c98a77bb3e8f2b6dd054eb317dc2433db51 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Wed, 24 Jul 2024 22:28:34 -0400 Subject: [PATCH 05/17] initial working code --- src/registrar/assets/js/get-gov.js | 11 ++++-- src/registrar/models/user.py | 29 ++++----------- .../templates/includes/domains_table.html | 3 ++ src/registrar/tests/test_models.py | 9 ----- src/registrar/views/domains_json.py | 35 +++++++++++++++---- 5 files changed, 47 insertions(+), 40 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index f83966756..bec7e861e 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -1141,6 +1141,8 @@ document.addEventListener('DOMContentLoaded', function() { const statusIndicator = document.querySelector('.domain__filter-indicator'); const statusToggle = document.querySelector('.usa-button--filter'); const noPortfolioFlag = document.getElementById('no-portfolio-js-flag'); + const portfolioElement = document.getElementById('portfolio-js-value'); + const portfolioValue = portfolioElement ? portfolioElement.getAttribute('data-portfolio') : null; /** * Loads rows in the domains list, as well as updates pagination around the domains list @@ -1150,10 +1152,15 @@ document.addEventListener('DOMContentLoaded', function() { * @param {*} order - the sort order {asc, desc} * @param {*} scroll - control for the scrollToElement functionality * @param {*} searchTerm - the search term + * @param {*} portfolio - the portfolio id */ - function loadDomains(page, sortBy = currentSortBy, order = currentOrder, scroll = scrollToTable, status = currentStatus, searchTerm = currentSearchTerm) { + function loadDomains(page, sortBy = currentSortBy, order = currentOrder, scroll = scrollToTable, status = currentStatus, searchTerm = currentSearchTerm, portfolio = portfolioValue) { // fetch json of page of domains, given params - fetch(`/get-domains-json/?page=${page}&sort_by=${sortBy}&order=${order}&status=${status}&search_term=${searchTerm}`) + let url = `/get-domains-json/?page=${page}&sort_by=${sortBy}&order=${order}&status=${status}&search_term=${searchTerm}` + if (portfolio) + url += `&portfolio=${portfolio}` + + fetch(url) .then(response => response.json()) .then(data => { if (data.error) { diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py index b135e30c7..b1c9473db 100644 --- a/src/registrar/models/user.py +++ b/src/registrar/models/user.py @@ -76,11 +76,6 @@ class User(AbstractUser): VIEW_ALL_DOMAINS = "view_all_domains", "View all domains and domain reports" VIEW_MANAGED_DOMAINS = "view_managed_domains", "View managed domains" - # EDIT_DOMAINS is really self.domains. We add is hear and leverage it in has_permission - # so we have one way to test for portfolio and domain edit permissions - # Do we need to check for portfolio domains specifically? - # NOTE: A user on an org can currently invite a user outside the org - EDIT_DOMAINS = "edit_domains", "User is a manager on a domain" VIEW_MEMBER = "view_member", "View members" EDIT_MEMBER = "edit_member", "Create and edit members" @@ -268,11 +263,6 @@ class User(AbstractUser): def _has_portfolio_permission(self, portfolio_permission): """The views should only call this function when testing for perms and not rely on roles.""" - # EDIT_DOMAINS === user is a manager on a domain (has UserDomainRole) - # NOTE: Should we check whether the domain is in the portfolio? - if portfolio_permission == self.UserPortfolioPermissionChoices.EDIT_DOMAINS and self.domains.exists(): - return True - if not self.portfolio: return False @@ -286,21 +276,14 @@ class User(AbstractUser): return self._has_portfolio_permission(User.UserPortfolioPermissionChoices.VIEW_PORTFOLIO) def has_domains_portfolio_permission(self): - return ( - self._has_portfolio_permission(User.UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS) - or self._has_portfolio_permission(User.UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS) - # or self._has_portfolio_permission(User.UserPortfolioPermissionChoices.EDIT_DOMAINS) - ) - - def has_edit_domains_portfolio_permission(self): - return self._has_portfolio_permission(User.UserPortfolioPermissionChoices.EDIT_DOMAINS) + return self._has_portfolio_permission( + User.UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS + ) or self._has_portfolio_permission(User.UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS) def has_domain_requests_portfolio_permission(self): - return ( - self._has_portfolio_permission(User.UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS) - or self._has_portfolio_permission(User.UserPortfolioPermissionChoices.VIEW_CREATED_REQUESTS) - # or self._has_portfolio_permission(User.UserPortfolioPermissionChoices.EDIT_REQUESTS) - ) + return self._has_portfolio_permission( + User.UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS + ) or self._has_portfolio_permission(User.UserPortfolioPermissionChoices.VIEW_CREATED_REQUESTS) @classmethod def needs_identity_verification(cls, email, uuid): diff --git a/src/registrar/templates/includes/domains_table.html b/src/registrar/templates/includes/domains_table.html index 3a7aee80b..b7ec4d3f3 100644 --- a/src/registrar/templates/includes/domains_table.html +++ b/src/registrar/templates/includes/domains_table.html @@ -8,6 +8,9 @@

Domains

+ {% else %} + + {% endif %}
diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py index 8daf15933..9f2872f5d 100644 --- a/src/registrar/tests/test_models.py +++ b/src/registrar/tests/test_models.py @@ -1221,7 +1221,6 @@ class TestUser(TestCase): 1. Returns False when a user does not have a portfolio 2. Returns True when user has direct permission 3. Returns True when user has permission through a role - 4. Returns True EDIT_DOMAINS when user does not have the perm but has UserDomainRole Note: This tests _get_portfolio_permissions as a side effect """ @@ -1233,11 +1232,9 @@ class TestUser(TestCase): user_can_view_all_domains = self.user.has_domains_portfolio_permission() user_can_view_all_requests = self.user.has_domain_requests_portfolio_permission() - user_can_edit_domains = self.user.has_edit_domains_portfolio_permission() self.assertFalse(user_can_view_all_domains) self.assertFalse(user_can_view_all_requests) - self.assertFalse(user_can_edit_domains) self.user.portfolio = portfolio self.user.save() @@ -1245,11 +1242,9 @@ class TestUser(TestCase): user_can_view_all_domains = self.user.has_domains_portfolio_permission() user_can_view_all_requests = self.user.has_domain_requests_portfolio_permission() - user_can_edit_domains = self.user.has_edit_domains_portfolio_permission() self.assertTrue(user_can_view_all_domains) self.assertFalse(user_can_view_all_requests) - self.assertFalse(user_can_edit_domains) self.user.portfolio_roles = [User.UserPortfolioRoleChoices.ORGANIZATION_ADMIN] self.user.save() @@ -1257,11 +1252,9 @@ class TestUser(TestCase): user_can_view_all_domains = self.user.has_domains_portfolio_permission() user_can_view_all_requests = self.user.has_domain_requests_portfolio_permission() - user_can_edit_domains = self.user.has_edit_domains_portfolio_permission() self.assertTrue(user_can_view_all_domains) self.assertTrue(user_can_view_all_requests) - self.assertFalse(user_can_edit_domains) UserDomainRole.objects.all().get_or_create( user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER @@ -1269,11 +1262,9 @@ class TestUser(TestCase): user_can_view_all_domains = self.user.has_domains_portfolio_permission() user_can_view_all_requests = self.user.has_domain_requests_portfolio_permission() - user_can_edit_domains = self.user.has_edit_domains_portfolio_permission() self.assertTrue(user_can_view_all_domains) self.assertTrue(user_can_view_all_requests) - self.assertTrue(user_can_edit_domains) Portfolio.objects.all().delete() diff --git a/src/registrar/views/domains_json.py b/src/registrar/views/domains_json.py index 3b3cae2c7..d4c09d808 100644 --- a/src/registrar/views/domains_json.py +++ b/src/registrar/views/domains_json.py @@ -6,6 +6,8 @@ from django.contrib.auth.decorators import login_required from django.urls import reverse from django.db.models import Q +from registrar.models.domain_information import DomainInformation + logger = logging.getLogger(__name__) @@ -14,10 +16,9 @@ def get_domains_json(request): """Given the current request, get all domains that are associated with the UserDomainRole object""" - user_domain_roles = UserDomainRole.objects.filter(user=request.user).select_related("domain_info__sub_organization") - domain_ids = user_domain_roles.values_list("domain_id", flat=True) + domain_ids = get_domain_ids_from_request(request) - objects = Domain.objects.filter(id__in=domain_ids) + objects = Domain.objects.filter(id__in=domain_ids).select_related("domain_info__sub_organization") unfiltered_total = objects.count() objects = apply_search(objects, request) @@ -28,7 +29,7 @@ def get_domains_json(request): page_number = request.GET.get("page") page_obj = paginator.get_page(page_number) - domains = [serialize_domain(domain) for domain in page_obj.object_list] + domains = [serialize_domain(domain, request.user) for domain in page_obj.object_list] return JsonResponse( { @@ -43,6 +44,21 @@ def get_domains_json(request): ) +def get_domain_ids_from_request(request): + """Get domain ids from request. + + If portfolio specified, return domain ids associated with portfolio. + Otherwise, return domain ids associated with request.user. + """ + portfolio = request.GET.get("portfolio") + if portfolio: + domain_infos = DomainInformation.objects.filter(portfolio=portfolio) + return domain_infos.values_list("domain_id", flat=True) + else: + user_domain_roles = UserDomainRole.objects.filter(user=request.user) + return user_domain_roles.values_list("domain_id", flat=True) + + def apply_search(queryset, request): search_term = request.GET.get("search_term") if search_term: @@ -94,7 +110,7 @@ def apply_sorting(queryset, request): return queryset.order_by(sort_by) -def serialize_domain(domain): +def serialize_domain(domain, user): suborganization_name = None try: domain_info = domain.domain_info @@ -106,6 +122,9 @@ def serialize_domain(domain): domain_info = None logger.debug(f"Issue in domains_json: We could not find domain_info for {domain}") + # Check if there is a UserDomainRole for this domain and user + user_domain_role_exists = UserDomainRole.objects.filter(domain_id=domain.id, user=user).exists() + return { "id": domain.id, "name": domain.name, @@ -114,7 +133,11 @@ def serialize_domain(domain): "state_display": domain.state_display(), "get_state_help_text": domain.get_state_help_text(), "action_url": reverse("domain", kwargs={"pk": domain.id}), - "action_label": ("View" if domain.state in [Domain.State.DELETED, Domain.State.ON_HOLD] else "Manage"), + "action_label": ( + "View" + if not user_domain_role_exists or domain.state in [Domain.State.DELETED, Domain.State.ON_HOLD] + else "Manage" + ), "svg_icon": ("visibility" if domain.state in [Domain.State.DELETED, Domain.State.ON_HOLD] else "settings"), "suborganization": suborganization_name, } From ed669cc4c6715702f8794dd884ea5ca7a6f8f788 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Thu, 25 Jul 2024 08:25:57 -0400 Subject: [PATCH 06/17] updated test code --- .../tests/test_views_domains_json.py | 91 ++++++++++++++++++- 1 file changed, 88 insertions(+), 3 deletions(-) diff --git a/src/registrar/tests/test_views_domains_json.py b/src/registrar/tests/test_views_domains_json.py index 28a7308f5..09a233768 100644 --- a/src/registrar/tests/test_views_domains_json.py +++ b/src/registrar/tests/test_views_domains_json.py @@ -1,4 +1,4 @@ -from registrar.models import UserDomainRole, Domain +from registrar.models import UserDomainRole, Domain, DomainInformation, Portfolio from django.urls import reverse from .test_views import TestWithUser from django_webtest import WebTest # type: ignore @@ -15,16 +15,25 @@ class GetDomainsJsonTest(TestWithUser, WebTest): self.domain1 = Domain.objects.create(name="example1.com", expiration_date="2024-01-01", state="unknown") self.domain2 = Domain.objects.create(name="example2.com", expiration_date="2024-02-01", state="dns needed") self.domain3 = Domain.objects.create(name="example3.com", expiration_date="2024-03-01", state="ready") + self.domain4 = Domain.objects.create(name="example4.com", expiration_date="2024-03-01", state="ready") # Create UserDomainRoles UserDomainRole.objects.create(user=self.user, domain=self.domain1) UserDomainRole.objects.create(user=self.user, domain=self.domain2) UserDomainRole.objects.create(user=self.user, domain=self.domain3) + # Create Portfolio + self.portfolio = Portfolio.objects.create(creator=self.user, organization_name="Example org") + + # Add domain3 and domain4 to portfolio + DomainInformation.objects.create(creator=self.user, domain=self.domain3, portfolio=self.portfolio) + DomainInformation.objects.create(creator=self.user, domain=self.domain4, portfolio=self.portfolio) + def tearDown(self): + UserDomainRole.objects.all().delete() + DomainInformation.objects.all().delete() + Portfolio.objects.all().delete() super().tearDown() - UserDomainRole.objects.all().delete() - UserDomainRole.objects.all().delete() @less_console_noise_decorator def test_get_domains_json_unauthenticated(self): @@ -105,6 +114,82 @@ class GetDomainsJsonTest(TestWithUser, WebTest): ) self.assertEqual(svg_icon_expected, svg_icons[i]) + @less_console_noise_decorator + def test_get_domains_json_with_portfolio(self): + """Test that an authenticated user gets the list of 2 domains for portfolio.""" + + response = self.app.get(reverse("get_domains_json"), {"portfolio": self.portfolio.id}) + self.assertEqual(response.status_code, 200) + data = response.json + + # Check pagination info + self.assertEqual(data["page"], 1) + self.assertFalse(data["has_next"]) + self.assertFalse(data["has_previous"]) + self.assertEqual(data["num_pages"], 1) + + # Check the number of domains + self.assertEqual(len(data["domains"]), 2) + + # Expected domains + expected_domains = [self.domain3, self.domain4] + + # Extract fields from response + domain_ids = [domain["id"] for domain in data["domains"]] + names = [domain["name"] for domain in data["domains"]] + expiration_dates = [domain["expiration_date"] for domain in data["domains"]] + states = [domain["state"] for domain in data["domains"]] + state_displays = [domain["state_display"] for domain in data["domains"]] + get_state_help_texts = [domain["get_state_help_text"] for domain in data["domains"]] + action_urls = [domain["action_url"] for domain in data["domains"]] + action_labels = [domain["action_label"] for domain in data["domains"]] + svg_icons = [domain["svg_icon"] for domain in data["domains"]] + + # Check fields for each domain + for i, expected_domain in enumerate(expected_domains): + self.assertEqual(expected_domain.id, domain_ids[i]) + self.assertEqual(expected_domain.name, names[i]) + self.assertEqual(expected_domain.expiration_date, expiration_dates[i]) + self.assertEqual(expected_domain.state, states[i]) + + # Parsing the expiration date from string to date + parsed_expiration_date = parse_date(expiration_dates[i]) + expected_domain.expiration_date = parsed_expiration_date + + # Check state_display and get_state_help_text + self.assertEqual(expected_domain.state_display(), state_displays[i]) + self.assertEqual(expected_domain.get_state_help_text(), get_state_help_texts[i]) + + self.assertEqual(reverse("domain", kwargs={"pk": expected_domain.id}), action_urls[i]) + + # Check action_label + user_domain_role_exists = UserDomainRole.objects.filter( + domain_id=expected_domains[i].id, user=self.user + ).exists() + action_label_expected = ( + "View" + if not user_domain_role_exists + or expected_domains[i].state + in [ + Domain.State.DELETED, + Domain.State.ON_HOLD, + ] + else "Manage" + ) + self.assertEqual(action_label_expected, action_labels[i]) + + # Check svg_icon + svg_icon_expected = ( + "visibility" + if expected_domains[i].state + in [ + Domain.State.DELETED, + Domain.State.ON_HOLD, + ] + else "settings" + ) + self.assertEqual(svg_icon_expected, svg_icons[i]) + @less_console_noise_decorator def test_get_domains_json_search(self): """Test search.""" From edb33bba6979f54c2dd8cd01f0ff140570e07d65 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Thu, 25 Jul 2024 08:35:52 -0400 Subject: [PATCH 07/17] cleaned up some comments --- src/registrar/context_processors.py | 1 - src/registrar/templates/includes/domains_table.html | 3 --- 2 files changed, 4 deletions(-) diff --git a/src/registrar/context_processors.py b/src/registrar/context_processors.py index 06ef07050..9854cf404 100644 --- a/src/registrar/context_processors.py +++ b/src/registrar/context_processors.py @@ -61,7 +61,6 @@ def add_path_to_context(request): def add_has_profile_feature_flag_to_context(request): return {"has_profile_feature_flag": flag_is_active(request, "profile_feature")} - def portfolio_permissions(request): """Make portfolio permissions for the request user available in global context""" try: diff --git a/src/registrar/templates/includes/domains_table.html b/src/registrar/templates/includes/domains_table.html index b7ec4d3f3..cd9ea372f 100644 --- a/src/registrar/templates/includes/domains_table.html +++ b/src/registrar/templates/includes/domains_table.html @@ -2,7 +2,6 @@
- {% if portfolio is None %}

Domains

@@ -41,7 +40,6 @@
- {% if portfolio %}
Filter by @@ -145,7 +143,6 @@ Domain name Expires Status - {% if portfolio %} Suborganization {% endif %} From 784b005968ec9ced1406b689911b21dc6018bc14 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Thu, 25 Jul 2024 08:36:14 -0400 Subject: [PATCH 08/17] cleaned up some comments --- src/registrar/templates/includes/domain_requests_table.html | 1 - 1 file changed, 1 deletion(-) diff --git a/src/registrar/templates/includes/domain_requests_table.html b/src/registrar/templates/includes/domain_requests_table.html index efebd1e28..ad91699ef 100644 --- a/src/registrar/templates/includes/domain_requests_table.html +++ b/src/registrar/templates/includes/domain_requests_table.html @@ -2,7 +2,6 @@
- {% if portfolio is None %}

Domain requests

From 06a5803bba480d26fbc7dc2f79e2666c53f3e98e Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Thu, 25 Jul 2024 08:47:52 -0400 Subject: [PATCH 09/17] lint plus migrations --- src/registrar/context_processors.py | 1 + ...r_user_portfolio_additional_permissions.py | 38 +++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 src/registrar/migrations/0114_alter_user_portfolio_additional_permissions.py diff --git a/src/registrar/context_processors.py b/src/registrar/context_processors.py index 9854cf404..06ef07050 100644 --- a/src/registrar/context_processors.py +++ b/src/registrar/context_processors.py @@ -61,6 +61,7 @@ def add_path_to_context(request): def add_has_profile_feature_flag_to_context(request): return {"has_profile_feature_flag": flag_is_active(request, "profile_feature")} + def portfolio_permissions(request): """Make portfolio permissions for the request user available in global context""" try: diff --git a/src/registrar/migrations/0114_alter_user_portfolio_additional_permissions.py b/src/registrar/migrations/0114_alter_user_portfolio_additional_permissions.py new file mode 100644 index 000000000..55645298f --- /dev/null +++ b/src/registrar/migrations/0114_alter_user_portfolio_additional_permissions.py @@ -0,0 +1,38 @@ +# Generated by Django 4.2.10 on 2024-07-25 12:45 + +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("registrar", "0113_user_portfolio_user_portfolio_additional_permissions_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="user", + name="portfolio_additional_permissions", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.CharField( + choices=[ + ("view_all_domains", "View all domains and domain reports"), + ("view_managed_domains", "View managed domains"), + ("view_member", "View members"), + ("edit_member", "Create and edit members"), + ("view_all_requests", "View all requests"), + ("view_created_requests", "View created requests"), + ("edit_requests", "Create and edit requests"), + ("view_portfolio", "View organization"), + ("edit_portfolio", "Edit organization"), + ], + max_length=50, + ), + blank=True, + help_text="Select one or more additional permissions.", + null=True, + size=None, + ), + ), + ] From 44fea22cf976efccc1aaf7be47b544c239282c10 Mon Sep 17 00:00:00 2001 From: Alysia Broddrick Date: Tue, 30 Jul 2024 09:13:50 -0700 Subject: [PATCH 10/17] Add new developer sandbox 'ad' infrastructure --- .github/workflows/migrate.yaml | 1 + .github/workflows/reset-db.yaml | 1 + ops/manifests/manifest-ad.yaml | 32 ++++++++++++++++++++++++++++++++ src/registrar/config/settings.py | 1 + 4 files changed, 35 insertions(+) create mode 100644 ops/manifests/manifest-ad.yaml diff --git a/.github/workflows/migrate.yaml b/.github/workflows/migrate.yaml index 3ebee59f9..70ff8ee95 100644 --- a/.github/workflows/migrate.yaml +++ b/.github/workflows/migrate.yaml @@ -16,6 +16,7 @@ on: - stable - staging - development + - ad - ms - ag - litterbox diff --git a/.github/workflows/reset-db.yaml b/.github/workflows/reset-db.yaml index 49e4b5e5f..b6fa0fec5 100644 --- a/.github/workflows/reset-db.yaml +++ b/.github/workflows/reset-db.yaml @@ -16,6 +16,7 @@ on: options: - staging - development + - ad - ms - ag - litterbox diff --git a/ops/manifests/manifest-ad.yaml b/ops/manifests/manifest-ad.yaml new file mode 100644 index 000000000..73d6f96ff --- /dev/null +++ b/ops/manifests/manifest-ad.yaml @@ -0,0 +1,32 @@ +--- +applications: +- name: getgov-ad + buildpacks: + - python_buildpack + path: ../../src + instances: 1 + memory: 512M + stack: cflinuxfs4 + timeout: 180 + command: ./run.sh + health-check-type: http + health-check-http-endpoint: /health + health-check-invocation-timeout: 40 + env: + # Send stdout and stderr straight to the terminal without buffering + PYTHONUNBUFFERED: yup + # Tell Django where to find its configuration + DJANGO_SETTINGS_MODULE: registrar.config.settings + # Tell Django where it is being hosted + DJANGO_BASE_URL: https://getgov-ad.app.cloud.gov + # Tell Django how much stuff to log + DJANGO_LOG_LEVEL: INFO + # default public site location + GETGOV_PUBLIC_SITE_URL: https://get.gov + # Flag to disable/enable features in prod environments + IS_PRODUCTION: False + routes: + - route: getgov-ad.app.cloud.gov + services: + - getgov-credentials + - getgov-ad-database diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index 3da0a104a..8dc2587b9 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -665,6 +665,7 @@ ALLOWED_HOSTS = [ "getgov-stable.app.cloud.gov", "getgov-staging.app.cloud.gov", "getgov-development.app.cloud.gov", + "getgov-ad.app.cloud.gov", "getgov-ms.app.cloud.gov", "getgov-ag.app.cloud.gov", "getgov-litterbox.app.cloud.gov", From 6c82ec9dc296438a352890585f1d778ca8647ab4 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Tue, 30 Jul 2024 13:33:14 -0700 Subject: [PATCH 11/17] Update for organization to have election --- src/registrar/models/domain_request.py | 8 ++++++++ src/registrar/utility/csv_export.py | 5 +++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index a7252e16b..1ff1e501a 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -215,6 +215,14 @@ class DomainRequest(TimeStampedModel): } return org_election_map + @classmethod + def get_org_label(cls, org_name: str): + # Translating the key that is given to the direct readable value + if not org_name: + return None + + return cls(org_name).label if org_name else None + class OrganizationChoicesVerbose(models.TextChoices): """ Tertiary organization choices diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index 5fbd255aa..d852df5db 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -374,8 +374,9 @@ class DomainExport(BaseExport): if first_ready_on is None: first_ready_on = "(blank)" - domain_org_type = model.get("generic_org_type") - human_readable_domain_org_type = DomainRequest.OrganizationChoices.get_org_label(domain_org_type) + # organization_type has generic_org_type AND is_election + domain_org_type = model.get("organization_type") + human_readable_domain_org_type = DomainRequest.OrgChoicesElectionOffice.get_org_label(domain_org_type) domain_federal_type = model.get("federal_type") human_readable_domain_federal_type = BranchChoices.get_branch_label(domain_federal_type) domain_type = human_readable_domain_org_type From 723d8ecc80655fe21fed8ce3d7540eb558a36b94 Mon Sep 17 00:00:00 2001 From: asaki222 Date: Wed, 31 Jul 2024 15:05:46 -0400 Subject: [PATCH 12/17] added uuid --- src/registrar/fixtures_users.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/registrar/fixtures_users.py b/src/registrar/fixtures_users.py index 74fd4d15d..a96cb3483 100644 --- a/src/registrar/fixtures_users.py +++ b/src/registrar/fixtures_users.py @@ -22,6 +22,11 @@ class UserFixture: """ ADMINS = [ + { + "username": "aad084c3-66cc-4632-80eb-41cdf5c5bcbf", + "first_name": "Aditi", + "last_name": "Green", + }, { "username": "be17c826-e200-4999-9389-2ded48c43691", "first_name": "Matthew", @@ -120,6 +125,11 @@ class UserFixture: ] STAFF = [ + { + "username": "ffec5987-aa84-411b-a05a-a7ee5cbcde54", + "first_name": "Aditi-Analyst", + "last_name": "Green-Analyst" + }, { "username": "d6bf296b-fac5-47ff-9c12-f88ccc5c1b99", "first_name": "Matthew-Analyst", From 2edcabe76fa9f90413192c527583623f055f7dd5 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 1 Aug 2024 08:34:56 -0600 Subject: [PATCH 13/17] Readd deleted content --- src/registrar/templates/includes/domains_table.html | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/registrar/templates/includes/domains_table.html b/src/registrar/templates/includes/domains_table.html index 528f56151..348ab21d7 100644 --- a/src/registrar/templates/includes/domains_table.html +++ b/src/registrar/templates/includes/domains_table.html @@ -1,11 +1,9 @@ {% load static %}
-
+
{% if portfolio is None %} -
-

Domains

-
+

Domains

{% else %} From 9faaeb7d91619e5bd6d1ad93a0e332803ca50505 Mon Sep 17 00:00:00 2001 From: asaki222 Date: Thu, 1 Aug 2024 10:50:56 -0400 Subject: [PATCH 14/17] reformatted file --- src/registrar/fixtures_users.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/fixtures_users.py b/src/registrar/fixtures_users.py index a96cb3483..7ce63d364 100644 --- a/src/registrar/fixtures_users.py +++ b/src/registrar/fixtures_users.py @@ -26,7 +26,7 @@ class UserFixture: "username": "aad084c3-66cc-4632-80eb-41cdf5c5bcbf", "first_name": "Aditi", "last_name": "Green", - }, + }, { "username": "be17c826-e200-4999-9389-2ded48c43691", "first_name": "Matthew", @@ -128,7 +128,7 @@ class UserFixture: { "username": "ffec5987-aa84-411b-a05a-a7ee5cbcde54", "first_name": "Aditi-Analyst", - "last_name": "Green-Analyst" + "last_name": "Green-Analyst", }, { "username": "d6bf296b-fac5-47ff-9c12-f88ccc5c1b99", From c6943daa21b6c281f8179c1de8f8289b162bbe7c Mon Sep 17 00:00:00 2001 From: Cameron Dixon Date: Thu, 1 Aug 2024 12:21:29 -0400 Subject: [PATCH 15/17] Update src/registrar/templates/django/admin/includes/descriptions/domain_invitation_description.html Co-authored-by: Katherine-Osos <119689946+Katherine-Osos@users.noreply.github.com> --- .../includes/descriptions/domain_invitation_description.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/django/admin/includes/descriptions/domain_invitation_description.html b/src/registrar/templates/django/admin/includes/descriptions/domain_invitation_description.html index 23c617f1c..ff277a444 100644 --- a/src/registrar/templates/django/admin/includes/descriptions/domain_invitation_description.html +++ b/src/registrar/templates/django/admin/includes/descriptions/domain_invitation_description.html @@ -6,7 +6,7 @@ accept and become a domain manager.

An “invited” status indicates that the recipient has not logged in to the registrar since the invitation was sent. Deleting an invitation with an "invited" status will prevent the user from signing in. -A “received” status indicates that the recipient has logged in. Deleting an invitation with a "received" status will stop a user's email address from showing in the domain managers list, but it will not revoke their access from the domain. To remove a user who has already signed in, go to User domain roles and delete the role for the correct domain/manager combination. +A “received” status indicates that the recipient has logged in. Deleting an invitation with a "received" status will not revoke that user's access from the domain. To remove a user who has already signed in, go to User domain roles and delete the role for the correct domain/manager combination.

From 056bc214523953f1bec677a0c5ad8c746cf7893d Mon Sep 17 00:00:00 2001 From: Alysia Broddrick Date: Thu, 1 Aug 2024 10:58:45 -0700 Subject: [PATCH 16/17] added missing ad mentions --- .github/workflows/deploy-branch-to-sandbox.yaml | 1 + .github/workflows/deploy-sandbox.yaml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/deploy-branch-to-sandbox.yaml b/.github/workflows/deploy-branch-to-sandbox.yaml index f57961fa8..652aec207 100644 --- a/.github/workflows/deploy-branch-to-sandbox.yaml +++ b/.github/workflows/deploy-branch-to-sandbox.yaml @@ -29,6 +29,7 @@ on: - hotgov - litterbox - ms + - ad # GitHub Actions has no "good" way yet to dynamically input branches branch: description: 'Branch to deploy' diff --git a/.github/workflows/deploy-sandbox.yaml b/.github/workflows/deploy-sandbox.yaml index 57561919c..fe0a19089 100644 --- a/.github/workflows/deploy-sandbox.yaml +++ b/.github/workflows/deploy-sandbox.yaml @@ -29,6 +29,7 @@ jobs: || startsWith(github.head_ref, 'litterbox/') || startsWith(github.head_ref, 'ag/') || startsWith(github.head_ref, 'ms/') + || startsWith(github.head_ref, 'ad/') outputs: environment: ${{ steps.var.outputs.environment}} runs-on: "ubuntu-latest" From 4b2919d10850e8eed88bfa5e0c844852998af8bd Mon Sep 17 00:00:00 2001 From: "Rebecca H." Date: Thu, 1 Aug 2024 11:55:36 -0700 Subject: [PATCH 17/17] Update src/registrar/models/domain_request.py Co-authored-by: zandercymatics <141044360+zandercymatics@users.noreply.github.com> --- src/registrar/models/domain_request.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index 1ff1e501a..363de213b 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -218,9 +218,6 @@ class DomainRequest(TimeStampedModel): @classmethod def get_org_label(cls, org_name: str): # Translating the key that is given to the direct readable value - if not org_name: - return None - return cls(org_name).label if org_name else None class OrganizationChoicesVerbose(models.TextChoices):