mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-24 19:48:36 +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>
|
||||
Reset
|
||||
</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>
|
||||
{% endif %}
|
||||
<input
|
||||
class="usa-input"
|
||||
id="domain-requests__search-field"
|
||||
type="search"
|
||||
name="search"
|
||||
{% if portfolio %}
|
||||
placeholder="Search by domain name or creator"
|
||||
{% else %}
|
||||
placeholder="Search by domain name"
|
||||
{% endif %}
|
||||
/>
|
||||
<button class="usa-button" type="submit" id="domain-requests__search-field-submit">
|
||||
<img
|
||||
|
@ -42,6 +50,125 @@
|
|||
</section>
|
||||
</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">
|
||||
<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>
|
||||
|
|
|
@ -64,7 +64,7 @@
|
|||
aria-expanded="false"
|
||||
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">
|
||||
<use xlink:href="/public/img/sprite.svg#expand_more"></use>
|
||||
</svg>
|
||||
|
|
|
@ -458,3 +458,81 @@ class GetRequestsJsonTest(TestWithUser, WebTest):
|
|||
# Ensure no approved requests are included
|
||||
for domain_request in data["domain_requests"]:
|
||||
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()
|
||||
|
||||
objects = apply_search(objects, request)
|
||||
objects = apply_status_filter(objects, request)
|
||||
objects = apply_sorting(objects, request)
|
||||
|
||||
paginator = Paginator(objects, 10)
|
||||
|
@ -63,6 +64,7 @@ def get_domain_request_ids_from_request(request):
|
|||
|
||||
def apply_search(queryset, request):
|
||||
search_term = request.GET.get("search_term")
|
||||
is_portfolio = request.GET.get("portfolio")
|
||||
|
||||
if search_term:
|
||||
search_term_lower = search_term.lower()
|
||||
|
@ -75,11 +77,34 @@ def apply_search(queryset, request):
|
|||
queryset = queryset.filter(
|
||||
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:
|
||||
queryset = queryset.filter(Q(requested_domain__name__icontains=search_term))
|
||||
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):
|
||||
sort_by = request.GET.get("sort_by", "id") # Default to 'id'
|
||||
order = request.GET.get("order", "asc") # Default to 'asc'
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue