# coding: utf-8 from datetime import date from django import forms from django.contrib.auth.decorators import login_required from django.conf import settings from django.core.cache import cache from django.core.paginator import Paginator from django.db.models import Q, Case, When from django.http import JsonResponse, HttpResponseBadRequest from django.shortcuts import render, redirect, get_object_or_404 import json import logging USE_ELASTICSEARCH = getattr(settings, "USE_ELASTICSEARCH", True) if USE_ELASTICSEARCH: from .documents import StageDocument from .decorators import en_scolarite_required from .models import Stage from .statics import TYPE_LIEU_OPTIONS, TYPE_STAGE_OPTIONS, NIVEAU_SCOL_OPTIONS logger = logging.getLogger("recherche") # Recherche class SearchForm(forms.Form): generique = forms.CharField(required=False) sujet = forms.CharField(label=u'À propos de', required=False) contexte = forms.CharField(label=u'Contexte (lieu, encadrant⋅e⋅s, structure)', required=False) apres_annee = forms.IntegerField(label=u'Après cette année', required=False) avant_annee = forms.IntegerField(label=u'Avant cette année', required=False) type_stage = forms.ChoiceField(label="Type de stage", choices=([('', u'')] + list(TYPE_STAGE_OPTIONS)), required=False) niveau_scol = forms.ChoiceField(label="Année d'étude", choices=([('', u'')] + list(NIVEAU_SCOL_OPTIONS)), required=False) type_lieu = forms.ChoiceField(label=u"Type de lieu d'accueil", choices=([('', u'')] + list(TYPE_LIEU_OPTIONS)), required=False) tri = forms.ChoiceField(label=u'Tri par', choices=[('pertinence', u'Pertinence'), ('-date_maj',u'Dernière mise à jour')], required=False, initial='pertinence') def cherche(**kwargs): filtres = Q(public=True) use_dsl = False def field_relevant(field, test_string=True): return field in kwargs and \ kwargs[field] is not None and \ ((not test_string) or kwargs[field].strip() != '') if USE_ELASTICSEARCH: dsl = StageDocument.search() # # Recherche libre AVEC ELASTICSEARCH # # Champ générique : recherche dans tous les champs if field_relevant("generique"): #print("Filtre generique", kwargs['generique']) dsl = dsl.query( "match", _all={"query": kwargs["generique"], "fuzziness": "auto"}) use_dsl = True # Sujet -> Recherche dan les noms de sujets et les thématiques if field_relevant("sujet"): dsl = dsl.query("multi_match", query = kwargs["sujet"], fields = ['sujet^2', 'thematiques', 'matieres'], fuzziness = "auto") use_dsl = True # Contexte -> Encadrants, structure, lieu if field_relevant("contexte"): dsl = dsl.query("multi_match", query = kwargs["contexte"], fields = ['encadrants', 'structure^2', 'lieux.nom', 'lieux.pays', 'lieux.ville'], fuzziness = "auto") use_dsl = True else: # Sans ElasticSearch, on active quand même une approximation de # recherche en base de données if field_relevant("generique"): generique = kwargs["generique"] filtres = (Q(sujet__icontains=generique) | Q(thematiques__name__icontains=generique) | Q(matieres__nom__icontains=generique) | Q(lieux__nom__icontains=generique)) # Autres champs -> non fonctionnels if field_relevant("sujet") or field_relevant("contexte"): raise NotImplementedError( "ElasticSearch doit être activé pour ce type de recherche") # # Filtres directs db # # Dates if field_relevant('avant_annee', False): dte = date(kwargs['avant_annee']+1, 1, 1) filtres &= Q(date_fin__lt=dte) if field_relevant('apres_annee', False): dte = date(kwargs['apres_annee'], 1, 1) filtres &= Q(date_debut__gte=dte) # Type de stage if field_relevant('type_stage'): filtres &= Q(type_stage=kwargs["type_stage"]) if field_relevant('niveau_scol'): filtres &= Q(niveau_scol=kwargs["niveau_scol"]) # Type de lieu if field_relevant('type_lieu'): filtres &= Q(lieux__type_lieu=kwargs["type_lieu"]) # Application if USE_ELASTICSEARCH and use_dsl: filtres &= Q(id__in=[s.meta.id for s in dsl.scan()]) #print(filtres) resultat = Stage.objects.filter(filtres) tri = 'pertinence' if not use_dsl: kwargs['tri'] = '-date_maj' if field_relevant('tri') and kwargs['tri'] in ['-date_maj']: tri = kwargs['tri'] resultat = resultat.order_by(tri) return resultat, tri @login_required @en_scolarite_required def recherche(request): form = SearchForm() return render(request, 'avisstage/recherche/recherche.html', {"form": form}) @login_required @en_scolarite_required def recherche_resultats(request): stages = [] tri = '' vue = 'vue-liste' lieux = [] stageids = [] if request.method == "GET": form = SearchForm(request.GET) if form.is_valid(): page = request.GET.get("page", 1) search_args = form.cleaned_data # Gestion du cache lsearch_args = {key: val for key, val in search_args.items() if val != "" and val is not None} cache_key = json.dumps(lsearch_args, sort_keys=True) cached = cache.get(cache_key) if cached is None: # Requête effective stages, tri = cherche(**search_args) stageids = list(stages.values_list('id', flat=True)) lieux = [[stageid, lieuid] for (stageid, lieuid) in stages.values_list('id', 'lieux') if lieuid is not None] # Sauvegarde dans le cache to_cache = {"stages": stageids, "lieux": lieux, "tri": tri} cache.set(cache_key, to_cache, 600) logger.info(cache_key) else: # Lecture du cache stageids = cached["stages"] lieux = cached["lieux"] tri = cached["tri"] logger.info("recherche en cache") # Pagination paginator = Paginator(stageids, 25) try: stageids = paginator.page(page) except InvalidPage: stageids = [] if cached is None: stages = stages[max(0, stageids.start_index()-1): stageids.end_index()] else: orderer = Case(*[When(pk=pk, then=pos) for pos, pk in enumerate(stageids)]) stages = Stage.objects.filter(id__in=stageids).order_by(orderer) stages = stages.prefetch_related('lieux', 'auteur', 'matieres', 'thematiques') else: form = SearchForm() if stages: vue = 'vue-hybride' # Version JSON pour recherche dynamique if request.GET.get("format") == "json": return JsonResponse({"stages": stages, "page": page, "num_pages": paginator.num_pages}) template_name = 'avisstage/recherche/resultats.html' if request.GET.get("format") == "raw": template_name = 'avisstage/recherche/stage_items.html' return render(request, template_name, {"form": form, "stages": stages, "paginator": stageids, "tri": tri, "vue": vue, "lieux": lieux, "MAPBOX_API_KEY": settings.MAPBOX_API_KEY}) @login_required @en_scolarite_required def stage_items(request): try: stageids = [int(a) for a in request.GET.get("ids", "").split(';')] except ValueError: return HttpResponseBadRequest("Paramètre incorrect") stages = Stage.objects.filter(id__in=stageids)\ .prefetch_related('lieux', 'auteur', 'matieres', 'thematiques') return render(request, 'avisstage/recherche/stage_items.html', {"stages": stages})