Skip to content

Commit a06a08b

Browse files
authored
Merge branch 'main' into add-unaccent-to-search-filter
2 parents 69967f9 + 99f619f commit a06a08b

1 file changed

Lines changed: 107 additions & 0 deletions

File tree

tests/test_filters.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,109 @@ class SearchListView(generics.ListAPIView):
311311
]
312312

313313

314+
@pytest.mark.requires_postgres
315+
class SearchFilterFullTextTests(TestCase):
316+
@classmethod
317+
def setUpTestData(cls):
318+
SearchFilterModel.objects.create(title='The quick brown fox', text='jumps over the lazy dog')
319+
SearchFilterModel.objects.create(title='The slow brown turtle', text='crawls under the fence')
320+
SearchFilterModel.objects.create(title='A bright sunny day', text='in the park with friends')
321+
322+
def test_full_text_search_single_term(self):
323+
class SearchListView(generics.ListAPIView):
324+
queryset = SearchFilterModel.objects.all()
325+
serializer_class = SearchFilterSerializer
326+
filter_backends = (filters.SearchFilter,)
327+
search_fields = ('@title',)
328+
329+
view = SearchListView.as_view()
330+
request = factory.get('/', {'search': 'fox'})
331+
response = view(request)
332+
assert len(response.data) == 1
333+
assert response.data[0]['title'] == 'The quick brown fox'
334+
335+
def test_full_text_search_multiple_results(self):
336+
class SearchListView(generics.ListAPIView):
337+
queryset = SearchFilterModel.objects.all()
338+
serializer_class = SearchFilterSerializer
339+
filter_backends = (filters.SearchFilter,)
340+
search_fields = ('@title',)
341+
342+
view = SearchListView.as_view()
343+
request = factory.get('/', {'search': 'brown'})
344+
response = view(request)
345+
assert len(response.data) == 2
346+
titles = {item['title'] for item in response.data}
347+
assert titles == {'The quick brown fox', 'The slow brown turtle'}
348+
349+
def test_full_text_search_no_results(self):
350+
class SearchListView(generics.ListAPIView):
351+
queryset = SearchFilterModel.objects.all()
352+
serializer_class = SearchFilterSerializer
353+
filter_backends = (filters.SearchFilter,)
354+
search_fields = ('@title',)
355+
356+
view = SearchListView.as_view()
357+
request = factory.get('/', {'search': 'elephant'})
358+
response = view(request)
359+
assert len(response.data) == 0
360+
361+
def test_full_text_search_multiple_fields(self):
362+
class SearchListView(generics.ListAPIView):
363+
queryset = SearchFilterModel.objects.all()
364+
serializer_class = SearchFilterSerializer
365+
filter_backends = (filters.SearchFilter,)
366+
search_fields = ('@title', '@text')
367+
368+
view = SearchListView.as_view()
369+
request = factory.get('/', {'search': 'lazy'})
370+
response = view(request)
371+
assert len(response.data) == 1
372+
assert response.data[0]['title'] == 'The quick brown fox'
373+
374+
def test_full_text_search_stemming(self):
375+
"""Full text search should match stemmed words (e.g. 'jumping' matches 'jumps')."""
376+
class SearchListView(generics.ListAPIView):
377+
queryset = SearchFilterModel.objects.all()
378+
serializer_class = SearchFilterSerializer
379+
filter_backends = (filters.SearchFilter,)
380+
search_fields = ('@text',)
381+
382+
view = SearchListView.as_view()
383+
request = factory.get('/', {'search': 'jumping'})
384+
response = view(request)
385+
assert len(response.data) == 1
386+
assert response.data[0]['text'] == 'jumps over the lazy dog'
387+
388+
def test_full_text_search_multiple_terms(self):
389+
"""Each search term must match (AND semantics across terms)."""
390+
class SearchListView(generics.ListAPIView):
391+
queryset = SearchFilterModel.objects.all()
392+
serializer_class = SearchFilterSerializer
393+
filter_backends = (filters.SearchFilter,)
394+
search_fields = ('@title', '@text')
395+
396+
view = SearchListView.as_view()
397+
request = factory.get('/', {'search': 'brown lazy'})
398+
response = view(request)
399+
assert len(response.data) == 1
400+
assert response.data[0]['title'] == 'The quick brown fox'
401+
402+
def test_full_text_search_mixed_with_icontains(self):
403+
"""Full text search fields can be mixed with regular icontains fields."""
404+
class SearchListView(generics.ListAPIView):
405+
queryset = SearchFilterModel.objects.all()
406+
serializer_class = SearchFilterSerializer
407+
filter_backends = (filters.SearchFilter,)
408+
search_fields = ('@title', 'text')
409+
410+
view = SearchListView.as_view()
411+
request = factory.get('/', {'search': 'park'})
412+
response = view(request)
413+
assert len(response.data) == 1
414+
assert response.data[0]['title'] == 'A bright sunny day'
415+
416+
314417
class AttributeModel(models.Model):
315418
label = models.CharField(max_length=32)
316419

@@ -359,6 +462,10 @@ def test_custom_lookup_to_related_model(self):
359462
assert 'attribute__label__icontains' == filter_.construct_search('attribute__label', SearchFilterModelFk._meta)
360463
assert 'attribute__label__iendswith' == filter_.construct_search('attribute__label__iendswith', SearchFilterModelFk._meta)
361464

465+
def test_construct_search_with_at_prefix(self):
466+
filter_ = filters.SearchFilter()
467+
assert 'title__search' == filter_.construct_search('@title', SearchFilterModelFk._meta)
468+
362469

363470
class SearchFilterModelM2M(models.Model):
364471
title = models.CharField(max_length=20)

0 commit comments

Comments
 (0)