diff --git a/pedagogy/search_indexes.py b/pedagogy/search_indexes.py new file mode 100644 index 00000000..3ea75343 --- /dev/null +++ b/pedagogy/search_indexes.py @@ -0,0 +1,58 @@ +# -*- coding:utf-8 -* +# +# Copyright 2019 +# - 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/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) %} 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/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") diff --git a/pedagogy/views.py b/pedagogy/views.py index 38309a56..bdc2fa2f 100644 --- a/pedagogy/views.py +++ b/pedagogy/views.py @@ -31,6 +31,9 @@ from django.views.generic import ( FormView, View, ) +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 ( @@ -41,6 +44,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 +149,56 @@ class UVListView(CanViewMixin, CanCreateUVFunctionMixin, ListView): ordering = ["code"] 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 resp + + # Return serialized response + return HttpResponse( + serializers.serialize("json", self.get_queryset()), + content_type="application/json", + ) + + def get_queryset(self): + queryset = super(UVListView, self).get_queryset() + search = self.request.GET.get("search", 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 + + queryset = queryset.filter(**additional_filters) + if not search: + 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: + qs = ( + SearchQuerySet() + .models(self.model) + .autocomplete(auto=html.escape(search)) + ) + except TypeError: + return self.model.objects.none() + + return queryset.filter(id__in=([o.object.id for o in qs])) + class UVCommentReportCreateView(CreateView): """