"""
    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, 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, existe_iterateur, les_modeles, \
    Disciplines, Filieres, Series, TypeMateriel, Commandes, Personnels, \
    Details, Niveaux, Rachat, DICT_MODES_PAIEMENT_CHOIX, \
    Digraphe_select, Digraphe, les_tabous, Accueil
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, timedelta
from django.utils import timezone
from io import StringIO
import difflib
import re
import os
from subprocess import run
from xml.dom import minidom

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:
        eleve = Eleves.objects.get(NomPrenomEleve=nomprenom)
        details = eleve.prets_en_detail()
        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
        les_prets = details.get("prets")
        nouveaux_prets = details.get("prets_nouveaux")
        payes = details.get("payes")
        date_unique = details.get("date_unique")
        maxdate = details.get("maxdate")
    except ObjectDoesNotExist:
        les_prets = ""
        nouveaux_prets = ""
        payes = ""
        date_unique = True
        maxdate = DATE111
        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,
                Date_Declaration_Perte = DATE111,
                inventaire__paye__isnull = True) \
              if les_prets else [], # ne pas afficher les livres rendus !
            "nouveaux_prets": len(nouveaux_prets),
            "date_unique": date_unique,
            "date_prets": maxdate,
            "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,
        "ident": eleve[0].id,        # pour effacer le message perso ?
        "message": eleve[0].message, # message personnel
        "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
    On documente aussi les données du retour pour tenir compte des livres payés
    """
    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)
        # au cas où le livre aurait été déclaré perdu, il faut effacer
        # la date de perte
        pret.Date_Declaration_Perte = DATE111
        pret.inventaire.Perdu = False
        pret.inventaire.save()
        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 = ""
    # on documente si un livre avait été payé
    est_paye = False
    paye = None
    date_paiement = None
    eleve_parti = False # devient True si l'élève est rayé des listes
    if manuel and manuel.paye: # une instance de Rachat existe
        est_paye = True
        paye = "{:.2f}".format(manuel.paye.prix)
        date_paiement = manuel.paye.date.strftime("%d %b %Y")
        if not nomprenom:
            nomprenom = _("[élève parti]")
            eleve_parti = True
    data={
        "rendu": rendu,
        "nomprenom": nomprenom,
        "manuel": titre,
        "code": code,
        "ligne": ligne,
        "est_paye": est_paye,
        "paye": paye,
        "date_paiement": date_paiement,
        "eleve_parti": eleve_parti,
    }
    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)
        if found:
            found = found[0]
            rachat = found.paye
        else:
            found = None
            rachat = 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(),
        "rachat": rachat,
    })

@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 ; un paramètre showclass, s'il existe, dit de montrer
    aussi la classe entre parenthèses
    """
    query = request.POST.get("term", "")
    query = sans_accent(query).lower()
    eleves = Eleves.objects.filter(sans_accent__contains=query)
    showclass = request.POST.get("showclass", False)
    classes_a_chercher = [
        c.libelle for c in Classes.classes_importantes()] + [NOCLASS]
    correspond = sorted([e.np(showclass) for e in eleves
                         if e.Lib_Structure in classes_a_chercher ])
    return JsonResponse(correspond, safe=False)

def _correspondance_nom(nom):
    """
    Recherche des noms/prénoms correspondant au début donné par le
    paramètre nom
    """
    nom = sans_accent(nom).lower()
    eleves = Eleves.objects.filter(sans_accent__contains=nom)
    result=[]
    for e in eleves:
        i = e.sans_accent.index(nom)
        c = e.Lib_Structure
        ml = e.sans_accent[:i] + "<span style='color: red'>" + \
            e.sans_accent[i: i+len(nom)] + "</span>" + \
            e.sans_accent[i+len(nom):]
        result.append(f"{ml} ({c})")
    return result

@ensure_csrf_cookie
@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('Lib_Structure','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 cartons_jaunes_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.CartonsJaunes() for i in range(6 * nb)]
    pages = etiquettes.make_carte_membre_verso_PDF(etiqs)
    return FileResponse(pages, as_attachment=True, filename="cartons_jaunes.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 fiches_suivi_2volets_go(request):
    """
    Imprime les fiches de suivi des élèves
    paramètres de type POST :
    - liste_eleves une liste d'identifiants numériques d'élèves
    - annee une chaîne comme "2023/24"
    Cependant, si on vient de la page "/prepare_fiche_suivi", il y a
    plusieurs autres paramètres à prendre en compte, pour construire une
    instance d'Accueil, par effet de bord. Ces paramètres sont :
    
    - submit              ce paramètre vient du bouton de validation
    - id_eleve            l'identifiant d'un élève
    - location            choix entre fsl, especes, cheque et hdf
    - num_hdf             le numéro d'une carte HDF
    - fameco_val          true/false (si on a utilisé un Fameco ?)
    - masse               choix entre fsl, especes, cheque
    - caution             choix entre fsl, especes, cheque
    - banque              banque pour les chèques
    - titulaire           nom de la personne qui émet les chèques
    - ch_location_val     true/false (si on a fait un chèque pour la location)
    - ch_masse_val        true/false (si on a fait un chèque pour la masse)
    - ch_caution_val      true/false (si on a fait un chèque pour la caution)
    - ch_caution2_val     true/false (si on a fait 2 chèques pour la caution)
    - ch_location_masse   true/false (si on a fait un chèque pour location+masse)
    - ch_total            true/false (si on a fait un chèque global)
    - msg                 message rédigé lors de l'accueil
    """
    if "submit" in request.POST:
        # on a affaire à un appel depuis la page /prepare_fiche_suivi
        # il faut enregistrer les paramètres de l'accueil
        id_eleve = request.POST.get("id_eleve")
        location = request.POST.get("location")
        num_hdf = request.POST.get("num_hdf")
        fameco_val = request.POST.get("fameco_val")
        masse = request.POST.get("masse")
        caution = request.POST.get("caution")
        banque = request.POST.get("banque")
        titulaire = request.POST.get("titulaire")
        ch_location_val = request.POST.get("ch_location_val")
        ch_masse_val = request.POST.get("ch_masse_val")
        ch_caution_val = request.POST.get("ch_caution_val")
        ch_caution2_val = request.POST.get("ch_caution2_val")
        ch_location_masse = request.POST.get("ch_location_masse")
        ch_total = request.POST.get("ch_total")
        msg = request.POST.get("msg")
        
        accueil_recent = False
        params = Parametres.pour_la_date()
        objs = Accueil.objects.filter(eleve=id_eleve).order_by(
            '-date_mise_a_jour')
        if objs:
            # Voyons s'il y a une instance d'accueil pour l'élève, dont les
            # dates relèvent de l'année scolaire en cours.
            obj = objs[0]
            accueil_recent = Parametres.est_dans_l_annee_courante(
                obj.date_creation)
        if accueil_recent:
            # s'il y a une instance ok, on initialise paiements
            paiements = json.loads(obj.paiements)
        else:
            # sinon on crée une instance neuve (avec rien dans les paiements)
            obj = Accueil(
                eleve=Eleves.objects.get(id=id_eleve)
            )
            paiements={}
        assert isinstance(obj, Accueil) # obj est défini, voyons les paiements
        
        paiements["cheques"] = []       # remise à jour de la liste de chèques
        if ch_location_val == "true":
            paiements["cheques"].append(params.location)
        if ch_masse_val == "true":
            paiements["cheques"].append(params.masse)
        if ch_caution_val == "true":
            paiements["cheques"].append(params.caution)
        if ch_caution2_val == "true":
            paiements["cheques"].append(params.caution/2)
            paiements["cheques"].append(params.caution/2)
        if ch_location_masse == "true":
            paiements["cheques"].append(params.location)
            paiements["cheques"].append(params.masse)
        if ch_total == "true":
            paiements["cheques"].append(params.location)
            paiements["cheques"].append(params.masse)
            paiements["cheques"].append(params.caution)

        # cases à cocher espèce/chèque/hdf
        paiements["location"] = location
        paiements["masse"] = masse
        paiements["caution"] = caution
        paiements["hdf"] = {
            "carte": location == "hdf",
            "numero": num_hdf,
            "fameco": fameco_val == "true",
        },
        paiements["banque"] = banque
        paiements["titulaire"] = titulaire
        paiements["msg"] = msg
        obj.paiements = json.dumps(paiements)
        obj.save()
         
    liste_eleves = json.loads(request.POST.get("eleves", "[]"))
    annee = request.POST.get("annee", "0000/00")
    eleves = [ Eleves.objects.get(id=e) for e in liste_eleves]
    eleves.sort(key=lambda e: e.NomPrenomEleve)
    etiqs = [etiquettes.Suivi_2volets(e, annee, e.dus_codes()) for e in eleves]
    pages = etiquettes.make_suivi_2volets_PDF(etiqs)
    return FileResponse(pages, as_attachment=True, filename="fiches_suivi_2volets.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 = list(Eleves.objects.filter(sans_accent = sans_accent))
    # on ne renvoie rien si les élèves se distinguent par les prénoms 2 ou 3
    distincts = []
    for e in eleves:
        autres_nom_tous_prenoms = [a.nom_tous_prenoms() for a in eleves if a!=e]
        if e.nom_tous_prenoms() not in autres_nom_tous_prenoms:
            distincts.append(e)
    for e in distincts:
        eleves.remove(e)
    data = {
        "doublons": {
            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
        },
        "distincts": [e.nom_tous_prenoms() for e in distincts]
    }
    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.
    le POST contient un paramètre ids, une liste d'identifiants d'élèves
    de la base Eleves à effacer
    (si leur caution est rendue!)
    """
    ids = request.POST.get("ids", "")
    nb = 0
    liste_sortants1 = []
    html = ""
    title = ""
    if ids:
        ids = ids.split(",")
        sortants = Eleves.objects.filter(id__in = ids)
        liste_sortants1 = [e for e in sortants if e.caution_rendue]
        # traitement des élèves sans caution :
        # si tous les livres sont rendus, on peut les effacer
        try:
            for e in sortants:
                if (e.no_caution == 0 or e.sans_caution) and \
                   not e.prets_set.filter(date_retour=DATE111):
                    # l'élève est sans caution et n'a aucun prêt non-rendu
                    liste_sortants1.append(e)
        except Exception as err:
            print("Erreur pour l'élève", e, "no_caution =", e.no_caution)
        classes_touchees = set() # ensemble des classes modifiées
        pas_de_classe = Classes.objects.get(libelle = NOCLASS)
        for e in liste_sortants1 :
            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 pas encore supprimés
        # comme ils doivent un livre ou que leur caution n'est pas rendue
        sortants = [e for e in sortants if e not in liste_sortants1]
        html = _("<p>On a effacé {nb} élèves, à qui on a rendu la caution.</p>").format(nb = len(liste_sortants1))
        title = _("Nettoyage terminé")
    return JsonResponse({
        "sortants": {e.id: e.doit for e in sortants},
        "html": html,
        "title": title,
    })

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_nouveaux = [e for e in Eleves_de_siecle.objects.all() if
                      not Eleves.semblable_a(e, sans_echec=True)]
    nouveaux = {e.id : 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 Eleves.objects.all() if
                      not Eleves_de_siecle.semblable_a(e, sans_echec=True)]
    liste_modifier = []
    liste_restants = []
    for e in Eleves.objects.all():
        other = Eleves_de_siecle.semblable_a(e, sans_echec=True)
        if other:
            liste_restants.append(e)
            if e.doit_etre_mis_a_jour(other):
                liste_modifier.append(e)
    ## les doublons prévisibles
    liste = Eleves_de_siecle.objects.all()
    liste_doublons_siecle = [
        e for e in liste \
        if len(e.homonymes(liste)) > 1
    ]
    ### on va retravailler les doublons prévisibles : il peut y avoir
    ### des homonymes sur nom et prénom, mais si les prénoms 2 ou 3
    ### diffèrent, ce ne sont pas des doublons
    distincts=[]
    for e in liste_doublons_siecle:
        autres_nom_tous_prenoms = [a.nom_tous_prenoms()  for
                                   a in liste_doublons_siecle if a !=e]
        if e.nom_tous_prenoms() not in autres_nom_tous_prenoms:
            distincts.append(e)
    for e in distincts:
        liste_doublons_siecle.remove(e)
    sortants = {e.id : str(e.doit) for e in liste_sortants}
    a_modifier = {e.id : str(e) for e in liste_restants}
    a_modifier_automatiquement = {e.id: e.nom_tous_prenoms() for e in liste_modifier}
    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": nouveaux,
        "sortants": sortants,
        "a_modifier": a_modifier,
        "a_modifier_automatiquement": a_modifier_automatiquement,
        "liste_doublons_siecle": sorted([e.nom_tous_prenoms() for
                                         e in liste_doublons_siecle]),
        "quasi_homonymes": quasi_h,
        "donnees_siecle": donnees_siecle,
        "liste_modifier_manuellement": [], # à déboguer !!!
    })

"""     code qui pourrait servir à déboguer !!!
        "liste_modifier_manuellement": [ "{} {} {} {} {}".format(
            e.Nom_de_famille, e.Prenom, e.Prenom_2, e.Prenom_3, e.Lib_Structure
        ) for e in liste_modifier_manuellement],
    })
"""

@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 dont au moins une donnée est modifiée
    par un import SIÈCLE
    s'il le faut, des classes sont créées.
    Le POST contient un paramètre ids qui est une liste d'identifiants
    d'élèves (dans la table Eleves) séparés par des virgules.
    """
    ids = request.POST.get("ids").split(",")
    # liste d'élèves à modifier
    liste_restants =  Eleves.objects.filter(id__in = ids)
    # ici on commence les modifications
    classes_touchees = set() # ensemble des classes modifiées
    n = 0
    for e in liste_restants:
        other = Eleves_de_siecle.semblable_a(e)
        if e.doit_etre_mis_a_jour(other):
            # 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()
            n += 1
            # 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_modifier = [
        e for e in liste_restants
        if e.doit_etre_mis_a_jour(Eleves_de_siecle.semblable_a(e))
    ]
    # enfin, on prépare la réponse
    restants = {e.id: str(e) for e in liste_restants}
    a_modifier_automatiquement = {e.id: str(e) for e in liste_modifier}
    return JsonResponse({
        "ok": True,
        "message": "les élèves restants ont été traités",
        "restants": restants,
        "a_modifier_automatiquement": a_modifier_automatiquement,
        "html": _("<p>On a mis à jour {n} élèves.</p>").format(n = n),
        "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.
    le POST contient une variable ids, identifiants des élèves séparés
    par des virgules
    """
    ok = False
    nouveaux = {}
    html = ""
    title = ""
    message = ""
    n = 0
    ids = request.POST.get("ids", "")
    if ids:
        ids = ids.split(",")
        liste_nouveaux = Eleves_de_siecle.objects.filter(id__in = ids).order_by("Nom_de_famille", "Prenom",)
        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()
            n +=1 # on incrémente le nombre de nouveaux élèves traités
        # 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_nouveaux if not e.homonymes(liste_eleves_SLM)]
        # enfin, on prépare la réponse
        nouveaux = {e.id: str(e) for e in liste_nouveaux}
        message = "les nouveaux élèves sont inscrits"
        html = _("<p>On a inscrit {n} élèves.</p>").format(n = n)
        title = _("Inscription terminée")
    return JsonResponse({
        "ok": True,
        "message": message,
        "nouveaux": nouveaux,
        "html": html,
        "title": title,
    })

@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
       et divers champs booléens qui correspondent à des cases à cocher
    - 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", "")
    inconnu = request.POST.get("inconnu") == "true"
    reussite = request.POST.get("reussite") == "true"
    perte = request.POST.get("perte") == "true"
    quitte = request.POST.get("quitte") == "true"
    sc = request.POST.get("sc") == "true"
    autre = request.POST.get("autre") == "true"
    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.inconnu = False
                la_caution.reussite = False
                la_caution.perte = False
                la_caution.quitte = False
                la_caution.sc = False
                la_caution.autre = False
                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()} ; « {comment} »"
            valide = caution.est_valide
            if caution.est_valide:
                caution.inconnu = inconnu
                caution.reussite = reussite
                caution.perte = perte
                caution.quitte = quitte
                caution.sc = sc
                caution.autre = autre
                if caution.autre:
                    caution.commentaire = comment
                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=timezone.now().date()
        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, mode = "livre_html"):
    """
    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
    @param mode vaut "livre_html" par défaut, dans ce cas le résultat est
    composé de deux listes de codes HTML ; autrement c'est deux listes
    d'instances de Materiel
    """
    livres, livres_autres = eleve.doit_prendre
    if mode == "livre_html":
        return [livre_html(l) for l in livres], [livre_html(l) for l in livres_autres]
    return livres, 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
    """
    nomprenom = request.POST.get("nomprenom")
    eleve = Eleves.objects.get(NomPrenomEleve = nomprenom)
    classe = eleve.Lib_Structure
    livres, livres_autres = livres_pour_eleve(eleve)
    matieres = list(eleve.mat_enseignees)
    data = {
        "classe": classe,
        "matieres": matieres,
        "livres_tc": livres,
        "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 if not m.est_paye]

    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),
                "perdus": sum(tarifs) > 0,
            })
        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, et
    un paramètre contexte où sont les titres cochés/non cochés
    """
    abg = request.POST.get("abg")
    contexte = json.loads(request.POST.get("contexte"))
    titre = "abg" + " ??"
    livres = []
    for c in contexte.get("coches"):
        if c.get("abg") == abg and not c.get("dejapris"):
            livres.append(c.get("titre"))
    if livres:
        titre = " <b>et</b> ".join(livres)
    data = {
        "response": {
            "oui": _("<p>Je prends : « {titre} »</p>").format(titre=titre),
            "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)

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.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)

def rembourser(request):
    """
    pose la question préalable au remboursement d'un livre.
    le POST contient l'identifiant d'un prêt
    """
    pret = Prets.objects.get(id=request.POST.get("pret"))
    data = {
        "html": render_to_string('gestion/rembourser.html', {
            "pret": pret,
            "modes": DICT_MODES_PAIEMENT_CHOIX,
            }),
        "title": _("")
    }
    return JsonResponse(data)

def rembourser_go(request):
    """
    enregistre une instance de Rachat et met à jour une instance de Prets
    le POST contient l'identifiant d'un prêt et la clé d'un mode de paiement
    """
    pret = Prets.objects.get(id=request.POST.get("pret"))
    inv = pret.inventaire # l'instance d'Inventaire liée au prêt
    tarif = pret.tarif
    mode = int(request.POST.get("mode"))
    rachat = Rachat(
        date = date.today(),
        prix = tarif,
        mode = mode)
    inv.paye = rachat
    rachat.save()
    inv.save()
    data = {"OK": "ok"}
    return JsonResponse(data)

def save_digraphe_select(request):
    """
    fonction de rappel pour l'enregistrement de sélecteur de la page
    /digraph_select
    le POST peut contenir des paramètres name,
    left_selector, left_val, left_text, left_ordering,
    right_selector, right_val, right_text, right_ordering.
    """
    ok = False
    name = request.POST.get("name")
    left_selector = request.POST.get("left_selector")
    left_val = request.POST.get("left_val")
    left_text = request.POST.get("left_text")
    left_ordering = request.POST.get("left_ordering")
    right_selector = request.POST.get("right_selector")
    right_val = request.POST.get("right_val")
    right_text = request.POST.get("right_text")
    right_ordering = request.POST.get("right_ordering")

    erreur = ""
    try:
        assert name != ""
        new = Digraphe_select(
            name=name,
            left_selector=left_selector,
            left_val=left_val,
            left_text=left_text,
            left_ordering=left_ordering,
            right_selector=right_selector,
            right_val=right_val,
            right_text=right_text,
            right_ordering=right_ordering,
        )
        new.save()
        ok = True
    except Exception as err:
        ok = False
        erreur = str(err)
    html = render_to_string(
            'gestion/save_digraphe_select.html',
            { "ok": ok, "name": name, "err": erreur }
    )
    data = {
        "ok": ok,
        "html": html,
    }
    return JsonResponse(data)
    
def update_digraphs(request):
    """
    fonction de rappel pour le sélecteur de la page /digraph_select
    le POST peut contenir des paramètres select_id et digraph_ids
    """
    select_id = request.POST.get("select_id", None)
    digraph_ids = request.POST.get("digraph_ids", [])
    if select_id:
        select = Digraphe_select.objects.get(id=select_id)
        digraphs = Digraphe.objects.filter(select__id=select_id)
    else:
        select = None
        digraphs = None
    data = {
        "html": render_to_string(
            'gestion/update_digraphs.html',
            {
                "select": select,
                "digraphs": digraphs,
            }
        ),
        "OK": "ok"
    }
    return JsonResponse(data)
    
def test_selector(request):
    """
    Teste une expression qui est censée sélectionner des enregistrements de
    la base de données. le POST doit contenir un paramètre expr à évaluer ;
    expr doit renvoyer après évaluation, un itérable.
    le POST fournit aussi des paramètres valeur et label qui sont
    censés être des champs possibles des objets que renverra
    l'évaluation de expr ; dernier paramètre optionnel, mode qui peut
    être "l" pour une cause ou "r" pour un effet, "" par défaut
    et ça renvoie une réponse JSON avec un
    paramètre "message", et un paramètre "autres_boutons_html"
    """
    expr = request.POST.get("expr") or "[]"
    valeur = request.POST.get("valeur") or ""
    label = request.POST.get("label") or ""
    mode = request.POST.get("mode") or ""
    try:
        # on ne garde pas deux exemplaires avec le même doublet valeur, label :
        evaluation = {
            (valeur and getattr(e, valeur), label and getattr(e, label)):{
                "string": str(e),
                "valeur": valeur and getattr(e, valeur),
                "label": label and getattr(e, label),
                "mode": mode,
            } for e in eval(expr)}
        # une fois les doublons évacués, on s'intéresse à ce qui reste,
        # ordonné selon les labels :
        evaluation = list(evaluation.values())
        evaluation.sort(key = lambda e: e["label"])
        message = render_to_string('gestion/test_selector.html',{
            "evaluation": evaluation,
        })
        autres_boutons_html = render_to_string('gestion/autres_b_sel.html',{
            "evaluation": evaluation,
        })
    except Exception as err:
        message = "ERREUR : " + str(err)
        autres_boutons_html=""
    data = {
        "message" : message,
        "autres_boutons_html" : autres_boutons_html,
        "ok": "OK",
    }
    return JsonResponse(data)

def lob(request):
    """
    Liste de livres lob = list of books
    le POST contient un parametre nomprenom et un autre nommé dejapris qui
    contient une liste d'identifiants de livres déjà flashés
    """
    nomprenom = request.POST.get("nomprenom")
    dejapris = json.loads(request.POST.get("dejapris"))
    html = render_to_string("gestion/lob0.html", {})
    if nomprenom:
        ## le nom et le prénom sont connus, on fait plus détaillé que lob0.html
        ## des identifiants, on remonte à des livres de l'inventaire
        dejalivres = [l.materiel for l in
                      Inventaire.objects.filter(id__in=dejapris)]
        ## on élargit aux livres qui ont le même abrégé
        abreges = {l.abrege for l in dejalivres}
        dejalivres = Materiel.objects.filter(abrege__in=abreges)
        eleve = Eleves.objects.get(NomPrenomEleve = nomprenom)
        livres, livres_autres = livres_pour_eleve(eleve, mode="non_html")
        a_prendre = [l for l in livres if l not in dejalivres]
        html = render_to_string("gestion/lob.html", {
            "livres": a_prendre,
        })
    data = {
        "html": html,
    }
    return JsonResponse(data)

def dot_to_svg(request):
    """
    accepte un paramètre dot avec du code source au format .dot
    renvoie une réponse JSON avec un paramètre svg qui contient
    du code source SVG
    """
    dot = request.POST.get("dot")
    cp = run(
        ['dot','-T', 'svg'],
        input = dot,
        encoding='UTF-8',
        capture_output=True,
    )
    ok = cp.returncode == 0
    svg = ""
    if ok:
        svg_code = cp.stdout
        d = minidom.parseString(svg_code)
        svg = d.documentElement.toxml().replace("\n", " ")
    else:
        svg="<textarea rows='5' cols='60'>"+cp.stderr+"</textarea>"
    data = {
        "ok": ok,
        "svg": svg,
    }
    return JsonResponse(data)
    
def new_digraph(request):
    """
    enregistrement de données pour un nouveau digraphe
    le POST contient les paramètres suivants:
    - name: un nom,
    - dot: un graphe au format .dot
    - left_selector: sélecteur de cause
    - left_val: valeur pour la cause
    - left_text: label pour la cause
    - right_selector: sélecteur pour l'effet
    - right_val: valeur pour l'effet
    - right_text: label pour l'effet
    """
    name = request.POST.get("name")
    dot = request.POST.get("dot")
    left_selector = request.POST.get("left_selector")
    left_val = request.POST.get("left_val")
    left_text = request.POST.get("left_text")
    right_selector = request.POST.get("right_selector")
    right_val = request.POST.get("right_val")
    right_text = request.POST.get("right_text")
    
    ds, created = Digraphe_select.objects.get_or_create(
        name=name,
        left_selector=left_selector, left_val=left_val, left_text=left_text,
        right_selector=right_selector, right_val=right_val,
        right_text=right_text,
        left_ordering='', right_ordering=''
    )
    d = Digraphe(select=ds, dot_code=dot)
    d.save()
    data = {
        "message": "OK"
    }
    return JsonResponse(data)

def del_digraph(request):
    """
    Supprime un digraphe le POST donne le parmètre id
    """
    id = request.POST.get("id")
    dg = Digraphe.objects.get(id=id)
    dg.delete()
    data = {
        "message": "OK"
    }
    return JsonResponse(data)

def del_digraphe_select(request):
    """
    Supprime une instance de Digraphe_select
    le POST contient un paramètre name
    """
    ds = Digraphe_select.objects.get(name = request.POST.get("name"))
    ds.delete()
    data = {
        "message": "OK"
    }
    return JsonResponse(data)
    
def mv_digraphe_select(request):
    """
    Renomme une instance de Digraphe_select
    le POST contient les paramètres name1, name2
    """
    ds = Digraphe_select.objects.get(name = request.POST.get("name1"))
    ds.name = request.POST.get("name2")
    ds.save()
    data = {
        "message": "OK"
    }
    return JsonResponse(data)
    
def efface_caution(request):
    """
    Met en place le dialogue qui permettra d'effacer une caution
    (l'èlève ne se présentera jamais).
    Le POST fournit un paramètre sans_accent
    """
    sans_accent = request.POST.get("sans_accent","")
    cautions = Caution.objects.filter(sans_accent=sans_accent)
    ok = bool(cautions)
    if ok:
        html = render_to_string("gestion/efface_caution.html", {
            "caution": cautions[0],
        })
    else:
        html = render_to_string("gestion/err_efface_caution.html", {})
    data = {
        "ok": ok,
        "html": html,
    }
    return JsonResponse(data)

def efface_caution_go(request):
    """
    efface une caution pour de bon
    (l'èlève ne se présentera jamais).
    Le POST fournit les paramètres nomprenom et n
    """
    nomprenom = request.POST.get("nomprenom")
    eleve = Eleves.objects.filter(NomPrenomEleve=nomprenom)
    try:
        n = int(request.POST.get("n"))
    except Exception:
        n = 0
    ok = eleve and eleve[0].no_caution == n
    if ok:
        caution = Caution.objects.get(numero = n)
        caution.efface() # efface et enregistre
        html =""
    else:
        html = "ERROR"
    data = {
        "ok": ok,
        "html": html,
    }
    return JsonResponse(data)

def recompte_livres(request):
    """
    Recompte les livres nécessaires selon la population des élèves
    et leurs options. Le POST a un paramètre ident, qui permet de cibler un
    matériel
    """
    ok = "KO"
    compte = None
    date = None
    mat = Materiel.objects.filter(id = request.POST.get("ident"))
    if mat:
        mat[0].compte()
        cpt = mat[0].compte_recent()
        ok = "OK"
    data = {"ok": ok, "compte": cpt.count, "date": cpt.date.strftime("%d/%m/%Y")}
    return JsonResponse(data)

def del_message(request):
    """
    fonction de rappel pour effacer le message destiné à un élève
    le POST donne un paramètre ident
    """
    eleve = Eleves.objects.get(id=request.POST.get("ident"))
    eleve.message=""
    eleve.save()
    data = {
        "ok": True,
        "message": _("plus de message pour {nomprenom}").format(
            nomprenom = eleve.NomPrenomEleve)
    }
    return JsonResponse(data)

def recompte_pour_maintenant(request):
    """
    lance un routine de comptage des manuels nécessaires pour la cohorte
    actuelle d'élèves, compte tenu de leurs besoins.
    """
    Materiel.recompte_pour_maintenant()
    data = {"ok": True}
    return JsonResponse(data)
    
def eleves_de_classe(request):
    """
    renvoie le nomprenom de chaque elève d'une classe
    """
    classe = request.POST.get("classe")
    showclass = request.POST.get("showclass", False)
    nomprenoms=[e.np(showclass) for e in
                Eleves.objects.filter(Lib_Structure=classe)]
    data = {
        "nomprenoms": nomprenoms
    }
    return JsonResponse(data)

def amp(request):
    """
    renvoie les élèves à qui on a Au Moins Prêté
    n livres ; le POST contient ce paramètre-là
    """
    n = request.POST.get("n","100")
    ok = True
    nomprenoms = []
    try:
        n=int(n)
        nomprenoms = [e.np(showclass=True) for e in Eleves.objects.all() if
                      len(e.dus_codes()) >= n]
    except Exception as err:
        ok = False
    data = {
        "ok": ok,
        "nomprenoms" : nomprenoms,
    }
    return JsonResponse(data)

def app(request):
    """
    renvoie les élèves à qui on a Au Plus Prêté
    n livres sauf si on a prêté zéro livres; le POST contient ce paramètre-là
    """
    n = request.POST.get("n","1")
    ok = True
    nomprenoms = []
    try:
        n=int(n)
        nomprenoms = [e.np(showclass=True) for e in Eleves.objects.all() if
                      len(e.dus_codes()) in range(1, n+1)]
    except Exception as err:
        ok = False
    data = {
        "ok": ok,
        "nomprenoms" : nomprenoms,
    }
    return JsonResponse(data)

def etl(request):
    """
    renvoie les élèves à qui on prêté un livre donné ;
    le POST contient le code du livre
    """
    code = int(request.POST.get("code","0"))
    ok = True
    nomprenoms = []
    try:
        livre = Inventaire.objects.get(id = code)
        nomprenoms = [pret.eleve.np(showclass=True) for pret in
                      livre.prets_set.all()]
    except Exception as err:
        ok = False
    data = {
        "ok": ok,
        "nomprenoms" : nomprenoms,
    }
    return JsonResponse(data)

def mcm(request):
    """
    renvoie les élèves à qui un message contenent un mot-clé
    le POST contient le paramètre cle
    """
    cle = request.POST.get("cle","un mot-clé impossible")
    ok = True
    nomprenoms = []
    try:
        nomprenoms = [e.np(showclass=True) for e in
                      Eleves.objects.filter(message__icontains = cle)]
    except Exception as err:
        ok = False
    data = {
        "ok": ok,
        "nomprenoms" : nomprenoms,
    }
    return JsonResponse(data)

@ensure_csrf_cookie
@i18n
def en_stock_more(request):
    """
    Renvoie les 40 premiers manuels en stock
    puis place un cookie de session pour faciliter l'accès aux
    suivants, en donnant accès un générateur
    Des paramètres peuvent être reçus par POST : mode, qui peut
    avoir pour valeur "en-stock", ...
    @return du code HTML semblable à l'inventaire complet
    """
    mode = request.POST.get("mode", "en-stock")
    if existe_iterateur(mode):
        generateur = iterusines[mode]["it"].generator
    elif mode == "en-stock":
        # création du générateur dans le mode "en-stock"
        manuels = Inventaire.objects.filter(revuestock__isnull = False)
        generateur = (
            m for m in Inventaire.objects.filter(revuestock__isnull = False)
            if not m.Perdu and
            not m.prets_set.filter(
                date_retour = DATE111, Date_Declaration_Perte = DATE111)
        )
        iterusine = IterUsine(mode, generateur)
    # le générateur est maintenant défini, il est dans Iterusine
    # identifié par le nom <mode>
    dernier, l = IterUsine.more(mode, request=request)
    data = {
        "manuels":
        [[m.id, m.materiel.titre if m.materiel else None] for m in l],
        "numero": dernier,
        "fini": dernier < 0,
    }
    return JsonResponse(data)

def fixEleveConfus(request):
    """
    Prépare un dialogue qui servira à démêler le cas d'élèves
    qu'on pourrait confindre. Le POST contient un champ 'sans_accent'
    et une copie du csrftoken
    """
    sans_accent = request.POST.get('sans_accent')
    confusions = Eleves.objects.filter(sans_accent=sans_accent)
    csrftoken = request.POST.get('csrftoken')
    data = {
        "ok": True,
        "html": render_to_string(
            "gestion/confusions.html",
            {
                "sans_accent": sans_accent,
                "confusions": confusions,
            }
        ),
        "title": _("Confusions possibles, pour {}").format(sans_accent),
    }
    return JsonResponse(data)

def montre_caution(request):
    """
    Montre des détails d'une caution
    POST a le paramètre no_caution
    """
    no_caution = request.POST.get("no_caution")
    c = Caution.objects.filter(numero = no_caution)
    ok = len(c) > 0
    if ok:
        c = c[0]
    html = render_to_string(
        "gestion/montre_caution.html",
        {
            "c": c,
            "props": [{"check": getattr(c, f), "text": c._meta.get_field(f).verbose_name} for f in ("inconnu", "reussite", "perte", "quitte", "sc", "autre")],
        }) if ok else ""
    title=_("Détails de la caution")
    data = {
        "ok": ok,
        "title": title,
        "html": html,

    }
    return JsonResponse(data)

