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
4 changes: 3 additions & 1 deletion src/.pa11yci
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
"http://localhost:8080/request/anything_else/",
"http://localhost:8080/request/requirements/",
"http://localhost:8080/request/finished/",
"http://localhost:8080/user-profile/"
"http://localhost:8080/user-profile/",
"http://localhost:8080/members/",
"http://localhost:8080/members/new-member"
]
}
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) {
hideElement(element);
}
});

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) {
showElement(elementToShow);
}
}
}

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
5 changes: 5 additions & 0 deletions src/registrar/config/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,11 @@
# 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
111 changes: 111 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,112 @@ 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": "Admin 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": "Admin 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": "Basic 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()

##########################################
# TODO: future ticket
# (invite new member)
##########################################
# 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.")

member_access_level = cleaned_data.get("member_access_level")

# Intercept the error messages so that we don't validate hidden inputs
if not member_access_level:
# If no member access level has been selected, delete error messages
# for all hidden inputs (which is everything except the e-mail input
# and member access selection)
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

basic_dom_req_error = "basic_org_domain_request_permissions"
admin_dom_req_error = "admin_org_domain_request_permissions"
admin_member_error = "admin_org_members_permissions"

if member_access_level == "admin" and basic_dom_req_error in self.errors:
# remove the error messages pertaining to basic permission inputs
del self.errors[basic_dom_req_error]
elif member_access_level == "basic":
# remove the error messages pertaining to admin permission inputs
if admin_dom_req_error in self.errors:
del self.errors[admin_dom_req_error]
if admin_member_error in self.errors:
del self.errors[admin_member_error]
return cleaned_data
CocoByte marked this conversation as resolved.
Show resolved Hide resolved
2 changes: 1 addition & 1 deletion src/registrar/templates/includes/header_extended.html
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@

{% if has_organization_members_flag %}
<li class="usa-nav__primary-item">
<a href="/members/" class="usa-nav-link {% if path|is_members_subpage %} usa-current{% endif %}">
<a href="{% url 'members' %}" class="usa-nav-link {% if path|is_members_subpage %} usa-current{% endif %}">
Members
</a>
</li>
Expand Down
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="{% url 'new-member' %}" class="usa-button"
>
Add a new member
</a>
Expand Down
119 changes: 119 additions & 0 deletions src/registrar/templates/portfolio_members_add_new.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
{% 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.
{% 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 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 %}
</p>
</label>
{% endfor %}
</div>
{% endwith %}

</fieldset>

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

<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">
<h2>Basic member permissions</h2>
<p>Member permissions available for basic-level access</p>
{% input_with_errors form.basic_org_domain_request_permissions %}
</div>

<!-- Submit/cancel buttons -->
<div class="margin-top-3">
<a
type="button"
href="{% url 'members' %}"
class="usa-button usa-button--outline"
name="btn-cancel-click"
aria-label="Cancel adding new member"
>Cancel
</a>
<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