diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index cee182ac..48fd9ed2 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -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 diff --git a/app/app/settings.py b/app/app/settings.py index e03a5c95..db46e5d6 100644 --- a/app/app/settings.py +++ b/app/app/settings.py @@ -48,6 +48,7 @@ "general", "simple_history", "accounts", + "django_filters", ] # Add django-extensions to the installed apps if DEBUG is True diff --git a/app/app/views.py b/app/app/views.py index da58000f..f2ebf8eb 100644 --- a/app/app/views.py +++ b/app/app/views.py @@ -1,12 +1,11 @@ -import os - -from django.contrib.postgres.search import SearchHeadline, SearchQuery, SearchRank -from django.core.paginator import Paginator +# from pprint import pprint +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 @@ -230,39 +229,35 @@ def institutions(request): def search(request): - q = request.GET.get("q") + # f = DocumentFileFilter(request.GET, queryset=DocumentFile.objects.all().defer("document_data")) + f = DocumentFileFilter(request.GET, queryset=DocumentFile.objects.all()) - if q: - queue = SearchQuery(q) - search_headline = SearchHeadline("document_data", queue) + template = "app/search.html" - documents = ( - DocumentFile.objects.annotate(rank=SearchRank("search_vector", queue)) - .annotate(search_headline=search_headline) - .filter(search_vector=queue) - .order_by("-rank") - ) + # print(f.qs) - else: - documents = None + paginator = Paginator(f.qs, 5) # 5 documents per page - # 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 + print(paginator) - # Get the page number from the request's GET parameters page_number = request.GET.get("page") - # 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 - - feature_flag = os.getenv("FEATURE_FLAG", False) - - template = "app/search.html" + try: + # Get the page + page_obj = paginator.page(page_number) + except PageNotAnInteger: + # If page is not an integer, deliver first page + page_obj = paginator.page(1) + except EmptyPage: + # If page is out of range (e.g., 9999), deliver last page of results + page_obj = paginator.page(paginator.num_pages) + + print(page_obj) + # Update the context with the page object 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, } 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..70d53310 --- /dev/null +++ b/app/general/filters.py @@ -0,0 +1,51 @@ +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) + search_rank = SearchRank(F("search_vector"), queue) + + queryset = ( + queryset.annotate( + rank=search_rank, + ) + .defer("document_data") + .select_related("institution") + .filter(search_vector=queue) + .order_by("-rank") + ) + return queryset + + else: + return "" diff --git a/app/general/forms.py b/app/general/forms.py new file mode 100644 index 00000000..72c61c73 --- /dev/null +++ b/app/general/forms.py @@ -0,0 +1,39 @@ +# forms.py +import django_filters +from django import forms + +from general.models import DocumentFile + + +class DocumentFileSearchForm(forms.Form): + title = django_filters.CharFilter(lookup_expr="iexact") + # title = django_filters.CharFilter(lookup_expr='iexact') + + class Meta: + model = DocumentFile + fields = ["subjects", "languages"] + + +# class DocumentFileSearchForm(forms.Form): +# query = forms.CharField(label='Search', max_length=255, required=False) +# +# subjects = forms.MultipleChoiceField( +# choices=[], +# required=False, +# widget=forms.SelectMultiple +# ) +# +# languages = forms.MultipleChoiceField( +# choices=[], +# required=False, +# # widget=forms.CheckboxSelectMultiple +# widget=forms.SelectMultiple +# ) +# +# def __init__(self, *args, **kwargs): +# super().__init__(*args, **kwargs) +# +# self.fields['subjects'].choices = [(cat, cat) for cat in +# Subject.objects.values_list('name', flat=True).distinct()] +# self.fields['languages'].choices = [(brand, brand) for brand in +# Language.objects.values_list('name', flat=True).distinct()] diff --git a/app/static/css/styles.css b/app/static/css/styles.css index 059f1586..369f472a 100755 --- a/app/static/css/styles.css +++ b/app/static/css/styles.css @@ -54,44 +54,44 @@ html { width: 100%; } -.nav > li > a{ +.nav > li > a { margin-top: 15px; color: var(--text-color); } -.nav > li > .active{ +.nav > li > .active { margin-top: 15px; background-color: var(--primary) !important; color: var(--primary-fg); } -.nav > li > a:hover{ +.nav > li > a:hover { margin-top: 15px; background-color: var(--body-quiet-color); color: var(--primary-fg); } -.nav > li > a:active{ +.nav > li > a:active { margin-top: 15px; background-color: var(--primary); color: var(--primary-fg); } -.btn-primary{ - margin-top:20px; +.btn-primary { + margin-top: 20px; outline-color: var(--primary); background-color: var(--primary); border-color: var(--primary); } -.btn-outline-primary{ - margin-top:20px; +.btn-outline-primary { + margin-top: 20px; background: #2D4481; outline-color: var(--primary); } -.btn-primary:hover{ - margin-top:20px; +.btn-primary:hover { + margin-top: 20px; background: var(--body-quiet-color); outline-color: var(--primary); } @@ -104,7 +104,7 @@ html { color: var(--primary-fg); } -.section{ +.section { margin-left: 10px; margin-right: 10px; margin-bottom: 10px; @@ -121,7 +121,7 @@ html { color: var(--primary-fg) !important; } -.page-link:hover{ +.page-link:hover { background-color: var(--body-quiet-color) !important; color: var(--primary-fg) !important; } @@ -154,12 +154,13 @@ html { margin-top: 0.5rem; } -.btn-language{ +.btn-language { color: var(--text-color); outline-color: var(--primary-fg); background-color: var(--primary-fg); border-color: var(--primary-fg); } + /*Home page*/ .content-card { border-color: var(--primary-red); @@ -186,27 +187,33 @@ html { margin: 30px; font-size: 0.875rem; } + .filter-form label { margin-right: 10px; } + .filter-form .form-group { margin-right: 10px; } + .filter-form .form-control { border: 1px solid #000; } + .filter-form select { padding: 0 10px; width: 230px; } -.btn-apply, .btn-reset{ + +.btn-apply, .btn-reset { color: var(--primary-fg); outline-color: var(--primary); background-color: var(--primary); border-color: var(--primary); margin-right: 10px; } -.btn-apply:hover, .btn-reset:hover{ + +.btn-apply:hover, .btn-reset:hover { color: var(--primary-fg); outline-color: var(--primary); background-color: var(--primary); @@ -236,46 +243,55 @@ html { width: auto; } + .project-border { margin: 0 0 5px 0; overflow: hidden; padding: 0 5px 0 0; word-wrap: break-word; } + .project-left-col { width: 70%; } + .project-right-col { width: 30%; flex: 0 0 auto; display: flex; justify-content: flex-end; } + .project-header { display: flex; justify-content: space-between; align-items: center; max-width: 800px; } + .project-body { width: 100%; max-width: 800px; } + .card-text-description { word-wrap: break-word; overflow: hidden; font-size: 0.875rem; margin: 3px 0 3px 0; } + .project-row { display: flex; justify-content: space-between; width: 100%; } + .project-text { font-size: 1.25rem; } + .icon-text { font-size: 0.95rem; } @@ -286,13 +302,16 @@ html { margin: 0 auto; font-size: 0.875rem; } + .detail h1 { font-size: 2em; } + .detail a { text-decoration: none; color: var(--link-text); } + .project-detail a:hover, .detail-col ul li a:hover { text-decoration: underline; @@ -354,13 +373,13 @@ html { display: flex; justify-content: left; } + .project-date-row { - display: flex; - font-size: 0.875rem; + display: flex; + font-size: 0.875rem; } - /*Error pages*/ .error-card { border-color: var(--primary-red); @@ -372,6 +391,7 @@ html { .bi { font-size: 50px; } + .detail-icon { font-size: 20px; margin-right: 3px; @@ -383,8 +403,9 @@ html { .header-container { width: 100%; } + .section { - padding-right: 30px; + padding-right: 30px; } /*Institutions page*/ @@ -392,13 +413,16 @@ html { width: 100%; font-size: smaller; } + .all-cards { width: 98%; padding-right: 30px; } + .col-sm-6 { width: 50%; } + /*Home page*/ .content-card { font-size: smaller; @@ -419,8 +443,9 @@ html { .header-container { width: 100%; } + .section { - padding-right: 20px; + padding-right: 20px; } /*Institutions page*/ @@ -428,12 +453,15 @@ html { width: 100%; padding-right: 10px; } + .left-col { width: 70%; } + .right-col { width: 30%; } + .uni-card { width: 98%; height: 200px; @@ -458,13 +486,15 @@ html { .header-container { width: 100%; } + .section { - padding-right: 20px; + padding-right: 20px; } + .footer-class { flex-direction: row; justify-content: center; - } + } .language-toggle { margin-top: 0; @@ -491,13 +521,16 @@ html { padding-right: 20px; height: 200px; } + .all-cards { width: 100%; padding-right: 10px; } + .left-col { width: 70%; } + .right-col { width: 30%; } @@ -509,60 +542,82 @@ html { } } - /*Larger screens (desktops, 1001px and up)*/ -@media screen and (min-width: 1001px){ - /*General*/ - .section { - padding-right: 20px; - } - .footer-class { - flex-direction: row; - justify-content: center; - } - - .language-toggle { - margin-top: 0; - position: absolute; - right: 1rem; - } - - .language-toggle form { - display: flex; - flex-direction: row; - align-items: center; - } - - .language-toggle select, - .language-toggle .btn-language { - margin-left: 0.5rem; - margin-top: 0; - } - /*Institutions page*/ - .institution-card{ - overflow: hidden; - } - .all-cards { - width: 48%; - margin: 10px 10px 10px 10px; - float: left; - padding-right: 10px; - } - .left-col { +/*Larger screens (desktops, 1001px and up)*/ +@media screen and (min-width: 1001px) { + /*General*/ + .section { + padding-right: 20px; + } + + .footer-class { + flex-direction: row; + justify-content: center; + } + + .language-toggle { + margin-top: 0; + position: absolute; + right: 1rem; + } + + .language-toggle form { + display: flex; + flex-direction: row; + align-items: center; + } + + .language-toggle select, + .language-toggle .btn-language { + margin-left: 0.5rem; + margin-top: 0; + } + + /*Institutions page*/ + .institution-card { + overflow: hidden; + } + + .all-cards { + width: 48%; + margin: 10px 10px 10px 10px; + float: left; + padding-right: 10px; + } + + .left-col { width: 70%; } - .right-col { + + .right-col { width: 30%; } - .uni-card { - margin: 10px; - width: 100%; - height: 200px; - font-size: small; - } - - /*Projects page*/ - .filter-form select { - padding: 0 10px; - width: 230px; - } + + .uni-card { + margin: 10px; + width: 100%; + height: 200px; + font-size: small; + } + + /*Projects page*/ + .filter-form select { + padding: 0 10px; + width: 230px; + } +} + +/*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..c2735cd6 100644 --- a/app/templates/app/search.html +++ b/app/templates/app/search.html @@ -4,27 +4,25 @@ {% block content %} -