Skip to content

Commit

Permalink
Merge pull request #90 from SADiLaR/feature/update-search-filters
Browse files Browse the repository at this point in the history
Feature/update search filters
  • Loading branch information
daniel-gray-tangent authored Jul 12, 2024
2 parents ae4fc6c + 5f700a7 commit b248001
Show file tree
Hide file tree
Showing 9 changed files with 402 additions and 97 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ jobs:
cp .env.testing app/.env
cd app/
mkdir -p static_files
python manage.py validate_templates
python manage.py validate_templates --ignore-app django_filters
- name: Run Tests
run: |
cp .env.testing app/.env
Expand Down
1 change: 1 addition & 0 deletions app/app/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"general",
"simple_history",
"accounts",
"django_filters",
]

# Add django-extensions to the installed apps if DEBUG is True
Expand Down
60 changes: 30 additions & 30 deletions app/app/views.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import os

from django.contrib.postgres.search import SearchHeadline, SearchQuery, SearchRank
from django.core.paginator import Paginator
from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
from django.db.models import Count
from django.http import HttpResponse
from django.shortcuts import get_object_or_404, render
from django.utils.http import urlencode
from django.utils.translation import gettext as _

from general.filters import DocumentFileFilter
from general.models import DocumentFile, Institution, Language, Project, Subject


Expand Down Expand Up @@ -230,39 +229,40 @@ def institutions(request):


def search(request):
q = request.GET.get("q")

if q:
queue = SearchQuery(q)
search_headline = SearchHeadline("document_data", queue)

documents = (
DocumentFile.objects.annotate(rank=SearchRank("search_vector", queue))
.annotate(search_headline=search_headline)
.filter(search_vector=queue)
.order_by("-rank")
)

else:
documents = None
page_number = request.GET.get("page", "1")
if not page_number.isdigit():
page_number = "1"

# Create a Paginator instance with the documents and the number of items per page
paginator = Paginator(documents, 10) if documents else None # Show 10 documents per page
f = DocumentFileFilter(request.GET, queryset=DocumentFile.objects.all())

# Get the page number from the request's GET parameters
page_number = request.GET.get("page")
template = "app/search.html"

# Use the get_page method to get the Page object for that page number
page_obj = paginator.get_page(page_number) if paginator else None
paginator = Paginator(f.qs, 5) # 5 documents per page

feature_flag = os.getenv("FEATURE_FLAG", False)
try:
page_obj = paginator.page(page_number)
except PageNotAnInteger:
page_obj = paginator.page(1)
except EmptyPage:
page_obj = paginator.page(paginator.num_pages)

template = "app/search.html"
context = {
"search_results": paginator.page(page_obj.number),
"filter": f,
"documents": page_obj,
"current_page": "search",
"document_count": len(documents) if documents else 0,
"feature_flag": feature_flag,
"search_params": pagination_url(request),
}

return render(request, template_name=template, context=context)


def pagination_url(request):
url_params = {
"search": request.GET.get("search", ""),
"document_type": request.GET.getlist("document_type", []),
"institution": request.GET.getlist("institution", []),
"subjects": request.GET.getlist("subjects", []),
"languages": request.GET.getlist("languages", []),
}

return "?" + urlencode(url_params, doseq=True)
59 changes: 59 additions & 0 deletions app/general/filters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import django_filters
from django import forms
from django.contrib.postgres.search import SearchHeadline, SearchQuery, SearchRank
from django.db.models import F
from django_filters import ModelMultipleChoiceFilter, MultipleChoiceFilter

from general.models import DocumentFile, Institution, Language, Subject


class DocumentFileFilter(django_filters.FilterSet):
search = django_filters.CharFilter(method="filter_search", label="Search")

institution = ModelMultipleChoiceFilter(
queryset=Institution.objects.all(), widget=forms.CheckboxSelectMultiple
)
document_type = MultipleChoiceFilter(
choices=DocumentFile.document_type_choices, widget=forms.CheckboxSelectMultiple
)
subjects = ModelMultipleChoiceFilter(
queryset=Subject.objects.all(), widget=forms.CheckboxSelectMultiple
)
languages = ModelMultipleChoiceFilter(
queryset=Language.objects.all(), widget=forms.CheckboxSelectMultiple
)

class Meta:
model = DocumentFile
fields = [
"document_type",
"institution",
"subjects",
"languages",
]

def filter_queryset(self, queryset):
queryset = super().filter_queryset(queryset)

search = self.form.cleaned_data.get("search", "")
queue = SearchQuery(search.strip())
search_rank = SearchRank(F("search_vector"), queue)
search_headline = SearchHeadline("document_data", queue)
queryset = (
queryset.annotate(
rank=search_rank,
search_headline=search_headline,
)
.defer("document_data")
.select_related("institution")
).order_by("-rank")

return queryset

def filter_search(self, queryset, name, value):
if value:
queue = SearchQuery(value.strip())
return queryset.filter(search_vector=queue)

else:
return queryset
91 changes: 91 additions & 0 deletions app/general/tests/test_filter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import unittest

from django.test import TestCase

from general.filters import DocumentFileFilter
from general.models import DocumentFile, Institution, Language, Subject


class TestSearchFilter(TestCase):
def setUp(self):
self.institution1 = Institution.objects.create(name="Institution 1")
self.institution2 = Institution.objects.create(name="Institution 2")

# Create languages
self.language1 = Language.objects.create(name="English", iso_code="EN")
self.language2 = Language.objects.create(name="Afrikaans", iso_code="AF")

# Create subjects
self.subject1 = Subject.objects.create(name="Science")
self.subject2 = Subject.objects.create(name="Math")

# Create DocumentFiles
self.doc1 = DocumentFile.objects.create(
title="Document 1",
document_data="Document 1 content",
institution=self.institution1,
document_type="glossary",
)
self.doc1.subjects.add(self.subject1)
self.doc1.languages.add(self.language1)

self.doc2 = DocumentFile.objects.create(
title="Document 2",
document_data="Document 2 content",
institution=self.institution2,
document_type="glossary",
)
self.doc2.subjects.add(self.subject2)
self.doc2.languages.add(self.language2)

def test_institution_filter(self):
data = {"institution": [self.institution1.id]}
filter = DocumentFileFilter(data=data)
self.assertEqual(len(filter.qs), 1)
self.assertIn(self.doc1, filter.qs)

def test_document_type_filter(self):
data = {"document_type": ["glossary"]}
filter = DocumentFileFilter(data=data)
self.assertEqual(len(filter.qs), 2)
self.assertIn(self.doc1, filter.qs)
self.assertIn(self.doc2, filter.qs)

def test_subjects_filter(self):
data = {"subjects": [self.subject1.id]}
filter = DocumentFileFilter(data=data)
self.assertEqual(len(filter.qs), 1)
self.assertIn(self.doc1, filter.qs)

def test_languages_filter(self):
data = {"languages": [self.language1.id]}
filter = DocumentFileFilter(data=data)
self.assertEqual(len(filter.qs), 1)
self.assertIn(self.doc1, filter.qs)

def test_combined_filters(self):
data = {
"institution": [self.institution1.id],
"document_type": ["glossary"],
"subjects": [self.subject1.id],
"languages": [self.language1.id],
}
filter = DocumentFileFilter(data=data)
self.assertEqual(len(filter.qs), 1)
self.assertIn(self.doc1, filter.qs)

def test_search_filter(self):
data = {"search": "Document"}
filter = DocumentFileFilter(data=data)
self.assertEqual(len(filter.qs), 2)
self.assertIn(self.doc1, filter.qs)
self.assertIn(self.doc2, filter.qs)

data = {"search": "Document 1"}
filter = DocumentFileFilter(data=data)
self.assertEqual(len(filter.qs), 1)
self.assertIn(self.doc1, filter.qs)


if __name__ == "__main__":
unittest.main()
75 changes: 75 additions & 0 deletions app/general/tests/test_view_search.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import unittest

from django.test import Client, TestCase
from django.urls import reverse

from general.models import DocumentFile, Institution, Language, Subject


class SearchViewTest(TestCase):
def setUp(self):
# Create institutions
self.institution1 = Institution.objects.create(name="Institution 1")
self.institution2 = Institution.objects.create(name="Institution 2")

# Create languages
self.language1 = Language.objects.create(name="English", iso_code="EN")
self.language2 = Language.objects.create(name="Afrikaans", iso_code="AF")

# Create subjects
self.subject1 = Subject.objects.create(name="Science")
self.subject2 = Subject.objects.create(name="Math")

# Create DocumentFiles
for i in range(10):
doc = DocumentFile.objects.create(
title=f"Document {i + 1}",
institution=self.institution1 if i % 2 == 0 else self.institution2,
document_type="report" if i % 2 == 0 else "article",
document_data="Document {i + 1} content",
)
doc.subjects.add(self.subject1 if i % 2 == 0 else self.subject2)
doc.languages.add(self.language1 if i % 2 == 0 else self.language2)

def test_search_pagination(self):
client = Client()
response = client.get(reverse("search"), {"page": "1"})
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.context["documents"]), 5)

response = client.get(reverse("search"), {"page": "2"})
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.context["documents"]), 5)

response = client.get(reverse("search"), {"page": "3"})
self.assertEqual(response.status_code, 200)

def test_search_filtering(self):
client = Client()
response = client.get(reverse("search"), {"search": "Document 1"})
self.assertEqual(response.status_code, 200)
self.assertEqual(response.context["documents"][0].title, "Document 1")

def test_invalid_page_number(self):
client = Client()
response = client.get(reverse("search"), {"page": "invalid"})
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.context["documents"]), 5)

def test_combined_filters(self):
client = Client()
response = client.get(
reverse("search"),
{
"institution": self.institution1.id,
"document_type": "report",
"subjects": self.subject1.id,
"languages": self.language1.id,
},
)
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.context["documents"]), 5)


if __name__ == "__main__":
unittest.main()
9 changes: 9 additions & 0 deletions app/static/css/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -611,3 +611,12 @@ html {
display: flex;
}
}

/*Search Css*/
.checkbox-container {
cursor: pointer;
max-height: 130px; /* Adjust based on your line-height and padding to show only 4 rows */
overflow-y: auto; /* Enable vertical scrollbar when content overflows */
border: 1px solid #ced4da; /* Bootstrap's form control border color */
border-radius: 0.25rem; /* Bootstrap's form control border radius */
}
Loading

0 comments on commit b248001

Please sign in to comment.