# 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 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})