Skip to content

Commit

Permalink
feat(search): Adds view to export search results
Browse files Browse the repository at this point in the history
  • Loading branch information
ERosendo committed Jan 16, 2025
1 parent d4b2f1a commit 92cddf5
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 1 deletion.
118 changes: 118 additions & 0 deletions cl/search/tests/tests_es_export.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
from django.core import mail
from django.core.management import call_command
from django.http import QueryDict
from django.urls import reverse

from cl.lib.search_utils import fetch_es_results_for_csv
from cl.lib.test_helpers import RECAPSearchTestCase
from cl.search.models import SEARCH_TYPES
from cl.tests.cases import ESIndexTestCase, TestCase
from cl.users.factories import UserProfileWithParentsFactory


class ExportSearchTest(RECAPSearchTestCase, ESIndexTestCase, TestCase):

errors = [
("Unbalance Quotes", 'q="test&type=o'),
("Unbalance Parentheses", "q=Leave)&type=o"),
("Bad syntax", "q=Leave /:&type=o"),
]

@classmethod
def setUpTestData(cls):
cls.user_profile = UserProfileWithParentsFactory()
cls.rebuild_index("search.Docket")
super().setUpTestData()
cls.rebuild_index("people_db.Person")
call_command(
"cl_index_parent_and_child_docs",
search_type=SEARCH_TYPES.RECAP,
queue="celery",
pk_offset=0,
testing_mode=True,
)

def test_returns_empty_list_when_query_with_error(self) -> None:
"""Confirms the search helper returns an empty list when provided with
invalid query parameters."""
for description, query in self.errors:
with self.subTest(description):
results = fetch_es_results_for_csv(
QueryDict(query.encode(), mutable=True),
SEARCH_TYPES.OPINION,
)
self.assertEqual(len(results), 0)

def test_limit_number_of_search_results(self) -> None:
"""Checks hat `fetch_es_results_for_csv` returns a list with a size
equal to MAX_SEARCH_RESULTS_EXPORTED or the actual number of search
results (if it's less than `MAX_SEARCH_RESULTS_EXPORTED`)
"""
# This query should match all 5 judges indexed for this test
query = "q=gender:Female&type=p"
for i in range(6):
with self.subTest(
f"try to fetch only {i+1} results"
), self.settings(MAX_SEARCH_RESULTS_EXPORTED=i + 1):
results = fetch_es_results_for_csv(
QueryDict(query.encode(), mutable=True),
SEARCH_TYPES.PEOPLE,
)
expected_result_count = min(
i + 1, 5
) # Cap at 5 (total matching results)
self.assertEqual(len(results), expected_result_count)

def test_can_flatten_nested_results(self) -> None:
"""checks `fetch_es_results_for_csv` correctly handles and flattens
nested results."""
# this query should match both docket records indexed
query = "type=r&q=12-1235 OR Jackson"
results = fetch_es_results_for_csv(
QueryDict(query.encode(), mutable=True), SEARCH_TYPES.RECAP
)
# We expect 3 results because:
# - Docket 21-bk-1234 has 2 associated documents.
# - Docket 12-1235 has 1 associated document.
#
# The `fetch_es_results_for_csv` helper function should:
# - Flatten the results.
# - Add a row for each child document.
self.assertEqual(len(results), 3)

def test_avoids_sending_email_for_query_with_error(self) -> None:
"Confirms we don't send emails when provided with invalid query"
self.client.login(
username=self.user_profile.user.username, password="password"
)
for description, query in self.errors:
with self.subTest(description):
self.client.post(
reverse("export_search_results"), {"query": query}
)
self.assertEqual(len(mail.outbox), 0)

def test_do_not_send_empty_emails(self) -> None:
"""Confirms that no emails are sent when the search query returns no
results"""
self.client.login(
username=self.user_profile.user.username, password="password"
)
self.client.post(
reverse("export_search_results"), {"query": 'q="word"&type=r'}
)
self.assertEqual(len(mail.outbox), 0)

def test_sends_email_with_attachment(self) -> None:
"Confirms we dont send emails when provided with invalid query"
self.client.login(
username=self.user_profile.user.username, password="password"
)
self.client.post(
reverse("export_search_results"), {"query": 'q="Jackson"&type=r'}
)
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(
mail.outbox[0].subject, "Your Search Results are Ready!"
)
self.assertEqual(mail.outbox[0].to[0], self.user_profile.user.email)
10 changes: 9 additions & 1 deletion cl/search/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,19 @@
SearchFeed,
search_feed_error_handler,
)
from cl.search.views import advanced, es_search, show_results
from cl.search.views import (
advanced,
es_search,
export_search_results,
show_results,
)

urlpatterns = [
# Search pages
path("", show_results, name="show_results"),
path(
"search/export/", export_search_results, name="export_search_results"
),
path("opinion/", advanced, name="advanced_o"),
path("audio/", advanced, name="advanced_oa"),
path("person/", advanced, name="advanced_p"),
Expand Down
15 changes: 15 additions & 0 deletions cl/search/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from asgiref.sync import async_to_sync
from cache_memoize import cache_memoize
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from django.db.models import Count, Sum
from django.http import HttpRequest, HttpResponse
Expand All @@ -12,6 +13,7 @@
from django.urls import reverse
from django.utils.timezone import make_aware
from django.views.decorators.cache import never_cache
from django.views.decorators.http import require_POST
from waffle.decorators import waffle_flag

from cl.alerts.forms import CreateAlertForm
Expand All @@ -20,16 +22,19 @@
from cl.custom_filters.templatetags.text_filters import naturalduration
from cl.lib.bot_detector import is_bot
from cl.lib.elasticsearch_utils import get_only_status_facets
from cl.lib.ratelimiter import ratelimiter_unsafe_5_per_d
from cl.lib.redis_utils import get_redis_interface
from cl.lib.search_utils import (
do_es_search,
make_get_string,
merge_form_with_courts,
store_search_query,
)
from cl.lib.types import AuthenticatedHttpRequest
from cl.search.documents import OpinionClusterDocument
from cl.search.forms import SearchForm, _clean_form
from cl.search.models import SEARCH_TYPES, Court, Opinion
from cl.search.tasks import email_search_results
from cl.stats.models import Stat
from cl.stats.utils import tally_stat
from cl.visualizations.models import SCOTUSMap
Expand Down Expand Up @@ -361,3 +366,13 @@ def es_search(request: HttpRequest) -> HttpResponse:
)

return render(request, template, render_dict)


@login_required
@ratelimiter_unsafe_5_per_d
@require_POST
def export_search_results(request: AuthenticatedHttpRequest) -> HttpResponse:
email_search_results.delay(request.user.pk, request.POST.get("query", ""))
# TODO: Update the frontend using Htmx to show a message indicating the
# export of search results is in progress.
return HttpResponse("It worked.")

0 comments on commit 92cddf5

Please sign in to comment.