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

#2725: member management page - [NL] #2837

Merged
merged 37 commits into from
Oct 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
6954c1c
Initial Scaffolding (in progress)
CocoByte Sep 20, 2024
64b62f1
Initial scaffold for members page completed
CocoByte Sep 21, 2024
7bd0b06
Merge remote-tracking branch 'origin/main' into nl/2725-member-manage…
CocoByte Sep 21, 2024
d7fa9e8
data loads now
CocoByte Sep 23, 2024
ecf2e4d
Updated data, added manage/view icon, search & sort functionality
CocoByte Sep 23, 2024
2122745
Add member button (scaffold)
CocoByte Sep 23, 2024
64e623e
Delete permission settings around "add member" button
CocoByte Sep 23, 2024
5e2a1ce
Added admin tag, added user invites logic
CocoByte Sep 24, 2024
21b7cba
Merge remote-tracking branch 'origin/main' into nl/2725-member-manage…
CocoByte Sep 24, 2024
a8b26e3
Delete Admin read-only (comment out, just in case)
CocoByte Sep 25, 2024
b85c031
Update comments
CocoByte Sep 25, 2024
8937892
Adjust permissions so admins do not automatically have ability to vie…
CocoByte Sep 25, 2024
8673bc8
Cleanup
CocoByte Sep 25, 2024
1ad805b
linted
CocoByte Sep 25, 2024
91553ab
Removed unnecessary SCSS color override for tags
CocoByte Sep 25, 2024
140d342
Added logic for restricting editing members to only those who can edi…
CocoByte Sep 25, 2024
6ca8f69
Added some unit tests (need to fix failures still)
CocoByte Sep 25, 2024
08583ae
Cleanup logs / linted
CocoByte Sep 25, 2024
3dbb8f0
Unit test tweaks (still needs work)
CocoByte Sep 26, 2024
35fec1e
fixed unit tests
CocoByte Sep 26, 2024
ada24a6
JK on last commit -- more unit test fixes
CocoByte Sep 26, 2024
8508c99
added a test
CocoByte Sep 26, 2024
93fbfa3
linted
CocoByte Sep 26, 2024
6118ca1
fixes
CocoByte Sep 26, 2024
f624a49
last fix...
CocoByte Sep 26, 2024
92abc78
Cleanup
zandercymatics Sep 27, 2024
b84beef
Update create_federal_portfolio.py
zandercymatics Sep 30, 2024
fe31dbd
Update members_table.html
zandercymatics Sep 30, 2024
95cf224
Update src/registrar/tests/test_views_portfolio.py
zandercymatics Oct 1, 2024
c85ef25
Pr suggestions ( minus tests )
zandercymatics Oct 1, 2024
18e4647
Merge branch 'nl/2725-member-management-page' of github.com:cisagov/m…
zandercymatics Oct 1, 2024
7b09b35
fix bug
zandercymatics Oct 1, 2024
72afa03
pass in portfolio
zandercymatics Oct 1, 2024
4e9a58b
fix tests
zandercymatics Oct 2, 2024
b497931
fix sorting + unit tests
zandercymatics Oct 2, 2024
d451297
lint
zandercymatics Oct 2, 2024
76473e2
fix test
zandercymatics Oct 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 136 additions & 0 deletions src/registrar/assets/js/get-gov.js
Original file line number Diff line number Diff line change
Expand Up @@ -1853,6 +1853,125 @@ class DomainRequestsTable extends LoadTableBase {
}
}

class MembersTable extends LoadTableBase {

constructor() {
super('.members__table', '.members__table-wrapper', '#members__search-field', '#members__search-field-submit', '.members__reset-search', '.members__reset-filters', '.members__no-data', '.members__no-search-results');
}
/**
* Loads rows in the members list, as well as updates pagination around the members list
* based on the supplied attributes.
* @param {*} page - the page number of the results (starts with 1)
* @param {*} sortBy - the sort column option
* @param {*} order - the sort order {asc, desc}
* @param {*} scroll - control for the scrollToElement functionality
* @param {*} status - control for the status filter
* @param {*} searchTerm - the search term
* @param {*} portfolio - the portfolio id
*/
loadTable(page, sortBy = this.currentSortBy, order = this.currentOrder, scroll = this.scrollToTable, status = this.currentStatus, searchTerm =this.currentSearchTerm, portfolio = this.portfolioValue) {

// --------- SEARCH
let searchParams = new URLSearchParams(
{
"page": page,
"sort_by": sortBy,
"order": order,
"status": status,
"search_term": searchTerm
}
);
if (portfolio)
searchParams.append("portfolio", portfolio)


// --------- FETCH DATA
// fetch json of page of domais, given params
let baseUrl = document.getElementById("get_members_json_url");
if (!baseUrl) {
return;
}

let baseUrlValue = baseUrl.innerHTML;
if (!baseUrlValue) {
return;
}

let url = `${baseUrlValue}?${searchParams.toString()}` //TODO: uncomment for search function
fetch(url)
.then(response => response.json())
.then(data => {
if (data.error) {
console.error('Error in AJAX call: ' + data.error);
return;
}

// handle the display of proper messaging in the event that no members exist in the list or search returns no results
this.updateDisplay(data, this.tableWrapper, this.noTableWrapper, this.noSearchResultsWrapper, this.currentSearchTerm);

// identify the DOM element where the domain list will be inserted into the DOM
const memberList = document.querySelector('.members__table tbody');
memberList.innerHTML = '';

data.members.forEach(member => {
// const actionUrl = domain.action_url;
const member_name = member.name;
const member_email = member.email;
const last_active = member.last_active;
const action_url = member.action_url;
const action_label = member.action_label;
const svg_icon = member.svg_icon;

const row = document.createElement('tr');

let admin_tagHTML = ``;
if (member.is_admin)
admin_tagHTML = `<span class="usa-tag margin-left-1 bg-primary">Admin</span>`

row.innerHTML = `
<th scope="row" role="rowheader" data-label="member email">
${member_email ? member_email : member_name} ${admin_tagHTML}
</th>
<td data-sort-value="${last_active}" data-label="last_active">
${last_active}
</td>
<td>
<a href="${action_url}">
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
<use xlink:href="/public/img/sprite.svg#${svg_icon}"></use>
</svg>
${action_label} <span class="usa-sr-only">${member_name}</span>
</a>
</td>
`;
memberList.appendChild(row);
});

// Do not scroll on first page load
if (scroll)
ScrollToElement('class', 'members');
this.scrollToTable = true;

// update pagination
this.updatePagination(
'member',
'#members-pagination',
'#members-pagination .usa-pagination__counter',
'#members',
data.page,
data.num_pages,
data.has_previous,
data.has_next,
data.total,
);
this.currentSortBy = sortBy;
this.currentOrder = order;
this.currentSearchTerm = searchTerm;
})
.catch(error => console.error('Error fetching members:', error));
}
}


/**
* An IIFE that listens for DOM Content to be loaded, then executes. This function
Expand Down Expand Up @@ -1926,6 +2045,23 @@ const utcDateString = (dateString) => {
};



/**
* An IIFE that listens for DOM Content to be loaded, then executes. This function
* initializes the domains list and associated functionality on the home page of the app.
*
*/
document.addEventListener('DOMContentLoaded', function() {
const isMembersPage = document.querySelector("#members")
if (isMembersPage){
const membersTable = new MembersTable();
if (membersTable.tableWrapper) {
// Initial load
membersTable.loadTable(1);
}
}
});

/**
* An IIFE that displays confirmation modal on the user profile page
*/
Expand Down
21 changes: 17 additions & 4 deletions src/registrar/config/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,21 @@
ExportDataTypeUser,
)

from registrar.views.domain_request import Step
# --jsons
from registrar.views.domain_requests_json import get_domain_requests_json
from registrar.views.transfer_user import TransferUserView
from registrar.views.domains_json import get_domains_json
from registrar.views.portfolio_members_json import get_portfolio_members_json
from registrar.views.utility.api_views import (
get_senior_official_from_federal_agency_json,
get_federal_and_portfolio_types_from_federal_agency_json,
get_action_needed_email_for_user_json,
)
from registrar.views.domains_json import get_domains_json

from registrar.views.domain_request import Step
from registrar.views.transfer_user import TransferUserView
from registrar.views.utility import always_404
from api.views import available, get_current_federal, get_current_full


DOMAIN_REQUEST_NAMESPACE = views.DomainRequestWizard.URL_NAMESPACE
domain_request_urls = [
path("", views.DomainRequestWizard.as_view(), name=""),
Expand Down Expand Up @@ -74,6 +76,16 @@
views.PortfolioNoDomainsView.as_view(),
name="no-portfolio-domains",
),
path(
"members/",
views.PortfolioMembersView.as_view(),
name="members",
),
# path(
# "no-organization-members/",
# views.PortfolioNoMembersView.as_view(),
# name="no-portfolio-members",
# ),
path(
"requests/",
views.PortfolioDomainRequestsView.as_view(),
Expand Down Expand Up @@ -275,6 +287,7 @@
),
path("get-domains-json/", get_domains_json, name="get_domains_json"),
path("get-domain-requests-json/", get_domain_requests_json, name="get_domain_requests_json"),
path("get-portfolio-members-json/", get_portfolio_members_json, name="get_portfolio_members_json"),
]

# Djangooidc strips out context data from that context, so we define a custom error
Expand Down
2 changes: 1 addition & 1 deletion src/registrar/context_processors.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,5 +97,5 @@ def portfolio_permissions(request):


def is_widescreen_mode(request):
widescreen_paths = ["/domains/", "/requests/"]
widescreen_paths = ["/domains/", "/requests/", "/members/"]
return {"is_widescreen_mode": any(path in request.path for path in widescreen_paths) or request.path == "/"}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Generated by Django 4.2.10 on 2024-09-25 00:49

import django.contrib.postgres.fields
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("registrar", "0128_alter_domaininformation_state_territory_and_more"),
]

operations = [
migrations.AlterField(
model_name="portfolioinvitation",
name="portfolio_roles",
field=django.contrib.postgres.fields.ArrayField(
base_field=models.CharField(
choices=[("organization_admin", "Admin"), ("organization_member", "Member")], max_length=50
),
blank=True,
help_text="Select one or more roles.",
null=True,
size=None,
),
),
migrations.AlterField(
model_name="userportfoliopermission",
name="roles",
field=django.contrib.postgres.fields.ArrayField(
base_field=models.CharField(
choices=[("organization_admin", "Admin"), ("organization_member", "Member")], max_length=50
),
blank=True,
help_text="Select one or more roles.",
null=True,
size=None,
),
),
]
10 changes: 0 additions & 10 deletions src/registrar/models/user_portfolio_permission.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ class Meta:
PORTFOLIO_ROLE_PERMISSIONS = {
UserPortfolioRoleChoices.ORGANIZATION_ADMIN: [
UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS,
UserPortfolioPermissionChoices.VIEW_MEMBERS,
UserPortfolioPermissionChoices.EDIT_MEMBERS,
Comment on lines -18 to -19
Copy link
Contributor

Choose a reason for hiding this comment

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

tagging for myself: figure out why these were deleted

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I discussed this with Alysia. It is regarding the AC where admins cannot, by default, view or edit members. The AC says "this needs to be layered on", and it was clarified to me that this meant deleting these permissions from admin so that they have to be explicitly added in "additional permissions"

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah thank you @CocoByte!

UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
UserPortfolioPermissionChoices.EDIT_REQUESTS,
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
Expand All @@ -25,14 +23,6 @@ class Meta:
UserPortfolioPermissionChoices.VIEW_SUBORGANIZATION,
UserPortfolioPermissionChoices.EDIT_SUBORGANIZATION,
],
UserPortfolioRoleChoices.ORGANIZATION_ADMIN_READ_ONLY: [
UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS,
UserPortfolioPermissionChoices.VIEW_MEMBERS,
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
# Domain: field specific permissions
UserPortfolioPermissionChoices.VIEW_SUBORGANIZATION,
],
UserPortfolioRoleChoices.ORGANIZATION_MEMBER: [
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
],
Expand Down
1 change: 0 additions & 1 deletion src/registrar/models/utility/portfolio_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ class UserPortfolioRoleChoices(models.TextChoices):
"""

ORGANIZATION_ADMIN = "organization_admin", "Admin"
ORGANIZATION_ADMIN_READ_ONLY = "organization_admin_read_only", "Admin read only"
ORGANIZATION_MEMBER = "organization_member", "Member"


Expand Down
4 changes: 2 additions & 2 deletions src/registrar/templates/includes/header_extended.html
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,9 @@
</li>
{% endif %}

{% if has_organization_members_flag %}
{% if has_organization_members_flag and has_view_members_portfolio_permission %}
<li class="usa-nav__primary-item">
<a href="#" class="usa-nav-link">
<a href="/members/" class="usa-nav-link {% if path|is_members_subpage %} usa-current{% endif %}">
Members
</a>
</li>
Expand Down
80 changes: 80 additions & 0 deletions src/registrar/templates/includes/members_table.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
{% load static %}

<!-- Embedding the portfolio value in a data attribute -->
<span id="portfolio-js-value" class="display-none" data-portfolio="{{ portfolio.id }}"></span>
{% comment %} Stores the json endpoint in a url for easier access {% endcomment %}
{% url 'get_portfolio_members_json' as url %}
<span id="get_members_json_url" class="display-none">{{url}}</span>
<section class="section-outlined members margin-top-0 section-outlined--border-base-light" id="members">
<div class="section-outlined__header margin-bottom-3 grid-row">
<!-- ---------- SEARCH ---------- -->
<div class="section-outlined__search mobile:grid-col-12 desktop:grid-col-6">
<section aria-label="Members search component" class="margin-top-2">
<form class="usa-search usa-search--small" method="POST" role="search">
{% csrf_token %}
<button class="usa-button usa-button--unstyled margin-right-3 members__reset-search display-none" type="button">
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
<use xlink:href="{%static 'img/sprite.svg'%}#close"></use>
</svg>
Reset
</button>
<label class="usa-sr-only" for="members__search-field">Search by member name</label>
<input
class="usa-input"
id="members__search-field"
type="search"
name="search"
placeholder="Search by member name"
/>
<button class="usa-button" type="submit" id="members__search-field-submit">
<img
src="{% static 'img/usa-icons-bg/search--white.svg' %}"
class="usa-search__submit-icon"
alt="Search"
/>
</button>
</form>
</section>
</div>
</div>

<!-- ---------- MAIN TABLE ---------- -->
<div class="members__table-wrapper display-none usa-table-container--scrollable margin-top-0" tabindex="0">
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked members__table">
<caption class="sr-only">Your registered members</caption>
<thead>
<tr>
<th data-sortable="member" scope="col" role="columnheader">Member</th>
<th data-sortable="last_active" scope="col" role="columnheader">Last Active</th>
<th
scope="col"
role="columnheader"
>
<span class="usa-sr-only">Action</span>
</th>
</tr>
</thead>
<tbody>
<!-- AJAX will populate this tbody -->
</tbody>
</table>
<div
class="usa-sr-only usa-table__announcement-region"
aria-live="polite"
></div>
</div>
<div class="members__no-data display-none">
<p>You don't have any members.</p>
</div>
<div class="members__no-search-results display-none">
<p>No results found</p>
</div>
</section>
<nav aria-label="Pagination" class="usa-pagination flex-justify" id="members-pagination">
<span class="usa-pagination__counter text-base-dark padding-left-2 margin-bottom-1">
<!-- Count will be dynamically populated by JS -->
</span>
<ul class="usa-pagination__list">
<!-- Pagination links will be dynamically populated by JS -->
</ul>
</nav>
Loading
Loading