From ed9d21557793f13f268577c719eaaf6f4dffd250 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 9 Dec 2024 15:03:39 -0700 Subject: [PATCH] Form structure --- src/registrar/forms/portfolio.py | 179 +++++++++++++++--- .../models/utility/portfolio_helper.py | 17 ++ .../django/forms/widgets/multiple_input.html | 14 +- .../portfolio_member_permissions.html | 54 ++---- src/registrar/templatetags/custom_filters.py | 8 + 5 files changed, 201 insertions(+), 71 deletions(-) diff --git a/src/registrar/forms/portfolio.py b/src/registrar/forms/portfolio.py index 5309f7263..65911200b 100644 --- a/src/registrar/forms/portfolio.py +++ b/src/registrar/forms/portfolio.py @@ -110,52 +110,169 @@ class PortfolioSeniorOfficialForm(forms.ModelForm): return cleaned_data -class PortfolioMemberForm(forms.ModelForm): +class BasePortfolioMemberForm(forms.ModelForm): + role = forms.ChoiceField( + label="Select permission", + choices=[ + (UserPortfolioRoleChoices.ORGANIZATION_ADMIN.value, "Admin Access"), + (UserPortfolioRoleChoices.ORGANIZATION_MEMBER.value, "Basic Access") + ], + widget=forms.RadioSelect, + required=True, + error_messages={ + "required": "Member access level is required", + }, + ) + # Permissions for admins + domain_request_permissions_admin = forms.ChoiceField( + label="Select permission", + choices=[ + (UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value, "View all requests"), + (UserPortfolioPermissionChoices.EDIT_REQUESTS.value, "Create and edit requests") + ], + widget=forms.RadioSelect, + required=False, + error_messages={ + "required": "Admin domain request permission is required", + }, + ) + member_permissions_admin = forms.ChoiceField( + label="Select permission", + choices=[ + (UserPortfolioPermissionChoices.VIEW_MEMBERS.value, "View all members"), + (UserPortfolioPermissionChoices.EDIT_MEMBERS.value, "Create and edit members") + ], + widget=forms.RadioSelect, + required=False, + error_messages={ + "required": "Admin member permission is required", + }, + ) + domain_request_permissions_member = forms.ChoiceField( + label="Select permission", + choices=[ + (UserPortfolioPermissionChoices.VIEW_MEMBERS.value, "View all members"), + (UserPortfolioPermissionChoices.EDIT_MEMBERS.value, "Create and edit members") + ], + widget=forms.RadioSelect, + required=False, + error_messages={ + "required": "Basic member permission is required", + }, + ) + + # this form dynamically shows/hides some fields, depending on what + # was selected prior. This toggles which field is required or not. + ROLE_REQUIRED_FIELDS = { + UserPortfolioRoleChoices.ORGANIZATION_ADMIN: [ + "domain_request_permissions_admin", + "member_permissions_admin", + ], + UserPortfolioRoleChoices.ORGANIZATION_MEMBER: [ + "domain_request_permissions_member", + ], + } + + def _map_instance_to_form(self, instance): + """Maps model instance data to form fields""" + if not instance: + return {} + mapped_data = {} + # Map roles with priority for admin + if instance.roles: + if UserPortfolioRoleChoices.ORGANIZATION_ADMIN.value in instance.roles: + mapped_data['role'] = UserPortfolioRoleChoices.ORGANIZATION_ADMIN.value + else: + mapped_data['role'] = UserPortfolioRoleChoices.ORGANIZATION_MEMBER.value + + perms = UserPortfolioPermission.get_portfolio_permissions(instance.roles, instance.additional_permissions) + # Map permissions with priority for edit permissions + if perms: + if UserPortfolioPermissionChoices.EDIT_REQUESTS.value in perms: + mapped_data['domain_request_permissions_admin'] = UserPortfolioPermissionChoices.EDIT_REQUESTS.value + elif UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value in perms: + mapped_data['domain_request_permissions_admin'] = UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value + + if UserPortfolioPermissionChoices.EDIT_MEMBERS.value in perms: + mapped_data['member_permissions_admin'] = UserPortfolioPermissionChoices.EDIT_MEMBERS.value + elif UserPortfolioPermissionChoices.VIEW_MEMBERS.value in perms: + mapped_data['member_permissions_admin'] = UserPortfolioPermissionChoices.VIEW_MEMBERS.value + + return mapped_data + + def _map_form_to_instance(self, instance): + """Maps form data to model instance""" + if not self.is_valid(): + return + + role = self.cleaned_data.get("role") + domain_request_permissions_member = self.cleaned_data.get("domain_request_permissions_member") + domain_request_permissions_admin = self.cleaned_data.get('domain_request_permissions_admin') + member_permissions_admin = self.cleaned_data.get('member_permissions_admin') + + instance.roles = [role] + additional_permissions = [] + if domain_request_permissions_member: + additional_permissions.append(domain_request_permissions_member) + elif domain_request_permissions_admin: + additional_permissions.append(domain_request_permissions_admin) + + if member_permissions_admin: + additional_permissions.append(member_permissions_admin) + + instance.additional_permissions = additional_permissions + return instance + + def clean(self): + cleaned_data = super().clean() + role = cleaned_data.get("role") + + # Get required fields for the selected role. + # Then validate all required fields for the role. + required_fields = self.ROLE_REQUIRED_FIELDS.get(role, []) + for field_name in required_fields: + if not cleaned_data.get(field_name): + self.add_error( + field_name, + self.fields.get(field_name).error_messages.get("required") + ) + + return cleaned_data + + +class PortfolioMemberForm(BasePortfolioMemberForm): """ Form for updating a portfolio member. """ - - roles = forms.MultipleChoiceField( - choices=UserPortfolioRoleChoices.choices, - widget=forms.SelectMultiple(attrs={"class": "usa-select"}), - required=False, - label="Roles", - ) - - additional_permissions = forms.MultipleChoiceField( - choices=UserPortfolioPermissionChoices.choices, - widget=forms.SelectMultiple(attrs={"class": "usa-select"}), - required=False, - label="Additional Permissions", - ) - class Meta: model = UserPortfolioPermission fields = [ "roles", "additional_permissions", ] + def __init__(self, *args, instance=None, **kwargs): + super().__init__(*args, **kwargs) + self.fields['role'].descriptions = { + "organization_admin": UserPortfolioRoleChoices.get_role_description(UserPortfolioRoleChoices.ORGANIZATION_ADMIN), + "organization_member": UserPortfolioRoleChoices.get_role_description(UserPortfolioRoleChoices.ORGANIZATION_MEMBER) + } + self.instance = instance + self.initial = self._map_instance_to_form(self.instance) + + def save(self): + """Save form data to instance""" + if not self.instance: + self.instance = self.Meta.model() + self._map_form_to_instance(self.instance) + self.instance.save() + return self.instance -class PortfolioInvitedMemberForm(forms.ModelForm): +class PortfolioInvitedMemberForm(BasePortfolioMemberForm): """ Form for updating a portfolio invited member. """ - roles = forms.MultipleChoiceField( - choices=UserPortfolioRoleChoices.choices, - widget=forms.SelectMultiple(attrs={"class": "usa-select"}), - required=False, - label="Roles", - ) - - additional_permissions = forms.MultipleChoiceField( - choices=UserPortfolioPermissionChoices.choices, - widget=forms.SelectMultiple(attrs={"class": "usa-select"}), - required=False, - label="Additional Permissions", - ) - class Meta: model = PortfolioInvitation fields = [ diff --git a/src/registrar/models/utility/portfolio_helper.py b/src/registrar/models/utility/portfolio_helper.py index 3768aa77a..60fa2170a 100644 --- a/src/registrar/models/utility/portfolio_helper.py +++ b/src/registrar/models/utility/portfolio_helper.py @@ -17,6 +17,23 @@ class UserPortfolioRoleChoices(models.TextChoices): @classmethod def get_user_portfolio_role_label(cls, user_portfolio_role): return cls(user_portfolio_role).label if user_portfolio_role else None + + @classmethod + def get_role_description(cls, user_portfolio_role): + """Returns a detailed description for a given role.""" + descriptions = { + cls.ORGANIZATION_ADMIN: ( + "Grants this member access to the organization-wide information " + "on domains, domain requests, and members. Domain management can be assigned separately." + ), + cls.ORGANIZATION_MEMBER: ( + "Grants this member access to the organization. They can be given extra permissions to view all " + "organization domain requests and submit domain requests on behalf of the organization. Basic access " + "members can’t view all members of an organization or manage them. " + "Domain management can be assigned separately." + ) + } + return descriptions.get(user_portfolio_role) class UserPortfolioPermissionChoices(models.TextChoices): diff --git a/src/registrar/templates/django/forms/widgets/multiple_input.html b/src/registrar/templates/django/forms/widgets/multiple_input.html index 90c241366..76e19b169 100644 --- a/src/registrar/templates/django/forms/widgets/multiple_input.html +++ b/src/registrar/templates/django/forms/widgets/multiple_input.html @@ -1,3 +1,5 @@ +{% load static custom_filters %} +
{% for group, options, index in widget.optgroups %} {% if group %}
{% endif %} @@ -13,7 +15,17 @@ + > + {{ option.label }} + {% comment %} Add a description on each, if available {% endcomment %} + {% if field and field.field and field.field.descriptions %} + {% with description=field.field.descriptions|get_dict_value:option.value %} + {% if description %} +

{{ description }}

+ {% endif %} + {% endwith %} + {% endif %} + {% endfor %} {% if group %}
{% endif %} {% endfor %} diff --git a/src/registrar/templates/portfolio_member_permissions.html b/src/registrar/templates/portfolio_member_permissions.html index ca816ee2d..a5f1731d0 100644 --- a/src/registrar/templates/portfolio_member_permissions.html +++ b/src/registrar/templates/portfolio_member_permissions.html @@ -10,12 +10,6 @@ {% block portfolio_content %} - -{% include "includes/form_errors.html" with form=form %} -{% block messages %} - {% include "includes/form_messages.html" %} -{% endblock messages%} - -{% block new_member_header %}

Member access and permissions

-{% endblock new_member_header %} {% include "includes/required_fields.html" %} @@ -45,7 +37,6 @@

Member email

- {% comment %} TODO should default to name {% endcomment %}

{% if member %} {{ member.email }} @@ -64,24 +55,15 @@ Select the level of access for this member. * - {% with group_classes="usa-form-editable usa-form-editable--no-border padding-top-0" %} -

- {% for radio in form.member_access_level %} - {{ radio.tag }} - - {% endfor %} -
+ {% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %} + {% input_with_errors form.role %} {% endwith %} + {% comment %} {% if radio.value == "organization_admin" %} + Grants this member access to the organization-wide information on domains, domain requests, and members. Domain management can be assigned separately. + {% elif radio.value == "organization_member" %} + Grants this member access to the organization. They can be given extra permissions to view all organization domain requests and submit domain requests on behalf of the organization. Basic access members can’t view all members of an organization or manage them. Domain management can be assigned separately. + {% endif %} {% endcomment %} @@ -93,7 +75,7 @@ text-primary-dark margin-bottom-0">Organization domain requests {% with group_classes="usa-form-editable usa-form-editable--no-border padding-top-0" %} - {% input_with_errors form.admin_org_domain_request_permissions %} + {% input_with_errors form.domain_request_permissions_admin %} {% endwith %}

Organization members

{% with group_classes="usa-form-editable usa-form-editable--no-border padding-top-0" %} - {% input_with_errors form.admin_org_members_permissions %} + {% input_with_errors form.member_permissions_admin %} {% endwith %}
@@ -112,7 +94,7 @@

Organization domain requests

{% with group_classes="usa-form-editable usa-form-editable--no-border padding-top-0" %} - {% input_with_errors form.basic_org_domain_request_permissions %} + {% input_with_errors form.domain_request_permissions_member %} {% endwith %} @@ -123,17 +105,11 @@ href="{% url 'members' %}" class="usa-button usa-button--outline" name="btn-cancel-click" - aria-label="Cancel adding new member" - >Cancel - - - + aria-label="Cancel editing member" + > + Cancel + + diff --git a/src/registrar/templatetags/custom_filters.py b/src/registrar/templatetags/custom_filters.py index e88830156..6140130c8 100644 --- a/src/registrar/templatetags/custom_filters.py +++ b/src/registrar/templatetags/custom_filters.py @@ -282,3 +282,11 @@ def display_requesting_entity(domain_request): ) return display + + +@register.filter +def get_dict_value(dictionary, key): + """Get a value from a dictionary. Returns a string on empty.""" + if isinstance(dictionary, dict): + return dictionary.get(key, "") + return ""