mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-28 05:26:28 +02:00
checkbox column
This commit is contained in:
parent
4b547b40b3
commit
cd41b233df
18 changed files with 211 additions and 24 deletions
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 #}
|
||||
|
||||
|
|
|
@ -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="
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 #}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
121
src/registrar/views/member_domains_edit_json.py
Normal file
121
src/registrar/views/member_domains_edit_json.py
Normal 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,
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue