experiENS/avisstage/views_search.py
Robin Champenois 26ad68ff69 Fix search
2021-06-28 23:29:58 +02:00

266 lines
9.3 KiB
Python

# 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(
"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(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"])
# 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)
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})