From 061320a5df07802d5be8cd3eb41453067014ffeb Mon Sep 17 00:00:00 2001 From: Bartuccio Antoine Date: Tue, 18 Jun 2019 21:41:11 +0200 Subject: [PATCH 1/7] pedagogy: search index for uvs and search api --- pedagogy/search_indexes.py | 58 +++++++++++++++++++ .../search/indexes/pedagogy/uv_auto.txt | 2 + .../search/indexes/pedagogy/uv_text.txt | 2 + pedagogy/views.py | 24 ++++++++ 4 files changed, 86 insertions(+) create mode 100644 pedagogy/search_indexes.py create mode 100644 pedagogy/templates/search/indexes/pedagogy/uv_auto.txt create mode 100644 pedagogy/templates/search/indexes/pedagogy/uv_text.txt diff --git a/pedagogy/search_indexes.py b/pedagogy/search_indexes.py new file mode 100644 index 00000000..604987d4 --- /dev/null +++ b/pedagogy/search_indexes.py @@ -0,0 +1,58 @@ +# -*- coding:utf-8 -* +# +# Copyright 2017 +# - Sli +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a published by the Free Software +# Foundation; either version 3 of the License, or (at your option) any later +# version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + +from django.db import models + +from haystack import indexes, signals + +from core.search_indexes import BigCharFieldIndex +from pedagogy.models import UV + + +class IndexSignalProcessor(signals.BaseSignalProcessor): + """ + Auto update index on CRUD operations + """ + + def setup(self): + # Listen only to the ``UV`` model. + models.signals.post_save.connect(self.handle_save, sender=UV) + models.signals.post_delete.connect(self.handle_delete, sender=UV) + + def teardown(self): + # Disconnect only to the ``UV`` model. + models.signals.post_save.disconnect(self.handle_save, sender=UV) + models.signals.post_delete.disconnect(self.handle_delete, sender=UV) + + +class UVIndex(indexes.SearchIndex, indexes.Indexable): + """ + Indexer class for UVs + """ + + text = BigCharFieldIndex(document=True, use_template=True) + auto = indexes.EdgeNgramField(use_template=True) + + def get_model(self): + return UV diff --git a/pedagogy/templates/search/indexes/pedagogy/uv_auto.txt b/pedagogy/templates/search/indexes/pedagogy/uv_auto.txt new file mode 100644 index 00000000..9472642f --- /dev/null +++ b/pedagogy/templates/search/indexes/pedagogy/uv_auto.txt @@ -0,0 +1,2 @@ +{{ object.code }} +{{ object.manager }} \ No newline at end of file diff --git a/pedagogy/templates/search/indexes/pedagogy/uv_text.txt b/pedagogy/templates/search/indexes/pedagogy/uv_text.txt new file mode 100644 index 00000000..0bd9adb6 --- /dev/null +++ b/pedagogy/templates/search/indexes/pedagogy/uv_text.txt @@ -0,0 +1,2 @@ +{{ object.code }} +{{ object.manager }} diff --git a/pedagogy/views.py b/pedagogy/views.py index 38309a56..234e12b7 100644 --- a/pedagogy/views.py +++ b/pedagogy/views.py @@ -32,6 +32,7 @@ from django.views.generic import ( View, ) from django.core.urlresolvers import reverse_lazy +from django.utils import html from core.views import ( DetailFormView, @@ -41,6 +42,8 @@ from core.views import ( CanEditPropMixin, ) +from haystack.query import SearchQuerySet + from pedagogy.forms import UVForm, UVCommentForm from pedagogy.models import UV, UVComment @@ -144,6 +147,27 @@ class UVListView(CanViewMixin, CanCreateUVFunctionMixin, ListView): ordering = ["code"] template_name = "pedagogy/guide.jinja" + def get_queryset(self): + query = self.request.GET.get("query", None) + + if not query: + return super(UVListView, self).get_queryset() + + try: + queryset = ( + SearchQuerySet() + .models(self.model) + .autocomplete(auto=html.escape(query)) + ) + except TypeError: + return self.model.objects.none() + + return ( + super(UVListView, self) + .get_queryset() + .filter(id__in=([o.object.id for o in queryset])) + ) + class UVCommentReportCreateView(CreateView): """ From e11d45b51e357ba9a39aed1d1c843f8d722e8007 Mon Sep 17 00:00:00 2001 From: Bartuccio Antoine Date: Tue, 18 Jun 2019 21:48:13 +0200 Subject: [PATCH 2/7] pedagogy: more details on uv_detail for tests purpose --- pedagogy/templates/pedagogy/uv_detail.jinja | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pedagogy/templates/pedagogy/uv_detail.jinja b/pedagogy/templates/pedagogy/uv_detail.jinja index 42cb6580..e18f1b1d 100644 --- a/pedagogy/templates/pedagogy/uv_detail.jinja +++ b/pedagogy/templates/pedagogy/uv_detail.jinja @@ -12,6 +12,7 @@

{{ object.program|markdown }}

{{ object.skills|markdown }}

{{ object.key_concepts|markdown }}

+

{% trans %}UV manager: {% endtrans %}{{ object.manager }}

{% if object.comments.exists() %}

{% trans %}Comments{% endtrans %}

@@ -21,7 +22,7 @@

{{ comment.grade_interest }}

{{ comment.grade_teaching }}

{{ comment.grade_work_load }}

-

{{ comment.comment }}

+

{{ comment.comment|markdown }}

{% trans %}Published: {% endtrans %}{{ comment.publish_date }}

{% trans %}Author: {% endtrans %}{{ comment.author }}

{% if user.is_owner(comment) %} From 2cbef2babc04c5ab11aaa3e616859f68f46fa899 Mon Sep 17 00:00:00 2001 From: Bartuccio Antoine Date: Wed, 19 Jun 2019 00:56:59 +0200 Subject: [PATCH 3/7] pedagogy: support json response from search API --- pedagogy/views.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/pedagogy/views.py b/pedagogy/views.py index 234e12b7..fd3d9ed3 100644 --- a/pedagogy/views.py +++ b/pedagogy/views.py @@ -31,8 +31,10 @@ from django.views.generic import ( FormView, View, ) -from django.core.urlresolvers import reverse_lazy +from django.core import serializers from django.utils import html +from django.http import HttpResponse +from django.core.urlresolvers import reverse_lazy from core.views import ( DetailFormView, @@ -147,6 +149,17 @@ class UVListView(CanViewMixin, CanCreateUVFunctionMixin, ListView): ordering = ["code"] template_name = "pedagogy/guide.jinja" + def get(self, *args, **kwargs): + if not self.request.GET.get("json", None): + # Return normal full template response + return super(UVListView, self).get(*args, **kwargs) + + # Return serialized response + return HttpResponse( + serializers.serialize("json", self.get_queryset()), + content_type="application/json", + ) + def get_queryset(self): query = self.request.GET.get("query", None) From 502ae0952311b644c126067a3a48d0cde68fa4a7 Mon Sep 17 00:00:00 2001 From: Bartuccio Antoine Date: Wed, 19 Jun 2019 01:26:11 +0200 Subject: [PATCH 4/7] pedagogy: add filters to search api --- pedagogy/views.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/pedagogy/views.py b/pedagogy/views.py index fd3d9ed3..4cb06506 100644 --- a/pedagogy/views.py +++ b/pedagogy/views.py @@ -150,9 +150,10 @@ class UVListView(CanViewMixin, CanCreateUVFunctionMixin, ListView): template_name = "pedagogy/guide.jinja" def get(self, *args, **kwargs): + resp = super(UVListView, self).get(*args, **kwargs) if not self.request.GET.get("json", None): # Return normal full template response - return super(UVListView, self).get(*args, **kwargs) + return resp # Return serialized response return HttpResponse( @@ -163,8 +164,22 @@ class UVListView(CanViewMixin, CanCreateUVFunctionMixin, ListView): def get_queryset(self): query = self.request.GET.get("query", None) + additional_filters = {} + + for filter_type in ["credit_type", "language", "department"]: + arg = self.request.GET.get(filter_type, None) + if arg: + additional_filters[filter_type] = arg + + semester = self.request.GET.get("semester", None) + if semester: + if semester in ["AUTUMN", "SPRING"]: + additional_filters["semester__in"] = [semester, "AUTOMN_AND_SPRING"] + else: + additional_filters["semester"] = semester + if not query: - return super(UVListView, self).get_queryset() + return super(UVListView, self).get_queryset().filter(**additional_filters) try: queryset = ( @@ -179,6 +194,7 @@ class UVListView(CanViewMixin, CanCreateUVFunctionMixin, ListView): super(UVListView, self) .get_queryset() .filter(id__in=([o.object.id for o in queryset])) + .filter(**additional_filters) ) From 22c028af11453034825ecb793a4f3dc7a766b6bd Mon Sep 17 00:00:00 2001 From: Bartuccio Antoine Date: Wed, 19 Jun 2019 01:53:02 +0200 Subject: [PATCH 5/7] pedagogy: rename query to search in search API --- pedagogy/views.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pedagogy/views.py b/pedagogy/views.py index 4cb06506..775ac7ef 100644 --- a/pedagogy/views.py +++ b/pedagogy/views.py @@ -162,7 +162,7 @@ class UVListView(CanViewMixin, CanCreateUVFunctionMixin, ListView): ) def get_queryset(self): - query = self.request.GET.get("query", None) + search = self.request.GET.get("search", None) additional_filters = {} @@ -178,14 +178,14 @@ class UVListView(CanViewMixin, CanCreateUVFunctionMixin, ListView): else: additional_filters["semester"] = semester - if not query: + if not search: return super(UVListView, self).get_queryset().filter(**additional_filters) try: queryset = ( SearchQuerySet() .models(self.model) - .autocomplete(auto=html.escape(query)) + .autocomplete(auto=html.escape(search)) ) except TypeError: return self.model.objects.none() From e21821ace5e73d754029498e49203917d634a033 Mon Sep 17 00:00:00 2001 From: Bartuccio Antoine Date: Wed, 19 Jun 2019 02:00:00 +0200 Subject: [PATCH 6/7] pedagogy: handle one letter search --- pedagogy/views.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/pedagogy/views.py b/pedagogy/views.py index 775ac7ef..bdc2fa2f 100644 --- a/pedagogy/views.py +++ b/pedagogy/views.py @@ -162,6 +162,7 @@ class UVListView(CanViewMixin, CanCreateUVFunctionMixin, ListView): ) def get_queryset(self): + queryset = super(UVListView, self).get_queryset() search = self.request.GET.get("search", None) additional_filters = {} @@ -178,11 +179,17 @@ class UVListView(CanViewMixin, CanCreateUVFunctionMixin, ListView): else: additional_filters["semester"] = semester + queryset = queryset.filter(**additional_filters) if not search: - return super(UVListView, self).get_queryset().filter(**additional_filters) + return queryset + + if len(search) == 1: + # It's a search with only one letter + # Hastack doesn't work well with only one letter + return queryset.filter(code__startswith=search) try: - queryset = ( + qs = ( SearchQuerySet() .models(self.model) .autocomplete(auto=html.escape(search)) @@ -190,12 +197,7 @@ class UVListView(CanViewMixin, CanCreateUVFunctionMixin, ListView): except TypeError: return self.model.objects.none() - return ( - super(UVListView, self) - .get_queryset() - .filter(id__in=([o.object.id for o in queryset])) - .filter(**additional_filters) - ) + return queryset.filter(id__in=([o.object.id for o in qs])) class UVCommentReportCreateView(CreateView): From 624f1d653d9a992ba6de3a7adbd0e64dd0927b40 Mon Sep 17 00:00:00 2001 From: Bartuccio Antoine Date: Wed, 19 Jun 2019 02:34:51 +0200 Subject: [PATCH 7/7] pedagogy: tests for search API --- pedagogy/search_indexes.py | 2 +- pedagogy/tests.py | 161 +++++++++++++++++++++++++++++++++++++ 2 files changed, 162 insertions(+), 1 deletion(-) diff --git a/pedagogy/search_indexes.py b/pedagogy/search_indexes.py index 604987d4..3ea75343 100644 --- a/pedagogy/search_indexes.py +++ b/pedagogy/search_indexes.py @@ -1,6 +1,6 @@ # -*- coding:utf-8 -* # -# Copyright 2017 +# Copyright 2019 # - Sli # # Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, diff --git a/pedagogy/tests.py b/pedagogy/tests.py index 0de5b405..89874ca6 100644 --- a/pedagogy/tests.py +++ b/pedagogy/tests.py @@ -566,3 +566,164 @@ class UVCommentUpdateTest(TestCase): ) self.assertEquals(response.status_code, 200) self.assertEquals(self.comment.author, self.krophil) + + +class UVSearchTest(TestCase): + """ + Test UV guide rights for view and API + Test that the API is working well + """ + + def setUp(self): + call_command("populate") + call_command("update_index", "pedagogy") + + def test_get_page_authorized_success(self): + # Test with root user + self.client.login(username="root", password="plop") + response = self.client.get(reverse("pedagogy:guide")) + self.assertEquals(response.status_code, 200) + + # Test with pedagogy admin + self.client.login(username="tutu", password="plop") + response = self.client.get(reverse("pedagogy:guide")) + self.assertEquals(response.status_code, 200) + + # Test with subscribed user + self.client.login(username="sli", password="plop") + response = self.client.get(reverse("pedagogy:guide")) + self.assertEquals(response.status_code, 200) + + def test_get_page_unauthorized_fail(self): + # Test with anonymous user + response = self.client.get(reverse("pedagogy:guide")) + self.assertEquals(response.status_code, 403) + + # Test with not subscribed user + self.client.login(username="guy", password="plop") + response = self.client.get(reverse("pedagogy:guide")) + self.assertEquals(response.status_code, 403) + + def test_search_pa00_success(self): + self.client.login(username="sli", password="plop") + + # Search with UV code + response = self.client.get(reverse("pedagogy:guide"), {"search": "PA00"}) + self.assertContains(response, text="PA00") + + # Search with first letter of UV code + response = self.client.get(reverse("pedagogy:guide"), {"search": "P"}) + self.assertContains(response, text="PA00") + + # Search with UV manager + response = self.client.get(reverse("pedagogy:guide"), {"search": "HEYBERGER"}) + self.assertContains(response, text="PA00") + + # Search with department + response = self.client.get(reverse("pedagogy:guide"), {"department": "HUMA"}) + self.assertContains(response, text="PA00") + + # Search with semester + response = self.client.get(reverse("pedagogy:guide"), {"semester": "AUTUMN"}) + self.assertContains(response, text="PA00") + + response = self.client.get(reverse("pedagogy:guide"), {"semester": "SPRING"}) + self.assertContains(response, text="PA00") + + response = self.client.get( + reverse("pedagogy:guide"), {"semester": "AUTOMN_AND_SPRING"} + ) + self.assertContains(response, text="PA00") + + # Search with language + response = self.client.get(reverse("pedagogy:guide"), {"language": "FR"}) + self.assertContains(response, text="PA00") + + # Search with credit type + response = self.client.get(reverse("pedagogy:guide"), {"credit_type": "OM"}) + self.assertContains(response, text="PA00") + + # Search with combinaison of all + response = self.client.get( + reverse("pedagogy:guide"), + { + "search": "P", + "department": "HUMA", + "semester": "AUTUMN", + "language": "FR", + "credit_type": "OM", + }, + ) + self.assertContains(response, text="PA00") + + # Test json briefly + response = self.client.get( + reverse("pedagogy:guide"), + { + "json": "t", + "search": "P", + "department": "HUMA", + "semester": "AUTUMN", + "language": "FR", + "credit_type": "OM", + }, + ) + self.assertJSONEqual( + response.content, + [ + { + "model": "pedagogy.uv", + "pk": 1, + "fields": { + "code": "PA00", + "author": 0, + "credit_type": "OM", + "semester": "AUTOMN_AND_SPRING", + "language": "FR", + "credits": 5, + "department": "HUMA", + "title": "Participation dans une association \u00e9tudiante", + "manager": "Laurent HEYBERGER", + "objectives": "* Permettre aux \u00e9tudiants de r\u00e9aliser, pendant un semestre, un projet culturel ou associatif et de le valoriser.", + "program": "* Semestre pr\u00e9c\u00e9dent proposition d'un projet et d'un cahier des charges\n* Evaluation par un jury de six membres\n* Si accord r\u00e9alisation dans le cadre de l'UV\n* Compte-rendu de l'exp\u00e9rience\n* Pr\u00e9sentation", + "skills": "* G\u00e9rer un projet associatif ou une action \u00e9ducative en autonomie:\n* en produisant un cahier des charges qui -d\u00e9finit clairement le contexte du projet personnel -pose les jalons de ce projet -estime de mani\u00e8re r\u00e9aliste les moyens et objectifs du projet -d\u00e9finit exactement les livrables attendus\n * en \u00e9tant capable de respecter ce cahier des charges ou, le cas \u00e9ch\u00e9ant, de r\u00e9viser le cahier des charges de mani\u00e8re argument\u00e9e.\n* Relater son exp\u00e9rience dans un rapport:\n* qui permettra \u00e0 d'autres \u00e9tudiants de poursuivre les actions engag\u00e9es\n* qui montre la capacit\u00e9 \u00e0 s'auto-\u00e9valuer et \u00e0 adopter une distance critique sur son action.", + "key_concepts": "* Autonomie\n* Responsabilit\u00e9\n* Cahier des charges\n* Gestion de projet", + "hours_CM": 0, + "hours_TD": 0, + "hours_TP": 0, + "hours_THE": 121, + "hours_TE": 4, + }, + } + ], + ) + + def test_search_pa00_fail(self): + + # Search with UV code + response = self.client.get(reverse("pedagogy:guide"), {"search": "IFC"}) + self.assertNotContains(response, text="PA00") + + # Search with first letter of UV code + response = self.client.get(reverse("pedagogy:guide"), {"search": "I"}) + self.assertNotContains(response, text="PA00") + + # Search with UV manager + response = self.client.get(reverse("pedagogy:guide"), {"search": "GILLES"}) + self.assertNotContains(response, text="PA00") + + # Search with department + response = self.client.get(reverse("pedagogy:guide"), {"department": "TC"}) + self.assertNotContains(response, text="PA00") + + # Search with semester + response = self.client.get(reverse("pedagogy:guide"), {"semester": "CLOSED"}) + self.assertNotContains(response, text="PA00") + + # Search with language + response = self.client.get(reverse("pedagogy:guide"), {"language": "EN"}) + self.assertNotContains(response, text="PA00") + + # Search with credit type + response = self.client.get(reverse("pedagogy:guide"), {"credit_type": "TM"}) + self.assertNotContains(response, text="PA00")