diff --git a/app/app/views.py b/app/app/views.py index 8285f7ce..5efe5b2d 100644 --- a/app/app/views.py +++ b/app/app/views.py @@ -1,12 +1,13 @@ 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.translation import gettext as _ +from general.filters import DocumentFileFilter from general.models import DocumentFile, Institution, Language, Project, Subject @@ -266,3 +267,32 @@ def search(request): } return render(request, template_name=template, context=context) + + +def search(request): + page_number = request.GET.get("page", "1") + if not page_number.isdigit(): + page_number = "1" + + f = DocumentFileFilter(request.GET, queryset=DocumentFile.objects.all()) + + template = "app/search.html" + + ordered_queryset = f.qs.order_by("id") + + paginator = Paginator(ordered_queryset, 5) # 5 documents per page + + try: + page_obj = paginator.page(page_number) + except PageNotAnInteger: + page_obj = paginator.page(1) + except EmptyPage: + page_obj = paginator.page(paginator.num_pages) + + context = { + "search_results": paginator.page(page_obj.number), + "filter": f, + "documents": page_obj, + } + + return render(request, template_name=template, context=context) diff --git a/app/general/filters.py b/app/general/filters.py new file mode 100644 index 00000000..18cc44b6 --- /dev/null +++ b/app/general/filters.py @@ -0,0 +1,54 @@ +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_search(self, queryset, name, value): + if value: + queue = SearchQuery(value.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") + .filter(search_vector=queue) + .order_by("-rank", "id") + ) + return queryset + + else: + return queryset diff --git a/app/general/tests/test_filter.py b/app/general/tests/test_filter.py new file mode 100644 index 00000000..1cc5650c --- /dev/null +++ b/app/general/tests/test_filter.py @@ -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() diff --git a/app/general/tests/test_view_search.py b/app/general/tests/test_view_search.py new file mode 100644 index 00000000..5d0386ab --- /dev/null +++ b/app/general/tests/test_view_search.py @@ -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() diff --git a/app/static/css/styles.css b/app/static/css/styles.css index 438c5e2e..054d0a70 100755 --- a/app/static/css/styles.css +++ b/app/static/css/styles.css @@ -611,3 +611,19 @@ html { display: flex; } } + +/*Search Css*/ +.subjects-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 */ + /*padding: 0.375rem; !* Bootstrap's form control padding *!*/ + /*white-space: nowrap; !* Prevent text wrapping *!*/ +} + +.subjects-checkbox-container input[type="checkbox"] { + cursor: pointer; + +} diff --git a/app/templates/app/search.html b/app/templates/app/search.html index 55264498..f6cf93b7 100644 --- a/app/templates/app/search.html +++ b/app/templates/app/search.html @@ -4,27 +4,25 @@ {% block content %} -
-
-
-
-
{% trans "Search a term" %}
- {% if feature_flag == "search_feature" %} - {# test section start#} -
-
- -
- -
-
+
+
+
+
+
{% trans "Search a term" %}
+ + + + +
+ -
- {% trans "Results:" %} - {{ documents|length }} -
-
- {% for document in documents %} +
+
+
+ {% for document in search_results %} +
  • @@ -47,6 +45,10 @@
    {% trans "Search a term" %}
    {% trans "License:" %} {{ document.license }}
  • +
  • + {% trans "License:" %} + {{ document.document_type }} +
  • {% trans "Mime Type:" %} {{ document.mime_type }} @@ -59,29 +61,113 @@
    {% trans "Search a term" %}
{% endfor %}
- +
+
+ +
+
+
+ + + +
+ +
+
+
+ +
+ {% for checkbox in filter.form.institution %} +
+ {{ checkbox.tag }} + +
+ {% endfor %} +
+
+
+
+
+ +
+ {% for checkbox in filter.form.document_type %} +
+ {{ checkbox.tag }} + +
+ {% endfor %} +
+
+
+
+
+ +
+ {% for checkbox in filter.form.subjects %} +
+ {{ checkbox.tag }} + +
+ {% endfor %} +
+
+
+
+
+ +
+ {% for checkbox in filter.form.languages %} +
+ {{ checkbox.tag }} + +
+ {% endfor %} +
+
+
- {# test section end#} - {% else %} -

{% trans "Functionality of this page coming soon" %}

- {% endif %} + +
+
+ +
+
-
{% endblock content %}