checkbox column

This commit is contained in:
Rachid Mrad 2024-12-05 18:25:17 -05:00
parent 4b547b40b3
commit cd41b233df
No known key found for this signature in database
18 changed files with 211 additions and 24 deletions

View file

@ -9,6 +9,7 @@ import { initDomainsTable } from './table-domains.js';
import { initDomainRequestsTable } from './table-domain-requests.js';
import { initMembersTable } from './table-members.js';
import { initMemberDomainsTable } from './table-member-domains.js';
import { initEditMemberDomainsTable } from './table-edit-member-domains.js';
import { initPortfolioMemberPageToggle } from './portfolio-member-page.js';
import { initAddNewMemberPageListeners } from './portfolio-member-page.js';
@ -41,6 +42,7 @@ initDomainsTable();
initDomainRequestsTable();
initMembersTable();
initMemberDomainsTable();
initEditMemberDomainsTable();
initPortfolioMemberPageToggle();
initAddNewMemberPageListeners();

View file

@ -49,7 +49,7 @@ export function initPortfolioMemberPageToggle() {
* on the Add New Member page.
*/
export function initAddNewMemberPageListeners() {
add_member_form = document.getElementById("add_member_form")
let add_member_form = document.getElementById("add_member_form");
if (!add_member_form){
return;
}

View file

@ -431,7 +431,7 @@ export class BaseTable {
let searchParams = this.getSearchParams(page, sortBy, order, searchTerm, status, portfolio);
// --------- FETCH DATA
// fetch json of page of domains, given params
// fetch json of page of objects, given params
const baseUrlValue = this.getBaseUrl()?.innerHTML ?? null;
if (!baseUrlValue) return;

View file

@ -0,0 +1,54 @@
import { BaseTable } from './table-base.js';
export class EditMemberDomainsTable extends BaseTable {
constructor() {
super('edit-member-domain');
this.currentSortBy = 'name';
}
getBaseUrl() {
return document.getElementById("get_member_domains_edit_json_url");
}
getDataObjects(data) {
return data.domains;
}
addRow(dataObject, tbody, customTableOptions) {
const domain = dataObject;
const row = document.createElement('tr');
row.innerHTML = `
<td data-label="Selected" data-sort-value="0">
<div class="usa-checkbox">
<input
class="usa-checkbox__input"
id="${domain.id}"
type="checkbox"
name="${domain.name}"
value="${domain.id}"
/>
<label class="usa-checkbox__label" for="${domain.id}">
<span class="sr-only">${domain.id}</span>
</label>
</div>
</td>
<td data-label="Domain name">
${domain.name}
</td>
`;
tbody.appendChild(row);
}
}
export function initEditMemberDomainsTable() {
document.addEventListener('DOMContentLoaded', function() {
const isEditMemberDomainsPage = document.getElementById("edit-member-domains");
if (isEditMemberDomainsPage){
const editMemberDomainsTable = new EditMemberDomainsTable();
if (editMemberDomainsTable.tableWrapper) {
// Initial load
editMemberDomainsTable.loadTable(1);
}
}
});
}

View file

@ -383,6 +383,7 @@ urlpatterns = [
path("get-domain-requests-json/", get_domain_requests_json, name="get_domain_requests_json"),
path("get-portfolio-members-json/", views.PortfolioMembersJson.as_view(), name="get_portfolio_members_json"),
path("get-member-domains-json/", views.PortfolioMemberDomainsJson.as_view(), name="get_member_domains_json"),
path("get-member-domains-edit-json/", views.PortfolioMemberDomainsEditJson.as_view(), name="get_member_domains_edit_json"),
]
# Djangooidc strips out context data from that context, so we define a custom error

View file

@ -99,7 +99,7 @@ def portfolio_permissions(request):
def is_widescreen_mode(request):
widescreen_paths = []
widescreen_paths = [] # If this list is meant to include specific paths, populate it.
portfolio_widescreen_paths = [
"/domains/",
"/requests/",
@ -108,10 +108,20 @@ def is_widescreen_mode(request):
"/no-organization-domains/",
"/domain-request/",
]
exclude_paths = [
"/domains/edit",
]
# Check if the current path matches a widescreen path or the root path.
is_widescreen = any(path in request.path for path in widescreen_paths) or request.path == "/"
is_portfolio_widescreen = bool(
# Check if the user is an organization user and the path matches portfolio paths.
is_portfolio_widescreen = (
hasattr(request.user, "is_org_user")
and request.user.is_org_user(request)
and any(path in request.path for path in portfolio_widescreen_paths)
and not any(exclude_path in request.path for exclude_path in exclude_paths)
)
# Return a dictionary with the widescreen mode status.
return {"is_widescreen_mode": is_widescreen or is_portfolio_widescreen}

View file

@ -5,7 +5,7 @@
{% block title %}{% translate "Unauthorized | " %}{% endblock %}
{% block content %}
<main id="main-content" class="grid-container {% if is_widescreen_mode %} grid-container--widescreen {% endif %}">
<main id="main-content" class="grid-container {% if is_widescreen_mode %} grid-container--widescreen{% endif %}">
<div class="grid-row grow-gap">
<div class="tablet:grid-col-6 usa-prose margin-bottom-3">
<h1>

View file

@ -5,7 +5,7 @@
{% block title %}{% translate "Forbidden | " %}{% endblock %}
{% block content %}
<main id="main-content" class="grid-container {% if is_widescreen_mode %} grid-container--widescreen {% endif %}">
<main id="main-content" class="grid-container {% if is_widescreen_mode %} grid-container--widescreen{% endif %}">
<div class="grid-row grow-gap">
<div class="tablet:grid-col-6 usa-prose margin-bottom-3">
<h1>

View file

@ -5,7 +5,7 @@
{% block title %}{% translate "Page not found | " %}{% endblock %}
{% block content %}
<main id="main-content" class="grid-container {% if is_widescreen_mode %} grid-container--widescreen {% endif %}">
<main id="main-content" class="grid-container {% if is_widescreen_mode %} grid-container--widescreen{% endif %}">
<div class="grid-row grid-gap">
<div class="tablet:grid-col-6 usa-prose margin-bottom-3">
<h1>

View file

@ -5,7 +5,7 @@
{% block title %}{% translate "Server error | " %}{% endblock %}
{% block content %}
<main id="main-content" class="grid-container {% if is_widescreen_mode %} grid-container--widescreen {% endif %}">
<main id="main-content" class="grid-container {% if is_widescreen_mode %} grid-container--widescreen{% endif %}">
<div class="grid-row grid-gap">
<div class="tablet:grid-col-6 usa-prose margin-bottom-3">
<h1>

View file

@ -5,7 +5,7 @@
{% block title %} Home | {% endblock %}
{% block content %}
<main id="main-content" class="grid-container {% if is_widescreen_mode %} grid-container--widescreen {% endif %}">
<main id="main-content" class="grid-container {% if is_widescreen_mode %} grid-container--widescreen{% endif %}">
{% if user.is_authenticated %}
{# the entire logged in page goes here #}

View file

@ -3,7 +3,7 @@
<footer class="usa-footer">
<div class="usa-footer__secondary-section">
<div class="grid-container {% if is_widescreen_mode %} grid-container--widescreen {% endif %}">
<div class="grid-container {% if is_widescreen_mode %} grid-container--widescreen{% endif %}">
<div class="grid-row grid-gap">
<div
class="

View file

@ -65,7 +65,7 @@
type="search"
name="member-domains-search"
/>
<button class="usa-button" type="submit" id="member-domains__search-field-submit">
<button class="usa-button" type="submit" id="edit-member-domains__search-field-submit">
<span class="usa-search__submit-text">Search </span>
<img
src="{% static 'img/usa-icons-bg/search--white.svg' %}"
@ -80,11 +80,12 @@
</div>
<!-- ---------- MAIN TABLE ---------- -->
<div class="display-none margin-top-0" id="member-domains__table-wrapper">
<div class="display-none margin-top-0" id="edit-member-domains__table-wrapper">
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked">
<caption class="sr-only">member domains</caption>
<thead>
<tr>
<th data-sortable="checked" scope="col" role="columnheader"><span class="sr-only">Selected</span></th>
<!-- We override default sort to be name/ascending in the JSON endpoint. We add the correct aria-sort attribute here to reflect that in the UI -->
<th data-sortable="name" scope="col" role="columnheader" aria-sort="descending">Domains</th>
</tr>
@ -94,18 +95,18 @@
</tbody>
</table>
<div
class="usa-sr-only usa-table__announcement-region" id="member-domains__usa-table__announcement-region"
class="usa-sr-only usa-table__announcement-region" id="edit-member-domains__usa-table__announcement-region"
aria-live="polite"
></div>
</div>
<div class="display-none" id="member-domains__no-data">
<div class="display-none" id="edit-member-domains__no-data">
<p>This member does not manage any domains. Click the Edit domain assignments buttons to assign domains.</p>
</div>
<div class="display-none" id="member-domains__no-search-results">
<div class="display-none" id="edit-member-domains__no-search-results">
<p>No results found</p>
</div>
</section>
<nav aria-label="Pagination" class="usa-pagination flex-justify" id="member-domains-pagination">
<nav aria-label="Pagination" class="usa-pagination flex-justify" id="edit-member-domains-pagination">
<span class="usa-pagination__counter text-base-dark padding-left-2 margin-bottom-1">
<!-- Count will be dynamically populated by JS -->
</span>

View file

@ -4,7 +4,7 @@
<div id="wrapper" class="{% block wrapper_class %}wrapper--padding-top-6{% endblock %}">
{% block content %}
<main class="grid-container {% if is_widescreen_mode %} grid-container--widescreen {% endif %}">
<main class="grid-container {% if is_widescreen_mode %} grid-container--widescreen{% endif %}">
{% if user.is_authenticated %}
{# the entire logged in page goes here #}

View file

@ -25,7 +25,7 @@
<a href="{{ url2 }}" class="usa-breadcrumb__link"><span>Manage member</span></a>
</li>
<li class="usa-breadcrumb__list-item usa-current" aria-current="page">
<span>Manage member</span>
<span>Domain assignments</span>
</li>
</ol>
</nav>

View file

@ -24,15 +24,12 @@
<li class="usa-breadcrumb__list-item">
<a href="{{ url2 }}" class="usa-breadcrumb__link"><span>Manage member</span></a>
</li>
<li class="usa-breadcrumb__list-item">
<a href="{{ url3 }}" class="usa-breadcrumb__link"><span>Domain assignments</span></a>
</li>
<li class="usa-breadcrumb__list-item usa-current edit-domain-assignments-breadcrumb" aria-current="page">
<span>Edit domain assignments</span>
</li>
<li class="usa-breadcrumb__list-item review-domain-assignments-breadcrumb display-none">
<a href="{{ url3 }}" class="usa-breadcrumb__link"><span>Edit domain assignments</span></a>
</li>
<li class="usa-breadcrumb__list-item usa-current review-domain-assignments-breadcrumb display-none" aria-current="page">
<span>Review</span>
</li>
</ol>
</nav>

View file

@ -20,4 +20,5 @@ from .index import *
from .portfolios import *
from .transfer_user import TransferUserView
from .member_domains_json import PortfolioMemberDomainsJson
from .member_domains_edit_json import PortfolioMemberDomainsEditJson
from .portfolio_members_json import PortfolioMembersJson

View file

@ -0,0 +1,121 @@
import logging
from django.http import JsonResponse
from django.core.paginator import Paginator
from django.shortcuts import get_object_or_404
from django.views import View
from registrar.models import UserDomainRole, Domain, DomainInformation, User
from django.urls import reverse
from django.db.models import Q
from registrar.models.domain_invitation import DomainInvitation
from registrar.views.utility.mixins import PortfolioMemberDomainsEditPermission
logger = logging.getLogger(__name__)
class PortfolioMemberDomainsEditJson(PortfolioMemberDomainsEditPermission, View):
def get(self, request):
"""Given the current request,
get all domains that are associated with the portfolio, or
associated with the member/invited member"""
domain_ids = self.get_domain_ids_from_request(request)
objects = Domain.objects.filter(id__in=domain_ids).select_related("domain_info__sub_organization")
unfiltered_total = objects.count()
objects = self.apply_search(objects, request)
objects = self.apply_sorting(objects, request)
paginator = Paginator(objects, 10)
page_number = request.GET.get("page")
page_obj = paginator.get_page(page_number)
domains = [self.serialize_domain(domain, request.user) for domain in page_obj.object_list]
return JsonResponse(
{
"domains": domains,
"page": page_obj.number,
"num_pages": paginator.num_pages,
"has_previous": page_obj.has_previous(),
"has_next": page_obj.has_next(),
"total": paginator.count,
"unfiltered_total": unfiltered_total,
}
)
def get_domain_ids_from_request(self, request):
"""Get domain ids from request.
request.get.email - email address of invited member
request.get.member_id - member id of member
request.get.portfolio - portfolio id of portfolio
request.get.member_only - whether to return only domains associated with member
or to return all domains in the portfolio
"""
portfolio = request.GET.get("portfolio")
email = request.GET.get("email")
member_id = request.GET.get("member_id")
member_only = request.GET.get("member_only", "false").lower() in ["true", "1"]
if member_only:
if member_id:
member = get_object_or_404(User, pk=member_id)
domain_info_ids = DomainInformation.objects.filter(portfolio=portfolio).values_list(
"domain_id", flat=True
)
user_domain_roles = UserDomainRole.objects.filter(user=member).values_list("domain_id", flat=True)
return domain_info_ids.intersection(user_domain_roles)
elif email:
domain_info_ids = DomainInformation.objects.filter(portfolio=portfolio).values_list(
"domain_id", flat=True
)
domain_invitations = DomainInvitation.objects.filter(email=email).values_list("domain_id", flat=True)
return domain_info_ids.intersection(domain_invitations)
else:
domain_infos = DomainInformation.objects.filter(portfolio=portfolio)
return domain_infos.values_list("domain_id", flat=True)
logger.warning("Invalid search criteria, returning empty results list")
return []
def apply_search(self, queryset, request):
search_term = request.GET.get("search_term")
if search_term:
queryset = queryset.filter(Q(name__icontains=search_term))
return queryset
def apply_sorting(self, queryset, request):
sort_by = request.GET.get("sort_by", "name")
order = request.GET.get("order", "asc")
if order == "desc":
sort_by = f"-{sort_by}"
return queryset.order_by(sort_by)
def serialize_domain(self, domain, user):
suborganization_name = None
try:
domain_info = domain.domain_info
if domain_info:
suborganization = domain_info.sub_organization
if suborganization:
suborganization_name = suborganization.name
except Domain.domain_info.RelatedObjectDoesNotExist:
domain_info = None
logger.debug(f"Issue in domains_json: We could not find domain_info for {domain}")
# Check if there is a UserDomainRole for this domain and user
user_domain_role_exists = UserDomainRole.objects.filter(domain_id=domain.id, user=user).exists()
view_only = not user_domain_role_exists or domain.state in [Domain.State.DELETED, Domain.State.ON_HOLD]
return {
"id": domain.id,
"name": domain.name,
"expiration_date": domain.expiration_date,
"state": domain.state,
"state_display": domain.state_display(),
"get_state_help_text": domain.get_state_help_text(),
"action_url": reverse("domain", kwargs={"pk": domain.id}),
"action_label": ("View" if view_only else "Manage"),
"svg_icon": ("visibility" if view_only else "settings"),
"domain_info__sub_organization": suborganization_name,
}