diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 8c94d1fc6..6811f2e55 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -35,6 +35,8 @@ from django_admin_multiple_choice_list_filter.list_filters import MultipleChoice from import_export import resources from import_export.admin import ImportExportModelAdmin from django.core.exceptions import ObjectDoesNotExist +from django.contrib.postgres.forms import SimpleArrayField +from django.contrib.admin.widgets import FilteredSelectMultiple from django.utils.translation import gettext_lazy as _ @@ -89,12 +91,33 @@ class UserResource(resources.ModelResource): class Meta: model = models.User +class FilteredSelectMultipleArrayWidget(FilteredSelectMultiple): + def __init__(self, verbose_name, is_stacked=False, choices=(), **kwargs): + super().__init__(verbose_name, is_stacked, **kwargs) + self.choices = choices + def value_from_datadict(self, data, files, name): + values = super().value_from_datadict(data, files, name) + # print(f'value_from_datadict - values: {values}') + return values or [] + + def get_context(self, name, value, attrs): + # print(f'get_context - initial value: {value}') + if value is None: + value = [] + elif isinstance(value, str): + value = value.split(',') + # print(f'get_context - processed value: {value}') + self.choices = [(choice, label) for choice, label in self.choices if choice in value] + [(choice, label) for choice, label in self.choices if choice not in value] + # print(f'get_context - choices: {self.choices}') + context = super().get_context(name, value, attrs) + return context + class MyUserAdminForm(UserChangeForm): """This form utilizes the custom widget for its class's ManyToMany UIs. It inherits from UserChangeForm which has special handling for the password and username fields.""" - + class Meta: model = models.User fields = "__all__" @@ -102,6 +125,8 @@ class MyUserAdminForm(UserChangeForm): widgets = { "groups": NoAutocompleteFilteredSelectMultiple("groups", False), "user_permissions": NoAutocompleteFilteredSelectMultiple("user_permissions", False), + "portfolio_roles": FilteredSelectMultipleArrayWidget("portfolio_roles", is_stacked=False, choices=User.UserPortfolioRoleChoices.choices), + "portfolio_additional_permissions": FilteredSelectMultipleArrayWidget("portfolio_additional_permissions", is_stacked=False, choices=User.UserPortfolioPermissionChoices.choices), } def __init__(self, *args, **kwargs): @@ -625,6 +650,10 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin): "status", ) + # For each filter_horizontal, init in admin js extendFilterHorizontalWidgets + # to activate the edit/delete/view buttons + # filter_horizontal = ("portfolio_roles",) + # Renames inherited AbstractUser label 'email_address to 'email' def formfield_for_dbfield(self, dbfield, **kwargs): field = super().formfield_for_dbfield(dbfield, **kwargs) @@ -654,6 +683,7 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin): "user_permissions", "portfolio", "portfolio_roles", + "portfolio_additional_permissions", ) }, ), @@ -684,6 +714,7 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin): "groups", "portfolio", "portfolio_roles", + "portfolio_additional_permissions", ) }, ), @@ -715,6 +746,7 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin): "date_joined", "portfolio", "portfolio_roles", + "portfolio_additional_permissions", ] list_filter = ( diff --git a/src/registrar/migrations/0113_user_portfolio_user_portfolio_roles_and_more.py b/src/registrar/migrations/0113_user_portfolio_user_portfolio_additional_permissions_and_more.py similarity index 60% rename from src/registrar/migrations/0113_user_portfolio_user_portfolio_roles_and_more.py rename to src/registrar/migrations/0113_user_portfolio_user_portfolio_additional_permissions_and_more.py index 787ee99c2..342e125c3 100644 --- a/src/registrar/migrations/0113_user_portfolio_user_portfolio_roles_and_more.py +++ b/src/registrar/migrations/0113_user_portfolio_user_portfolio_additional_permissions_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.10 on 2024-07-17 17:12 +# Generated by Django 4.2.10 on 2024-07-17 19:10 from django.conf import settings import django.contrib.postgres.fields @@ -24,6 +24,29 @@ class Migration(migrations.Migration): to="registrar.portfolio", ), ), + migrations.AddField( + model_name="user", + name="portfolio_additional_permissions", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.CharField( + choices=[ + ("view_domains", "View all domains and domain reports"), + ("edit_domains", "User is a manager on a domain"), + ("view_member", "View members"), + ("edit_member", "Create and edit members"), + ("view_requests", "View 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, + ), + ), migrations.AddField( model_name="user", name="portfolio_roles", diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py index 6e278e730..9b0a14b07 100644 --- a/src/registrar/models/user.py +++ b/src/registrar/models/user.py @@ -78,6 +78,7 @@ class User(AbstractUser): # 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" @@ -150,15 +151,15 @@ class User(AbstractUser): help_text="Select one or more roles.", ) - # portfolio_permissions = ArrayField( - # models.CharField( - # max_length=50, - # choices=UserPortfolioPermissionChoices.choices, - # ), - # null=True, - # blank=True, - # help_text="Select one or more permissions.", - # ) + portfolio_additional_permissions = ArrayField( + models.CharField( + max_length=50, + choices=UserPortfolioPermissionChoices.choices, + ), + null=True, + blank=True, + help_text="Select one or more additional permissions.", + ) phone = PhoneNumberField( null=True, @@ -256,9 +257,10 @@ class User(AbstractUser): def has_portfolio_permission(self, portfolio_permission): """The views should only call this guy when testing for perms and not rely on roles""" - # TODO: this does not seem to be working - # if portfolio_permission == self.UserPortfolioPermissionChoices.EDIT_DOMAINS and self.domains.exists(): - # return True + # 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 @@ -276,6 +278,7 @@ class User(AbstractUser): for role in self.portfolio_roles: if role in self.PORTFOLIO_ROLE_PERMISSIONS: portfolio_permissions.update(self.PORTFOLIO_ROLE_PERMISSIONS[role]) + portfolio_permissions.update(self.portfolio_additional_permissions) return list(portfolio_permissions) # Convert back to list if necessary @classmethod