Biz logic

This commit is contained in:
Rebecca Hsieh 2024-10-01 07:27:19 -07:00
parent 65174dfcd9
commit 8b8c176b0c
No known key found for this signature in database
6 changed files with 335 additions and 186 deletions

View file

@ -1498,12 +1498,36 @@ class DomainsTable extends LoadTableBase {
} }
} }
function showExportElement(element) {
console.log(`Showing element: ${element.id}`);
element.style.display = 'block';
}
function hideExportElement(element) {
console.log(`Hiding element: ${element.id}`);
element.style.display = 'none';
}
class DomainRequestsTable extends LoadTableBase { class DomainRequestsTable extends LoadTableBase {
constructor() { constructor() {
super('.domain-requests__table', '.domain-requests__table-wrapper', '#domain-requests__search-field', '#domain-requests__search-field-submit', '.domain-requests__reset-search', '.domain-requests__reset-filters', '.domain-requests__no-data', '.domain-requests__no-search-results'); super('.domain-requests__table', '.domain-requests__table-wrapper', '#domain-requests__search-field', '#domain-requests__search-field-submit', '.domain-requests__reset-search', '.domain-requests__reset-filters', '.domain-requests__no-data', '.domain-requests__no-search-results');
} }
toggleExportButton(requests) {
console.log("Toggling Export Button Visibility");
const exportButton = document.getElementById('export-csv-button');
if (exportButton) {
console.log(`Current requests length: ${requests.length}`);
if (requests.length > 0) {
showExportElement(exportButton);
} else {
hideExportElement(exportButton);
}
console.log(exportButton);
}
}
/** /**
* Loads rows in the domains list, as well as updates pagination around the domains list * Loads rows in the domains list, as well as updates pagination around the domains list
* based on the supplied attributes. * based on the supplied attributes.
@ -1517,6 +1541,7 @@ class DomainRequestsTable extends LoadTableBase {
*/ */
loadTable(page, sortBy = this.currentSortBy, order = this.currentOrder, scroll = this.scrollToTable, status = this.currentStatus, searchTerm = this.currentSearchTerm, portfolio = this.portfolioValue) { loadTable(page, sortBy = this.currentSortBy, order = this.currentOrder, scroll = this.scrollToTable, status = this.currentStatus, searchTerm = this.currentSearchTerm, portfolio = this.portfolioValue) {
let baseUrl = document.getElementById("get_domain_requests_json_url"); let baseUrl = document.getElementById("get_domain_requests_json_url");
if (!baseUrl) { if (!baseUrl) {
return; return;
} }
@ -1548,6 +1573,9 @@ class DomainRequestsTable extends LoadTableBase {
return; return;
} }
// Call toggleExportButton to manage button visibility
this.toggleExportButton(data.domain_requests);
// handle the display of proper messaging in the event that no requests exist in the list or search returns no results // handle the display of proper messaging in the event that no requests exist in the list or search returns no results
this.updateDisplay(data, this.tableWrapper, this.noTableWrapper, this.noSearchResultsWrapper, this.currentSearchTerm); this.updateDisplay(data, this.tableWrapper, this.noTableWrapper, this.noSearchResultsWrapper, this.currentSearchTerm);

View file

@ -20,6 +20,7 @@ from registrar.views.report_views import (
AnalyticsView, AnalyticsView,
ExportDomainRequestDataFull, ExportDomainRequestDataFull,
ExportDataTypeUser, ExportDataTypeUser,
ExportDataTypeRequests,
) )
from registrar.views.domain_request import Step from registrar.views.domain_request import Step
@ -165,6 +166,11 @@ urlpatterns = [
ExportDataTypeUser.as_view(), ExportDataTypeUser.as_view(),
name="export_data_type_user", name="export_data_type_user",
), ),
path(
"reports/export_data_type_requests/",
ExportDataTypeRequests.as_view(),
name="export_data_type_requests",
),
path( path(
"domain-request/<id>/edit/", "domain-request/<id>/edit/",
views.DomainRequestWizard.as_view(), views.DomainRequestWizard.as_view(),

View file

@ -229,6 +229,10 @@ class User(AbstractUser):
"""Determines if the current user can view all available domains in a given portfolio""" """Determines if the current user can view all available domains in a given portfolio"""
return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS) return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS)
def has_view_all_domain_requests_portfolio_permission(self, portfolio):
"""Determines if the current user can view all available domains in a given portfolio"""
return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS)
def has_any_requests_portfolio_permission(self, portfolio): def has_any_requests_portfolio_permission(self, portfolio):
# BEGIN # BEGIN
# Note code below is to add organization_request feature # Note code below is to add organization_request feature
@ -458,3 +462,12 @@ class User(AbstractUser):
return DomainInformation.objects.filter(portfolio=portfolio).values_list("domain_id", flat=True) return DomainInformation.objects.filter(portfolio=portfolio).values_list("domain_id", flat=True)
else: else:
return UserDomainRole.objects.filter(user=self).values_list("domain_id", flat=True) return UserDomainRole.objects.filter(user=self).values_list("domain_id", flat=True)
def get_user_domain_request_ids(self, request):
"""Returns either the domain request ids associated with this user on UserDomainRole or Portfolio"""
portfolio = request.session.get("portfolio")
if self.is_org_user(request) and self.has_view_all_domain_requests_portfolio_permission(portfolio):
return DomainRequest.objects.filter(portfolio=portfolio).values_list("id", flat=True)
else:
return UserDomainRole.objects.filter(user=self).values_list("domain_request_id", flat=True)

View file

@ -3,208 +3,200 @@
{% comment %} Stores the json endpoint in a url for easier access {% endcomment %} {% comment %} Stores the json endpoint in a url for easier access {% endcomment %}
{% url 'get_domain_requests_json' as url %} {% url 'get_domain_requests_json' as url %}
<span id="get_domain_requests_json_url" class="display-none">{{url}}</span> <span id="get_domain_requests_json_url" class="display-none">{{url}}</span>
<section class="section-outlined domain-requests{% if portfolio %} section-outlined--border-base-light{% endif %}" id="domain-requests"> <section class="section-outlined domain-requests{% if portfolio %} section-outlined--border-base-light{% endif %}" id="domain-requests">
<div class="grid-row"> <div class="grid-row">
{% if not portfolio %} {% if not portfolio %}
<div class="mobile:grid-col-12 desktop:grid-col-6"> <div class="mobile:grid-col-12 desktop:grid-col-6">
<h2 id="domain-requests-header" class="flex-6">Domain requests</h2> <h2 id="domain-requests-header" class="flex-6">Domain requests</h2>
</div> </div>
{% else %} {% else %}
<!-- Embedding the portfolio value in a data attribute --> <!-- Embedding the portfolio value in a data attribute -->
<span id="portfolio-js-value" data-portfolio="{{ portfolio.id }}"></span> <span id="portfolio-js-value" data-portfolio="{{ portfolio.id }}"></span>
{% endif %} {% endif %}
<div class="mobile:grid-col-12 desktop:grid-col-6">
<section aria-label="Domain requests search component" class="flex-6 margin-y-2"> <div class="mobile:grid-col-12 desktop:grid-col-6 display-flex flex-align-end flex-justify-end">
<form class="usa-search usa-search--small" method="POST" role="search"> <section aria-label="Domain requests search component" class="flex-6 margin-y-2">
{% csrf_token %} <form class="usa-search usa-search--small" method="POST" role="search">
<button class="usa-button usa-button--unstyled margin-right-2 domain-requests__reset-search display-none" type="button"> {% csrf_token %}
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24"> <button class="usa-button usa-button--unstyled margin-right-2 domain-requests__reset-search display-none" type="button">
<use xlink:href="{%static 'img/sprite.svg'%}#close"></use> <svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
</svg> <use xlink:href="{%static 'img/sprite.svg'%}#close"></use>
Reset </svg>
</button> Reset
{% if portfolio %} </button>
<label class="usa-sr-only" for="domain-requests__search-field">Search by domain name or creator</label> {% if portfolio %}
{% else %} <label class="usa-sr-only" for="domain-requests__search-field">Search by domain name or creator</label>
<label class="usa-sr-only" for="domain-requests__search-field">Search by domain name</label> {% else %}
{% endif %} <label class="usa-sr-only" for="domain-requests__search-field">Search by domain name</label>
<input {% endif %}
class="usa-input" <input
id="domain-requests__search-field" class="usa-input"
type="search" id="domain-requests__search-field"
name="search" type="search"
{% if portfolio %} name="search"
placeholder="Search by domain name or creator" {% if portfolio %}
{% else %} placeholder="Search by domain name or creator"
placeholder="Search by domain name" {% else %}
{% endif %} placeholder="Search by domain name"
/> {% endif %}
<button class="usa-button" type="submit" id="domain-requests__search-field-submit"> />
<img <button class="usa-button" type="submit" id="domain-requests__search-field-submit">
src="{% static 'img/usa-icons-bg/search--white.svg' %}" <img
class="usa-search__submit-icon" src="{% static 'img/usa-icons-bg/search--white.svg' %}"
alt="Search" class="usa-search__submit-icon"
/> alt="Search"
</button> />
</form> </button>
</section> </form>
</div> </section>
<section aria-label="Domain Requests report component" class="margin-top-205 margin-left-2">
<a id="export-csv-button" href="{% url 'export_data_type_requests' %}" class="usa-button usa-button--unstyled usa-button--with-icon usa-button--justify-right" role="button">
<svg class="usa-icon usa-icon--big" aria-hidden="true" focusable="false" role="img" width="24" height="24">
<use xlink:href="{% static 'img/sprite.svg' %}#file_download"></use>
</svg>
Export as CSV
</a>
</section>
</div>
</div> </div>
{% if portfolio %} {% if portfolio %}
<div class="display-flex flex-align-center"> <div class="display-flex flex-align-center">
<span class="margin-right-2 margin-top-neg-1 usa-prose text-base-darker">Filter by</span> <span class="margin-right-2 margin-top-neg-1 usa-prose text-base-darker">Filter by</span>
<div class="usa-accordion usa-accordion--select margin-right-2"> <div class="usa-accordion usa-accordion--select margin-right-2">
<div class="usa-accordion__heading"> <div class="usa-accordion__heading">
<button <button
type="button"
class="usa-button usa-button--small padding--8-8-9 usa-button--outline usa-button--filter usa-accordion__button"
aria-expanded="false"
aria-controls="filter-status"
>
<span class="filter-indicator text-bold display-none"></span> Status
<svg class="usa-icon top-2px" aria-hidden="true" focusable="false" role="img" width="24">
<use xlink:href="/public/img/sprite.svg#expand_more"></use>
</svg>
</button>
</div>
<div id="filter-status" class="usa-accordion__content usa-prose shadow-1">
<h2>Status</h2>
<fieldset class="usa-fieldset margin-top-0">
<legend class="usa-legend">Select to apply <span class="sr-only">status</span> filter</legend>
<div class="usa-checkbox">
<input
class="usa-checkbox__input"
id="filter-status-started"
type="checkbox"
name="filter-status"
value="started"
/>
<label class="usa-checkbox__label" for="filter-status-started">Started</label>
</div>
<div class="usa-checkbox">
<input
class="usa-checkbox__input"
id="filter-status-submitted"
type="checkbox"
name="filter-status"
value="submitted"
/>
<label class="usa-checkbox__label" for="filter-status-submitted">Submitted</label>
</div>
<div class="usa-checkbox">
<input
class="usa-checkbox__input"
id="filter-status-in-review"
type="checkbox"
name="filter-status"
value="in review"
/>
<label class="usa-checkbox__label" for="filter-status-in-review">In review</label>
</div>
<div class="usa-checkbox">
<input
class="usa-checkbox__input"
id="filter-status-action-needed"
type="checkbox"
name="filter-status"
value="action needed"
/>
<label class="usa-checkbox__label" for="filter-status-action-needed">Action needed</label>
</div>
<div class="usa-checkbox">
<input
class="usa-checkbox__input"
id="filter-status-rejected"
type="checkbox"
name="filter-status"
value="rejected"
/>
<label class="usa-checkbox__label" for="filter-status-rejected">Rejected</label>
</div>
<div class="usa-checkbox">
<input
class="usa-checkbox__input"
id="filter-status-withdrawn"
type="checkbox"
name="filter-status"
value="withdrawn"
/>
<label class="usa-checkbox__label" for="filter-status-withdrawn">Withdrawn</label>
</div>
<div class="usa-checkbox">
<input
class="usa-checkbox__input"
id="filter-status-ineligible"
type="checkbox"
name="filter-status"
value="ineligible"
/>
<label class="usa-checkbox__label" for="filter-status-ineligible">Ineligible</label>
</div>
</fieldset>
</div>
</div>
<button
type="button" type="button"
class="usa-button usa-button--small padding--8-8-9 usa-button--outline usa-button--filter usa-accordion__button" class="usa-button usa-button--small padding--8-12-9-12 usa-button--outline usa-button--filter domain-requests__reset-filters display-none"
aria-expanded="false" >
aria-controls="filter-status" Clear filters
> <svg class="usa-icon top-1px" aria-hidden="true" focusable="false" role="img" width="24">
<span class="filter-indicator text-bold display-none"></span> Status <use xlink:href="/public/img/sprite.svg#close"></use>
<svg class="usa-icon top-2px" aria-hidden="true" focusable="false" role="img" width="24">
<use xlink:href="/public/img/sprite.svg#expand_more"></use>
</svg> </svg>
</button> </button>
</div>
<div id="filter-status" class="usa-accordion__content usa-prose shadow-1">
<h2>Status</h2>
<fieldset class="usa-fieldset margin-top-0">
<legend class="usa-legend">Select to apply <span class="sr-only">status</span> filter</legend>
<div class="usa-checkbox">
<input
class="usa-checkbox__input"
id="filter-status-started"
type="checkbox"
name="filter-status"
value="started"
/>
<label class="usa-checkbox__label" for="filter-status-started"
>Started</label
>
</div>
<div class="usa-checkbox">
<input
class="usa-checkbox__input"
id="filter-status-submitted"
type="checkbox"
name="filter-status"
value="submitted"
/>
<label class="usa-checkbox__label" for="filter-status-submitted"
>Submitted</label
>
</div>
<div class="usa-checkbox">
<input
class="usa-checkbox__input"
id="filter-status-in-review"
type="checkbox"
name="filter-status"
value="in review"
/>
<label class="usa-checkbox__label" for="filter-status-in-review"
>In review</label
>
</div>
<div class="usa-checkbox">
<input
class="usa-checkbox__input"
id="filter-status-action-needed"
type="checkbox"
name="filter-status"
value="action needed"
/>
<label class="usa-checkbox__label" for="filter-status-action-needed"
>Action needed</label
>
</div>
<div class="usa-checkbox">
<input
class="usa-checkbox__input"
id="filter-status-rejected"
type="checkbox"
name="filter-status"
value="rejected"
/>
<label class="usa-checkbox__label" for="filter-status-rejected"
>Rejected</label
>
</div>
<div class="usa-checkbox">
<input
class="usa-checkbox__input"
id="filter-status-withdrawn"
type="checkbox"
name="filter-status"
value="withdrawn"
/>
<label class="usa-checkbox__label" for="filter-status-withdrawn"
>Withdrawn</label
>
</div>
<div class="usa-checkbox">
<input
class="usa-checkbox__input"
id="filter-status-ineligible"
type="checkbox"
name="filter-status"
value="ineligible"
/>
<label class="usa-checkbox__label" for="filter-status-ineligible"
>Ineligible</label
>
</div>
</fieldset>
</div>
</div>
<button
type="button"
class="usa-button usa-button--small padding--8-12-9-12 usa-button--outline usa-button--filter domain-requests__reset-filters display-none"
>
Clear filters
<svg class="usa-icon top-1px" aria-hidden="true" focusable="false" role="img" width="24">
<use xlink:href="/public/img/sprite.svg#close"></use>
</svg>
</button>
</div> </div>
{% endif %} {% endif %}
<div class="domain-requests__table-wrapper display-none usa-table-container--scrollable margin-top-0" tabindex="0"> <div class="domain-requests__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 domain-requests__table"> <table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked domain-requests__table">
<caption class="sr-only">Your domain requests</caption> <caption class="sr-only">Your domain requests</caption>
<thead> <thead>
<tr> <tr>
<th data-sortable="requested_domain__name" scope="col" role="columnheader">Domain name</th> <th data-sortable="requested_domain__name" scope="col" role="columnheader">Domain name</th>
<th data-sortable="last_submitted_date" scope="col" role="columnheader">Submitted</th> <th data-sortable="last_submitted_date" scope="col" role="columnheader">Submitted</th>
{% if portfolio %} {% if portfolio %}
<th data-sortable="creator" scope="col" role="columnheader">Created by</th> <th data-sortable="creator" scope="col" role="columnheader">Created by</th>
{% endif %} {% endif %}
<th data-sortable="status" scope="col" role="columnheader">Status</th> <th data-sortable="status" scope="col" role="columnheader">Status</th>
<th scope="col" role="columnheader"><span class="usa-sr-only">Action</span></th> <th scope="col" role="columnheader"><span class="usa-sr-only">Action</span></th>
<!-- AJAX will conditionally add a th for delete actions --> <!-- AJAX will conditionally add a th for delete actions -->
</tr> </tr>
</thead> </thead>
<tbody id="domain-requests-tbody"> <tbody id="domain-requests-tbody">
<!-- AJAX will populate this tbody --> <!-- AJAX will populate this tbody -->
</tbody> </tbody>
</table> </table>
<div <div class="usa-sr-only usa-table__announcement-region" aria-live="polite"></div>
class="usa-sr-only usa-table__announcement-region"
aria-live="polite"
></div>
</div> </div>
<div class="domain-requests__no-data display-none"> <div class="domain-requests__no-data display-none">
<p>You haven't requested any domains.</p> <p>You haven't requested any domains.</p>
</div> </div>
<div class="domain-requests__no-search-results display-none"> <div class="domain-requests__no-search-results display-none">
<p>No results found</p> <p>No results found</p>
</div> </div>
</section> </section>
<nav aria-label="Pagination" class="usa-pagination flex-justify" id="domain-requests-pagination">
<span class="usa-pagination__counter text-base-dark padding-left-2 margin-bottom-1"> <nav aria-label="Pagination" class="usa-pagination flex-justify" id="domain
<!-- Count will be dynamically populated by JS -->
</span>
<ul class="usa-pagination__list">
<!-- Pagination links will be dynamically populated by JS -->
</ul>
</nav>

View file

@ -583,6 +583,105 @@ class DomainDataTypeUser(DomainDataType):
return Q(domain__id__in=request.user.get_user_domain_ids(request)) return Q(domain__id__in=request.user.get_user_domain_ids(request))
class DomainRequestsDataType:
"""
The DomainRequestsDataType report, but filtered based on the current request user
"""
@classmethod
def get_filter_conditions(cls, request=None):
if request is None or not hasattr(request, "user") or not request.user.is_authenticated:
return Q(id__in=[])
request_ids = request.user.get_user_domain_request_ids(request)
return Q(id__in=request_ids)
@classmethod
def get_queryset(cls, request):
return DomainRequest.objects.filter(cls.get_filter_conditions(request))
def safe_get(attribute, default="N/A"):
# Return the attribute value or default if not present
return attribute if attribute is not None else default
@classmethod
def export_data_to_csv(cls, response, request=None):
import csv
writer = csv.writer(response)
# CSV headers
writer.writerow(
[
"Domain request",
"Region",
"Status",
"Election office",
"Federal type",
"Domain type",
"Request additional details",
"Creator approved domains count",
"Creator active requests count",
"Alternative domains",
"Other contacts",
"Current websites",
"Federal agency",
"SO first name",
"SO last name",
"SO email",
"SO title/role",
"Creator first name",
"Creator last name",
"Creator email",
"Organization name",
"City",
"State/territory",
"Request purpose",
"CISA regional representative",
"Last submitted date",
"First submitted date",
"Last status update",
]
)
queryset = cls.get_queryset(request)
for request in queryset:
writer.writerow(
[
request.requested_domain,
cls.safe_get(getattr(request, "region_field", None)),
request.status,
cls.safe_get(getattr(request, "election_office", None)),
request.federal_type,
cls.safe_get(getattr(request, "domain_type", None)),
cls.safe_get(getattr(request, "additional_details", None)),
cls.safe_get(getattr(request, "creator_approved_domains_count", None)),
cls.safe_get(getattr(request, "creator_active_requests_count", None)),
cls.safe_get(getattr(request, "all_alternative_domains", None)),
cls.safe_get(getattr(request, "all_other_contacts", None)),
cls.safe_get(getattr(request, "all_current_websites", None)),
cls.safe_get(getattr(request, "federal_agency", None)),
cls.safe_get(getattr(request.senior_official, "first_name", None)),
cls.safe_get(getattr(request.senior_official, "last_name", None)),
cls.safe_get(getattr(request.senior_official, "email", None)),
cls.safe_get(getattr(request.senior_official, "title", None)),
cls.safe_get(getattr(request.creator, "first_name", None)),
cls.safe_get(getattr(request.creator, "last_name", None)),
cls.safe_get(getattr(request.creator, "email", None)),
cls.safe_get(getattr(request, "organization_name", None)),
cls.safe_get(getattr(request, "city", None)),
cls.safe_get(getattr(request, "state_territory", None)),
cls.safe_get(getattr(request, "purpose", None)),
cls.safe_get(getattr(request, "cisa_representative_email", None)),
cls.safe_get(getattr(request, "last_submitted_date", None)),
cls.safe_get(getattr(request, "first_submitted_date", None)),
cls.safe_get(getattr(request, "last_status_update", None)),
]
)
return response
class DomainDataFull(DomainExport): class DomainDataFull(DomainExport):
""" """
Shows security contacts, filtered by state Shows security contacts, filtered by state

View file

@ -169,6 +169,17 @@ class ExportDataTypeUser(View):
return response return response
class ExportDataTypeRequests(View):
"""Returns a domain requests report for a given user on the request"""
def get(self, request, *args, **kwargs):
response = HttpResponse(content_type="text/csv")
response["Content-Disposition"] = 'attachment; filename="domain-requests.csv"'
csv_export.DomainRequestsDataType.export_data_to_csv(response, request=request)
return response
class ExportDataFull(View): class ExportDataFull(View):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
# Smaller export based on 1 # Smaller export based on 1