Skip to content

Commit

Permalink
Merge branch 'main' into za/2927-blocked-from-starting-requests
Browse files Browse the repository at this point in the history
  • Loading branch information
zandercymatics committed Nov 1, 2024
2 parents 20a5774 + 0f3ae4f commit 42bf6ef
Show file tree
Hide file tree
Showing 9 changed files with 541 additions and 39 deletions.
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 @@ -117,6 +117,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"}),
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):
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
67 changes: 67 additions & 0 deletions src/registrar/models/domain_information.py
Original file line number Diff line number Diff line change
Expand Up @@ -425,3 +425,70 @@ def get_state_display_of_domain(self):
return self.domain.get_state_display()
else:
return None

@property
def converted_organization_name(self):
if self.portfolio:
return self.portfolio.organization_name
return self.organization_name

# ----- Portfolio Properties -----
@property
def converted_generic_org_type(self):
if self.portfolio:
return self.portfolio.organization_type
return self.generic_org_type

@property
def converted_federal_agency(self):
if self.portfolio:
return self.portfolio.federal_agency
return self.federal_agency

@property
def converted_federal_type(self):
if self.portfolio:
return self.portfolio.federal_type
return self.federal_type

@property
def converted_senior_official(self):
if self.portfolio:
return self.portfolio.senior_official
return self.senior_official

@property
def converted_address_line1(self):
if self.portfolio:
return self.portfolio.address_line1
return self.address_line1

@property
def converted_address_line2(self):
if self.portfolio:
return self.portfolio.address_line2
return self.address_line2

@property
def converted_city(self):
if self.portfolio:
return self.portfolio.city
return self.city

@property
def converted_state_territory(self):
if self.portfolio:
return self.portfolio.state_territory
return self.state_territory

@property
def converted_zipcode(self):
if self.portfolio:
return self.portfolio.zipcode
return self.zipcode

@property
def converted_urbanization(self):
if self.portfolio:
return self.portfolio.urbanization
return self.urbanization
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
Loading

0 comments on commit 42bf6ef

Please sign in to comment.