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

from .models import Eleves, Classes, Materiel, Livre_a_lire, \
    LivreSupplement, Materiel_regroupe, Inventaire, Prets, RevueStock, \
    Caution, MIN_NO_CAUTION, Eleves_de_siecle, les_modeles, Courriers, \
    Parametres, chaine_sans_accent, DICT_ETAT_MATERIEL_CHOIX, \
    etat_AUTRE
from .correlation import CORR
import gestion.etiquettes as etiquettes

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

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

@ensure_csrf_cookie
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 \
        f"Erreur : il n'y a pas d'élève appelé(e) « {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)
            les_prets.append(p)
    etats =  DICT_ETAT_MATERIEL_CHOIX
    lignes = [render_to_string(
        'gestion/tableau_retours.html',
        {"p": p,
         "etats": etats,
         "mat": p.inventaire.materiel.type_materiel,
         }) for p in les_prets]
    data = {
        "lignes": lignes,
        "msg": msg,
        "nomprenom": eleve[0].NomPrenomEleve if eleve else "",
        "classe": eleve[0].Lib_Structure if eleve else "",
        "now": now,
    }
    return JsonResponse(data)

@ensure_csrf_cookie
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.
    """
    nomprenom = request.POST.get("nomprenom")
    code = request.POST.get("code")
    eleve = Eleves.objects.get(NomPrenomEleve = nomprenom)
    now = date.today().strftime("%Y-%m-%d")
    titre = ""
    try:
        manuel = Inventaire.objects.get(id = code)
        titre = manuel.materiel.titre
    except  StopIteration:
        pass
    pret = Prets.objects.filter(
        eleve = eleve,
        inventaire__id = code,
    ).filter(~Q(date_retour__gt = DATE111))
    rendu = False
    ligne = ""
    if pret:
        rendu = True
        pret = pret[0]
        pret.date_retour = date.fromisoformat(now)
        pret.save()
        ## on modifie le nombre de manuels prêtés au niveau du catalogue
        entree_catalogue = Materiel.objects.get(id = pret.inventaire.materiel.id)
        # au cas où ce n'aurait jamais été fait par le passé, on refait
        # un inventaire pour le titre courant
        if entree_catalogue.Pretes is None:
            entree_catalogue.refait_inventaire(id_materiel = entree_catalogue.id)
            entree_catalogue = Materiel.objects.get(id = pret.inventaire.materiel.id)
        # enfin on acte du rendu
        entree_catalogue.Pretes -=1
        entree_catalogue.save()
        ## on prépare la réponse
        etats =  DICT_ETAT_MATERIEL_CHOIX
        ligne = render(request, "gestion/tableau_retours.html",{
            "p": pret,
            "etats": etats,
        }).content.decode()
    else:
        pret =  Prets.objects.filter(
            inventaire__id = code
        ).filter(~Q(date_retour__gt = DATE111))
        if pret:
            nomprenom = pret[0].eleve.NomPrenomEleve
        else:
            nomprenom = ""
    data={
        "rendu": rendu,
        "nomprenom": nomprenom,
        "manuel": titre,
        "code": code,
        "ligne": ligne,
    }
    return JsonResponse(data)

@ensure_csrf_cookie
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 len(pret)
    if ok:
        pret = pret[0]
        pret.Etat_Final = int(code_etat)
        pret.save()
        degrade = pret.isDegrade
        ligne = render(request, "gestion/tableau_retours.html",{
            "p": pret,
            "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
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
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
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"
    """
    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 = pret_unique and eleve and manuel and not possesseur
    tr_manuel = ""
    if ok:
        tr_manuel = render(request, "gestion/pret_ligne.html",{
            "manuel": manuel,
            }).content.decode()
    data = {
        "ok": ok,
        "messages" : messages,
        "tr_manuel" : tr_manuel,
        "nomprenom" : nomprenom,
        "code": code,
    }
    return JsonResponse(data)

@ensure_csrf_cookie
def prets_go(request):
    """
    Force le prêt de livres à quelq'un
    Paramètres postés : "nomprenom", "liste_codes"
    """
    nomprenom = request.POST.get("nomprenom", "")
    liste_codes = json.loads(request.POST.get("liste_codes", ""))
    eleve = Eleves.objects.get(NomPrenomEleve = nomprenom)
    date = datetime.now().strftime("%Y-%m-%d")
    manuels = [ Inventaire.objects.get(id = code) for code in liste_codes ]
    for manuel in manuels:
        if manuel.actuellement_a() is not None:
            continue # on ne prête pas un manuel actuellement prêté ni perdu
        etat = manuel.Etat_Materiel
        pret = Prets(
            eleve=eleve,
            inventaire = manuel,
            date_pret = date,
            Etat_Initial = etat,
            Etat_Final = 0,
        )
        pret.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
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
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
def aqui(request):
    """
    répond à la question "à qui est ce livre ?" par du code html
    """
    ident = request.POST.get("ident")
    found = None
    prets = []
    revue = []
    flashes = []
    if ident:
        found = Inventaire.objects.filter(id = ident)
        found = found[0] if found else None
        prets = Prets.objects.filter(inventaire = ident)
        revue = RevueStock.objects.filter(code__id = ident)
        flashes = [(p.date_pret, p, "prete") for p in prets] + \
            [(p.date_retour, p, "retour") for p in prets \
             if p.date_retour > DATE111] + \
                [(r.date.date(), r, "revue") for r in revue]
        flashes.sort(key = lambda x: x[0])
    return render(request,'gestion/aqui.html', {
        "ident": ident,
        "found": found,
        "flashes": flashes,
        "today": date.today(),
    })

@ensure_csrf_cookie
def inventaire_premiers(request):
    """
    Renvoie les 40 premiers manuels non prêtés et non perdus,
    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 ?)"
    it, tstamp = Inventaire.iter_non_prete(id_catalogue)
    en_stock = []
    i = 0
    numero = 0
    while i < 40:
        try:
            numero, manuel = next(it)
            en_stock.append(manuel)
        except StopIteration:
            break
        i += 1
    if not id_catalogue:
        titres = Materiel.objects.all()
    else:
        titres = Materiel.objects.filter(
            id=id_catalogue)
    total = sum([(m.Achetes - m.Pretes - m.Perdus) for m in titres \
                 if m.Achetes is not None and m.Pretes is not None and \
                 m.Perdus is not None])
    request.session["inventaire_progressif"] = tstamp
    data = {
        "manuels": [[m.id, m.materiel.titre if m.materiel else None] for
                      m in en_stock],
        "message": message,
        "numero": numero,
        "total": total,
    }
    return JsonResponse(data)
    
@ensure_csrf_cookie
def inventaire_premiers_pretes(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 ?)"
    it, tstamp = Inventaire.iter_prete(id_catalogue)
    en_stock = []
    i = 0
    numero = 0
    while i < 40:
        try:
            numero, manuel = next(it)
            en_stock.append(manuel)
        except StopIteration:
            break
        i += 1
    if not id_catalogue:
        titres = Materiel.objects.all()
    else:
        titres = Materiel.objects.filter(
            id=id_catalogue)
    total = sum([
        m.Pretes-m.Perdus for m in titres if m.Pretes is not None and \
        m.Perdus is not None])
    request.session["inventaire_progressif_pretes"] = tstamp
    data = {
        "manuels": [[m.id, m.materiel.titre if m.materiel else None] for m in en_stock],
        "message": message,
        "numero": numero,
        "total": total,
    }
    return JsonResponse(data)

@ensure_csrf_cookie
def inventaire_suivants(request):
    """
    Renvoie les 40 suivants manuels non prêtés, en utilisant un cookie
    de session pour récupérer l'itérateur paresseux.
    @return du code JSON pour continuer l'inventaire
    """
    tstamp = request.session.get("inventaire_progressif", "")
    it = Inventaire.iterateurs_livres[tstamp]
    en_stock = []
    i = 0
    fini = False
    numero = 0
    while i < 40:
        try:
            numero, manuel = next(it)
            en_stock.append(manuel)
        except StopIteration:
            fini = True
            break
        i += 1
    data = {
        "manuels": [[m.id, m.materiel.titre if m.materiel else None]
                      for m in en_stock],
        "fini": "fini" if fini else "",
        "numero": numero,
    }
    return JsonResponse(data)
    
@ensure_csrf_cookie
def inventaire_suivantsPretes(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
    """
    tstamp = request.session.get("inventaire_progressif_pretes", "")
    it = Inventaire.iterateurs_livres[tstamp]
    en_stock = []
    i = 0
    fini = False
    numero = 0
    while i < 40:
        try:
            numero, manuel = next(it)
            en_stock.append(manuel)
        except StopIteration:
            fini = True
            break
        i += 1
    data = {
        "manuels": [[m.id, m.materiel.titre if m.materiel else None]
                      for m in en_stock],
        "fini": "fini" if fini else "",
        "numero": numero,
    }
    return JsonResponse(data)
    
@ensure_csrf_cookie
def correspondance_nom(request):
    """
    Recherche des noms/prénoms correspondant au début donné par le
    paramètre term
    """
    if request.headers.get('x-requested-with') == 'XMLHttpRequest':
        query = request.POST.get("term", "")
        query = chaine_sans_accent(query).lower()
        eleves = Eleves.objects.filter(sans_accent__contains=query)
        
        classes_a_chercher = [c.libelle for c in Classes.classes_importantes()]\
            + ["PAS_DE_CLASSE"]
        correspond = [e.NomPrenomEleve for e in \
                       eleves if e.Lib_Structure in classes_a_chercher ]
    else:
        correspond = []
    return JsonResponse(correspond, safe=False)

@ensure_csrf_cookie
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
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
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
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
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
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_Heure_Pret_Materiel__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
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
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_Heure_Pret_Materiel__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,
    }
    return JsonResponse(data)

@ensure_csrf_cookie
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
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")

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

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

@ensure_csrf_cookie
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
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
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 = ""
    it, tstamp, total = Eleves.iter_eleves_prete(id_classe)
    eleves = []
    i = 0
    numero = 0
    while i < 40:
        try:
            numero, eleve = next(it)
            eleves.append(eleve)
        except StopIteration:
            break
        i += 1
    request.session["eleves_pretes_progressif"] = tstamp
    data = {
        "eleves": [[e.id, e.NomPrenomEleve, e.prets_en_cours] for \
                   e in eleves],
        "message": message,
        "numero": numero,
        "total": total,
    }
    return JsonResponse(data)
    
@ensure_csrf_cookie
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
    """
    tstamp = request.session.get("eleves_pretes_progressif", "")
    it = Eleves.iterateurs_eleves_prete[tstamp]
    eleves = []
    i = 0
    numero = 0
    fini=False
    while i < 40:
        try:
            numero, eleve = next(it)
            eleves.append(eleve)
        except StopIteration:
            fini = True
            break
        i += 1
    data = {
        "eleves": [[e.id, e.NomPrenomEleve, e.prets_en_cours] for \
                   e in eleves],
        "numero": numero,
        "fini": "fini" if fini else "",
    }
    return JsonResponse(data)
    
@ensure_csrf_cookie
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 = ""
    it, tstamp, total = Eleves.iter_eleves_sans_prets(id_classe)
    eleves = []
    i = 0
    numero = 0
    while i < 40:
        try:
            numero, eleve = next(it)
            eleves.append(eleve)
        except StopIteration:
            break
        i += 1
    request.session["eleves_sans_prets_progressif"] = tstamp
    data = {
        "eleves": [[e.id, e.NomPrenomEleve, 0] for \
                   e in eleves],
        "message": message,
        "numero": numero,
        "total": total,
    }
    return JsonResponse(data)
    
@ensure_csrf_cookie
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
    """
    tstamp = request.session.get("eleves_sans_prets_progressif", "")
    it = Eleves.iterateurs_eleves_sans_prets[tstamp]
    eleves = []
    i = 0
    numero = 0
    fini=False
    while i < 40:
        try:
            numero, eleve = next(it)
            eleves.append(eleve)
        except StopIteration:
            fini = True
            break
        i += 1
    data = {
        "eleves": [[e.id, e.NomPrenomEleve, 0] for \
                   e in eleves],
        "numero": numero,
        "fini": "fini" if fini else "",
    }
    return JsonResponse(data)
    
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()])

            
        
    
    
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 = list(v_dic.keys())[0]
            the_field =  v_dic[the_type]
            if the_type == 'foreign':
                the_field =  the_field[0]
            if  the_field is None:
                the_field = k
            try:
                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 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
def doublonsSLM(request):
    """
    fonction de rappel pour traiter un doublon dans la base de données de SLM
    le POST contient le paramètre sans_accent
    """
    sans_accent = request.POST.get("sans_accent", "")
    eleves = Eleves.objects.filter(sans_accent = sans_accent)
    data = {
        str(e): {
            "prets": [ str(q) for q in Prets.objects.filter(eleve = e.pk)],
            "key": e.pk,
            "nomprenom": e.NomPrenomEleve,
            "caution": e.no_caution,
        } for e in eleves
    }
    return JsonResponse(data)

@ensure_csrf_cookie
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"})

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
    ne sont pas effacés.
    """
    liste_eleves = Eleves_de_siecle.objects.all()
    liste_sans_prets = Eleves.sans_prets_en_cours()
    liste_sortants = [e for e in Eleves.objects.all() if not e.homonymes(liste_eleves)]
    pas_de_classe = Classes.objects.get(libelle = "PAS_DE_CLASSE")
    for e in liste_sortants :
        e.classe = pas_de_classe
        e.Lib_Structure = "PAS_DE_CLASSE"
        e.save()
    liste_sortants_sans_prets = [
                e for e in liste_sans_prets if not e.homonymes(liste_eleves)]
    nb = len(liste_sortants_sans_prets)
    classes_touchees = set() # ensemble des classes modifiées
    for e in liste_sortants_sans_prets:
        classes_touchees.add(e.classe)
        e.delete()
    # on remet à jour l'effectif des classes touchées par l'inscription
    for classe in classes_touchees:
        classe.recompte()
    # on recalcule la liste des sortants
    liste_eleves_SLM = Eleves.objects.all()
    liste_eleves = list(Eleves_de_siecle.objects.all())
    liste_sortants1 = [
        str(e.doit) for e in liste_eleves_SLM if not e.homonymes(liste_eleves)]
    return JsonResponse({
        "nb": nb,
        "sortants": liste_sortants1,
    })

@ensure_csrf_cookie
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)
    format_str.seek(0)
    reader = csv.DictReader(format_str, delimiter=",")
    liste_eleves = list(Eleves_de_siecle.from_csv(reader))
    if liste_eleves:
        # efface ment de la table temporaire avant repeuplement
        Eleves_de_siecle.objects.all().delete()
    nouvelles_classes = set()
    for e in liste_eleves:
        nouvelles_classes.add(e.Lib_Structure)
        e.save() # on peuple la table gestion_eleves_de_siecle
    ancienne_classes = set([c.libelle for c in Classes.objects.all()])
    classes_a_creer = [c for c in nouvelles_classes if c not in ancienne_classes]
    for c in classes_a_creer:
        laclasse = Classes(code = c, libelle = c, compte =0)
        laclasse.save()
    a_afficher = [str(e) for e in liste_eleves]
    return JsonResponse({
        "ok": ok,
        "message": message,
        "a_afficher": sorted(a_afficher),
    })

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

@ensure_csrf_cookie
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,
    })

@ensure_csrf_cookie
def modificationParSiecle(request):
    """
    Modifie les attributs des élèves qui ont un homonyme dans l'import SIECLE ;
    s'il le faut, des classes sont créées.
    """
    liste_eleves_SLM = Eleves.objects.all()
    liste_eleves = list(Eleves_de_siecle.objects.all())
    liste_modifier = [
        e for e in liste_eleves_SLM if e.homonymes(liste_eleves)]
    liste_modifier_vraiment = [
        e for e in liste_modifier if \
        e.doit_etre_mis_a_jour(
            Eleves_de_siecle.objects.get(sans_accent = e.sans_accent))
    ]
    # ici on commence les modifications
    classes_touchees = set() # ensemble des classes modifiées
    for e in liste_modifier_vraiment:
        other = Eleves_de_siecle.objects.get(sans_accent = e.sans_accent)
        # on touche la classe ancienne
        classes_touchees.add(e.classe)
        for c in e.champs_importants:
            setattr(e, c, getattr(other,c))
        # ajout d'une classe s'il le faut
        try:
            classe = Classes.objects.get(libelle = e.Lib_Structure)
        except Classes.DoesNotExist:
            classe = Classes(code = e.Lib_Structure, libelle = e.Lib_Structure)
            classe.save()
        e.classe = classe
        e.save()
        # et on touche la classe nouvelle
        classes_touchees.add(e.classe)
        
    # on remet à jour l'effectif des classes touchées par l'inscription
    for classe in classes_touchees:
        classe.recompte()
    # et là, on réévalue les listes :
    liste_eleves_SLM = Eleves.objects.all()
    liste_modifier = [
        e for e in liste_eleves_SLM if e.homonymes(liste_eleves)]
    liste_modifier_vraiment = [
        e for e in liste_modifier if \
        e.doit_etre_mis_a_jour(
            Eleves_de_siecle.objects.get(sans_accent = e.sans_accent))
    ]
    # enfin, on prépare la réponse
    restants = [str(e) for e in liste_modifier]
    a_modifier_vraiment = [str(e) for e in liste_modifier_vraiment]
    return JsonResponse({
        "ok": True,
        "message": "les élèves restants ont été traités",
        "restants": sorted(restants),
        "a_modifier_vraiment": sorted(a_modifier_vraiment),
    })
    
@ensure_csrf_cookie
def entreeParSiecle(request):
    """
    Inscription de nouveaux élèves, importés par SIÈCLE.
    S'il faut créer une classe, on la crée.
    """
    liste_eleves_SLM = Eleves.objects.all()
    liste_eleves = list(
        Eleves_de_siecle.objects.all().order_by("Nom_de_famille", "Prenom",))
    liste_nouveaux = [
                e for e in liste_eleves if not e.homonymes(liste_eleves_SLM)]
    classes_touchees = set() # ensemble des classes modifiées
    for e in liste_nouveaux:
        args = {c: getattr(e,c) for c in Eleves.champs_importants}
        nouveau = Eleves(**args)
        nouveau.a_verifier = False
        try:
            classe = Classes.objects.get(
                libelle = e.Lib_Structure, code = e.Lib_Structure)
        except Classes.DoesNotExist:
            classe = Classes(code = e.Lib_Structure, libelle = e.Lib_Structure)
            classe.save()
        nouveau.classe = classe
        classes_touchees.add(classe)
        nouveau.save()
    # on remet à jour l'effectif des classes touchées par l'inscription
    for classe in classes_touchees:
        classe.recompte()
    # et là, on réévalue les listes :
    liste_eleves_SLM = Eleves.objects.all()
    liste_nouveaux = [
                e for e in liste_eleves if not e.homonymes(liste_eleves_SLM)]
    # enfin, on prépare la réponse
    nouveaux = [str(e) for e in liste_nouveaux]
    return JsonResponse({
        "ok": True,
        "message": "les nouveaux élèves sont inscrits",
        "nouveaux": sorted(nouveaux),
    })

@ensure_csrf_cookie
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.NomPrenomEleve, 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
def fin_caution(request):
    """
    Ajoute une date de fin à un enregistrement de caution,
    renseigne la somme remboursée. POST amène les paramètres
    nomprenom et rembourse et aussi le paramètre numéro, utile si
    le numéro de caution connu est zéro
    """
    nomprenom = request.POST.get("nomprenom")
    rembourse = request.POST.get("rembourse").replace(",", ".")
    numero = int(request.POST.get("numero"))
    ok = True
    try:
        eleve = Eleves.objects.get(NomPrenomEleve = nomprenom)
        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.somme_rendue = float(rembourse)
        caution.rendue_le = datetime.now().date()
        caution.save()
    except ObjectDoesNotExist:
        ok = False
    return JsonResponse({
        "ok": ok,
        "nomprenom": nomprenom,
        "rembourse": rembourse,
    })

@ensure_csrf_cookie
def commente_caution(request):
    """
    Édite les commentaires d'une caution,
    POST amène les paramètres nomprenom et val
    """
    nomprenom = request.POST.get("nomprenom")
    val = request.POST.get("val")
    ok = True
    try:
        eleve = Eleves.objects.get(NomPrenomEleve = nomprenom)
        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
def annule_caution(request):
    """
    Annule une caution (statut « sans caution ».
    POST amène le paramètre nomprenom
    """
    nomprenom = request.POST.get("nomprenom")
    ok = True
    try:
        eleve = Eleves.objects.get(NomPrenomEleve = nomprenom)
        caution = Caution.objects.get(numero = eleve.no_caution)
        caution.somme_payee = 0
        caution.save()
    except ObjectDoesNotExist:
        ok=False
    return JsonResponse({
        "ok": ok,
        "nomprenom": nomprenom,
    })

@ensure_csrf_cookie
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
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 = ""
    try:
        eleve = Eleves.objects.get(NomPrenomEleve = nomprenom)
        cautions = Caution.objects.filter(
            nom = eleve.Nom_de_famille, prenom = eleve.Prenom)
    except ObjectDoesNotExist:
        pass
    data = {
        "html": render_to_string('gestion/affiche_caution_eleve.html', {
            "eleve": eleve,
            "cautions": cautions,
	}),
        "nomprenom": nomprenom,
        "ok": bool(eleve),
    }
    return JsonResponse(data)

@ensure_csrf_cookie
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", "PAS_DE_CLASSE")
    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
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,
    }
    return JsonResponse(data)
    
@ensure_csrf_cookie
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
def au_pilon(request):
    """
    Mise d'un livre au pilon. Le POST contient le paramètre ident
    """
    ident = int(request.POST.get("ident"))
    ok = False
    livre = Inventaire.objects.filter(id=ident)
    if livre:
        ok=True
        livre[0].pilon=date.today()
        livre[0].save()
    data = {
        "ok": ok,
    }
    return JsonResponse(data)

def livre_html(li, option=None):
    """
    représentation d'un livre au format html, pour la boutique
    @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,
    })

    
@ensure_csrf_cookie
def mes_livres_en_boutique(request):
    """
    Permet de définir les livres du tronc commun et les autres, étant donné
    un élève. POST fournit le paramètre nomprenom
    
    la constante CORRELATION représente un tableau
    de corrélation entre options de SIÈCLE et abrégés de livres
    """
    nomprenom = request.POST.get("nomprenom")
    eleve = Eleves.objects.get(NomPrenomEleve = nomprenom)
    classe = eleve.Lib_Structure
    categorie = Classes.to_categorie(classe)
    matieres = []
    matieres.append(eleve.Lib_Mat_Enseignee_1)
    matieres.append(eleve.Lib_Mat_Enseignee_2)
    matieres.append(eleve.Lib_Mat_Enseignee_3)
    matieres.append(eleve.Lib_Mat_Enseignee_4)
    matieres.append(eleve.Lib_Mat_Enseignee_5)
    matieres.append(eleve.Lib_Mat_Enseignee_6)
    matieres.append(eleve.Lib_Mat_Enseignee_7)
    matieres.append(eleve.Lib_Mat_Enseignee_8)
    matieres.append(eleve.Lib_Mat_Enseignee_9)
    matieres.append(eleve.Lib_Mat_Enseignee_10)
    # livres du tronc commun
    livres_tc = [
        livre_html(li) for li in
        Materiel_regroupe.regroupe(
            Materiel.objects.filter(
                categories__contains = categorie, tronc = True))]
    # livres des options
    livres_options = []
    for m in matieres:
        if m in CORR:
            # on trouve le livre le mieux corrélé à la matière
            max=0
            abg= None
            for k,v in CORR[m].items():
                if v > max:
                    max = v
                    abg = k
            if abg:
                livres_options += list((Materiel.objects.filter(abrege = abg)))
    livres_tc += [
        livre_html(li) for li in Materiel_regroupe.regroupe(livres_options)]
    # autres livres
    livres_autres = [
        livre_html(li) for li in
        Materiel_regroupe.regroupe(
            set(Materiel.objects.filter(
                categories__contains = categorie, tronc = False)) \
            - set(livres_options)
        )
    ]
    data = {
        "classe": classe,
        "matieres": matieres,
        "livres_tc": livres_tc,
        "livres_autres": sorted( # on zyeute l'attribut data-abg pour ordonner
            livres_autres,
            key = lambda li: re.match(
                r'.*data-abg="([^"]+)".*', li, re.M|re.DOTALL).group(1)) ,
    }
    return JsonResponse(data)

@ensure_csrf_cookie
def qr_boutique_print_go(request):
    """
    Création des planches de codes_barres au format PDF.
    Paramètres postés : "tabou", et une série de paramètres id-xxx
    qui correspondent à des idnetifiants de livres, avec pour chacun
    le nombre d'étiquettes nécessaires
    """
    tabou = json.loads(request.POST.get("tabou","[]"))
    etiqs = []
    for key in request.POST:
        if not str(key).startswith("id-"):
            continue
        id = key.replace("id-", "")
        code = key.replace("id-", "coop-")
        premier_livre = Materiel.objects.filter(id = id)[0]
        abrege = premier_livre.abrege
        nombre = int(request.POST[key])
        etiqs += nombre * [etiquettes.EtiquetteBoutique(code, abrege)]
    pages = etiquettes.makePDF(etiqs, tabou)
    return FileResponse(pages, as_attachment=True, filename="qr_boutique.pdf")


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)

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)

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"]]
    data = {
        "date": li.date.isoformat(),
        "commentaire": li.commentaire,
        "prof": d["prof"],
        "livres": livres,
        "eleves": eleves,
        "id": id,
    }
    return JsonResponse(data)


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)
        print("GRRRR d =", d)
        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)

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)

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)

