305 lines
9.2 KiB
Python
305 lines
9.2 KiB
Python
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})
|