import json import logging from datetime import date from django import forms from django.conf import settings from django.contrib.auth.decorators import login_required from django.core.cache import cache from django.core.paginator import InvalidPage, Paginator from django.db.models import Case, Q, When from django.http import HttpResponseBadRequest, JsonResponse from django.shortcuts import render 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 NIVEAU_SCOL_OPTIONS, TYPE_LIEU_OPTIONS, TYPE_STAGE_OPTIONS logger = logging.getLogger("recherche") # Recherche class SearchForm(forms.Form): generique = forms.CharField(required=False) sujet = forms.CharField(label="À propos de", required=False) contexte = forms.CharField( label="Contexte (lieu, encadrant·e·s, structure)", required=False ) apres_annee = forms.IntegerField(label="Après cette année", required=False) avant_annee = forms.IntegerField(label="Avant cette année", required=False) type_stage = forms.ChoiceField( label="Type de stage", choices=([("", "")] + list(TYPE_STAGE_OPTIONS)), required=False, ) niveau_scol = forms.ChoiceField( label="Année d'étude", choices=([("", "")] + list(NIVEAU_SCOL_OPTIONS)), required=False, ) type_lieu = forms.ChoiceField( label="Type de lieu d'accueil", choices=([("", "")] + list(TYPE_LIEU_OPTIONS)), required=False, ) tri = forms.ChoiceField( label="Tri par", choices=[("pertinence", "Pertinence"), ("-date_maj", "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( "multi_match", query=kwargs["generique"], fuzziness="auto", fields=[ "sujet^3", "encadrants", "type_stage", "niveau_scol", "structure", "lieux.*^2", "auteur.nom^2", "thematiques^2", "matieres", ], ) 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(min(2100, kwargs["avant_annee"]) + 1, 1, 1) filtres &= Q(date_fin__lt=dte) if field_relevant("apres_annee", False): dte = date(max(2000, 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"]) # Tri tri = "pertinence" if field_relevant("tri") and kwargs["tri"] in ["-date_maj"]: tri = kwargs["tri"] if not use_dsl: tri = "-date_maj" # Application resultat = Stage.objects.filter(filtres).distinct() if USE_ELASTICSEARCH and use_dsl: dsl_res = [s.meta.id for s in dsl.scan()] resultat = resultat.filter(id__in=dsl_res) if tri == "pertinence": resultat = resultat.order_by( Case(*[When(pk=pk, then=pos) for pos, pk in enumerate(dsl_res)]) ) else: resultat = resultat.order_by(tri) else: 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})