experiENS/avisstage/views_search.py
2019-10-05 00:57:32 +02:00

246 lines
8.7 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
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})