mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-25 20:18:38 +02:00
Merge pull request #2739 from cisagov/dk/2593-domain-request-search-bar
#2593: Domain request search bar and filter - [DK]
This commit is contained in:
commit
8adb33b430
5 changed files with 975 additions and 735 deletions
File diff suppressed because it is too large
Load diff
|
@ -23,13 +23,21 @@
|
||||||
</svg>
|
</svg>
|
||||||
Reset
|
Reset
|
||||||
</button>
|
</button>
|
||||||
|
{% if portfolio %}
|
||||||
|
<label class="usa-sr-only" for="domain-requests__search-field">Search by domain name or creator</label>
|
||||||
|
{% else %}
|
||||||
<label class="usa-sr-only" for="domain-requests__search-field">Search by domain name</label>
|
<label class="usa-sr-only" for="domain-requests__search-field">Search by domain name</label>
|
||||||
|
{% endif %}
|
||||||
<input
|
<input
|
||||||
class="usa-input"
|
class="usa-input"
|
||||||
id="domain-requests__search-field"
|
id="domain-requests__search-field"
|
||||||
type="search"
|
type="search"
|
||||||
name="search"
|
name="search"
|
||||||
|
{% if portfolio %}
|
||||||
|
placeholder="Search by domain name or creator"
|
||||||
|
{% else %}
|
||||||
placeholder="Search by domain name"
|
placeholder="Search by domain name"
|
||||||
|
{% endif %}
|
||||||
/>
|
/>
|
||||||
<button class="usa-button" type="submit" id="domain-requests__search-field-submit">
|
<button class="usa-button" type="submit" id="domain-requests__search-field-submit">
|
||||||
<img
|
<img
|
||||||
|
@ -42,6 +50,125 @@
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% if portfolio %}
|
||||||
|
<div class="display-flex flex-align-center">
|
||||||
|
<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__heading">
|
||||||
|
<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"
|
||||||
|
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>
|
||||||
|
{% 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>
|
||||||
|
|
|
@ -64,7 +64,7 @@
|
||||||
aria-expanded="false"
|
aria-expanded="false"
|
||||||
aria-controls="filter-status"
|
aria-controls="filter-status"
|
||||||
>
|
>
|
||||||
<span class="domain__filter-indicator text-bold display-none"></span> 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">
|
<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>
|
<use xlink:href="/public/img/sprite.svg#expand_more"></use>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
|
@ -458,3 +458,81 @@ class GetRequestsJsonTest(TestWithUser, WebTest):
|
||||||
# Ensure no approved requests are included
|
# Ensure no approved requests are included
|
||||||
for domain_request in data["domain_requests"]:
|
for domain_request in data["domain_requests"]:
|
||||||
self.assertNotEqual(domain_request["status"], DomainRequest.DomainRequestStatus.APPROVED)
|
self.assertNotEqual(domain_request["status"], DomainRequest.DomainRequestStatus.APPROVED)
|
||||||
|
|
||||||
|
def test_search(self):
|
||||||
|
"""Tests our search functionality. We expect that search filters on creator only when we are in a portfolio"""
|
||||||
|
# Test search for domain name
|
||||||
|
response = self.app.get(reverse("get_domain_requests_json"), {"search_term": "lamb"})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = response.json
|
||||||
|
self.assertEqual(len(data["domain_requests"]), 1)
|
||||||
|
|
||||||
|
requested_domain = data["domain_requests"][0]["requested_domain"]
|
||||||
|
self.assertEqual(requested_domain, "lamb-chops.gov")
|
||||||
|
|
||||||
|
# Test search for 'New domain request'
|
||||||
|
response = self.app.get(reverse("get_domain_requests_json"), {"search_term": "new domain"})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = response.json
|
||||||
|
self.assertTrue(any(req["requested_domain"] is None for req in data["domain_requests"]))
|
||||||
|
|
||||||
|
# Test search with portfolio (including creator search)
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
with override_flag("organization_feature", active=True), override_flag("organization_requests", active=True):
|
||||||
|
user_perm, _ = UserPortfolioPermission.objects.get_or_create(
|
||||||
|
user=self.user,
|
||||||
|
portfolio=self.portfolio,
|
||||||
|
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
|
||||||
|
)
|
||||||
|
response = self.app.get(
|
||||||
|
reverse("get_domain_requests_json"), {"search_term": "info", "portfolio": self.portfolio.id}
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = response.json
|
||||||
|
self.assertTrue(any(req["creator"].startswith("info") for req in data["domain_requests"]))
|
||||||
|
|
||||||
|
# Test search without portfolio (should not search on creator)
|
||||||
|
with override_flag("organization_feature", active=False), override_flag("organization_requests", active=False):
|
||||||
|
user_perm.delete()
|
||||||
|
response = self.app.get(reverse("get_domain_requests_json"), {"search_term": "info"})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = response.json
|
||||||
|
self.assertEqual(len(data["domain_requests"]), 0)
|
||||||
|
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
@override_flag("organization_requests", active=True)
|
||||||
|
def test_status_filter(self):
|
||||||
|
"""Test that status filtering works properly"""
|
||||||
|
# Test a single status
|
||||||
|
response = self.app.get(reverse("get_domain_requests_json"), {"status": "started"})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = response.json
|
||||||
|
self.assertTrue(all(req["status"] == "Started" for req in data["domain_requests"]))
|
||||||
|
|
||||||
|
# Test an invalid status
|
||||||
|
response = self.app.get(reverse("get_domain_requests_json"), {"status": "approved"})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = response.json
|
||||||
|
self.assertEqual(len(data["domain_requests"]), 0)
|
||||||
|
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
@override_flag("organization_requests", active=True)
|
||||||
|
def test_combined_filtering_and_sorting(self):
|
||||||
|
"""Test that combining filters and sorting works properly"""
|
||||||
|
user_perm, _ = UserPortfolioPermission.objects.get_or_create(
|
||||||
|
user=self.user,
|
||||||
|
portfolio=self.portfolio,
|
||||||
|
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
|
||||||
|
)
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
response = self.app.get(
|
||||||
|
reverse("get_domain_requests_json"),
|
||||||
|
{"search_term": "beef", "status": "started", "portfolio": self.portfolio.id},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = response.json
|
||||||
|
self.assertTrue(all("beef" in req["requested_domain"] for req in data["domain_requests"]))
|
||||||
|
self.assertTrue(all(req["status"] == "Started" for req in data["domain_requests"]))
|
||||||
|
created_at_dates = [req["created_at"] for req in data["domain_requests"]]
|
||||||
|
self.assertEqual(created_at_dates, sorted(created_at_dates, reverse=True))
|
||||||
|
user_perm.delete()
|
||||||
|
|
|
@ -20,6 +20,7 @@ def get_domain_requests_json(request):
|
||||||
unfiltered_total = objects.count()
|
unfiltered_total = objects.count()
|
||||||
|
|
||||||
objects = apply_search(objects, request)
|
objects = apply_search(objects, request)
|
||||||
|
objects = apply_status_filter(objects, request)
|
||||||
objects = apply_sorting(objects, request)
|
objects = apply_sorting(objects, request)
|
||||||
|
|
||||||
paginator = Paginator(objects, 10)
|
paginator = Paginator(objects, 10)
|
||||||
|
@ -63,6 +64,7 @@ def get_domain_request_ids_from_request(request):
|
||||||
|
|
||||||
def apply_search(queryset, request):
|
def apply_search(queryset, request):
|
||||||
search_term = request.GET.get("search_term")
|
search_term = request.GET.get("search_term")
|
||||||
|
is_portfolio = request.GET.get("portfolio")
|
||||||
|
|
||||||
if search_term:
|
if search_term:
|
||||||
search_term_lower = search_term.lower()
|
search_term_lower = search_term.lower()
|
||||||
|
@ -75,11 +77,34 @@ def apply_search(queryset, request):
|
||||||
queryset = queryset.filter(
|
queryset = queryset.filter(
|
||||||
Q(requested_domain__name__icontains=search_term) | Q(requested_domain__isnull=True)
|
Q(requested_domain__name__icontains=search_term) | Q(requested_domain__isnull=True)
|
||||||
)
|
)
|
||||||
|
elif is_portfolio:
|
||||||
|
queryset = queryset.filter(
|
||||||
|
Q(requested_domain__name__icontains=search_term)
|
||||||
|
| Q(creator__first_name__icontains=search_term)
|
||||||
|
| Q(creator__last_name__icontains=search_term)
|
||||||
|
| Q(creator__email__icontains=search_term)
|
||||||
|
)
|
||||||
|
# For non org users
|
||||||
else:
|
else:
|
||||||
queryset = queryset.filter(Q(requested_domain__name__icontains=search_term))
|
queryset = queryset.filter(Q(requested_domain__name__icontains=search_term))
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
|
def apply_status_filter(queryset, request):
|
||||||
|
status_param = request.GET.get("status")
|
||||||
|
if status_param:
|
||||||
|
status_list = status_param.split(",")
|
||||||
|
statuses = [status for status in status_list if status in DomainRequest.DomainRequestStatus.values]
|
||||||
|
# Construct Q objects for statuses that can be queried through ORM
|
||||||
|
status_query = Q()
|
||||||
|
if statuses:
|
||||||
|
status_query |= Q(status__in=statuses)
|
||||||
|
# Apply the combined query
|
||||||
|
queryset = queryset.filter(status_query)
|
||||||
|
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
def apply_sorting(queryset, request):
|
def apply_sorting(queryset, request):
|
||||||
sort_by = request.GET.get("sort_by", "id") # Default to 'id'
|
sort_by = request.GET.get("sort_by", "id") # Default to 'id'
|
||||||
order = request.GET.get("order", "asc") # Default to 'asc'
|
order = request.GET.get("order", "asc") # Default to 'asc'
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue