Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#2769: Org Member Page - Invite A New Member - [NL] #2974

Merged
merged 13 commits into from
Oct 31, 2024
Merged
71 changes: 56 additions & 15 deletions src/registrar/assets/js/get-gov.js
Original file line number Diff line number Diff line change
Expand Up @@ -297,28 +297,56 @@ function clearValidators(el) {
* radio button is false (hides this element if true)
* **/
function HookupYesNoListener(radioButtonName, elementIdToShowIfYes, elementIdToShowIfNo) {
HookupRadioTogglerListener(radioButtonName, {
'True': elementIdToShowIfYes,
'False': elementIdToShowIfNo
});
}

/**
* Hookup listeners for radio togglers in form fields.
*
* Parameters:
* - radioButtonName: The "name=" value for the radio buttons being used as togglers
* - valueToElementMap: An object where keys are the values of the radio buttons,
* and values are the corresponding DOM element IDs to show. All other elements will be hidden.
*
* Usage Example:
* Assuming you have radio buttons with values 'option1', 'option2', and 'option3',
* and corresponding DOM IDs 'section1', 'section2', 'section3'.
*
* HookupValueBasedListener('exampleRadioGroup', {
* 'option1': 'section1',
* 'option2': 'section2',
* 'option3': 'section3'
* });
**/
function HookupRadioTogglerListener(radioButtonName, valueToElementMap) {
// Get the radio buttons
let radioButtons = document.querySelectorAll('input[name="'+radioButtonName+'"]');

// Extract the list of all element IDs from the valueToElementMap
let allElementIds = Object.values(valueToElementMap);

function handleRadioButtonChange() {
// Check the value of the selected radio button
// Attempt to find the radio button element that is checked
// Find the checked radio button
let radioButtonChecked = document.querySelector('input[name="'+radioButtonName+'"]:checked');

// Check if the element exists before accessing its value
let selectedValue = radioButtonChecked ? radioButtonChecked.value : null;

switch (selectedValue) {
case 'True':
toggleTwoDomElements(elementIdToShowIfYes, elementIdToShowIfNo, 1);
break;

case 'False':
toggleTwoDomElements(elementIdToShowIfYes, elementIdToShowIfNo, 2);
break;
// Hide all elements by default
allElementIds.forEach(function (elementId) {
let element = document.getElementById(elementId);
if (element) {
element.style.display = 'none';
CocoByte marked this conversation as resolved.
Show resolved Hide resolved
}
});

default:
toggleTwoDomElements(elementIdToShowIfYes, elementIdToShowIfNo, 0);
// Show the relevant element for the selected value
if (selectedValue && valueToElementMap[selectedValue]) {
let elementToShow = document.getElementById(valueToElementMap[selectedValue]);
if (elementToShow) {
elementToShow.style.display = 'block';
CocoByte marked this conversation as resolved.
Show resolved Hide resolved
}
}
}

Expand All @@ -328,11 +356,12 @@ function HookupYesNoListener(radioButtonName, elementIdToShowIfYes, elementIdToS
radioButton.addEventListener('change', handleRadioButtonChange);
});

// initialize
// Initialize by checking the current state
handleRadioButtonChange();
}
}


// A generic display none/block toggle function that takes an integer param to indicate how the elements toggle
function toggleTwoDomElements(ele1, ele2, index) {
let element1 = document.getElementById(ele1);
Expand Down Expand Up @@ -912,6 +941,18 @@ function setupUrbanizationToggle(stateTerritoryField) {
HookupYesNoListener("additional_details-has_anything_else_text",'anything-else', null)
})();


/**
* An IIFE that listens to the yes/no radio buttons on the anything else form and toggles form field visibility accordingly
*
*/
(function newMemberFormListener() {
HookupRadioTogglerListener('member_access_level', {
'admin': 'new-member-admin-permissions',
'basic': 'new-member-basic-permissions'
});
})();

/**
* An IIFE that disables the delete buttons on nameserver forms on page load if < 3 forms
*
Expand Down
10 changes: 5 additions & 5 deletions src/registrar/config/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,11 +106,11 @@
views.PortfolioInvitedMemberEditView.as_view(),
name="invitedmember-permissions",
),
# path(
# "no-organization-members/",
# views.PortfolioNoMembersView.as_view(),
# name="no-portfolio-members",
# ),
path(
"members/new-member/",
views.NewMemberView.as_view(),
name="new-member",
),
path(
"requests/",
views.PortfolioDomainRequestsView.as_view(),
Expand Down
99 changes: 99 additions & 0 deletions src/registrar/forms/portfolio.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
import logging
from django import forms
from django.core.validators import RegexValidator
from django.core.validators import MaxLengthValidator

from registrar.models import (
PortfolioInvitation,
UserPortfolioPermission,
DomainInformation,
Portfolio,
SeniorOfficial,
User,
)
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices

Expand Down Expand Up @@ -160,3 +162,100 @@ class Meta:
"roles",
"additional_permissions",
]


class NewMemberForm(forms.ModelForm):
member_access_level = forms.ChoiceField(
label="Select permission",
choices=[("admin", "Admin Access"), ("basic", "Basic Access")],
widget=forms.RadioSelect(attrs={"class": "usa-radio__input usa-radio__input--tile"}),
CocoByte marked this conversation as resolved.
Show resolved Hide resolved
required=True,
error_messages={
"required": "Member access level is required",
},
)
admin_org_domain_request_permissions = forms.ChoiceField(
label="Select permission",
choices=[("view_only", "View all requests"), ("view_and_create", "View all requests plus create requests")],
widget=forms.RadioSelect,
required=True,
error_messages={
"required": "Domain request permission is required",
},
)
admin_org_members_permissions = forms.ChoiceField(
label="Select permission",
choices=[("view_only", "View all members"), ("view_and_create", "View all members plus manage members")],
widget=forms.RadioSelect,
required=True,
error_messages={
"required": "Member permission is required",
},
)
basic_org_domain_request_permissions = forms.ChoiceField(
label="Select permission",
choices=[
("view_only", "View all requests"),
("view_and_create", "View all requests plus create requests"),
("no_access", "No access"),
],
widget=forms.RadioSelect,
required=True,
error_messages={
"required": "Member permission is required",
},
)

email = forms.EmailField(
label="Enter the email of the member you'd like to invite",
max_length=None,
error_messages={
"invalid": ("Enter an email address in the required format, like [email protected]."),
"required": ("Enter an email address in the required format, like [email protected]."),
},
validators=[
MaxLengthValidator(
320,
message="Response must be less than 320 characters.",
)
],
required=True,
)

class Meta:
model = User
fields = ["email"]

def clean(self):
CocoByte marked this conversation as resolved.
Show resolved Hide resolved
CocoByte marked this conversation as resolved.
Show resolved Hide resolved
cleaned_data = super().clean()

# Lowercase the value of the 'email' field
email_value = cleaned_data.get("email")
if email_value:
cleaned_data["email"] = email_value.lower()

# Check for an existing user (if there isn't any, send an invite)
# if email_value:
# try:
# existingUser = User.objects.get(email=email_value)
# except User.DoesNotExist:
# raise forms.ValidationError("User with this email does not exist.")

# Get the grade and sport from POST data
permission_level = cleaned_data.get("member_access_level")
# permission_level = self.data.get('new_member-permission_level')
if not permission_level:
for field in self.fields:
if field in self.errors and field != "email" and field != "member_access_level":
del self.errors[field]
return cleaned_data
CocoByte marked this conversation as resolved.
Show resolved Hide resolved

# Validate the sport based on the selected grade
CocoByte marked this conversation as resolved.
Show resolved Hide resolved
if permission_level == "True":
# remove the error messages pertaining to basic permission inputs
del self.errors["basic_org_domain_request_permissions"]
else:
# remove the error messages pertaining to admin permission inputs
del self.errors["admin_org_domain_request_permissions"]
del self.errors["admin_org_members_permissions"]
return cleaned_data
CocoByte marked this conversation as resolved.
Show resolved Hide resolved
2 changes: 1 addition & 1 deletion src/registrar/templates/portfolio_members.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ <h1 id="members-header">Members</h1>
{% if has_edit_members_portfolio_permission %}
<div class="mobile:grid-col-12 tablet:grid-col-6">
<p class="float-right-tablet tablet:margin-y-0">
<a href="#" class="usa-button"
<a href="new-member" class="usa-button"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(blocking) you should use the {% url 'new-member' as url %} here

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remember to change this!

>
Add a new member
</a>
Expand Down
123 changes: 123 additions & 0 deletions src/registrar/templates/portfolio_members_add_new.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
{% extends 'portfolio_base.html' %}
{% load static url_helpers %}
{% load field_helpers %}

{% block title %} Members | New Member {% endblock %}

{% block wrapper_class %}
{{ block.super }} dashboard--grey-1
{% endblock %}

{% block portfolio_content %}

<!-- Form mesages -->
{% include "includes/form_errors.html" with form=form %}
{% block messages %}
{% include "includes/form_messages.html" %}
{% endblock messages%}

<!-- Navigation breadcrumbs -->
<nav class="usa-breadcrumb padding-top-0" aria-label="Domain request breadcrumb">
<ol class="usa-breadcrumb__list">
<li class="usa-breadcrumb__list-item">
<a href="/members/" class="usa-breadcrumb__link"><span>Members</span></a>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(blocking) Url should be used here as well

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here as well

</li>
<li class="usa-breadcrumb__list-item usa-current" aria-current="page">
<span>Add a new member</span>
</li>
</ol>
</nav>

<!-- Page header -->
{% block new_member_header %}
<h1>Add a new member</h1>
{% endblock new_member_header %}

{% include "includes/required_fields.html" %}

<form class="usa-form usa-form--large" method="post" novalidate>
<fieldset class="usa-fieldset margin-top-2">
<legend>
<h2>Email</h2>
</legend>
<!-- Member email -->
{% csrf_token %}
{% with group_classes="usa-form-editable usa-form-editable--no-border padding-top-0" %}
{% input_with_errors form.email %}
{% endwith %}

<!-- {{ form.as_p }} -->
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<!-- {{ form.as_p }} -->

</fieldset>

<!-- Member access radio buttons (Toggles other sections) -->
<fieldset class="usa-fieldset margin-top-2">
<legend>
<h2>Member Access</h2>
</legend>

<em>Select the level of access for this member. <abbr class="usa-hint usa-hint--required" title="required">*</abbr></em>

{% with group_classes="usa-form-editable usa-form-editable--no-border padding-top-0" %}
<div class="usa-radio">
{% for radio in form.member_access_level %}
{{ radio.tag }}
<label class="usa-radio__label usa-legend" for="{{ radio.id_for_label }}">
{{ radio.choice_label }}
<p class="margin-0 margin-top-2">
{% if radio.choice_label == "Admin Access" %}
Grants this member access to the organization-wide information on domains, domain requests, and members. Domain management can be assigned separately.
CocoByte marked this conversation as resolved.
Show resolved Hide resolved
{% else %}
Grants this member access to the organization. They can be given extra permissions to view all organization domain requests and submit domain requests on behald of the organization. Basica access members can't view all members of an organization or manage them. Domain management can be assigned separacterly.
CocoByte marked this conversation as resolved.
Show resolved Hide resolved
{% endif %}
</p>
</label>
{% endfor %}
</div>
{% endwith %}

</fieldset>

<!-- Admin access form -->
<div id="new-member-admin-permissions" class="margin-top-2">
<legend>
<h2>Admin access permissions</h2>
<p>Member permissions available for admin-level acccess.</p>
</legend>

<h3 class="margin-bottom-0">Organization domain requests</h3>
{% with group_classes="usa-form-editable usa-form-editable--no-border padding-top-0" %}
{% input_with_errors form.admin_org_domain_request_permissions %}
{% endwith %}

<h3 class="margin-bottom-0 margin-top-3">Organization members</h3>
{% with group_classes="usa-form-editable usa-form-editable--no-border padding-top-0" %}
{% input_with_errors form.admin_org_members_permissions %}
{% endwith %}
</div>

<!-- Basic access form -->
<div id="new-member-basic-permissions" class="margin-top-2">
<legend>
<h2>Basic member permissions</h2>
<p>Member permissions available for basic-level access</p>
{% input_with_errors form.basic_org_domain_request_permissions %}
</legend>

</div>

<!-- Submit/cancel buttons -->
<div class="margin-top-3">
<button
href="/members/"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah looks like this should be added as well

class="usa-button usa-button--outline"
name="btn-cancel-click"
aria-label="Cancel adding new member"
>Cancel
</button>
<button type="submit" class="usa-button">Invite Member</button>
</div>
</form>

{% endblock portfolio_content%}


2 changes: 1 addition & 1 deletion src/registrar/templates/portfolio_no_domains.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{% extends 'portfolio_base.html' %}
{% extends 'portfolio_no_domains.html' %}

{% load static %}

Expand Down
Loading
Loading