"""
    gestion.ajaxviews, un module pour SLM
    - rendu des portions de pages récupérées par des requêtes AJAX

    Copyright (C) 2023 Georges Khaznadar <georgesk@debian.org>

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
"""

from django.shortcuts import render
from django.http import JsonResponse, FileResponse
from django.core.exceptions import ObjectDoesNotExist
from django.db import IntegrityError
from django.db.models import Q
from django.views.decorators.csrf import ensure_csrf_cookie
from django.template.loader import render_to_string
from django.db.models import Max, Min, Count
from django.utils.translation import gettext_lazy as _

from .models import Eleves, Classes, Materiel, Livre_a_lire, \
    LivreSupplement, Materiel_regroupe, Inventaire, Prets, RevueStock, \
    Caution, MIN_NO_CAUTION, Eleves_de_siecle, les_modeles, Courriers, \
    Parametres, sans_accent, DICT_ETAT_MATERIEL_CHOIX, \
    etat_AUTRE, NOCLASS, IterUsine, iterusines, les_modeles, \
    Disciplines, Filieres, Series, TypeMateriel, Commandes, Personnels, Details, \
    Niveaux
from .correlation import CORR
import gestion.etiquettes as etiquettes
from manuels.settings import BASE_DIR
from .decorators import *

import io
import json
import csv
import time
import traceback
import sys
import Levenshtein
from datetime import date, datetime
from io import StringIO
import difflib
import re
import os

DATE111 = date.fromisoformat('0001-01-01')

def quasi_homonymes(noms, autres = []):
    """
    Crée un dictionnaire des quasi-homonymes trouvés dans une liste de noms.
    Un quasi-homonyme est trouvé dans le cas où il est possible de trouver une
    coïncidence où un nom contient tous les caractères de l'autre
    @param noms une liste de noms
    @param autres une liste de noms, peut-être la même où seront cherchés les
           quasi-homonymes
    @param un dictionnaire nom => autre_nom
    """
    noms = sorted(noms)
    if autres:
        autres = sorted(autres)
    else:
        autres = noms
    common = {}
    for n in noms:
        min_matched=0
        common[n] = (0, "")
        for a in autres:
            if a <= n:
                continue
            s = difflib.SequenceMatcher(None, n, a)
            matched = sum((m.size for m in s.get_matching_blocks()))
            if matched > min_matched:
                common[n] = (matched, a)
                min_matched = matched
    return {c: common[c][1] for c in common if \
            common[c][0] >= len(c) or \
            common[c][0] >= len(common[c][1])}

@ensure_csrf_cookie
@i18n
def tableau_prets(request):
    """
    Crée un tableau des prêts existants, étant donné un paramètre nomprenom
    Si la caution de cet élève a été rendue, ajoute un signalement dans les
    données qui sont renvoyées. Si la date des prêts court sur plus d'un jour,
    propose des boutons d'avenants pour la fiche de prêts.
    """
    nomprenom = request.POST.get("nomprenom", "")
    try:
        les_prets = Prets.objects.filter(
            eleve__NomPrenomEleve = nomprenom
        ).filter(
            Date_Declaration_Perte = DATE111).order_by(
            "inventaire__materiel__discipline__abrege",
            "inventaire__id"
        )
        min_max_date = Prets.objects.filter(
            eleve__NomPrenomEleve = nomprenom
        ).aggregate(
            m=Min('date_pret'), M=Max('date_pret'), R=Max('date_retour'))
        premiere_date = min_max_date["m"]
        derniere_date= max(min_max_date["M"], min_max_date["R"]) if \
            min_max_date["M"] else None
        date_unique = premiere_date == derniere_date
        nouveaux_prets = les_prets.filter(
            Q(date_pret = derniere_date) | Q(date_retour = derniere_date))
        eleve = Eleves.objects.get(NomPrenomEleve = nomprenom)
        les_option = [
            eleve.Lib_Mat_Enseignee_1, eleve.Lib_Mat_Enseignee_2,
            eleve.Lib_Mat_Enseignee_3, eleve.Lib_Mat_Enseignee_4,
            eleve.Lib_Mat_Enseignee_5, eleve.Lib_Mat_Enseignee_6,
            eleve.Lib_Mat_Enseignee_7, eleve.Lib_Mat_Enseignee_8,
            eleve.Lib_Mat_Enseignee_9, eleve.Lib_Mat_Enseignee_10]
        eleve.options = ", ".join([o for o in les_option if o])
        msg = ""
        # il y a caution implicite, si un numéro est entre 0 et MIN_NO_CAUTION
        # et qu'il n'y a PAS d'enregistrement de caution
        caution_implicite = eleve.no_caution < MIN_NO_CAUTION and \
            eleve.no_caution >= 0 and \
            len(Caution.objects.filter(numero = eleve.no_caution)) == 0
        caution_en_cours = caution_implicite or \
            Caution.objects.get( numero = eleve.no_caution).est_valide
    except ObjectDoesNotExist:
        les_prets = ""
        nouveaux_prets = ""
        date_unique = True
        eleve = "" #Ce nom d'élève n'est pas dans la base de données.
        msg = _("Souci dans la base de données, pour le nom ") + nomprenom
        caution_en_cours = False
    data = {
        "caution_en_cours": caution_en_cours,
        "msg": msg,
        "title": _("Caution ?"),
        "title_error": _("Erreur de nom ?"),
        "pas_de_caution": _("<p>Attention : pas de caution en cours de validité.</p><p>On crée une nouvelle caution ?</p>"),
        "html_prets": render_to_string('gestion/tableau_prets.html', {
            "les_prets": les_prets.filter(date_retour = DATE111) \
              if les_prets else [], # ne pas afficher les livres rendus !
            "nouveaux_prets": len(nouveaux_prets),
            "date_unique": date_unique,
            "date_prets": derniere_date,
            "eleve": eleve,
            "nomprenom": eleve.NomPrenomEleve if eleve else "",
            "classe": eleve.Lib_Structure if eleve else "",
            "no_caution": eleve.no_caution if eleve else 0,
            "sans_caution": eleve.sans_caution if eleve else False,
            "besoin_carte": eleve.peut_recevoir_carte_membre if eleve else False,
        }),
        "classe": eleve.Lib_Structure if eleve else "",
        "nomprenom": eleve.NomPrenomEleve if eleve else "",
    }
    
    return JsonResponse(data)

@ensure_csrf_cookie
@i18n
def classe_pour_prets(request):
    """
    Renvoie du code HTML au sujet d'une classe : un titre, et un tableau avec
    un élève de la classe par ligne, hyperlié vers une action utile ...
    POST apporte les paramètres "classe" et "nomprenom"
    """
    classe = request.POST.get("classe")
    nomprenom = request.POST.get("nomprenom")
    eleves = Eleves.objects.filter(Lib_Structure = classe).order_by(
        'NomPrenomEleve')
    e = Eleves.objects.get(NomPrenomEleve = nomprenom)
    besoin_carte = e.peut_recevoir_carte_membre if e else False
    data = {
        "html": render_to_string('gestion/classe_pour_prets.html', {
            "classe": classe,
            "eleves": eleves,
            "nomprenom": nomprenom,
            "besoin_carte": besoin_carte,
            "no_caution": e.no_caution,
            "sans_caution": e.sans_caution,
            "caution_rendue": e.caution_rendue,
        }),
    }
    return JsonResponse(data)

@ensure_csrf_cookie
@i18n
def classe_pour_retours(request):
    """
    Renvoie du code HTML au sujet d'une classe : un titre, et un tableau avec
    un élève de la classe par ligne, hyperlié vers une action utile de retour
    POST apporte les paramètres "classe" et "nomprenom"
    """
    classe = request.POST.get("classe")
    nomprenom = request.POST.get("nomprenom")
    eleves = Eleves.objects.filter(Lib_Structure = classe).order_by(
        'NomPrenomEleve')
    e = Eleves.objects.get(NomPrenomEleve = nomprenom)
    sans_accent = e.sans_accent
    data = {
        "html": render_to_string('gestion/classe_pour_retours.html', {
            "classe": classe,
            "eleves": eleves,
            "nomprenom": nomprenom,
            "sans_accent": sans_accent,
            "no_caution": e.no_caution,
            "sans_caution": e.sans_caution,
            "caution_rendue": e.caution_rendue,
        }),
    }
    return JsonResponse(data)

@ensure_csrf_cookie
@i18n
def tableau_retours(request):
    """
    Crée un tableau des prêts existants, étant donné un paramètre nomprenom
    """
    nomprenom = request.POST.get("nomprenom", "")
    now = date.today().strftime("%Y-%m-%d")
    eleve = Eleves.objects.filter(NomPrenomEleve = nomprenom)
    msg = "" if len(eleve) > 0 else \
        _("Erreur : il n'y a pas d'élève appelé(e) « {nomprenom} »").format(
            nomprenom = nomprenom)
    # on liste tous les prêts de l'élève, qu'ils soient rendu ou non,
    # perdus ou pas ; on évite juste de garder plusieurs prêts à la fois pour
    # un même livre !!!
    tous_les_prets = Prets.objects.filter(
        eleve__NomPrenomEleve = nomprenom).order_by('-date_pret')
    les_prets = []
    dejavu = []
    # on calcule les_prets de telle façon qu'on ne retienne que le prêt le
    # plus récent si un même matériel est prêté/rendu plus d'une fois
    for p in tous_les_prets:
        if p.inventaire_id not in dejavu:
            dejavu.append(p.inventaire_id)
            p.Etat_Final = p.inventaire.Etat_Materiel
            p.save()
            les_prets.append(p)
    etats =  DICT_ETAT_MATERIEL_CHOIX
    lignes = [render_to_string(
        'gestion/tableau_retours.html',
        {"p": p,
         "etats": etats,
         "etat_actuel": p.inventaire.Etat_Materiel,
         "mat": p.inventaire.materiel.type_materiel,
         }) for p in les_prets]
    data = {
        "lignes": lignes,
        "msg": msg,
        "title_error": _("Erreur de nom ?"),
        "nomprenom": eleve[0].NomPrenomEleve if eleve else "",
        "classe": eleve[0].Lib_Structure if eleve else "",
        "now": now,
    }
    return JsonResponse(data)

@ensure_csrf_cookie
@i18n
def rend_livre(request):
    """
    Gestion du rendu d'un livre. Deux paramètres sont postés :
    "nomprenom" qui désigne un élève et "code" qui désigne un livre.
    Remarque: si "nomprenom" est vide, c'est forcément rendu pour un tiers
    """
    nomprenom = request.POST.get("nomprenom")
    code = request.POST.get("code")
    if not code:
        code = "0"
    eleve = Eleves.objects.filter(NomPrenomEleve = nomprenom)
    eleve = eleve[0] if eleve else None
    now = date.today().strftime("%Y-%m-%d")
    titre = ""
    manuel = None
    try:
        manuels = Inventaire.objects.filter(id = code)
        if manuels and manuels[0].materiel:
            manuel = manuels[0]
            titre = manuel.materiel.titre
    except  (StopIteration, ObjectDoesNotExist) :
        pass
    if eleve:
        pret = Prets.objects.filter(
            eleve = eleve,
            inventaire__id = code,
        ).filter(~Q(date_retour__gt = DATE111))
    else:
        pret = []
    rendu = False
    ligne = ""
    if pret:
        rendu = True
        pret = pret[0]
        pret.date_retour = date.fromisoformat(now)
        pret.save()
        ## on modifie le nombre de manuels prêtés au niveau du catalogue
        entree_catalogue = Materiel.objects.get(id = pret.inventaire.materiel.id)
        # au cas où ce n'aurait jamais été fait par le passé, on refait
        # un inventaire pour le titre courant
        if entree_catalogue.Pretes is None:
            entree_catalogue.refait_inventaire(id_materiel = entree_catalogue.id)
            entree_catalogue = Materiel.objects.get(id = pret.inventaire.materiel.id)
        # enfin on acte du rendu
        entree_catalogue.Pretes -=1
        entree_catalogue.save()
        ## on prépare la réponse
        etats =  DICT_ETAT_MATERIEL_CHOIX
        ligne = render(request, "gestion/tableau_retours.html",{
            "p": pret,
            "etats": etats,
        }).content.decode()
    else:
        pret =  Prets.objects.filter(
            inventaire__id = code
        ).filter(~Q(date_retour__gt = DATE111))
        if pret:
            nomprenom = pret[0].eleve.NomPrenomEleve
        else:
            nomprenom = ""
    data={
        "rendu": rendu,
        "nomprenom": nomprenom,
        "manuel": titre,
        "code": code,
        "ligne": ligne,
    }
    return JsonResponse(data)

@ensure_csrf_cookie
@i18n
def change_etat_manuel_rendu(request):
    """
    Gestion du rendu d'un livre, modification de son état final.
    Trois paramètres sont postés :
    "code_etat" (pour l'état final), "date" pour la date du retour,
    et "code" qui désigne un livre.
    """
    code_etat = request.POST.get("code_etat")
    code = request.POST.get("code")
    date = request.POST.get("date","")
    ok = bool(date)
    degrade = False
    manuel = Inventaire.objects.get(
        id = code)
    titre = manuel.materiel.titre
    pret = None
    ligne=""
    etats =  DICT_ETAT_MATERIEL_CHOIX
    if ok:
        pret = Prets.objects.filter(
            inventaire__id = code,
            date_retour = date)
    ok = ok and bool(pret)
    if ok:
        pret = pret[0]
        pret.Etat_Final = int(code_etat)
        pret.save()
        if date > DATE111.strftime("%Y-%m-%d"):
            # on n'écrit dans l'inventaire que quand le livre est rendu
            pret.inventaire.Etat_Materiel = int(code_etat)
            pret.inventaire.save()
        degrade = pret.isDegrade
        ligne = render(request, "gestion/tableau_retours.html",{
            "p": pret,
            "etat_actuel": pret.inventaire.Etat_Materiel,
            "etats": etats,
        }).content.decode()    
    data={
        "ok": ok,
        "rendu": True,
        "degrade": degrade,
        "code": code,
        "etat": etats[int(code_etat)],
        "date": date,
        "ligne": ligne,
        "titre": titre,
    }
    return JsonResponse(data)

@ensure_csrf_cookie
@i18n
def annule_retour(request):
    """
    Gestion du rendu d'un livre : annule un retour. Paramètres sont postés :
    "code" qui désigne un livre et "date" qui est la date du retour
    """
    code = request.POST.get("code")
    date = request.POST.get("date")
    ok = True
    retour = Prets.objects.filter(
        date_retour = date,
        inventaire__id = code,
    )
    ok = len(retour) > 0
    if ok:
        retour = retour[0]
        retour.date_retour = DATE111
        retour.Etat_Final = etat_AUTRE
        retour.save()
        ## on modifie le nombre de manuels prêtés au niveau du catalogue
        entree_catalogue = Materiel.objects.get(id = retour.inventaire.materiel.id)
        # au cas où ce n'aurait jamais été fait par le passé, on refait
        # un inventaire pour le titre courant
        if entree_catalogue.Pretes is None:
            entree_catalogue.refait_inventaire(id_materiel = entree_catalogue.id)
        # enfin on acte du rendu
        entree_catalogue.Pretes +=1
        entree_catalogue.save()
    data={
        "ok": ok,
        "code": code,
        "date": date,
    }
    return JsonResponse(data)
    
@ensure_csrf_cookie
@i18n
def traite_perte(request):
    """
    Gestion du rendu d'un livre : déclaration de perte, ou rétractation
    Les paramètres postés sont :
    "code" qui désigne un livre et "perte", un booléen déclaration/rétractation
    """
    code = request.POST.get("code")
    perte = request.POST.get("perte") == "true"
    manuel = Inventaire.objects.get(
        id = code)
    pret = Prets.objects.filter(
        inventaire__id = code
    ).filter(~Q(date_retour__gt = DATE111))
    ok = bool(pret)
    etatFinal = etat_AUTRE
    if ok:
        pret = pret[0]
        pret.EtatFinal = etatFinal
        entree_catalogue = Materiel.objects.get(id = manuel.materiel.id)
        ## on gère la déclaration/rétractation de perte
        manuel.Perdu = perte
        if perte:
            pret.Date_Declaration_Perte = date.today()
        else:
            pret.Date_Declaration_Perte = DATE111
        pret.save()
        ## on met à jour le compte des pertes dans le catalogue
        if entree_catalogue.Perdus is None:
            entree_catalogue.Perdus = 0
        entree_catalogue.Perdus += (1 if perte else -1)
        manuel.save()
        entree_catalogue.save()
    ## on prépare la réponse
    etats =  DICT_ETAT_MATERIEL_CHOIX
    ligne = render(request, "gestion/tableau_retours.html",{
        "p": pret,
        "etats": etats,
        "perte": perte,
    }).content.decode()
    data = {
        "ok": ok,
        "code": code,
        "perte": perte,
        "ligne": ligne,
        "etat": etats[etatFinal],
    }
    return JsonResponse(data)

@ensure_csrf_cookie
@i18n
def prete_livre(request):
    """
    Détermine si on peut prêter un livre à quelqu'un, puis le fait et
    renvoie des détails sur l'opération.
    Paramètres postés : "nomprenom", "code", "deja_codes"
    Remarque : si "nomprenom" est vide, il s'agit forcément d'un prêt
    pour un tiers
    """
    nomprenom = request.POST.get("nomprenom", "")
    code = request.POST.get("code", "")
    deja_codes = json.loads(request.POST.get("deja_codes", ""))
    messages = []
    pret_unique = True
    if code in deja_codes:
        pret_unique = False
        messages.append(
            "Le manuel de code {} a déjà été saisi à l'instant.".format(code))
    eleve = Eleves.objects.filter(NomPrenomEleve = nomprenom)
    if not eleve:
        messages.append("L'élève {} n'a pas été trouvé".format(nomprenom))
    else:
        eleve = eleve[0]
    manuel = Inventaire.objects.filter(
        id = code)
    if not manuel:
        messages.append("Il n'y a pas de manuel de code {}".format(code))
    else:
        manuel = manuel[0]
        possesseur = manuel.actuellement_a()
        nomprenom = ""
        if possesseur:
            nomprenom = possesseur.NomPrenomEleve
            messages.append("Ce manuel est déjà prêté à {np}".format(
                np = nomprenom))
    # détermine si on peut effectivement prêter
    ok = bool(pret_unique) and bool(eleve) and bool(manuel) and \
        not bool(possesseur)
    tr_manuel = ""
    if ok:
        tr_manuel = render(request, "gestion/pret_ligne.html",{
            "manuel": manuel,
            "etat_materiel_choix": DICT_ETAT_MATERIEL_CHOIX,
            "etat": manuel.Etat_Materiel,
            }).content.decode()
    data = {
        "ok": ok,
        "messages" : messages,
        "tr_manuel" : tr_manuel,
        "nomprenom" : nomprenom,
        "manuel": manuel.materiel.titre,
        "code": code,
    }
    return JsonResponse(data)

@ensure_csrf_cookie
@i18n
def prets_go(request):
    """
    Force le prêt de livres à quelq'un ; par effet de bord, ça efface
    des enregistrements d'instances de Livre_a_lire pour chaque livre prêté
    de la catégorie "Poche"
    Paramètres postés : "nomprenom", "liste_codes", "etats"
    """
    nomprenom = request.POST.get("nomprenom", "")
    liste_codes = json.loads(request.POST.get("liste_codes", "[]"))
    etats = json.loads(request.POST.get("etats", "[]"))
    eleve = Eleves.objects.get(NomPrenomEleve = nomprenom)
    date = datetime.now().strftime("%Y-%m-%d")
    manuels = [ Inventaire.objects.get(id = code) for code in liste_codes ]
    poches = [m.materiel for m in manuels if m.materiel.is_poche]
    for p in poches:
        a_lire = Livre_a_lire.objects.filter(eleve = eleve, livre = p)
        if a_lire:
            a_lire[0].delete()
    for etat, manuel in zip(etats, manuels):
        if manuel.actuellement_a() is not None:
            continue # on ne prête pas un manuel actuellement prêté ni perdu
        pret = Prets(
            eleve=eleve,
            inventaire = manuel,
            date_pret = date,
            Etat_Initial = etat,
            Etat_Final = 0,
        )
        pret.save()
        manuel.Etat_Materiel = etat
        manuel.save()
        manuel_catalogue = manuel.materiel
        manuel_catalogue.Pretes =  \
            manuel_catalogue.Pretes + 1 if manuel_catalogue.Pretes else 1
        manuel_catalogue.save()
    return JsonResponse({"ok": True})

@ensure_csrf_cookie
@i18n
def classe(request):
    """
    Récupération des données d'une classe et affichage dans un page.
    """
    c = request.GET.get("c", "")
    eleves = Eleves.objects.filter(classe__libelle = c)
    data = render(request,'gestion/classe.html',{
        "eleves": eleves,
        "classe": c,
    }).content.decode()
    return JsonResponse({"data": data})
    
@ensure_csrf_cookie
@i18n
def liste_manuels(request):
    """
    Crée une liste de numéros de manuels, rendue sous la forme de pavés
    à placer dans un conteneur flex ; des paramètres de sélection sont
    récupérés par request.POST ; les filtres implémentés sont:
    - titre du manuel
    - ...
    """
    cat_manuel = request.POST.get("cat_manuel", "0")
    debut = request.POST.get("debut", "0")
    manuels_en_pret = Prets.objects.all().order_by("inventaire")
    if int(cat_manuel) > 0:
        manuels_en_pret = manuels_en_pret.filter(inventaire__materiel__id = cat_manuel)
    nb_manuels_en_pret = manuels_en_pret.count()
    MAX = 40    
    manuels_en_pret = manuels_en_pret[int(debut):int(debut)+MAX]
    return render(request,'gestion/liste_manuels.html', {
        "nb_manuels_en_pret": nb_manuels_en_pret,
        "manuels_en_pret": manuels_en_pret,
        "MAX": MAX,
        "debut": debut,
        "debut1": int(debut) + 1,
        "fin":  int(debut) + manuels_en_pret.count(),
    })
   
@ensure_csrf_cookie
@i18n
def aqui(request):
    """
    répond à la question "à qui est ce livre ?" par du code html
    """
    ident = request.POST.get("ident")
    found = None
    prets = []
    revue = []
    flashes = []
    if ident:
        found = Inventaire.objects.filter(id = ident)
        found = found[0] if found else None
        prets = Prets.objects.filter(inventaire = ident)
        revue = RevueStock.objects.filter(code__id = ident)
        flashes = [(p.date_pret, p, "prete") for p in prets] + \
            [(p.date_retour, p, "retour") for p in prets \
             if p.date_retour > DATE111] + \
                [(r.date.date(), r, "revue") for r in revue]
        flashes.sort(key = lambda x: x[0])
    return render(request,'gestion/aqui.html', {
        "ident": ident,
        "found": found,
        "flashes": flashes,
        "today": date.today(),
    })

@ensure_csrf_cookie
@i18n
def inventaire_non_pretes_premiers(request):
    """
    Renvoie les 40 premiers manuels non prêtés,
    puis place un cookie de session pour faciliter l'accès aux
    suivants, en donnant accès à l'itérateur paresseux de
    l'inventaire. Des paramètres peuvent être reçus par POST :
    - id_catalogue permet directement de rechercher une classe de manuels
    - title permet éventuellement de définir id_catalogue
    @return du code HTML semblable à l'inventaire complet
    """
    id_catalogue = request.POST.get("id_catalogue", "")
    title = request.POST.get("title", None)
    message = ""
    if not id_catalogue and title is not None:
        # il y a eu passage d'un paramètre title, éventuellement vide
        if title :
            # le paramètre title n'est pas vide
            trouves = Materiel.objects.filter(titre = title)
            if trouves:
                id_catalogue = trouves[0].id
            else:
                message = _("Le titre « {title} »").format(title=title) + \
                    _(" n'existe pas dans le catalogue (attention aux majuscules ?)")
    # aucun livre non rendu
    queryset = Inventaire.non_pretes_qs()
    if id_catalogue:
        queryset = queryset.filter(materiel__id=id_catalogue)
        
    iterusine = IterUsine("manuels_non_pretes", queryset)
    dernier, l = IterUsine.more("manuels_non_pretes", request=request)
    data = {
        "manuels":
        [[m.id, m.materiel.titre if m.materiel else None] for m in l],
        "message": message,
        "numero": dernier,
        "total": iterusine.count,
        "fini": dernier < 0,
    }
    return JsonResponse(data)
    
@ensure_csrf_cookie
@i18n
def inventaire_non_pretes_suivants(request):
    """
    Renvoie les 40 suivants manuels non prêtés
    @return du code JSON pour continuer l'inventaire
    """
    dernier, l = IterUsine.more("manuels_non_pretes", request=request)
    data = {
        "fini": dernier < 0,
        "numero": dernier,
        "manuels": [(m.id, m.materiel.titre if m.materiel else None) for m in l]
    }  
    return JsonResponse(data)
    
@ensure_csrf_cookie
@i18n
def inventaire_pretes_premiers(request):
    """
    Renvoie les 40 premiers manuels prêtés à  quelqu'un, puis place un cookie
    de session pour faciliter l'accès aux suivants, en donnant accès à
    l'itérateur paresseux de l'inventaire. Des paramètres peuvent être
    reçus par POST :
    - id_catalogue permet directement de rechercher une classe de manuels
    - title permet éventuellement de définir id_catalogue
    @return du code HTML semblable à l'inventaire complet
    """
    id_catalogue = request.POST.get("id_catalogue", "")
    title = request.POST.get("title", None)
    message = ""
    if not id_catalogue and title is not None:
        # il y a eu passage d'un paramètre title, éventuellement vide
        if title :
            # le paramètre title n'est pas vide
            trouves = Materiel.objects.filter(titre = title)
            if trouves:
                id_catalogue = trouves[0].id
            else:
                message = _("Le titre « {title} »").format(title=title) + \
                    _(" n'existe pas dans le catalogue (attention aux majuscules ?)")
    # il y a un livre prêté
    queryset = Inventaire.pretes_qs()
    if id_catalogue:
        queryset = queryset.filter(materiel__id=id_catalogue)
        
    iterusine = IterUsine("manuels_pretes", queryset)
    dernier, l = IterUsine.more("manuels_pretes", request=request)
    data = {
        "manuels":
        [[m.id, m.materiel.titre if m.materiel else None] for m in l],
        "message": message,
        "numero": dernier,
        "total": iterusine.count,
        "fini": dernier < 0,
    }
    return JsonResponse(data)

@ensure_csrf_cookie
@i18n
def inventaire_pretes_suivants(request):
    """
    Renvoie les 40 suivants manuels prêtés à quelqu'un, en utilisant un cookie
    de session pour récupérer l'itérateur paresseux.
    @return du code HTML semblable à l'inventaire complet
    """
    dernier, l = IterUsine.more("manuels_pretes", request=request)
    data = {
        "fini": dernier < 0,
        "numero": dernier,
        "manuels": [(m.id, m.materiel.titre if m.materiel else None) for m in l]
    }  
    return JsonResponse(data)
    
@ensure_csrf_cookie
@i18n
def correspondance_nom(request):
    """
    Recherche des noms/prénoms correspondant au début donné par le
    paramètre term
    """
    if request.headers.get('x-requested-with') == 'XMLHttpRequest':
        query = request.POST.get("term", "")
        query = sans_accent(query).lower()
        eleves = Eleves.objects.filter(sans_accent__contains=query)
        
        classes_a_chercher = [c.libelle for c in Classes.classes_importantes()]\
            + [NOCLASS]
        correspond = sorted([e.NomPrenomEleve for e in \
                       eleves if e.Lib_Structure in classes_a_chercher ])
    else:
        correspond = []
    return JsonResponse(correspond, safe=False)

@ensure_csrf_cookie
@i18n
def correspondance_nom_caution(request):
    """
    Recherche des noms/prénoms correspondant au début donné par le
    paramètre term
    """
    if request.headers.get('x-requested-with') == 'XMLHttpRequest':
        query = request.POST.get("term", "")
        query = sans_accent(query).lower()
        cautions = Caution.objects.filter(sans_accent__contains=query)
        correspond = sorted(list(set([c.nomprenom for c in  cautions])))
    else:
        correspond = []
    return JsonResponse(correspond, safe=False)

@ensure_csrf_cookie
@i18n
def correspondance_classe(request):
    """
    Recherche des classes correspondant au début donné par le
    paramètre term
    """
    if request.headers.get('x-requested-with') == 'XMLHttpRequest':
        query = request.POST.get("term", "")
        correspond = [rec.libelle for rec in \
                      Classes.objects.filter(
                          libelle__icontains=query)]
    else:
        correspond = []
    return JsonResponse(correspond, safe=False)

@ensure_csrf_cookie
@i18n
def correspondance_livre(request):
    """
    Recherche des livres correspondant au début donné par le
    paramètre term
    """
    if request.headers.get('x-requested-with') == 'XMLHttpRequest':
        query = request.POST.get("term", "")
        # recherche des livres dans la catalogue qui contiennent
        # une sous-chaine dans leur titre ou dans le nom de la discipline
        # associée
        correspond = Materiel.objects.filter(
            Q(titre__icontains=query) | \
            Q(discipline__libelle__icontains=query)
        )
        result = [{"title": c.titre,
                   "id": c.id} for c in correspond]
    else:
        result = []
    return JsonResponse(result, safe=False)

@ensure_csrf_cookie
@i18n
def constantes_del(request):
    """
    Supprime un enregistrement de "constantes de l'établissement"
    """
    numero = request.POST.get("numero","")
    instance = Parametres.objects.filter(pk = int(numero))[0]
    instance.delete()
    return JsonResponse("done", safe=False)

@ensure_csrf_cookie
@i18n
def messages_del(request):
    """
    Supprime un enregistrement de "messages prédéfinis"
    """
    numero = request.POST.get("numero","")
    instance = Courriers.objects.filter(N = int(numero))[0]
    instance.delete()
    return JsonResponse("done", safe=False)

@ensure_csrf_cookie
@i18n
def catalogue_del(request):
    """
    Supprime un enregistrement dans le catalogue
    """
    numero = request.POST.get("numero","")
    instance = Materiel.objects.filter(pk = int(numero))[0]
    instance.delete()
    return JsonResponse("done", safe=False)

@ensure_csrf_cookie
@i18n
def purger_prets_premiers(request):
    """
    Renvoie les 40 premiers manuels rendus, puis place un cookie
    de session pour faciliter l'accès aux suivants, en donnant accès à
    l'itérateur paresseux des manuels rendus. Des paramètres peuvent être
    reçus par POST :
    - date1 (le prêt doit être postérieur)
    - date2 (le retour doit être antérieur)
    @return du code HTML semblable à l'inventaire complet
    """
    date1 = request.POST.get("date1", "")
    date2 = request.POST.get("date2", "")
    it, tstamp = Prets.iter_rendu(date1, date2)
    rendus = []
    i = 0
    numero = 0
    while i < 40:
        try:
            numero, manuel = next(it)
            rendus.append(manuel)
        except StopIteration:
            break
        i += 1
    # on commence par sélectionner les livres dont la date
    # de rendu est non vide ; le seul système qui semble fonctionner
    # c'est de compare avec le "0001-01-01"
    total= Prets.objects.filter(
        date_retour__gt = DATE111)
    if date1:
        total = total.filter(
            date_pret__gt = date1)
    if date2:
        total = total.filter(
            date_retour__lt = date2)
    total = len(total)
    request.session["livres_rendus"] = tstamp
    data = {
        "manuels": [m.to_dict for m in rendus],
        "numero": numero,
        "total": total,
    }
    return JsonResponse(data)
    
@ensure_csrf_cookie
@i18n
def purger_prets_suivants(request):
    """
    Renvoie les 40  manuels rendus suivants,  en utilisant un cookie
    de session pour récupérer l'itérateur paresseux.
    @return du code JSON pour continuer la liste de manuels
    """
    tstamp = request.session.get("livres_rendus", "")
    it = Prets.iterateurs_livres[tstamp]
    rendus = []
    i = 0
    fini = False
    numero = 0
    while i < 40:
        try:
            numero, manuel = next(it)
            rendus.append(manuel)
        except StopIteration:
            fini = True
            break
        i += 1
    data = {
        "manuels": [m.to_dict for m in rendus],
        "numero": numero,
        "fini": "fini" if fini else "",
    }
    return JsonResponse(data)
    
@ensure_csrf_cookie
@i18n
def purger_prets_go(request):
    """
    Lance une purge des livres prés et rendus. Deux paramètres postés sont
    considérés, date1 et date2 pour encadrer ce qui doit être purgé
    """
    date1 = request.POST.get("date1", "")
    date2 = request.POST.get("date2", "")
    # on commence par sélectionner les livres dont la date
    # de rendu est non vide ; le seul système qui semble fonctionner
    # c'est de compare avec le "0001-01-01"
    a_purger= Prets.objects.filter(
        date_retour__gt = DATE111)
    if date1:
        a_purger = a_purger.filter(
            date_pret__gt = date1)
    if date2:
        a_purger = a_purger.filter(
            date_retour__lt = date2)
    total = len(a_purger)
    message = _("On a purgé {total} enregistrements.").format(total=total)
    try:
        a_purger.delete()
    except Exception as e:
        message = _("L'opération de purge a échoué, avec le message « {e} »").format(e=e)
    data = {
        "message": message,
        "title": _("Rapport de la purge"),
    }
    return JsonResponse(data)

@ensure_csrf_cookie
@i18n
def codes_barres_preview(request):
    """
    Prévisualisation des codes_barres.
    Paramètres postés : "id_catalogue", "tabou" et "date"
    """
    id_catalogue = request.POST.get("id_catalogue")
    tabou = json.loads(request.POST.get("tabou","[]"))
    thedate = request.POST.get("date")
    if thedate == "":
        thedate = datetime.now().strftime("%Y-%m-%d")
    manuel = Materiel.objects.get(id = id_catalogue)
    codes = [m.id for \
             m in Inventaire.objects.filter(
                 Date_Achat_Materiel = thedate,
                 materiel = id_catalogue)]
    etiqs = [etiquettes.EtiquetteJeanBart(
        c,
        manuel.titre,
        manuel.discipline.abrege,
        date,
    ) for c in codes]
    svg, nb, pages = etiquettes.previewSVG(etiqs, tabou)
    # on crée le message qui se trouvera au-dessus de la
    # prévisualisation à l'aide du modèle codes_barres_preview.html
    msg = render(request, "gestion/codes_barres_preview.html",{
        "nb": nb,
        "pages": pages,        
        "date": thedate,
    }).content.decode()
    data={
        "nb": nb,
        "pages": pages,
        "svg": svg,
        "date": thedate,
        "tabou": json.dumps(tabou),
        "msg": msg,
    }
    return JsonResponse(data)

@ensure_csrf_cookie
@i18n
def codes_barres_print_go(request):
    """
    Création des planches de codes_barres au format PDF.
    Paramètres postés : "id_catalogue", "tabou" et "date"
    """
    id_catalogue = request.POST.get("id_catalogue")
    tabou = json.loads(request.POST.get("tabou","[]"))
    date = request.POST.get("date")
    manuel = Materiel.objects.get(id = id_catalogue)
    selection = Inventaire.objects.filter(
        Date_Achat_Materiel = date, materiel = id_catalogue)
    codes = [m.id for m in selection]
    # on marque les manuels comme imprimés
    nouveaux = 0 # compte des manuels "nouvellement achetés
    for m in selection:
        if m.a_imprimer:
            nouveaux += 1
        m.a_imprimer = 0
        m.save()
    # et on les additionne au compte de manuels achetés dans le catalogue
    # en mentionnant que zéro sont prêtés, initialement
    if not manuel.Achetes:
        manuel.Achetes = nouveaux
        manuel.Pretes = 0
    else:
        manuel.Achetes = manuel.Achetes + nouveaux
    manuel.save()
    etiqs = [etiquettes.EtiquetteJeanBart(
        c,
        manuel.titre,
        manuel.discipline.abrege,
        date,
    ) for c in codes]
    pages = etiquettes.makePDF(etiqs, tabou)
    return FileResponse(pages, as_attachment=True, filename="etiquettes.pdf")

@i18n
def une_etiquette_go(request, numero, lig, col):
    """
    Produit un PDF pour imprimer une étiquette de livre avec le code-barre
    @param numero le numéro d'inventaire du livre
    @param lig numéro de ligne   (à partir de zéro sur la planche d'étiquettes)
    @param col numéro de colonne (à partir de zéro sur la planche d'étiquettes)
    """
    livre = Inventaire.objects.get(id = int(numero))
    manuel = livre.materiel
    date = livre.Date_Achat_Materiel
    etiqs = [etiquettes.EtiquetteJeanBart(
        int(numero),
        manuel.titre,
        manuel.discipline.abrege,
        date,
    )]
    tabous = []
    for l in range(int(lig)):
        for c in range(3):
            tabous.append({"row": l+1, "col": c+1})
    l = int(lig)
    for c in range(int(col)):
        tabous.append({"row": l+1, "col": c+1})
    page = etiquettes.makePDF(etiqs, tabous)
    return FileResponse(
        page, as_attachment=True,
        filename=f"etiquette_{numero}_{lig}_{col}.pdf",
    )
    

@ensure_csrf_cookie
@i18n
def cartes_membres_print_go(request):
    """
    Imprime les cartes de membre
    paramètres de type POST :
    - id_eleves une liste d'ids d'élèves à qui on imprime une carte de membre
    - max_nb le nombre de cartes imprimables sur la première feuille si
      celle-ci est incomplète (zéro par défaut, ce qui signifie que la
      première feuille serait complète)
    """
    id_eleves = json.loads(request.POST.get("id_eleves", "[]"))
    max_nb = int(request.POST.get("max_nb","6"))
    if max_nb < 2:
        max_nb=2
    if max_nb > 6:
        max_nb=6
    tabou = []
    # il peut y avoir des positions tabou où ne pas imprimer de carte
    # dans le cas où il y a moins de cartes à imprimer que de cases
    # disponibles en première page, on met les premières positions
    # dans la liste des tabous
    if len(id_eleves) < max_nb:
        for n in range(max_nb-len(id_eleves)):
            row = n // 2
            col = n % 2
            tabou.append({"row": row+1, "col": col+1})

    # ensuite, plus rien entre les positions max_nb et 6
    for n in range(max_nb, 6):
        row = n // 2
        col = n % 2
        tabou.append({"row": row+1, "col": col+1})
    eleves = Eleves.objects.filter(pk__in = id_eleves).order_by('NomPrenomEleve')
    etiqs = [etiquettes.CartesMembre(e) for e in eleves]
    pages = etiquettes.make_carte_membre_PDF(etiqs, tabou)
    return FileResponse(pages, as_attachment=True, filename="cartes_membre.pdf")

@ensure_csrf_cookie
@i18n
def cartes_membres_print_verso_go(request):
    """
    Imprime les versos des cartes de membre
    paramètres de type POST :
    - nb nombre de pages de six cartes à imprimer
    """
    nb = int(request.POST.get("nb", "1"))
    etiqs = [etiquettes.CartesMembre_verso() for i in range(6 * nb)]
    pages = etiquettes.make_carte_membre_verso_PDF(etiqs)
    return FileResponse(pages, as_attachment=True, filename="cartes_membre_verso.pdf")
    
    
@ensure_csrf_cookie
@i18n
def fiches_suivi_print_go(request):
    """
    Imprime les fiches de suivi des élèves
    paramètres de type POST :
    - classes une liste de dictionnaires classe -> effectif,
      ou encore une liste d'identifiants numériques d'élèves
    - annee une chaîne comme "2023/2024"
      que des fiches nouvelles.
    """
    classes = json.loads(request.POST.get("classes", "{}"))
    annee = request.POST.get("annee", "0000/00")
    Qexpr = None
    # on examine le cas où on aurait reçu une liste d'élèves
    if classes and isinstance(classes[0], int):
        # c'est une liste d'élèves
        Qexpr = Q(pk=classes[0])
        for id_eleve in classes[1:]:
            Qexpr = Qexpr | Q(pk=id_eleve)
    else:
        # c'est une liste de classes
        Qexpr = Q(Lib_Structure=classes[0])
        for c in classes[1:]:
            Qexpr  = Qexpr | Q(Lib_Structure=c)
    eleves = Eleves.objects.filter(Qexpr).order_by("NomPrenomEleve")
    etiqs = [etiquettes.Suivi(e, annee, e.dus_codes()) for e in eleves]
    pages = etiquettes.make_suivi_PDF(etiqs)
    return FileResponse(pages, as_attachment=True, filename="fiches_suivi.pdf")

    
@ensure_csrf_cookie
@i18n
def eleves_pretes_premiers(request):
    """
    Renvoie les 40 premiers élèves avec au moins un manuel,
    puis place un cookie de session pour faciliter l'accès aux
    suivants, en donnant accès à l'itérateur paresseux de
    l'inventaire. Des paramètres peuvent être reçus par POST :
    - id_classe qui permet de limiter la recherche à une seule classe
    @return du code HTML semblable à l'inventaire complet
    """
    id_classe = request.POST.get("id_classe", "")
    message = ""
    queryset = eleves = Eleves.avec_prets_qs()
    if id_classe:
        queryset = queryset.filter(Lib_Structure__icontains = id_classe)

    iterusine = IterUsine("eleves_avec_prets", queryset)
    dernier, l = IterUsine.more("eleves_avec_prets", request=request)
    data = {
        "eleves": [[e.id, e.NomPrenomEleve, e.prets_en_cours] for e in l],
        "message": message,
        "numero": dernier,
        "total": iterusine.count,
    }
    return JsonResponse(data)
    
@ensure_csrf_cookie
@i18n
def eleves_pretes_suivants(request):
    """
    Renvoie les 40 suivants élèves avec au moins un livre prêté,
    en utilisant un cookie
    de session pour récupérer l'itérateur paresseux.
    @return du code JSON pour continuer l'inventaire
    """
    dernier, l = IterUsine.more("eleves_avec_prets", request=request)
    data = {
        "fini": "fini" if dernier < 0 else "",
        "numero": dernier,
        "eleves": [[e.id, e.NomPrenomEleve, e.prets_en_cours] for  e in l],
    }
    return JsonResponse(data)
    
@ensure_csrf_cookie
@i18n
def eleves_sans_prets_premiers(request):
    """
    Renvoie les 40 premiers élèves sans aucun manuel,
    puis place un cookie de session pour faciliter l'accès aux
    suivants, en donnant accès à l'itérateur paresseux de
    l'inventaire. Des paramètres peuvent être reçus par POST :
    - id_classe qui permet de limiter la recherche à une seule classe
    @return du code HTML semblable à l'inventaire complet
    """
    id_classe = request.POST.get("id_classe", "")
    message = ""
    queryset = eleves = Eleves.sans_prets_qs()
    
    if id_classe:
        queryset = queryset.filter(Lib_Structure__icontains = id_classe)

    iterusine = IterUsine("eleves_sans_prets", queryset)
    dernier, l = IterUsine.more("eleves_sans_prets", request=request)
    data = {
        "eleves": [[e.id, e.NomPrenomEleve, e.prets_en_cours] for e in l],
        "message": message,
        "numero": dernier,
        "total": iterusine.count,
    }
    return JsonResponse(data)
    
@ensure_csrf_cookie
@i18n
def eleves_sans_prets_suivants(request):
    """
    Renvoie les 40 suivants élèves sans aucun livre prêté,
    en utilisant un cookie
    de session pour récupérer l'itérateur paresseux.
    @return du code JSON pour continuer l'inventaire
    """
    dernier, l = IterUsine.more("eleves_sans_prets", request=request)
    data = {
        "fini": "fini" if dernier < 0 else "",
        "numero": dernier,
        "eleves": [[e.id, e.NomPrenomEleve, e.prets_en_cours] for  e in l],
    }
    return JsonResponse(data)
    
@i18n
def RAZmodele(request):
    """
    Retire toutes les instances d'un modele
    considérant le paramètre posté "modele", qui est soit une chaine
    soit une liste de chaines sérialisée par json.
    """
    modele = json.loads(request.POST.get("modele"))
    if isinstance(modele, str):
        eval(modele).objects.all().delete()
        data = {
            "modeles": [modele],
        }
    else:
        for m in modele:
            eval(m).objects.all().delete()
        data = {
            "modeles": modele,
        }
    return JsonResponse(data)

def concatTo(*args, joinstr='\n', csvRow={}):
    """
    Concatène les arguments et renvoie dest et la concaténation ; pendant
    l'évaluation de cette fonction, on suppose qu'une variable globale
    csvRow contient un dictionnaire de noms de champs CSV et de valeurs
    
    @param args le nom du champ de destination, puis une suite de noms
       de champs qui peuvent contenir des * (glob)
    @param joinstr une chaîne pour joindre les noms de champs (\n par défaut)
    @param csvRow dictionnaire de noms de champs CSV et de valeurs
       (dictionnaire vide par défaut)
    """
    result = []
    CSVfieldnames = list(csvRow.keys())
    dest = args[0]
    for fieldname in args[1:]:
        f = fieldname
        d = 999
        meilleurs = {}
        for csvF in CSVfieldnames:
            g = csvF.replace("Clé","").replace("Cle","")
            if Levenshtein.distance(f, csvF) <= d:
                d = Levenshtein.distance(f, g)
                if d not in meilleurs:
                    meilleurs[d] = []
                meilleurs[d].append(csvF)
        le_meilleur = meilleurs[min((k for k in meilleurs))]
        result += [csvRow[csvF] for csvF in sorted(le_meilleur)]
    return dest, joinstr.join([li for li in result if li.strip()])

            
        
    
    
@i18n
def importeCSV(request):
    """
    importe les données d'un fichier CSV dans un modèle

    considére les paramètres postés "modele", "csv"
    """
    modele = eval(request.POST.get("modele"))
    csvbuffer = io.StringIO(request.POST.get("csv"))
    reader = csv.DictReader(csvbuffer, delimiter=",")
    pattern = re.compile(
        r'(?P<function>\w+)\((?P<args>(?P<arg>[\s,]*"[^"]*")*)\)')
    n=0
    cree=0
    message = ""
    for dico in reader:
        kw = {}
        for k, v_dic in les_modeles[modele.__name__]['fieldtypes'].items():
            if list(v_dic.values())[0] == 'ignore':
                continue
            the_type, the_field =  list(v_dic.items())[0]
            if the_type == 'foreign':
                the_field =  the_field[0]
            if  the_field is None:
                the_field = k
            try:
                if k not in dico:
                    continue
                val=dico[k]
            except Exception as err:
                message= str(err) + " dico[k] inconnu avec k = " + repr(k)
                ok=False
                data = {"ok": ok, "message": message}
                return JsonResponse(data)
            if the_type == 'int':
                val = int (val) if val else None
            if the_type == 'choice':
                if val:
                    val=int(val)
                else:
                    val=None
            elif the_type == "function":
                m = pattern.match(the_field)
                function = eval(m.groupdict()["function"])
                args = [eval(arg) for arg in m.groupdict()["args"].split(",")]
                the_field, val = function.__call__(*args, csvRow = dico)
            elif the_type == 'foreign':
                if val:
                    related = modele._meta.get_field(the_field).related_model
                    try:
                        val = related.objects.get(pk=val)
                    except ObjectDoesNotExist:
                        message += f"\npas de clé {val} pour {related}"
                        continue
                else:
                    val = None
            elif the_type == 'bool':
                val = val.upper() in ("VRAI", "1")
            elif the_type == 'date':
                if val:
                    match = re.match(r'(\d+)/(\d+)/(\d+)', val)
                    if match:
                        val = "{Y}-{m}-{d}".format(Y = match.group(3),
                                                   m = match.group(2),
                                                   d = match.group(1))
                    val = date.fromisoformat(val)
                else:
                    val = DATE111
            kw[the_field] = val
        try:
            nouveau = True
            if modele == Eleves:
                # on fait en sorte que no_caution se mette à zéro
                kw["no_caution"] = 0
                kw["a_verifier"] = False
            trouves = modele.objects.filter(**kw)
            if trouves:
                nouveau = False
            else:
                m = modele(**kw)
                m.save()
                cree += 1
            n += 1
            print("*" if nouveau else ".", end="")
            sys.stdout.flush()
            if n%50 == 0:
                print(" ", n)
        except IntegrityError as err:
            message += str(err) + " modele = " + repr(modele) + \
                " kw = " + repr(kw) + "\n"
        except Exception as err:
            trace = "\n".join(traceback.format_exception(
                sys.exc_info()[0], sys.exc_info()[1], sys.exc_info()[2]))
            message= str(err) + "\n" + trace + \
                " échec de modele(**kw), modele = " + repr(modele) + \
                " kw = " + repr(kw)
            ok=False
            data = {"ok": ok, "message": message}
            return JsonResponse(data)
    print(" ", n, "enregistrements pour", modele.__name__)
    ok = True,
    message += "traité {n} instances de {m}, dont {cree} nouvelles".\
        format(n=n, m=modele.__name__, cree = cree)
    data = {
        "ok": ok,
        "message": message,
    }
    return JsonResponse(data)
    
@ensure_csrf_cookie
@i18n
def doublonsSLM(request):
    """
    fonction de rappel pour traiter un doublon dans la base de données de SLM
    le POST contient le paramètre sans_accent
    """
    sans_accent = request.POST.get("sans_accent", "")
    eleves = Eleves.objects.filter(sans_accent = sans_accent)
    data = {
        str(e): {
            "prets": [ str(q) for q in Prets.objects.filter(eleve = e.pk)],
            "key": e.pk,
            "nomprenom": e.NomPrenomEleve,
            "caution": e.no_caution,
            "li": _("Prêts sous le nom « {e} », n° de caution {n} : {p}").format(
                e=e.NomPrenomEleve,
                n=e.no_caution,
                p=[ str(q) for q in Prets.objects.filter(eleve = e.pk)]),
            "effacer": _("Effacer l'enregistrement n° {k}"),
        } for e in eleves
    }
    return JsonResponse(data)

@ensure_csrf_cookie
@i18n
def effaceEleve(request):
    """
    efface un élève de la base de données.
    le POST contient un paramètre key, la clé primaire de l'enregistrement
    à supprimer
    """
    key = int(request.POST.get("key", 0))
    eleve = Eleves.objects.get(pk=key)
    eleve.delete()
    return JsonResponse({
        "msg": "OK",
        "html": _("<p>L'enregistrement {key} a été effacé.</p>").format(key = key),
        "title": _("Suppression effectuée"),
    })

@i18n
def sortieParSiecle(request):
    """
    Effacement des élèves qui sont dans la base et ne sont plus dans
    l'import SIÈCLE. Les élèves qui ont encore quelque chose à rendre,
    ou dont la caution n'a pas été marquée comme rendue, ne sont pas effacés.
    """
    liste_eleves = Eleves_de_siecle.objects.all()
    liste_sans_prets = Eleves.sans_prets_en_cours()
    liste_sans_prets_ni_caution = [
        e for e in liste_sans_prets if e.caution_rendue
    ]
    liste_sortants = [e for e in Eleves.objects.all() if not e.homonymes(liste_eleves)]
    pas_de_classe = Classes.objects.get(libelle = NOCLASS)
    for e in liste_sortants :
        e.classe = pas_de_classe
        e.Lib_Structure = NOCLASS
        e.save()
    liste_sortants_sans_prets_ni_caution = [
        e for e in liste_sans_prets_ni_caution
        if not e.homonymes(liste_eleves)
    ]
    nb = len(liste_sortants_sans_prets_ni_caution)
    classes_touchees = set() # ensemble des classes modifiées
    for e in liste_sortants_sans_prets_ni_caution:
        classes_touchees.add(e.classe)
        e.delete()
    # on remet à jour l'effectif des classes touchées par l'inscription
    for classe in classes_touchees:
        classe.recompte()
    # on recalcule la liste des sortants
    liste_eleves_SLM = Eleves.objects.all()
    liste_eleves = list(Eleves_de_siecle.objects.all())
    liste_sortants1 = [
        str(e.doit) for e in liste_eleves_SLM if not e.homonymes(liste_eleves)]
    return JsonResponse({
        "nb": nb,
        "sortants": liste_sortants1,
        "html": _("<p>On a effacé {nb} élèves, qui n'ont pas de livres prêtés.</p>").format(nb = nb),
        "title": _("Nettoyage terminé"),
    })

def sortieParSiecle_msg(request):
    """
    Gère le message temporaire de la fonction JS sortieParSiecle
    """
    data = {
        "html": _("<p>On efface les élèves sortants ...</p>"),
        "alt_img": _("Attendre ..."),
        "title": _("Effacement en cours"),
    }
    return JsonResponse(data)

@ensure_csrf_cookie
@i18n
def importeSiecle(request):
    """
    importation d'un fichier SIECLE
    le POST contient un paramètre "csv"
    """
    ok = True
    message=_("fichier SIÈCLE importé")
    csvfile = request.POST.get("csv")
    format_str = StringIO(csvfile)
    # première passe : on repère tous les libellés des classes
    # puis on crée les classes manquantes
    format_str.seek(0)
    reader = csv.DictReader(format_str, delimiter=",")
    nouvelles_classes = set((d['Code Structure'] for d in reader))
    ancienne_classes = set([c.libelle for c in Classes.objects.all()])
    classes_a_creer = nouvelles_classes - ancienne_classes
    classes_a_supprimer = ancienne_classes - nouvelles_classes
    for c in classes_a_creer:
        laclasse = Classes(code = c, libelle = c, compte = 0)
        laclasse.save()
    for c in classes_a_supprimer:
        print("IMPORTANT : il faudrait arbitrer la suppression des classes de l'ensemble", classes_a_supprimer)
    # deuxième passe on se préoccupe des élèves
    format_str.seek(0)
    reader = csv.DictReader(format_str, delimiter=",")
    liste_eleves = list(Eleves_de_siecle.from_csv(reader))
    if liste_eleves:
        # effacement de la table temporaire avant repeuplement
        Eleves_de_siecle.objects.all().delete()
    for e in liste_eleves:
        e.save() # on peuple la table gestion_eleves_de_siecle
    a_afficher = [str(e) for e in liste_eleves]
    return JsonResponse({
        "ok": ok,
        "message": message,
        "a_afficher": sorted(a_afficher),
        "title": _("Fichier SIÈCLE traité"),
        "classes_a_supprimer": list(classes_a_supprimer),
    })
### !!! travail à faire : permettre une interaction directe pour supprimer
### !! (ou pas) des classes absentes de l'import siècle.

@ensure_csrf_cookie
@i18n
def nouveaux_sortants_modifier(request):
    """
    Renvoie la liste des élèves Siècle qui sont nouveaux par rapport à 
    la base de données de SLM, et aussi un dictionnaire de quasi-homonymies
    entre nouveaux et ancienne base de donnée
    """
    liste_eleves_SLM = Eleves.objects.all()
    liste_eleves = list(Eleves_de_siecle.objects.all())
    liste_nouveaux = [
                e for e in liste_eleves if not e.homonymes(liste_eleves_SLM)]
    nouveaux = [str(e) for e in liste_nouveaux]
    noms_nouveaux = [e.sans_accent for e in liste_nouveaux]
    noms_autres = [e.sans_accent for e in liste_eleves_SLM]
    qh = quasi_homonymes(noms_nouveaux, noms_autres)
    ## calcul de la liste des élèves sortants
    ## et de la liste des élèves à modifier
    liste_sortants = [
        e for e in liste_eleves_SLM if not e.homonymes(liste_eleves)]
    liste_modifier = [
        e for e in liste_eleves_SLM if e.homonymes(liste_eleves)]
    liste_modifier_vraiment = [
        e for e in liste_modifier if \
        e.doit_etre_mis_a_jour(
            Eleves_de_siecle.objects.get(sans_accent = e.sans_accent))
    ]
    ## les doublons prévisibles
    liste_doublons_siecle = [
        e.nom_prenom() for e in liste_eleves \
        if len(e.homonymes(liste_eleves)) > 1
    ]
    sortants = [str(e.doit) for e in liste_sortants]
    a_modifier = [str(e) for e in liste_modifier]
    a_modifier_vraiment = [str(e) for e in liste_modifier_vraiment]
    donnees_siecle = {}
    quasi_h = {}
    for n in qh:
        e = Eleves_de_siecle.objects.filter(sans_accent = n)[0]
        donnees_siecle[n] = {
            "nom": e.Nom_de_famille,
            "prenom": e.Prenom,
            "sans_accent": e.sans_accent,
            "nomprenom": e.Nom_de_famille+ " " + e.Prenom,
            "autre": qh[n],
        }
        candidats = Eleves.objects.filter(
            sans_accent = qh[n])
        if candidats:
            quasi_h[n] = candidats[0].doit
    return JsonResponse({
        "ok": True,
        "message": "nouveaux, sortants et à moditer trouvés",
        "nouveaux": sorted(nouveaux),
        "sortants": sorted(sortants),
        "a_modifier": sorted(a_modifier),
        "a_modifier_vraiment": sorted(a_modifier_vraiment),
        "liste_doublons_siecle": sorted(liste_doublons_siecle),
        "quasi_homonymes": quasi_h,
        "donnees_siecle": donnees_siecle,
    })

@ensure_csrf_cookie
@i18n
def unifier_siecle(request):
    """
    Unifie un des élèves de la base SLM avec les données de nom d'un des
    élèves importés de SIÈCLE. Le POST amène des paramètres nom, prenom,
    nomprenom, sans_accent et autre. Ce dernier paramètre désigne l'élève
    dans la base de données de SLM par son 'sans_accent'
    """
    nom = request.POST.get("nom")
    prenom = request.POST.get("prenom")
    nomprenom = request.POST.get("nomprenom")
    sans_accent = request.POST.get("sans_accent")
    autre = request.POST.get("autre")
    e = Eleves.objects.get(sans_accent = autre)
    e.Nom_de_famille = nom
    e.Prenom = prenom
    e.NomPrenomEleve = nomprenom
    e.sans_accent = sans_accent
    e.save()
    return JsonResponse({
        "ok": True,
        "html": _("<img alt='Unifié !' title='OK, c'est unifié'/>"),
    })

@ensure_csrf_cookie
@i18n
def modificationParSiecle(request):
    """
    Modifie les attributs des élèves qui ont un homonyme dans l'import SIECLE ;
    s'il le faut, des classes sont créées.
    """
    liste_eleves_SLM = Eleves.objects.all()
    liste_eleves = list(Eleves_de_siecle.objects.all())
    liste_modifier = [
        e for e in liste_eleves_SLM if e.homonymes(liste_eleves)]
    liste_modifier_vraiment = [
        e for e in liste_modifier if \
        e.doit_etre_mis_a_jour(
            Eleves_de_siecle.objects.get(sans_accent = e.sans_accent))
    ]
    # ici on commence les modifications
    classes_touchees = set() # ensemble des classes modifiées
    for e in liste_modifier_vraiment:
        other = Eleves_de_siecle.objects.get(sans_accent = e.sans_accent)
        # on touche la classe ancienne
        classes_touchees.add(e.classe)
        for c in e.champs_importants:
            setattr(e, c, getattr(other,c))
        # ajout d'une classe s'il le faut
        try:
            classe = Classes.objects.get(libelle = e.Lib_Structure)
        except Classes.DoesNotExist:
            classe = Classes(code = e.Lib_Structure, libelle = e.Lib_Structure)
            classe.save()
        e.classe = classe
        e.save()
        # et on touche la classe nouvelle
        classes_touchees.add(e.classe)
        
    # on remet à jour l'effectif des classes touchées par l'inscription
    for classe in classes_touchees:
        classe.recompte()
    # et là, on réévalue les listes :
    liste_eleves_SLM = Eleves.objects.all()
    liste_modifier = [
        e for e in liste_eleves_SLM if e.homonymes(liste_eleves)]
    liste_modifier_vraiment = [
        e for e in liste_modifier if \
        e.doit_etre_mis_a_jour(
            Eleves_de_siecle.objects.get(sans_accent = e.sans_accent))
    ]
    # enfin, on prépare la réponse
    restants = [str(e) for e in liste_modifier]
    a_modifier_vraiment = [str(e) for e in liste_modifier_vraiment]
    return JsonResponse({
        "ok": True,
        "message": "les élèves restants ont été traités",
        "restants": sorted(restants),
        "a_modifier_vraiment": sorted(a_modifier_vraiment),
        "html": _("<p>On a mis à jour {n} élèves.</p>").format(n = len(a_modifier_vraiment)),
        "title": _("Mise à jour terminée"),
    })
    
@ensure_csrf_cookie
@i18n
def modificationParSiecle_msg(request):
    """
    gère le message d'attente associé à la fonction JS modificationParSiecle
    """
    data= {
        "html": _("<p>Patience ... on modifie les attributs des élèves.</p>"),
        "alt_img": _("Attendre ..."),
        "title": _("Mise à jour en cours"),
    }
    return JsonResponse(data)

@ensure_csrf_cookie
@i18n
def entreeParSiecle(request):
    """
    Inscription de nouveaux élèves, importés par SIÈCLE.
    S'il faut créer une classe, on la crée.
    """
    liste_eleves_SLM = Eleves.objects.all()
    liste_eleves = list(
        Eleves_de_siecle.objects.all().order_by("Nom_de_famille", "Prenom",))
    liste_nouveaux = [
                e for e in liste_eleves if not e.homonymes(liste_eleves_SLM)]
    classes_touchees = set() # ensemble des classes modifiées
    for e in liste_nouveaux:
        args = {c: getattr(e,c) for c in Eleves.champs_importants}
        nouveau = Eleves(**args)
        nouveau.a_verifier = False
        try:
            classe = Classes.objects.get(
                libelle = e.Lib_Structure, code = e.Lib_Structure)
        except Classes.DoesNotExist:
            classe = Classes(code = e.Lib_Structure, libelle = e.Lib_Structure)
            classe.save()
        nouveau.classe = classe
        classes_touchees.add(classe)
        nouveau.save()
    # on remet à jour l'effectif des classes touchées par l'inscription
    for classe in classes_touchees:
        classe.recompte()
    # et là, on réévalue les listes :
    liste_eleves_SLM = Eleves.objects.all()
    liste_nouveaux = [
                e for e in liste_eleves if not e.homonymes(liste_eleves_SLM)]
    # enfin, on prépare la réponse
    nouveaux = [str(e) for e in liste_nouveaux]
    return JsonResponse({
        "ok": True,
        "message": "les nouveaux élèves sont inscrits",
        "nouveaux": sorted(nouveaux),
        "html": _("<p>On a inscrit {n} élèves.</p>").format(n = len(nouveaux)),
        "title": _("Inscription terminée"),
    })

@ensure_csrf_cookie
@i18n
def entreeParSiecle_msg(request):
    """
    gestion du message d'attente associé à la fonction JS entreeParSiecle
    """
    data = {
        "html": _("<p>Inscription de nouveaux élèves ...</p>"),
        "alt_img": _("Attendre ..."),
        "title": _("Inscription en cours"),
    }
    return JsonResponse(data)
    
@ensure_csrf_cookie
@i18n
def nouvelle_caution(request):
    """
    Vérifie qu'il n'y a pas de caution en cours de validité pour un élève puis
    crée une nouvelle caution si ça a du sens. Le POST fournit un paramètre
    nomprenom
    """
    ok = True
    nomprenom = request.POST.get("nomprenom")
    eleve = Eleves.objects.get(NomPrenomEleve  = nomprenom)
    autres_cautions = Caution.objects.filter(
        nom = eleve.Nom_de_famille, prenom = eleve.Prenom)
    for c in autres_cautions:
        if c.est_valide:
            ok=False
            break
    if ok:
        eleve.no_caution = Caution.next_no_caution()
        eleve.save()
    return JsonResponse({
        "ok": ok,
        "nomprenom": nomprenom,
    })

@ensure_csrf_cookie
@i18n
def fin_caution(request):
    """
    Ajoute une date de fin à un enregistrement de caution,
    renseigne la somme remboursée.
    Ou alors, si on est en train d'annuler une opération préalable de rendu,
    neutralise la date de fin de l'enregistrement de caution, annule
    la somme remboursée.
    POST amène les paramètres :
    - si on rend la caution :
       nomprenom et rembourse et aussi le paramètre numéro, utile si
       le numéro de caution connu est zéro
    - si on veut annuler un précédent rendu de caution:
      nomprenom, ok1, ok2, comment, annule_rendu_numero
    """
    nomprenom = request.POST.get("nomprenom")
    rembourse = request.POST.get("rembourse", "").replace(",", ".")
    numero = request.POST.get("numero", "-1")
    ok1 = bool(request.POST.get("ok1", ""))
    ok2 = bool(request.POST.get("ok2", ""))
    annule_rendu_numero = request.POST.get("annule_rendu_numero", "")
    comment = request.POST.get("comment", "")
    ok = True
    try:
        eleve = Eleves.objects.get(NomPrenomEleve = nomprenom)
        if annule_rendu_numero: # on annule le rendu précédent d'une caution
            la_caution = Caution.objects.get(numero = annule_rendu_numero)
            if ok1 and ok2 : # il faut être d'accord -- d'accord !
                la_caution.rendue_le = DATE111
                la_caution.somme_rendue = 0.0
                la_caution.commentaire = comment
                la_caution.save()
                valide = la_caution.est_valide
                rembourse = 0.0
            else:
                ok = False
                valide = False
                rembourse = la_caution.somme_rendue
        else: # on va rendre une caution
            definir_caution = eleve.no_caution == 0
            if definir_caution:
                eleve.no_caution = numero
                eleve.save()
            caution = Caution.fromEleve(eleve)
            if definir_caution:
                caution.date = DATE111
                caution.somme_payee = 70.0
                caution.commentaire = f"Caution créée automatiquement, le {date.today()}"
            valide = caution.est_valide
            if caution.est_valide:
                caution.somme_rendue = float(rembourse)
                caution.rendue_le = datetime.now().date()
                caution.save()
    except ObjectDoesNotExist:
        ok = False
    if ok:
        msg = _("La caution a été rendue ({r} €).").format(r=rembourse)
    else:
        msg = _("La valeur {r} n'a pas pu être interprétée.").format(r=rembourse)
    return JsonResponse({
        "ok": ok,
        "nomprenom": nomprenom,
        "est_valide": valide, # juste avant qu'on ait invalidé la caution
        "rembourse": rembourse,
        "msg": msg,
        "title": _("Caution"),
        "deja_rendu": _("La caution était déjà rendue"),
    })

@ensure_csrf_cookie
@i18n
def commente_caution_msg(request):
    """
    Gère le message avec un textarea pour éditer le commentaire d'une caution
    POST a les paramètres sans_accent et commentaire
    """
    c = request.POST.get("commentaire")
    e = request.POST.get("sans_accent")
    data = {
        "html": _("<fieldset><textarea id='comment' name='comment' rows='10' cols='80'>{c}</textarea></fieldset>").format(c = c),
        "title": _("Commenter la caution de {e}").format(e = e),
    }
    return JsonResponse(data)

@ensure_csrf_cookie
@i18n
def commente_caution(request):
    """
    Édite les commentaires d'une caution,
    POST amène les paramètres sans_accent et val
    """
    sans_accent = request.POST.get("sans_accent")
    val = request.POST.get("val")
    ok = True
    try:
        eleve = Eleves.objects.get(sans_accent = sans_accent)
        nomprenom = eleve.NomPrenomEleve
        caution = Caution.objects.get(numero = eleve.no_caution)
        caution.commentaire = val
        caution.save()
    except ObjectDoesNotExist:
        ok=False
    return JsonResponse({
        "ok": ok,
        "nomprenom": nomprenom,
    })

@ensure_csrf_cookie
@i18n
def annule_caution(request):
    """
    Annule une caution (statut « sans caution ».
    POST amène le paramètre sans_accent
    """
    sans_accent = request.POST.get("sans_accent")
    ok = True
    try:
        eleve = Eleves.objects.get(sans_accent = sans_accent)
        caution = Caution.objects.get(numero = eleve.no_caution)
        nomprenom = eleve.NomPrenomEleve
        caution.somme_payee = 0
        caution.save()
    except ObjectDoesNotExist:
        ok=False
    return JsonResponse({
        "ok": ok,
        "nomprenom": nomprenom,
    })

@ensure_csrf_cookie
@i18n
def annule_caution_msg(request):
    """
    Gère le message préalable à l'annulation d'une caution
    POST contient le paramètre sans_accent
    """
    data = {
        "html": _("<p>Dispenser {e} de caution ?</p>").format(e = request.POST.get("sans_accent")),
        "title": _("Faire un statut « sans caution »"),
    }
    return JsonResponse(data)

@ensure_csrf_cookie
@i18n
def ancienne_caution_msg(request):
    """
    Gestion du premier message invoqué par la fonction JS ancienne_caution
    POST fournit les paramètres numero et date
    """
    numero = request.POST.get("numero")
    date = request.POST.get("date")
    data = {
        "html": _("<p>N° {numero}, date : « {date} »</p>").format(numero=numero, date=date),
        "title": _("Veuillez confirmer"),
    }
    return JsonResponse(data)

@ensure_csrf_cookie
@i18n
def ancienne_caution(request):
    """
    Restaure une ancienne caution
    POST amène les paramètres nomprenom, no_caution et date (facultatif)
    """
    nomprenom = request.POST.get("nomprenom")
    numero = request.POST.get("no_caution")
    ladate = request.POST.get("date")
    ok = True
    try:
        eleve = Eleves.objects.get(NomPrenomEleve = nomprenom)
        numero = int(numero)
        assert (numero < MIN_NO_CAUTION)
        eleve.no_caution = numero
        eleve.save()
        time.sleep(1)
        if ladate == "":
            ladate = datetime.now().strftime("%Y-%m-%d")
        else:
            ladate = date.fromisoformat(ladate)
        caution = Caution.objects.get(numero = eleve.no_caution)
        caution.date = ladate
        caution.save()
    except ObjectDoesNotExist:
        ok = False
    return JsonResponse({
        "ok": ok,
        "nomprenom": nomprenom,
    })

@ensure_csrf_cookie
@i18n
def affiche_caution_eleve(request):
    """
    Prépare l'affichage de données de caution pour un élève
    Le POST apporte le paramètre nomprenom
    """
    nomprenom = request.POST.get("nomprenom")
    cautions = []
    eleve = ""
    ok = False
    try:
        cautions = Caution.objects.filter(sans_accent = sans_accent(nomprenom).lower())
        eleveList = Eleves.objects.filter(NomPrenomEleve = nomprenom)
        ok = True
    except ObjectDoesNotExist:
        pass
    data = {
        "html": render_to_string('gestion/affiche_caution_eleve.html', {
            "eleve": eleveList[0] if eleveList else None,
            "nomprenom": nomprenom,
            "cautions": cautions,
	}),
        "nomprenom": nomprenom,
        "ok": ok,
    }
    return JsonResponse(data)

@ensure_csrf_cookie
@i18n
def cautions_classe(request):
    """
    renvoie des codes html pour un tableau d'actions possibles, afin de
    gérer les cautions d'un ensemble d'élèves d'une classe. Le POST founit
    un parametre classe
    """
    classe = request.POST.get("classe", NOCLASS)
    query = Classes.objects.filter(libelle__iexact = classe)
    classeInstance = query[0] if query else None
    lignes = []
    head = render_to_string(
                    'gestion/cautions_classe_ligne.html', {"head": True})
    if classeInstance:
        eleves = Eleves.objects.filter(
            classe = classeInstance).order_by("NomPrenomEleve")
        for e in eleves:
            lignes.append (
                render_to_string(
                    'gestion/cautions_classe_ligne.html', {
                        "eleve": e,
                        "classe": classe,
                        "head" : False,
                        "MIN_NO_CAUTION": MIN_NO_CAUTION,
                    }
                )
            )
    data = {
        "ok": bool(classeInstance),
        "classe": classeInstance.libelle if classeInstance else "",
        "lignes": lignes,
        "caption": "Cautions dans la " + classe,
        "head": head,
    }
    return JsonResponse(data)
    
@ensure_csrf_cookie
@i18n
def maj_caution(request):
    """
    Mise à jour d'un n° de caution.
    La variable POST fournit les paramètres nomprenom et no_caution
    """
    nomprenom  = request.POST.get("nomprenom").strip()
    no_caution = int(request.POST.get("no_caution"))
    msg = ""
    ok = True
    if no_caution >= MIN_NO_CAUTION:
        ok = False
        msg += f" Le numéro de caution {no_caution} est plus grand que " + \
            f"{MIN_NO_CAUTION}, ce qui est interdit pour les anciens numéros."
    ee = Eleves.objects.filter(no_caution = no_caution)
    if ee:
        ok = False
        msg += f" L'élève {ee[0].NomPrenomEleve} possède déjà le numéro " + \
            f"de caution {no_caution} ; les numéros de caution sont uniques."
    if ok:
        eleve = Eleves.objects.get(NomPrenomEleve = nomprenom)
        eleve.no_caution = no_caution
        eleve.save()
    data = {
        "ok": ok,
        "msg": msg,
        "erreur": _("Erreur"),
    }
    return JsonResponse(data)
    
@ensure_csrf_cookie
@i18n
def revue(request):
    """
    Répond aux requêtes venant de la page revue_du_stock ; le POST peut contenir
    le paramètre code. Par effet de bord un objet RevueStock est créé ou
    mis à jour, puis cet objet est retravaillé selon le template revue.html ;
    une icône sonore est éventuellement sélectionnée pour être revoyée.
    """
    code = int(request.POST.get("code").strip())
    manuel = Inventaire.objects.filter(id = code)
    existe = bool(manuel) and not manuel[0].est_au_pilon
    pilon = bool(manuel) and manuel[0].est_au_pilon
    rs = None
    perdu = False
    if existe:
        manuel = manuel[0]
        rs = RevueStock.objects.filter(code = manuel)
        if rs:
            rs = rs[0]
        else:
            rs = RevueStock(code = manuel)
        rs.date = datetime.now()
        prets = Prets.objects.filter(inventaire = code).order_by("-date_pret")
        if prets:
            rs.prete = True
            rs.dernier_pret = prets[0]
            perdu = rs.dernier_pret.Date_Declaration_Perte > DATE111
        else:
            rs.prete = False
            rs.dernier_pret = None
        rs.save()
    sound = "percu" # les autres possibilités sont "xylo" et "alerte"
    if perdu:
        sound = "alerte"
    if not existe:
        sound = "xylo"
    if rs and rs.dernier_pret and rs.dernier_pret.date_retour == DATE111:
        sound = "alerte"
    html = render_to_string(
        'gestion/revue.html', {
            "rs": rs,
            "perdu": perdu,
            "existe": existe,
            "pilon": pilon,
	})
    data = {
        "html": html,
        "sound": sound,
    }
    return JsonResponse(data)

@ensure_csrf_cookie
@i18n
def au_pilon_msg(request):
    """
    Gère le message initial de la fonction JS au_pilon
    POST fournit le paramètre ident
    """
    ident = request.POST.get("ident")
    data = {
        "html": _("<p>Êtes-vous bien sûr de vouloir mettre au pilon le livre n° {ident} ?</p>").format(ident=ident),
        "title": _("Mise au pilon"),
    }
    return JsonResponse(data)
    
@ensure_csrf_cookie
@i18n
def au_pilon(request):
    """
    Mise d'un livre au pilon. Le POST contient le paramètre ident
    """
    ident = int(request.POST.get("ident"))
    ok = False
    livre = Inventaire.objects.filter(id=ident)
    if livre:
        ok=True
        livre[0].pilon=date.today()
        livre[0].save()
    data = {
        "ok": ok,
        "msg": _("Le livre n° {i} est mis au pilon !"),
    }
    return JsonResponse(data)

def livre_html(li, option=None):
    """
    représentation d'un livre au format html, pour la boutique.
    Dans le cas particulier des livres de poche, l'abrégé visible sera
    précisé plus avant.
    @param li une instance de Matériel
    @param option précise la présentation (None par défaut)
    @return une chaîne html sûre
    """
    return render_to_string(
        "gestion/livre.html", {
            "l": li,
    })


def livres_pour_eleve(eleve):
    """
    Crée deux listes de livres pour un élève : les livres qu'il devrait
    prendre et les livres pour sa classe qu'il devrait laisser
    @param eleve une instance de Eleves
    """
    classe = eleve.Lib_Structure
    categorie = Classes.to_categorie(classe)
    matieres = []
    matieres.append(eleve.Lib_Mat_Enseignee_1)
    matieres.append(eleve.Lib_Mat_Enseignee_2)
    matieres.append(eleve.Lib_Mat_Enseignee_3)
    matieres.append(eleve.Lib_Mat_Enseignee_4)
    matieres.append(eleve.Lib_Mat_Enseignee_5)
    matieres.append(eleve.Lib_Mat_Enseignee_6)
    matieres.append(eleve.Lib_Mat_Enseignee_7)
    matieres.append(eleve.Lib_Mat_Enseignee_8)
    matieres.append(eleve.Lib_Mat_Enseignee_9)
    matieres.append(eleve.Lib_Mat_Enseignee_10)
    # livres du tronc commun
    livres_tc = [
        livre_html(li) for li in
        Materiel_regroupe.regroupe(
            Materiel.objects.filter(
                categories__contains = categorie, tronc = True))]
    # livres des options
    livres_options = []
    for m in matieres:
        if m in CORR:
            # on trouve le livre le mieux corrélé à la matière
            max=0
            abg= None
            for k,v in CORR[m].items():
                if v > max:
                    max = v
                    abg = k
            if abg and abg != "poche":
                # les livres de poche ne sont pas ajoutés aux livres d'option
                # à ce stade ; on consultera plutôt la table des livres à lire
                livres_options += list((Materiel.objects.filter(abrege = abg)))
    livres_tc += [
        livre_html(li) for li in Materiel_regroupe.regroupe(livres_options)]
    livres_poche= list(
        (l.livre for l in Livre_a_lire.objects.filter(eleve = eleve)))
    livres_tc += [livre_html(li) for li in livres_poche]
    # autres livres
    livres_autres = [
        livre_html(li) for li in
        Materiel_regroupe.regroupe(
            set(Materiel.objects.filter(
                categories__contains = categorie, tronc = False)) \
            - set(livres_options)
        )
    ]
    return livres_tc, livres_autres

@ensure_csrf_cookie
@i18n
def mes_livres_en_boutique(request):
    """
    Permet de définir les livres du tronc commun et les autres, étant donné
    un élève. POST fournit le paramètre nomprenom
    
    la constante CORRELATION représente un tableau
    de corrélation entre options de SIÈCLE et abrégés de livres
    """
    nomprenom = request.POST.get("nomprenom")
    eleve = Eleves.objects.get(NomPrenomEleve = nomprenom)
    classe = eleve.Lib_Structure
    livres_tc, livres_autres = livres_pour_eleve(eleve)
    matieres = []
    matieres.append(eleve.Lib_Mat_Enseignee_1)
    matieres.append(eleve.Lib_Mat_Enseignee_2)
    matieres.append(eleve.Lib_Mat_Enseignee_3)
    matieres.append(eleve.Lib_Mat_Enseignee_4)
    matieres.append(eleve.Lib_Mat_Enseignee_5)
    matieres.append(eleve.Lib_Mat_Enseignee_6)
    matieres.append(eleve.Lib_Mat_Enseignee_7)
    matieres.append(eleve.Lib_Mat_Enseignee_8)
    matieres.append(eleve.Lib_Mat_Enseignee_9)
    matieres.append(eleve.Lib_Mat_Enseignee_10)
    data = {
        "classe": classe,
        "matieres": matieres,
        "livres_tc": livres_tc,
        "livres_autres": sorted( # on zyeute l'attribut data-abg pour ordonner
            livres_autres,
            key = lambda li: re.match(
                r'.*data-abg="([^"]+)".*', li, re.M|re.DOTALL).group(1)) ,
    }
    return JsonResponse(data)

@ensure_csrf_cookie
@i18n
def qr_boutique_print_go(request):
    """
    Création des planches de codes_barres au format PDF.
    Paramètres postés : etiquettes : une chaîne représentant une liste de
    dictionnaires {"nom": abrégé, "nombre": combien en imprimer}

    Remarque : quand il y a des mentions entre parenthèses dans un abrégé,
    elles sont ôtées. Par exemple "philo lire (marron)" et "philo lire (blanc)"
    sont équivalents à "philo lire"
    """
    et= json.loads(request.POST.get("etiquettes"))
    etiqs = []
    for e in et:
        abrege = e["nom"].strip()
        # on retire toute mention entre parenthèses
        abrege = re.sub(r" \(.*", "", abrege)
        m = Materiel.objects.filter(abrege = abrege)
        if m:
            code = f"coop-{Materiel.objects.filter(abrege = abrege)[0].id}"
            etiqs += e["nombre"] * [etiquettes.EtiquetteBoutique(code, abrege)]
    pages = etiquettes.makePDF(etiqs, [])
    return FileResponse(pages, as_attachment=True, filename="qr_boutique.pdf")

def action_courrier_go(request):
    """
    Création d'étiquettes pour du courrier
    le POST contient les paramètres tabous et noms
    """
    tabous = json.loads(request.POST.get("tabous","[]"))
    noms = json.loads(request.POST.get("noms","[]"))
    eleves = Eleves.objects.filter(NomPrenomEleve__in = noms)
    etiqs = [etiquettes.EtiquetteAdresse(e) for e in eleves]
    pages = etiquettes.makePDF(etiqs, tabous)
    return FileResponse(pages, as_attachment=True, filename="etiquettes_adresse.pdf")
    


@i18n
def plus_livre_supplement(request):
    titre = request.POST.get("titre")
    manuels = Materiel.objects.filter(titre = titre)
    ok = bool(manuels)
    html=""
    id = 0
    if ok:
        livre = manuels[0]
        html = render_to_string(
            'gestion/bouton_livre.html', {
                "livre": livre,
            })
        id = livre.id
    data = {
        "ok": ok,
        "html": html,
        "id": id,
    }
    return JsonResponse(data)

@i18n
def plus_nomprenom_supplement(request):
    nomprenom = request.POST.get("nomprenom")
    eleves = Eleves.objects.filter(NomPrenomEleve = nomprenom)
    ok = bool(eleves)
    id = 0
    html=""
    if ok:
        eleve = eleves[0]
        html = render_to_string(
            'gestion/bouton_eleve.html', {
                "eleve": eleve,
            })
        id = eleve.id
        classe = eleve.Lib_Structure
    data = {
        "ok": ok,
        "html": html,
        "id": id,
        "classe": classe,
    }
    return JsonResponse(data)

@i18n
def findLivreSupplement(request):
    """
    Renvoie les données concernant une instance de LivreSupplement
    """
    id = int(request.POST.get("id"))
    li = LivreSupplement.objects.get(id = id)
    d = json.loads(li.data)
    # liste de titres de livres
    livres = [Materiel.objects.get(id = int(id)).titre for id in d["livres"]]
    # liste des élèves
    eleves = [Eleves.objects.get(id = int(id)).NomPrenomEleve for
              id in d["eleves"]]
    date = li.date.isoformat(),
    data = {
        "date": date,
        "commentaire": _("{d} Commentaire : {c}").format(d=date, c=li.commentaire),
        "nocomment": _("{d} (sans commentaire)").format(d=date),
        "prof": d["prof"],
        "livres": livres,
        "eleves": eleves,
        "id": id,
    }
    return JsonResponse(data)


@i18n
def valide_livre_supplement_go(request):
    """
    Applique ou pas une demande de livre supplémentaire, et efface cette
    demande de la base ; le POST contient les paramètres id et mode
    """
    id = int(request.POST.get("id"))
    mode = request.POST.get("mode") == "true"
    ls = LivreSupplement.objects.get(id = id)
    if mode:
        d = json.loads(ls.data)
        for idl in d["livres"]:
            for ide in d["eleves"]:
                ll = Livre_a_lire(
                    eleve = Eleves.objects.get(id=int(ide)),
                    livre = Materiel.objects.get(id=int(idl)),
                )
                ll.save()
    ls.delete()
    data = {"ok": True, "mode": mode}
    return JsonResponse(data)

@i18n
def plus_classe_supplement(request):
    """
    Fabrique un bouton pour pouvoir rajouter un livre supplémentaire
    à toute une classe
    """
    classe = request.POST.get("classe")
    html = render_to_string(
            'gestion/bouton_classe.html', {
                "classe": Classes.objects.get(libelle = classe),
            })
    data = {
        "ok": True,
        "html": html,
    }
    return JsonResponse(data)

@i18n
def plus_classe_supplement_go(request):
    """
    renvoie tous les noms d'élèves d'une classe
    """
    classe = request.POST.get("classe")
    eleves = [e.NomPrenomEleve for e in Eleves.objects.filter(
        Lib_Structure = classe)]
    data = {
        "ok": True,
        "eleves": eleves,
    }
    return JsonResponse(data)

@i18n
def rend_caution(request):
    """
    Crée la source HTML du dialogue de rendu de caution, ou de la gestion
    de caution si celle-ci était déjà rendue
    request.POST fournit le paramètre sans_accent qui identifie un élève,
    et le paramètre no_caution qui identifie sa caution
    """
    sans_accent = request.POST.get("sans_accent")
    eleve = Eleves.objects.get(sans_accent = sans_accent)
    nomprenom = eleve.NomPrenomEleve
    liste_manuels = json.loads(request.POST.get("liste_manuels"))
    manuels = Prets.objects.filter(
        inventaire__id__in = liste_manuels
    ).filter(Q(Date_Declaration_Perte__gt = "0001-01-01")).order_by(
        "inventaire__materiel__discipline__abrege",
        "inventaire__id"
    )
    tarifs = [m.tarif for m in manuels]

    no_caution = request.POST.get("no_caution")
    la_caution = Caution.objects.get(numero = no_caution)
    if la_caution.est_valide:
        html = render_to_string(
            'gestion/rend_caution.html', {
                "nomprenom": nomprenom,
                "no_caution": no_caution,
                "a_rendre": 70 - sum(tarifs),
            })
        valider = "Valider"
    else:
        html = render_to_string(
            'gestion/gere_caution.html', {
                "nomprenom": nomprenom,
                "la_caution": la_caution,
            })
        valider = "Annuler le « rendu » de cette caution",
    data = {
        "ok": True,
        "html": html,
        "title": _("rendre la caution de "),
        "est_valide": la_caution.est_valide,
        "valider": valider,
    }
    return JsonResponse(data)
    
@i18n
def langs(request):
    """
    Affichage du rectangle de 80px de large pour le choix des langues
    """
    languages = {l: _("Cliquer pour choisir la langue ") + l for \
                 l in os.listdir(os.path.join(BASE_DIR, "locale")) if \
                 os.path.isdir(os.path.join(BASE_DIR, "locale", l))}
    html = render_to_string(
            'gestion/langs.html', {
                "lang": re.split(r"-|_", request.session["lang"])[0],
                "languages": languages,
            })
    data = {
        "ok": True,
        "html": html,
    }
    return JsonResponse(data)

@ensure_csrf_cookie
def change_language(request):
    """
    Changement de langue pour la session. Le POST contient un paramètre lang
    """
    lang = request.POST.get("lang")
    request.session["lang"] = lang
    data = {
        "ok": True,
    }
    return JsonResponse(data)
    
@ensure_csrf_cookie
@i18n
def dropSiecle(request):
    """
    messages à traduire pour la fonction JS de même nom
    """
    data={
        "html": _("<p>Importation de SIÈCLE ...</p><img src='/static/img/timer1.gif' alt='animation pour attendre'/>"),
        "title": _("Veuillez patienter"),
    }
    return JsonResponse(data)

@ensure_csrf_cookie
@i18n
def message_nouveaux_sortants(request):
    """
    messages à traduire pour la fonction JS async function nouveaux_sortants_modifier
    (boite de messages à affichier temporairement)
    """
    data = {
        "html": _("<p>Calcul des nouveaux, des sortants et des restants</p>"),
        "title": _("Patienter ..."),
        "alt_image": _("Patienter ..."),
    }
    return JsonResponse(data)

@ensure_csrf_cookie
@i18n
def montre_quasi_homonymes(request):
    """
    messages à traduire pour la fonction JS de même nom
    """
    data = {
        "html": _("<table class='tableau_prets'><tr><th>Nom dans siècle</th><th>Nom déjà connu</th><th>Unifier ?</th></table>"),
        "unifier": _('Unifier les noms'),
        "title": _("Unifier des noms ?"),
    }
    return JsonResponse(data)

@ensure_csrf_cookie
@i18n
def prendre_un_livre_msg(request):
    """
    messages à traduire pour la fonction JS prendre_un_livre
    """
    data = {
        "html": _("<p>Je scanne les QR-codes ...</p>"),
    }
    return JsonResponse(data)

@ensure_csrf_cookie
@i18n
def repondre_abrege(request):
    """
    Fabrique un tableau associatif response avec les clés oui, deja, laisser, non
    POST fournit un paramètre abg l'abrégé d'une pile de livres
    """
    abg = request.POST.get("abg")
    data = {
        "response": {
            "oui": _("<p>Je prends : « {abg} »</p>").format(abg=abg),
            "deja": _("<p>{abg} a déjà été pris ?</p>").format(abg=abg),
            "laisser": _("<p>« {abg} » n'est pas pour moi.</p>").format(abg=abg),
            "non": _("<p>Je laisse : {abg}</p>",).format(abg=abg),
        },
    }
    return JsonResponse(data)
    
@ensure_csrf_cookie
@i18n
def nouveaux_livres_caption(request):
    """
    Gère le traduction de messages pour la fonction JS ajuste_nouveaux_livres
    POST contient le paramètre nb_livres nombre de livres à prêter
    """
    nb_livres = int(request.POST.get("nb_livres"))
    if nb_livres == 1:
        caption = _("Un nouveau livre à prêter.")
    else:
        caption = _("{n} nouveaux livres à prêter.").format(n=nb_livres)
    data = {
        "caption": caption,
    }
    return JsonResponse(data)

@ensure_csrf_cookie
@i18n
def pret_multiple_msg(request):
    """
    Gère le traduction de messages pour la fonction JS ajuste_nouveaux_livres
    POST contient les paramètre lestitres qui est un tableau titre -> nombre
    et multiples qui est la valeur maximal de nombre de chaque titre
    """
    titres = json.loads(request.POST.get("lestitres", "{}"))
    multiples = int(request.POST.get("multiples", "1"))
    messages = []
    pat1 = _("Le manuel « {titre} » est prêté à {nb} exemplaires.")
    for titre, nb in titres.items():
        if nb  > multiples:
            messages.append(pat1.format(titre = titre, nb = nb))
    if messages:
        if multiples == 1:
            messages.append(_("Voulez-vous passer sous silence, par la suite, le prêt de plus d'un manuel par référence ?"))
        else:
            messages.append(_("Voulez-vous passer sous silence, par la suite, le prêt de plus de {m} manuels par référence ?").format(m = multiples))
    data = {
        "messages" : messages,
        "title": _("Livres en multiples exemplaires ?"),
    }
    return JsonResponse(data)

@ensure_csrf_cookie
@i18n
def button_codes_barres(request):
    """
    Texte du dialogue pour créer des codes-barres dans
    la fonction JS codes_barres_init
    """
    data = {
        "text": _("Je veux en effet créer {n} nouvelles entrées d'inventaire pour « {t} »."),
    }
    return JsonResponse(data)
    
@ensure_csrf_cookie
@i18n
def notifSaveSuccess(request):
    """
    renvoie le code html d'une notification de succès d'enregistrement
    """
    data = {
        "html": _('<div class="notif">Les données ont été enregistrées avec succès</div>'),
    }
    return JsonResponse(data)

@ensure_csrf_cookie
@i18n
def makeImmobile_msg(request):
    """
    Gère la traduction de messages de la fonction JS makeImmobile
    """
    data = {
        "inutile_de_bouger": _("Inutile de chercher à bouger des livres du Tronc Commun !")
    }
    return JsonResponse(data)

@ensure_csrf_cookie
@i18n
def que_prendre(request):
    """
    Renvoie du code html pour afficher dans un dialogue la liste des livres
    que doit prendre un élève
    Le POST fournit le paramètre nomprenom
    """
    nomprenom = request.POST.get("nomprenom")
    eleve = Eleves.objects.get(NomPrenomEleve = nomprenom)
    livres_tc, livres_autre = livres_pour_eleve(eleve)
    data = {
        "html": render_to_string('gestion/que_prendre.html', {
            "livres": livres_tc,
            "eleve": eleve,
            })
    }
    return JsonResponse(data)

def livres_de_classe(request):
    """
    Renvoie du code HTML utilisable pour montrer dans un dialogue les
    livres à préparer pour une classe donnée. Le POST contient le paramètre
    classe
    """
    classe = request.POST.get("classe")
    categorie = Classes.to_categorie(classe)
    livres = list(Materiel_regroupe.regroupe(
        Materiel.objects.filter(categories__contains = categorie)))
    eleves = Eleves.objects.filter(Lib_Structure = classe)
    livres_poche= set(
        (l.livre for e in eleves for l in Livre_a_lire.objects.filter(eleve = e)
         ))
    livres += list(livres_poche)
    data = {
        "html": render_to_string('gestion/livres_de_classe.html', {
            "livres": livres,
            "classe": classe,
            })
    }
    return JsonResponse(data)

@ensure_csrf_cookie
@i18n
def livre_retrouve_go(request):
    """
    Dé-perd un livre quand on a confirmé l'avoir retrouvé
    le POST donne le paramètre id
    """
    id = request.POST.get("id")
    l = Inventaire.objects.get(id = id)
    l.Perdu = False
    l.save()
    data = {
        "OK": True,
    }
    return JsonResponse(data)

