From 1bd887567e6fd2f4f6393ff9e125a47926038143 Mon Sep 17 00:00:00 2001 From: Sli Date: Mon, 29 Jul 2024 10:23:30 +0200 Subject: [PATCH] Use full text search in pedagogy uv search api --- pedagogy/schemas.py | 23 +++++++++++++++++++ pedagogy/tests/test_api.py | 46 +++++++++++++++++++++++++++++--------- 2 files changed, 59 insertions(+), 10 deletions(-) diff --git a/pedagogy/schemas.py b/pedagogy/schemas.py index 9729aa6d..716e9a3c 100644 --- a/pedagogy/schemas.py +++ b/pedagogy/schemas.py @@ -1,6 +1,8 @@ from typing import Literal from django.db.models import Q +from django.utils import html +from haystack.query import SearchQuerySet from ninja import FilterSchema, ModelSchema, Schema from pydantic import AliasPath, ConfigDict, Field, TypeAdapter from pydantic.alias_generators import to_camel @@ -120,6 +122,27 @@ class UvFilterSchema(FilterSchema): language: str = "FR" department: set[str] | None = Field(None, q="department__in") + def filter_search(self, value: str | None) -> Q: + """Special filter for the search text. + + It does a full text search if available. + """ + if not value: + return Q() + + if len(value) < 3 or (len(value) < 5 and any(c.isdigit() for c in value)): + # Likely to be an UV code + return Q(code__istartswith=value) + + qs = list( + SearchQuerySet() + .models(UV) + .autocomplete(auto=html.escape(value)) + .values_list("pk", flat=True) + ) + + return Q(id__in=qs) + def filter_semester(self, value: set[str] | None) -> Q: """Special filter for the semester. diff --git a/pedagogy/tests/test_api.py b/pedagogy/tests/test_api.py index 0ad920e3..f0667bd5 100644 --- a/pedagogy/tests/test_api.py +++ b/pedagogy/tests/test_api.py @@ -2,6 +2,7 @@ import json from django.conf import settings from django.test import TestCase +from django.test.testcases import call_command from django.urls import reverse from model_bakery import baker from model_bakery.recipe import Recipe @@ -21,16 +22,31 @@ class TestUVSearch(TestCase): uv_recipe = Recipe(UV, author=cls.root) uvs = [ uv_recipe.prepare( - code="AP4A", credit_type="CS", semester="AUTUMN", department="GI" + code="AP4A", + credit_type="CS", + semester="AUTUMN", + department="GI", + manager="francky", + title="Programmation Orientée Objet: Concepts fondamentaux et mise en pratique avec le langage C++", ), uv_recipe.prepare( - code="MT01", credit_type="CS", semester="AUTUMN", department="TC" + code="MT01", + credit_type="CS", + semester="AUTUMN", + department="TC", + manager="ben", + title="Intégration1. Algèbre linéaire - Fonctions de deux variables", ), uv_recipe.prepare( code="PHYS11", credit_type="CS", semester="AUTUMN", department="TC" ), uv_recipe.prepare( - code="TNEV", credit_type="TM", semester="SPRING", department="TC" + code="TNEV", + credit_type="TM", + semester="SPRING", + department="TC", + manager="moss", + title="tnetennba", ), uv_recipe.prepare( code="MT10", credit_type="TM", semester="AUTUMN", department="IMSI" @@ -40,9 +56,11 @@ class TestUVSearch(TestCase): credit_type="TM", semester="AUTUMN_AND_SPRING", department="GI", + manager="francky", ), ] UV.objects.bulk_create(uvs) + call_command("update_index") def test_permissions(self): # Test with anonymous user @@ -92,14 +110,22 @@ class TestUVSearch(TestCase): ], } - def test_search_by_code(self): + def test_search_by_text(self): self.client.force_login(self.root) - res = self.client.get(self.url + "?search=MT") - assert res.status_code == 200 - assert {uv["code"] for uv in json.loads(res.content)["results"]} == { - "MT01", - "MT10", - } + for query, expected in ( + # UV code search case insensitive + ("m", {"MT01", "MT10"}), + ("M", {"MT01", "MT10"}), + ("mt", {"MT01", "MT10"}), + ("MT", {"MT01", "MT10"}), + ("algèbre", {"MT01"}), # Title search case insensitive + # Manager search + ("moss", {"TNEV"}), + ("francky", {"DA50", "AP4A"}), + ): + res = self.client.get(self.url + f"?search={query}") + assert res.status_code == 200 + assert {uv["code"] for uv in json.loads(res.content)["results"]} == expected def test_search_by_credit_type(self): self.client.force_login(self.root)