diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bbdffc03..f1028b1b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -45,6 +45,7 @@ repos: - types-django-import-export - django-multiselectfield - django-tables2 + - django-filter - repo: https://github.com/igorshubovych/markdownlint-cli rev: v0.50.0 hooks: diff --git a/dev-requirements.txt b/dev-requirements.txt index 35e1b3f7..394a7979 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,11 +1,13 @@ # -# This file is autogenerated by pip-compile with Python 3.12 +# This file is autogenerated by pip-compile with Python 3.14 # by the following command: # -# pip-compile --extra=dev --output-file=dev-requirements.txt +# pip-compile --extra=dev --no-index --output-file=dev-requirements.txt # asgiref==3.13.0 # via django +ast-serialize==0.13.1 + # via mypy beautifulsoup4==4.15.0 # via direct_webapp (pyproject.toml) build==1.6.1 @@ -20,8 +22,6 @@ click==8.5.0 # via # djlint # pip-tools -colorama==0.4.6 - # via djlint confusable-homoglyphs==3.3.1 # via django-registration coverage[toml]==7.17.0 @@ -40,6 +40,7 @@ django==6.1.2 # direct_webapp (pyproject.toml) # django-bootstrap5 # django-crispy-forms + # django-filter # django-import-export # django-multiselectfield # django-registration @@ -52,6 +53,8 @@ django-crispy-forms==2.7 # via # crispy-bootstrap5 # direct_webapp (pyproject.toml) +django-filter==26.2 + # via direct_webapp (pyproject.toml) django-import-export==4.4.4 # via direct_webapp (pyproject.toml) django-multiselectfield==1.0.1 @@ -109,8 +112,8 @@ nodeenv==1.11.0 packaging==26.3 # via # build - # gunicorn # pytest + # virtualenv # wheel pathspec==1.1.1 # via @@ -119,9 +122,7 @@ pathspec==1.1.1 pip-tools==7.6.2 # via direct_webapp (pyproject.toml) platformdirs==4.14.0 - # via - # python-discovery - # virtualenv + # via virtualenv pluggy==1.7.0 # via # pytest @@ -158,18 +159,12 @@ requests==2.34.2 # via direct_webapp (pyproject.toml) ruff==0.17.3 # via direct_webapp (pyproject.toml) -six==1.17.0 - # via - # cssbeautifier - # jsbeautifier soupsieve==3.0.4 # via beautifulsoup4 sqlparse==0.6.0 # via django tablib==3.10.0 # via django-import-export -tqdm==4.70.1 - # via djlint types-django-import-export==4.4.0.20260813 # via direct_webapp (pyproject.toml) types-markdown==3.11.2.20261029 diff --git a/direct_webapp/settings/settings.py b/direct_webapp/settings/settings.py index b1604956..36fd91e2 100644 --- a/direct_webapp/settings/settings.py +++ b/direct_webapp/settings/settings.py @@ -128,6 +128,7 @@ "crispy_bootstrap5", "import_export", "django_tables2", + "django_filters", ] MIDDLEWARE.insert(1, "whitenoise.middleware.WhiteNoiseMiddleware") STATIC_ROOT = BASE_DIR / "staticfiles" diff --git a/doc-requirements.txt b/doc-requirements.txt index c91b3187..ec37ece4 100644 --- a/doc-requirements.txt +++ b/doc-requirements.txt @@ -1,8 +1,8 @@ # -# This file is autogenerated by pip-compile with Python 3.12 +# This file is autogenerated by pip-compile with Python 3.14 # by the following command: # -# pip-compile --extra=doc --output-file=doc-requirements.txt +# pip-compile --extra=doc --no-index --output-file=doc-requirements.txt # asgiref==3.13.0 # via django @@ -32,6 +32,7 @@ django==6.1.2 # direct_webapp (pyproject.toml) # django-bootstrap5 # django-crispy-forms + # django-filter # django-import-export # django-multiselectfield # django-registration @@ -43,6 +44,8 @@ django-crispy-forms==2.7 # via # crispy-bootstrap5 # direct_webapp (pyproject.toml) +django-filter==26.2 + # via direct_webapp (pyproject.toml) django-import-export==4.4.4 # via direct_webapp (pyproject.toml) django-multiselectfield==1.0.1 @@ -124,7 +127,6 @@ nh3==0.3.7 # via direct_webapp (pyproject.toml) packaging==26.3 # via - # gunicorn # mkdocs # properdocs paginate==0.5.7 @@ -173,6 +175,8 @@ tablib==3.10.0 # via django-import-export typing-extensions==4.16.0 # via django-stubs-ext +tzdata==2026.6 + # via python-dateutil urllib3==2.8.0 # via requests watchdog==6.0.0 diff --git a/main/filters.py b/main/filters.py new file mode 100644 index 00000000..afed98d3 --- /dev/null +++ b/main/filters.py @@ -0,0 +1,43 @@ +"""Filters for browsing learning resources.""" + +from django import forms +from django.db.models import Q, QuerySet +from django_filters import CharFilter, FilterSet + +from .models import LearningResource + + +class LearningResourceFilter(FilterSet): + """Search learning resources through a single cross-field search box.""" + + q = CharFilter( + method="filter_search", + label="Search learning resources", + widget=forms.SearchInput( + attrs={ + "id": "resource-filter", + "placeholder": "Filter learning resources", + } + ), + ) + + class Meta: + """Limit filtering to the explicitly supported query parameters.""" + + model = LearningResource + fields = ("q",) + + def filter_search( + self, + queryset: QuerySet[LearningResource], + name: str, + value: str, + ) -> QuerySet[LearningResource]: + """Search resource details and related names without duplicate rows.""" + return queryset.filter( + Q(name__icontains=value) + | Q(description__icontains=value) + | Q(language__icontains=value) + | Q(provider__name__icontains=value) + | Q(skill__name__icontains=value) + ).distinct() diff --git a/main/templates/main/pages/learning-resources.html b/main/templates/main/pages/learning-resources.html index 0ab0642a..f44771a0 100644 --- a/main/templates/main/pages/learning-resources.html +++ b/main/templates/main/pages/learning-resources.html @@ -1,5 +1,6 @@ {% extends "main/base.page.html" %} {% load static %} +{% load crispy_forms_tags %} {% load render_table from django_tables2 %} {% block title %} Learning Resources @@ -16,17 +17,14 @@