# -*- mode: python -*-
# -*- coding: utf-8 -*-

##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2021 Emmanuel Viennet.  All rights reserved.
#
# 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 2 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, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
#   Emmanuel Viennet      emmanuel.viennet@viennet.net
#
##############################################################################

"""Génération des bulletins de notes

"""
from types import StringType
import pprint
import urllib
from email.MIMEMultipart import (  # pylint: disable=no-name-in-module,import-error
    MIMEMultipart,
)
from email.MIMEText import MIMEText  # pylint: disable=no-name-in-module,import-error
from email.MIMEBase import MIMEBase  # pylint: disable=no-name-in-module,import-error
from email.Header import Header  # pylint: disable=no-name-in-module,import-error
from email import Encoders  # pylint: disable=no-name-in-module,import-error

import time
import htmlutils
from reportlab.lib.colors import Color

import sco_utils as scu
import notesdb as ndb
from notes_log import log
import scolars
from sco_permissions import ScoImplement, ScoEtudInscrit
from sco_exceptions import AccessDenied
import sco_codes_parcours
import sco_formsemestre
import sco_groups
import sco_pvjury
import sco_formsemestre_status
import sco_photos
import sco_abs
import sco_abs_views
import sco_preferences
import sco_codes_parcours
import sco_bulletins_generator
import sco_bulletins_xml
import sco_bulletins_json


def make_context_dict(context, sem, etud):
    """Construit dictionnaire avec valeurs pour substitution des textes
    (preferences bul_pdf_*)
    """
    C = sem.copy()
    C["responsable"] = " ,".join(
        [
            context.Users.user_info(user_name=responsable_id)["prenomnom"]
            for responsable_id in sem["responsables"]
        ]
    )

    annee_debut = sem["date_debut"].split("/")[2]
    annee_fin = sem["date_fin"].split("/")[2]
    if annee_debut != annee_fin:
        annee = "%s - %s" % (annee_debut, annee_fin)
    else:
        annee = annee_debut
    C["anneesem"] = annee
    C.update(etud)
    # copie preferences
    for name in sco_preferences.PREFS_NAMES:
        C[name] = context.get_preference(name, sem["formsemestre_id"])

    # ajoute groupes et group_0, group_1, ...
    sco_groups.etud_add_group_infos(context, etud, sem)
    C["groupes"] = etud["groupes"]
    n = 0
    for partition_id in etud["partitions"]:
        C["group_%d" % n] = etud["partitions"][partition_id]["group_name"]
        n += 1

    # ajoute date courante
    t = time.localtime()
    C["date_dmy"] = time.strftime("%d/%m/%Y", t)
    C["date_iso"] = time.strftime("%Y-%m-%d", t)

    return C


def formsemestre_bulletinetud_dict(
    context, formsemestre_id, etudid, version="long", REQUEST=None
):
    """Collecte informations pour bulletin de notes
    Retourne un dictionnaire (avec valeur par défaut chaine vide).
    Le contenu du dictionnaire dépend des options (rangs, ...)
    et de la version choisie (short, long, selectedevals).

    Cette fonction est utilisée pour les bulletins HTML et PDF, mais pas ceux en XML.
    """
    if not version in ("short", "long", "selectedevals"):
        raise ValueError("invalid version code !")

    prefs = context.get_preferences(formsemestre_id)
    nt = context._getNotesCache().get_NotesTable(
        context, formsemestre_id
    )  # > toutes notes

    I = scu.DictDefault(defaultvalue="")
    I["etudid"] = etudid
    I["formsemestre_id"] = formsemestre_id
    I["sem"] = nt.sem
    if REQUEST:
        I["server_name"] = REQUEST.BASE0
    else:
        I["server_name"] = ""

    # Formation et parcours
    I["formation"] = context.formation_list(
        args={"formation_id": I["sem"]["formation_id"]}
    )[0]
    I["parcours"] = sco_codes_parcours.get_parcours_from_code(
        I["formation"]["type_parcours"]
    )
    # Infos sur l'etudiant
    I["etud"] = context.getEtudInfo(etudid=etudid, filled=1)[0]
    I["descr_situation"] = I["etud"]["inscriptionstr"]
    if I["etud"]["inscription_formsemestre_id"]:
        I[
            "descr_situation_html"
        ] = """<a href="formsemestre_status?formsemestre_id=%s">%s</a>""" % (
            I["etud"]["inscription_formsemestre_id"],
            I["descr_situation"],
        )
    else:
        I["descr_situation_html"] = I["descr_situation"]
    # Groupes:
    partitions = sco_groups.get_partitions_list(
        context, formsemestre_id, with_default=False
    )
    partitions_etud_groups = {}  # { partition_id : { etudid : group } }
    for partition in partitions:
        pid = partition["partition_id"]
        partitions_etud_groups[pid] = sco_groups.get_etud_groups_in_partition(
            context, pid
        )
    # --- Absences
    AbsSemEtud = sco_abs.getAbsSemEtud(context, nt.sem, etudid)
    I["nbabs"] = AbsSemEtud.CountAbs()
    I["nbabsjust"] = AbsSemEtud.CountAbsJust()

    # --- Decision Jury
    infos, dpv = etud_descr_situation_semestre(
        context,
        etudid,
        formsemestre_id,
        format="html",
        show_date_inscr=prefs["bul_show_date_inscr"],
        show_decisions=prefs["bul_show_decision"],
        show_uevalid=prefs["bul_show_uevalid"],
        show_mention=prefs["bul_show_mention"],
    )

    if dpv:
        I["decision_sem"] = dpv["decisions"][0]["decision_sem"]
    else:
        I["decision_sem"] = ""
    I.update(infos)

    I["etud_etat_html"] = nt.get_etud_etat_html(etudid)
    I["etud_etat"] = nt.get_etud_etat(etudid)
    I["filigranne"] = ""
    I["demission"] = ""
    if I["etud_etat"] == "D":
        I["demission"] = "(Démission)"
        I["filigranne"] = "Démission"
    elif I["etud_etat"] == sco_codes_parcours.DEF:
        I["demission"] = "(Défaillant)"
        I["filigranne"] = "Défaillant"
    elif (prefs["bul_show_temporary"] and not I["decision_sem"]) or prefs[
        "bul_show_temporary_forced"
    ]:
        I["filigranne"] = prefs["bul_temporary_txt"]

    # --- Appreciations
    cnx = context.GetDBConnexion()
    apprecs = scolars.appreciations_list(
        cnx, args={"etudid": etudid, "formsemestre_id": formsemestre_id}
    )
    I["appreciations_list"] = apprecs
    I["appreciations_txt"] = [x["date"] + ": " + x["comment"] for x in apprecs]
    I["appreciations"] = I[
        "appreciations_txt"
    ]  # deprecated / keep it for backward compat in templates

    # --- Notes
    ues = nt.get_ues()
    modimpls = nt.get_modimpls()
    moy_gen = nt.get_etud_moy_gen(etudid)
    I["nb_inscrits"] = len(nt.rangs)
    I["moy_gen"] = scu.fmt_note(moy_gen)
    I["moy_min"] = scu.fmt_note(nt.moy_min)
    I["moy_max"] = scu.fmt_note(nt.moy_max)
    I["mention"] = ""
    if dpv:
        decision_sem = dpv["decisions"][0]["decision_sem"]
        if decision_sem and sco_codes_parcours.code_semestre_validant(
            decision_sem["code"]
        ):
            I["mention"] = scu.get_mention(moy_gen)

    if dpv and dpv["decisions"][0]:
        I["sum_ects"] = dpv["decisions"][0]["sum_ects"]
        I["sum_ects_capitalises"] = dpv["decisions"][0]["sum_ects_capitalises"]
    else:
        I["sum_ects"] = 0
        I["sum_ects_capitalises"] = 0
    I["moy_moy"] = scu.fmt_note(nt.moy_moy)  # moyenne des moyennes generales
    if type(moy_gen) != StringType and type(nt.moy_moy) != StringType:
        I["moy_gen_bargraph_html"] = "&nbsp;" + htmlutils.horizontal_bargraph(
            moy_gen * 5, nt.moy_moy * 5
        )
    else:
        I["moy_gen_bargraph_html"] = ""

    if prefs["bul_show_rangs"]:
        rang = str(nt.get_etud_rang(etudid))
    else:
        rang = ""

    rang_gr, ninscrits_gr, gr_name = get_etud_rangs_groups(
        context, etudid, formsemestre_id, partitions, partitions_etud_groups, nt
    )

    if nt.get_moduleimpls_attente():
        # n'affiche pas le rang sur le bulletin s'il y a des
        # notes en attente dans ce semestre
        rang = scu.RANG_ATTENTE_STR
        rang_gr = scu.DictDefault(defaultvalue=scu.RANG_ATTENTE_STR)
    I["rang"] = rang
    I["rang_gr"] = rang_gr
    I["gr_name"] = gr_name
    I["ninscrits_gr"] = ninscrits_gr
    I["nbetuds"] = len(nt.rangs)
    I["nb_demissions"] = nt.nb_demissions
    I["nb_defaillants"] = nt.nb_defaillants
    if prefs["bul_show_rangs"]:
        I["rang_nt"] = "%s / %d" % (
            rang,
            I["nbetuds"] - nt.nb_demissions - nt.nb_defaillants,
        )
        I["rang_txt"] = "Rang " + I["rang_nt"]
    else:
        I["rang_nt"], I["rang_txt"] = "", ""
    I["note_max"] = 20.0  # notes toujours sur 20
    I["bonus_sport_culture"] = nt.bonus[etudid]
    # Liste les UE / modules /evals
    I["ues"] = []
    I["matieres_modules"] = {}
    I["matieres_modules_capitalized"] = {}
    for ue in ues:
        u = ue.copy()
        ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"])
        u["ue_status"] = ue_status  # { 'moy', 'coef_ue', ...}
        if ue["type"] != sco_codes_parcours.UE_SPORT:
            u["cur_moy_ue_txt"] = scu.fmt_note(ue_status["cur_moy_ue"])
        else:
            x = scu.fmt_note(nt.bonus[etudid], keep_numeric=True)
            if type(x) == StringType:
                u["cur_moy_ue_txt"] = "pas de bonus"
            else:
                u["cur_moy_ue_txt"] = "bonus de %.3g points" % x
        u["moy_ue_txt"] = scu.fmt_note(ue_status["moy"])
        if ue_status["coef_ue"] != None:
            u["coef_ue_txt"] = scu.fmt_coef(ue_status["coef_ue"])
        else:
            # C'est un bug:
            log("u=" + pprint.pformat(u))
            raise Exception("invalid None coef for ue")

        if (
            dpv
            and dpv["decisions"][0]["decisions_ue"]
            and ue["ue_id"] in dpv["decisions"][0]["decisions_ue"]
        ):
            u["ects"] = dpv["decisions"][0]["decisions_ue"][ue["ue_id"]]["ects"]
            if ue["type"] == sco_codes_parcours.UE_ELECTIVE:
                u["ects"] = (
                    "%g+" % u["ects"]
                )  # ajoute un "+" pour indiquer ECTS d'une UE élective
        else:
            if ue_status["is_capitalized"]:
                u["ects"] = ue_status["ue"].get("ects", "-")
            else:
                u["ects"] = "-"
        modules, ue_attente = _ue_mod_bulletin(
            context, etudid, formsemestre_id, ue["ue_id"], modimpls, nt, version
        )
        #
        u["modules"] = modules  # detail des modules de l'UE (dans le semestre courant)
        # auparavant on filtrait les modules sans notes
        #   si ue_status['cur_moy_ue'] != 'NA' alors u['modules'] = [] (pas de moyenne => pas de modules)

        u[
            "modules_capitalized"
        ] = []  # modules de l'UE capitalisée (liste vide si pas capitalisée)
        if ue_status["is_capitalized"]:
            sem_origin = sco_formsemestre.get_formsemestre(
                context, ue_status["formsemestre_id"]
            )
            u["ue_descr_txt"] = "Capitalisée le %s" % ndb.DateISOtoDMY(
                ue_status["event_date"]
            )
            u[
                "ue_descr_html"
            ] = '<a href="formsemestre_bulletinetud?formsemestre_id=%s&amp;etudid=%s" title="%s" class="bull_link">%s</a>' % (
                sem_origin["formsemestre_id"],
                etudid,
                sem_origin["titreannee"],
                u["ue_descr_txt"],
            )
            # log('cap details   %s' % ue_status['moy'])
            if ue_status["moy"] != "NA" and ue_status["formsemestre_id"]:
                # detail des modules de l'UE capitalisee
                nt_cap = context._getNotesCache().get_NotesTable(
                    context, ue_status["formsemestre_id"]
                )  # > toutes notes

                u["modules_capitalized"], _ = _ue_mod_bulletin(
                    context,
                    etudid,
                    formsemestre_id,
                    ue_status["capitalized_ue_id"],
                    nt_cap.get_modimpls(),
                    nt_cap,
                    version,
                )
                I["matieres_modules_capitalized"].update(
                    _sort_mod_by_matiere(u["modules_capitalized"], nt_cap, etudid)
                )
        else:
            if prefs["bul_show_ue_rangs"] and ue["type"] != sco_codes_parcours.UE_SPORT:
                if ue_attente:  # nt.get_moduleimpls_attente():
                    u["ue_descr_txt"] = "%s/%s" % (
                        scu.RANG_ATTENTE_STR,
                        nt.ue_rangs[ue["ue_id"]][1],
                    )
                else:
                    u["ue_descr_txt"] = "%s/%s" % (
                        nt.ue_rangs[ue["ue_id"]][0][etudid],
                        nt.ue_rangs[ue["ue_id"]][1],
                    )
                u["ue_descr_html"] = u["ue_descr_txt"]
            else:
                u["ue_descr_txt"] = u["ue_descr_html"] = ""

        if ue_status["is_capitalized"] or modules:
            I["ues"].append(u)  # ne montre pas les UE si non inscrit

        # Accès par matieres
        I["matieres_modules"].update(_sort_mod_by_matiere(modules, nt, etudid))

    #
    C = make_context_dict(context, I["sem"], I["etud"])
    C.update(I)
    #
    # log( 'C = \n%s\n' % pprint.pformat(C) ) # tres pratique pour voir toutes les infos dispo
    return C


def _sort_mod_by_matiere(modlist, nt, etudid):
    matmod = {}  # { matiere_id : [] }
    for mod in modlist:
        matiere_id = mod["module"]["matiere_id"]
        if matiere_id not in matmod:
            moy = nt.get_etud_mat_moy(matiere_id, etudid)
            matmod[matiere_id] = {
                "titre": mod["mat"]["titre"],
                "modules": mod,
                "moy": moy,
                "moy_txt": scu.fmt_note(moy),
            }
    return matmod


def _ue_mod_bulletin(context, etudid, formsemestre_id, ue_id, modimpls, nt, version):
    """Infos sur les modules (et évaluations) dans une UE
    (ajoute les informations aux modimpls)
    Result: liste de modules de l'UE avec les infos dans chacun (seulement ceux où l'étudiant est inscrit).
    """
    bul_show_mod_rangs = context.get_preference("bul_show_mod_rangs", formsemestre_id)
    bul_show_abs_modules = context.get_preference(
        "bul_show_abs_modules", formsemestre_id
    )
    if bul_show_abs_modules:
        sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
        debut_sem = ndb.DateDMYtoISO(sem["date_debut"])
        fin_sem = ndb.DateDMYtoISO(sem["date_fin"])

    ue_modimpls = [mod for mod in modimpls if mod["module"]["ue_id"] == ue_id]
    mods = []  # result
    ue_attente = False  # true si une eval en attente dans cette UE
    for modimpl in ue_modimpls:
        mod_attente = False
        mod = modimpl.copy()
        mod_moy = nt.get_etud_mod_moy(
            modimpl["moduleimpl_id"], etudid
        )  # peut etre 'NI'
        is_malus = mod["module"]["module_type"] == scu.MODULE_MALUS
        if bul_show_abs_modules:
            mod_abs = [
                context.Absences.CountAbs(
                    etudid=etudid,
                    debut=debut_sem,
                    fin=fin_sem,
                    moduleimpl_id=modimpl["moduleimpl_id"],
                ),
                context.Absences.CountAbsJust(
                    etudid=etudid,
                    debut=debut_sem,
                    fin=fin_sem,
                    moduleimpl_id=modimpl["moduleimpl_id"],
                ),
            ]
            mod["mod_abs_txt"] = scu.fmt_abs(mod_abs)
        else:
            mod["mod_abs_txt"] = ""

        mod["mod_moy_txt"] = scu.fmt_note(mod_moy)
        if mod["mod_moy_txt"][:2] == "NA":
            mod["mod_moy_txt"] = "-"
        if is_malus:
            if mod_moy > 0:
                mod["mod_moy_txt"] = scu.fmt_note(mod_moy)
                mod["mod_coef_txt"] = "Malus"
            elif mod_moy < 0:
                mod["mod_moy_txt"] = scu.fmt_note(-mod_moy)
                mod["mod_coef_txt"] = "Bonus"
            else:
                mod["mod_moy_txt"] = "-"
                mod["mod_coef_txt"] = "-"
        else:
            mod["mod_coef_txt"] = scu.fmt_coef(modimpl["module"]["coefficient"])
        if mod["mod_moy_txt"] != "NI":  # ne montre pas les modules 'non inscrit'
            mods.append(mod)
            if is_malus:  # n'affiche pas les statistiques sur les modules malus
                mod["stats"] = {
                    "moy": "",
                    "max": "",
                    "min": "",
                    "nb_notes": "",
                    "nb_missing": "",
                    "nb_valid_evals": "",
                }
            else:
                mod["stats"] = nt.get_mod_stats(modimpl["moduleimpl_id"])
            mod["mod_descr_txt"] = "Module %s, coef. %s (%s)" % (
                modimpl["module"]["titre"],
                scu.fmt_coef(modimpl["module"]["coefficient"]),
                context.Users.user_info(modimpl["responsable_id"])["nomcomplet"],
            )
            link_mod = (
                '<a class="bull_link" href="moduleimpl_status?moduleimpl_id=%s" title="%s">'
                % (modimpl["moduleimpl_id"], mod["mod_descr_txt"])
            )
            if context.get_preference("bul_show_codemodules", formsemestre_id):
                mod["code"] = modimpl["module"]["code"]
                mod["code_html"] = link_mod + mod["code"] + "</a>"
            else:
                mod["code"] = mod["code_html"] = ""
            mod["name"] = (
                modimpl["module"]["abbrev"] or modimpl["module"]["titre"] or ""
            )
            mod["name_html"] = link_mod + mod["name"] + "</a>"

            mod_descr = "Module %s, coef. %s (%s)" % (
                modimpl["module"]["titre"],
                scu.fmt_coef(modimpl["module"]["coefficient"]),
                context.Users.user_info(modimpl["responsable_id"])["nomcomplet"],
            )
            link_mod = (
                '<a class="bull_link" href="moduleimpl_status?moduleimpl_id=%s" title="%s">'
                % (modimpl["moduleimpl_id"], mod_descr)
            )
            if context.get_preference("bul_show_codemodules", formsemestre_id):
                mod["code_txt"] = modimpl["module"]["code"]
                mod["code_html"] = link_mod + mod["code_txt"] + "</a>"
            else:
                mod["code_txt"] = ""
                mod["code_html"] = ""
            # Evaluations: notes de chaque eval
            evals = nt.get_evals_in_mod(modimpl["moduleimpl_id"])
            mod["evaluations"] = []
            for e in evals:
                e = e.copy()
                if int(e["visibulletin"]) == 1 or version == "long":
                    # affiche "bonus" quand les points de malus sont négatifs
                    if is_malus:
                        val = e["notes"].get(etudid, {"value": "NP"})[
                            "value"
                        ]  # NA si etud demissionnaire
                        if val > 0 or val == "NP":
                            e["name"] = "Points de malus sur cette UE"
                        else:
                            e["name"] = "Points de bonus sur cette UE"
                    else:
                        e["name"] = e["description"] or "le %s" % e["jour"]
                e["target_html"] = (
                    "evaluation_listenotes?evaluation_id=%s&amp;format=html&amp;tf-submitted=1"
                    % e["evaluation_id"]
                )
                e["name_html"] = '<a class="bull_link" href="%s">%s</a>' % (
                    e["target_html"],
                    e["name"],
                )
                val = e["notes"].get(etudid, {"value": "NP"})[
                    "value"
                ]  # NA si etud demissionnaire
                if val == "NP":
                    e["note_txt"] = "nd"
                    e["note_html"] = '<span class="note_nd">nd</span>'
                    e["coef_txt"] = scu.fmt_coef(e["coefficient"])
                else:
                    # (-0.15) s'affiche "bonus de 0.15"
                    if is_malus:
                        val = abs(val)
                    e["note_txt"] = scu.fmt_note(val, note_max=e["note_max"])
                    e["note_html"] = e["note_txt"]
                    if is_malus:
                        e["coef_txt"] = ""
                    else:
                        e["coef_txt"] = scu.fmt_coef(e["coefficient"])
                if e["evaluation_type"] == scu.EVALUATION_RATTRAPAGE:
                    e["coef_txt"] = "rat."
                if e["etat"]["evalattente"]:
                    mod_attente = True  # une eval en attente dans ce module
                if (not is_malus) or (val != "NP"):
                    mod["evaluations"].append(
                        e
                    )  # ne liste pas les eval malus sans notes

            # Evaluations incomplètes ou futures:
            mod["evaluations_incompletes"] = []
            if context.get_preference("bul_show_all_evals", formsemestre_id):
                complete_eval_ids = set([e["evaluation_id"] for e in evals])
                all_evals = context.do_evaluation_list(
                    args={"moduleimpl_id": modimpl["moduleimpl_id"]}
                )
                all_evals.reverse()  # plus ancienne d'abord
                for e in all_evals:
                    if e["evaluation_id"] not in complete_eval_ids:
                        e = e.copy()
                        mod["evaluations_incompletes"].append(e)
                        e["name"] = (e["description"] or "") + " (%s)" % e["jour"]
                        e["target_html"] = (
                            "evaluation_listenotes?evaluation_id=%s&amp;format=html&amp;tf-submitted=1"
                            % e["evaluation_id"]
                        )
                        e["name_html"] = '<a class="bull_link" href="%s">%s</a>' % (
                            e["target_html"],
                            e["name"],
                        )
                        e["note_txt"] = e["note_html"] = ""
                        e["coef_txt"] = scu.fmt_coef(e["coefficient"])
            # Classement
            if bul_show_mod_rangs and mod["mod_moy_txt"] != "-" and not is_malus:
                rg = nt.mod_rangs[modimpl["moduleimpl_id"]]
                if mod_attente:  # nt.get_moduleimpls_attente():
                    mod["mod_rang"] = scu.RANG_ATTENTE_STR
                else:
                    mod["mod_rang"] = rg[0][etudid]
                mod["mod_eff"] = rg[1]  # effectif dans ce module
                mod["mod_rang_txt"] = "%s/%s" % (mod["mod_rang"], mod["mod_eff"])
            else:
                mod["mod_rang_txt"] = ""
        if mod_attente:
            ue_attente = True
    return mods, ue_attente


def get_etud_rangs_groups(
    context, etudid, formsemestre_id, partitions, partitions_etud_groups, nt
):
    """Ramene rang et nb inscrits dans chaque partition"""
    rang_gr, ninscrits_gr, gr_name = {}, {}, {}
    for partition in partitions:
        if partition["partition_name"] != None:
            partition_id = partition["partition_id"]

            if etudid in partitions_etud_groups[partition_id]:
                group = partitions_etud_groups[partition_id][etudid]

                (
                    rang_gr[partition_id],
                    ninscrits_gr[partition_id],
                ) = nt.get_etud_rang_group(etudid, group["group_id"])
                gr_name[partition_id] = group["group_name"]
            else:  # etudiant non present dans cette partition
                rang_gr[partition_id], ninscrits_gr[partition_id] = "", ""
                gr_name[partition_id] = ""

    return rang_gr, ninscrits_gr, gr_name


def etud_descr_situation_semestre(
    context,
    etudid,
    formsemestre_id,
    ne="",
    format="html",  # currently unused
    show_decisions=True,
    show_uevalid=True,
    show_date_inscr=True,
    show_mention=False,
):
    """Dict décrivant la situation de l'étudiant dans ce semestre.
    Si format == 'html', peut inclure du balisage html (actuellement inutilisé)

    situation : chaine résumant en français la situation de l'étudiant.
                Par ex. "Inscrit le 31/12/1999. Décision jury: Validé. ..."

    date_inscription : (vide si show_date_inscr est faux)
    date_demission   : (vide si pas demission ou si show_date_inscr est faux)
    descr_inscription : "Inscrit" ou "Pas inscrit[e]"
    descr_demission   : "Démission le 01/02/2000" ou vide si pas de démission
    descr_defaillance  : "Défaillant" ou vide si non défaillant.
    decision_jury     :  "Validé", "Ajourné", ... (code semestre)
    descr_decision_jury : "Décision jury: Validé" (une phrase)
    decisions_ue        : noms (acronymes) des UE validées, séparées par des virgules.
    descr_decisions_ue  : ' UE acquises: UE1, UE2', ou vide si pas de dec. ou si pas show_uevalid
    descr_mention : 'Mention Bien', ou vide si pas de mention ou si pas show_mention
    """
    cnx = context.GetDBConnexion()
    infos = scu.DictDefault(defaultvalue="")

    # --- Situation et décisions jury

    # demission/inscription ?
    events = scolars.scolar_events_list(
        cnx, args={"etudid": etudid, "formsemestre_id": formsemestre_id}
    )
    date_inscr = None
    date_dem = None
    date_def = None
    for event in events:
        event_type = event["event_type"]
        if event_type == "INSCRIPTION":
            if date_inscr:
                # plusieurs inscriptions ???
                # date_inscr += ', ' +   event['event_date'] + ' (!)'
                # il y a eu une erreur qui a laissé un event 'inscription'
                # on l'efface:
                log(
                    "etud_descr_situation_semestre: removing duplicate INSCRIPTION event for etudid=%s !"
                    % etudid
                )
                scolars.scolar_events_delete(cnx, event["event_id"])
            else:
                date_inscr = event["event_date"]
        elif event_type == "DEMISSION":
            # assert date_dem == None, 'plusieurs démissions !'
            if date_dem:  # cela ne peut pas arriver sauf bug (signale a Evry 2013?)
                log(
                    "etud_descr_situation_semestre: removing duplicate DEMISSION event for etudid=%s !"
                    % etudid
                )
                scolars.scolar_events_delete(cnx, event["event_id"])
            else:
                date_dem = event["event_date"]
        elif event_type == "DEFAILLANCE":
            if date_def:
                log(
                    "etud_descr_situation_semestre: removing duplicate DEFAILLANCE event for etudid=%s !"
                    % etudid
                )
                scolars.scolar_events_delete(cnx, event["event_id"])
            else:
                date_def = event["event_date"]
    if show_date_inscr:
        if not date_inscr:
            infos["date_inscription"] = ""
            infos["descr_inscription"] = "Pas inscrit%s." % ne
        else:
            infos["date_inscription"] = date_inscr
            infos["descr_inscription"] = "Inscrit%s le %s." % (ne, date_inscr)
    else:
        infos["date_inscription"] = ""
        infos["descr_inscription"] = ""

    infos["situation"] = infos["descr_inscription"]

    if date_dem:
        infos["descr_demission"] = "Démission le %s." % date_dem
        infos["date_demission"] = date_dem
        infos["descr_decision_jury"] = "Démission"
        infos["situation"] += " " + infos["descr_demission"]
        return infos, None  # ne donne pas les dec. de jury pour les demissionnaires
    if date_def:
        infos["descr_defaillance"] = "Défaillant%s" % ne
        infos["date_defaillance"] = date_def
        infos["descr_decision_jury"] = "Défaillant%s" % ne
        infos["situation"] += " " + infos["descr_defaillance"]

    dpv = sco_pvjury.dict_pvjury(context, formsemestre_id, etudids=[etudid])

    if not show_decisions:
        return infos, dpv

    # Decisions de jury:
    pv = dpv["decisions"][0]
    dec = ""
    if pv["decision_sem_descr"]:
        infos["decision_jury"] = pv["decision_sem_descr"]
        infos["descr_decision_jury"] = (
            "Décision jury: " + pv["decision_sem_descr"] + ". "
        )
        dec = infos["descr_decision_jury"]
    else:
        infos["descr_decision_jury"] = ""

    if pv["decisions_ue_descr"] and show_uevalid:
        infos["decisions_ue"] = pv["decisions_ue_descr"]
        infos["descr_decisions_ue"] = " UE acquises: " + pv["decisions_ue_descr"] + ". "
        dec += infos["descr_decisions_ue"]
    else:
        # infos['decisions_ue'] = None
        infos["descr_decisions_ue"] = ""

    infos["mention"] = pv["mention"]
    if pv["mention"] and show_mention:
        dec += "Mention " + pv["mention"] + ". "

    infos["situation"] += " " + dec
    if not pv["validation_parcours"]:  # parcours non terminé
        if pv["autorisations_descr"]:
            infos["situation"] += (
                " Autorisé à s'inscrire en %s." % pv["autorisations_descr"]
            )
    else:
        infos["situation"] += " Diplôme obtenu."
    return infos, dpv


# ------ Page bulletin
def formsemestre_bulletinetud(
    context,
    etudid=None,
    formsemestre_id=None,
    format="html",
    version="long",
    xml_with_decisions=False,
    force_publishing=False,  # force publication meme si semestre non publie sur "portail"
    prefer_mail_perso=False,
    REQUEST=None,
):
    "page bulletin de notes"
    try:
        etud = context.getEtudInfo(filled=1, REQUEST=REQUEST)[0]
        etudid = etud["etudid"]
    except:
        return scu.log_unknown_etud(context, REQUEST, format=format)

    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)

    R = []
    if format == "html" or format == "pdfmail":
        R.append(
            _formsemestre_bulletinetud_header_html(
                context, etud, etudid, sem, formsemestre_id, format, version, REQUEST
            )
        )

    R.append(
        do_formsemestre_bulletinetud(
            context,
            formsemestre_id,
            etudid,
            format=format,
            version=version,
            xml_with_decisions=xml_with_decisions,
            force_publishing=force_publishing,
            prefer_mail_perso=prefer_mail_perso,
            REQUEST=REQUEST,
        )[0]
    )

    if format == "html" or format == "pdfmail":
        R.append("""<p>Situation actuelle: """)
        if etud["inscription_formsemestre_id"]:
            R.append(
                """<a class="stdlink" href="formsemestre_status?formsemestre_id=%s">"""
                % etud["inscription_formsemestre_id"]
            )
        R.append(etud["inscriptionstr"])
        if etud["inscription_formsemestre_id"]:
            R.append("""</a>""")
        R.append("""</p>""")
        if sem["modalite"] == "EXT":
            R.append(
                """<p><a 
            href="formsemestre_ext_edit_ue_validations?formsemestre_id=%s&amp;etudid=%s" 
            class="stdlink">
            Editer les validations d'UE dans ce semestre extérieur
            </a></p>"""
                % (formsemestre_id, etudid)
            )
        # Place du diagramme radar
        R.append(
            """<form id="params">
        <input type="hidden" name="etudid" id="etudid" value="%s"/>
        <input type="hidden" name="formsemestre_id" id="formsemestre_id" value="%s"/>
        </form>"""
            % (etudid, formsemestre_id)
        )
        R.append('<div id="radar_bulletin"></div>')

        # --- Pied de page
        R.append(context.sco_footer(REQUEST))

    return "".join(R)


def can_send_bulletin_by_mail(context, formsemestre_id, REQUEST):
    """True if current user is allowed to send a bulletin by mail"""
    authuser = REQUEST.AUTHENTICATED_USER
    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
    return (
        context.get_preference("bul_mail_allowed_for_all", formsemestre_id)
        or authuser.has_permission(ScoImplement, context)
        or str(authuser) in sem["responsables"]
    )


def do_formsemestre_bulletinetud(
    context,
    formsemestre_id,
    etudid,
    version="long",  # short, long, selectedevals
    format="html",
    REQUEST=None,
    nohtml=False,
    xml_with_decisions=False,  # force decisions dans XML
    force_publishing=False,  # force publication meme si semestre non publie sur "portail"
    prefer_mail_perso=False,  # mails envoyes sur adresse perso si non vide
):
    """Génère le bulletin au format demandé.
    Retourne: (bul, filigranne)
    où bul est au format demandé (html, pdf, pdfmail, pdfpart, xml)
    et filigranne est un message à placer en "filigranne" (eg "Provisoire").
    """
    if format == "xml":
        bul = repr(
            sco_bulletins_xml.make_xml_formsemestre_bulletinetud(
                context,
                formsemestre_id,
                etudid,
                REQUEST=REQUEST,
                xml_with_decisions=xml_with_decisions,
                force_publishing=force_publishing,
                version=version,
            )
        )
        return bul, ""

    elif format == "json":
        bul = sco_bulletins_json.make_json_formsemestre_bulletinetud(
            context,
            formsemestre_id,
            etudid,
            REQUEST=REQUEST,
            xml_with_decisions=xml_with_decisions,
            force_publishing=force_publishing,
            version=version,
        )
        return bul, ""

    I = formsemestre_bulletinetud_dict(
        context, formsemestre_id, etudid, REQUEST=REQUEST
    )
    etud = I["etud"]

    if format == "html":
        htm, _ = sco_bulletins_generator.make_formsemestre_bulletinetud(
            context, I, version=version, format="html", REQUEST=REQUEST
        )
        return htm, I["filigranne"]

    elif format == "pdf" or format == "pdfpart":
        bul, filename = sco_bulletins_generator.make_formsemestre_bulletinetud(
            context,
            I,
            version=version,
            format="pdf",
            stand_alone=(format != "pdfpart"),
            REQUEST=REQUEST,
        )
        if format == "pdf":
            return (
                scu.sendPDFFile(REQUEST, bul, filename),
                I["filigranne"],
            )  # unused ret. value
        else:
            return bul, I["filigranne"]

    elif format == "pdfmail":
        # format pdfmail: envoie le pdf par mail a l'etud, et affiche le html
        # check permission
        if not can_send_bulletin_by_mail(context, formsemestre_id, REQUEST):
            raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")

        if nohtml:
            htm = ""  # speed up if html version not needed
        else:
            htm, _ = sco_bulletins_generator.make_formsemestre_bulletinetud(
                context, I, version=version, format="html", REQUEST=REQUEST
            )

        pdfdata, filename = sco_bulletins_generator.make_formsemestre_bulletinetud(
            context, I, version=version, format="pdf", REQUEST=REQUEST
        )

        if prefer_mail_perso:
            recipient_addr = etud.get("emailperso", "") or etud.get("email", "")
        else:
            recipient_addr = etud["email_default"]

        if not recipient_addr:
            if nohtml:
                h = ""  # permet de compter les non-envois
            else:
                h = (
                    "<div class=\"boldredmsg\">%s n'a pas d'adresse e-mail !</div>"
                    % etud["nomprenom"]
                ) + htm
            return h, I["filigranne"]
        #
        mail_bulletin(context, formsemestre_id, I, pdfdata, filename, recipient_addr)
        emaillink = '<a class="stdlink" href="mailto:%s">%s</a>' % (
            recipient_addr,
            recipient_addr,
        )
        return (
            ('<div class="head_message">Message mail envoyé à %s</div>' % (emaillink))
            + htm,
            I["filigranne"],
        )

    else:
        raise ValueError("do_formsemestre_bulletinetud: invalid format (%s)" % format)


def mail_bulletin(context, formsemestre_id, I, pdfdata, filename, recipient_addr):
    """Send bulletin by email to etud
    If bul_mail_list_abs pref is true, put list of absences in mail body (text).
    """
    etud = I["etud"]
    webmaster = context.get_preference("bul_mail_contact_addr", formsemestre_id)
    dept = scu.unescape_html(context.get_preference("DeptName", formsemestre_id))
    copy_addr = context.get_preference("email_copy_bulletins", formsemestre_id)
    intro_mail = context.get_preference("bul_intro_mail", formsemestre_id)

    if intro_mail:
        hea = intro_mail % {
            "nomprenom": etud["nomprenom"],
            "dept": dept,
            "webmaster": webmaster,
        }
    else:
        hea = ""

    if context.get_preference("bul_mail_list_abs"):
        hea += "\n\n" + sco_abs_views.ListeAbsEtud(
            context, etud["etudid"], with_evals=False, format="text"
        )

    msg = MIMEMultipart()
    subj = Header("Relevé de notes de %s" % etud["nomprenom"], scu.SCO_ENCODING)
    recipients = [recipient_addr]
    msg["Subject"] = subj
    msg["From"] = context.get_preference("email_from_addr", formsemestre_id)
    msg["To"] = " ,".join(recipients)
    if copy_addr:
        msg["Bcc"] = copy_addr.strip()
    # Guarantees the message ends in a newline
    msg.epilogue = ""
    # Text
    txt = MIMEText(hea, "plain", scu.SCO_ENCODING)
    # log('hea:\n' + hea)
    msg.attach(txt)
    # Attach pdf
    att = MIMEBase("application", "pdf")
    att.add_header("Content-Disposition", "attachment", filename=filename)
    att.set_payload(pdfdata)
    Encoders.encode_base64(att)
    msg.attach(att)
    log("mail bulletin a %s" % msg["To"])
    context.sendEmail(msg)


def _formsemestre_bulletinetud_header_html(
    context,
    etud,
    etudid,
    sem,
    formsemestre_id=None,
    format=None,
    version=None,
    REQUEST=None,
):
    authuser = REQUEST.AUTHENTICATED_USER
    uid = str(authuser)
    H = [
        context.sco_header(
            page_title="Bulletin de %(nomprenom)s" % etud,
            REQUEST=REQUEST,
            javascripts=[
                "js/bulletin.js",
                "libjs/d3.v3.min.js",
                "js/radar_bulletin.js",
            ],
            cssstyles=["css/radar_bulletin.css"],
        ),
        """<table class="bull_head"><tr><td>
          <h2><a class="discretelink" href="ficheEtud?etudid=%(etudid)s">%(nomprenom)s</a></h2>
          """
        % etud,
        """
          <form name="f" method="GET" action="%s">"""
        % REQUEST.URL0,
        """Bulletin <span class="bull_liensemestre"><a href="formsemestre_status?formsemestre_id=%(formsemestre_id)s">
          %(titremois)s</a></span> 
          <br/>"""
        % sem,
        """<table><tr>""",
        """<td>établi le %s (notes sur 20)</td>""" % time.strftime("%d/%m/%Y à %Hh%M"),
        """<td><span class="rightjust">
             <input type="hidden" name="formsemestre_id" value="%s"></input>"""
        % formsemestre_id,
        """<input type="hidden" name="etudid" value="%s"></input>""" % etudid,
        """<input type="hidden" name="format" value="%s"></input>""" % format,
        """<select name="version" onchange="document.f.submit()" class="noprint">""",
    ]
    for (v, e) in (
        ("short", "Version courte"),
        ("selectedevals", "Version intermédiaire"),
        ("long", "Version complète"),
    ):
        if v == version:
            selected = " selected"
        else:
            selected = ""
        H.append('<option value="%s"%s>%s</option>' % (v, selected, e))
    H.append("""</select></td>""")
    # Menu
    url = REQUEST.URL0
    qurl = urllib.quote_plus(url + "?" + REQUEST.QUERY_STRING)

    menuBul = [
        {
            "title": "Réglages bulletins",
            "url": "formsemestre_edit_options?formsemestre_id=%s&amp;target_url=%s"
            % (formsemestre_id, qurl),
            "enabled": (uid in sem["responsables"])
            or authuser.has_permission(ScoImplement, context),
        },
        {
            "title": 'Version papier (pdf, format "%s")'
            % sco_bulletins_generator.bulletin_get_class_name_displayed(
                context, formsemestre_id
            ),
            "url": url
            + "?formsemestre_id=%s&amp;etudid=%s&amp;format=pdf&amp;version=%s"
            % (formsemestre_id, etudid, version),
        },
        {
            "title": "Envoi par mail à %s" % etud["email"],
            "url": url
            + "?formsemestre_id=%s&amp;etudid=%s&amp;format=pdfmail&amp;version=%s"
            % (formsemestre_id, etudid, version),
            "enabled": etud["email"]
            and can_send_bulletin_by_mail(
                context, formsemestre_id, REQUEST
            ),  # possible slt si on a un mail...
        },
        {
            "title": "Envoi par mail à %s (adr. personnelle)" % etud["emailperso"],
            "url": url
            + "?formsemestre_id=%s&amp;etudid=%s&amp;format=pdfmail&amp;version=%s&amp;prefer_mail_perso=1"
            % (formsemestre_id, etudid, version),
            "enabled": etud["emailperso"]
            and can_send_bulletin_by_mail(
                context, formsemestre_id, REQUEST
            ),  # possible slt si on a un mail...
        },
        {
            "title": "Version XML",
            "url": url
            + "?formsemestre_id=%s&amp;etudid=%s&amp;format=xml&amp;version=%s"
            % (formsemestre_id, etudid, version),
        },
        {
            "title": "Ajouter une appréciation",
            "url": "appreciation_add_form?etudid=%s&amp;formsemestre_id=%s"
            % (etudid, formsemestre_id),
            "enabled": (
                (authuser in sem["responsables"])
                or (authuser.has_permission(ScoEtudInscrit, context))
            ),
        },
        {
            "title": "Enregistrer un semestre effectué ailleurs",
            "url": "formsemestre_ext_create_form?etudid=%s&amp;formsemestre_id=%s"
            % (etudid, formsemestre_id),
            "enabled": authuser.has_permission(ScoImplement, context),
        },
        {
            "title": "Enregistrer une validation d'UE antérieure",
            "url": "formsemestre_validate_previous_ue?etudid=%s&amp;formsemestre_id=%s"
            % (etudid, formsemestre_id),
            "enabled": context._can_validate_sem(REQUEST, formsemestre_id),
        },
        {
            "title": "Enregistrer note d'une UE externe",
            "url": "external_ue_create_form?etudid=%s&amp;formsemestre_id=%s"
            % (etudid, formsemestre_id),
            "enabled": context._can_validate_sem(REQUEST, formsemestre_id),
        },
        {
            "title": "Entrer décisions jury",
            "url": "formsemestre_validation_etud_form?formsemestre_id=%s&amp;etudid=%s"
            % (formsemestre_id, etudid),
            "enabled": context._can_validate_sem(REQUEST, formsemestre_id),
        },
        {
            "title": "Editer PV jury",
            "url": "formsemestre_pvjury_pdf?formsemestre_id=%s&amp;etudid=%s"
            % (formsemestre_id, etudid),
            "enabled": True,
        },
    ]

    H.append("""<td class="bulletin_menubar"><div class="bulletin_menubar">""")
    H.append(htmlutils.make_menu("Autres opérations", menuBul, alone=True))
    H.append("""</div></td>""")
    H.append(
        '<td> <a href="%s">%s</a></td>'
        % (
            url
            + "?formsemestre_id=%s&amp;etudid=%s&amp;format=pdf&amp;version=%s"
            % (formsemestre_id, etudid, version),
            scu.ICON_PDF,
        )
    )
    H.append("""</tr></table>""")
    #
    H.append(
        """</form></span></td><td class="bull_photo">
    <a href="%s/ficheEtud?etudid=%s">%s</a>
    """
        % (
            context.ScoURL(),
            etudid,
            sco_photos.etud_photo_html(
                context, etud, title="fiche de " + etud["nom"], REQUEST=REQUEST
            ),
        )
    )
    H.append(
        """</td></tr>
    </table>
    """
    )

    return "".join(H)


def formsemestre_bulletins_choice(
    context, REQUEST, formsemestre_id, title="", explanation="", choose_mail=False
):
    """Choix d'une version de bulletin"""
    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
    H = [
        context.html_sem_header(REQUEST, title, sem),
        """
      <form name="f" method="GET" action="%s">
      <input type="hidden" name="formsemestre_id" value="%s"></input>
      """
        % (REQUEST.URL0, formsemestre_id),
    ]
    H.append("""<select name="version" class="noprint">""")
    for (v, e) in (
        ("short", "Version courte"),
        ("selectedevals", "Version intermédiaire"),
        ("long", "Version complète"),
    ):
        H.append('<option value="%s">%s</option>' % (v, e))

    H.append("""</select>&nbsp;&nbsp;<input type="submit" value="Générer"/>""")
    if choose_mail:
        H.append(
            """<div><input type="checkbox" name="prefer_mail_perso" value="1">Utiliser si possible les adresses personnelles</div>"""
        )

    H.append("""<p class="help">""" + explanation + """</p>""")

    return "\n".join(H) + context.sco_footer(REQUEST)


expl_bull = """Versions des bulletins:<ul><li><bf>courte</bf>: moyennes des modules</li><li><bf>intermédiaire</bf>: moyennes des modules et notes des évaluations sélectionnées</li><li><bf>complète</bf>: toutes les notes</li><ul>"""


def formsemestre_bulletins_pdf_choice(context, REQUEST, formsemestre_id, version=None):
    """Choix version puis envois classeur bulletins pdf"""
    if version:
        return context.formsemestre_bulletins_pdf(
            formsemestre_id, REQUEST, version=version
        )
    return formsemestre_bulletins_choice(
        context,
        REQUEST,
        formsemestre_id,
        title="Choisir la version des bulletins à générer",
        explanation=expl_bull,
    )


def formsemestre_bulletins_mailetuds_choice(
    context,
    REQUEST,
    formsemestre_id,
    version=None,
    dialog_confirmed=False,
    prefer_mail_perso=0,
):
    """Choix version puis envois classeur bulletins pdf"""
    if version:
        return context.formsemestre_bulletins_mailetuds(
            formsemestre_id,
            REQUEST,
            version=version,
            dialog_confirmed=dialog_confirmed,
            prefer_mail_perso=prefer_mail_perso,
        )
    return formsemestre_bulletins_choice(
        context,
        REQUEST,
        formsemestre_id,
        title="Choisir la version des bulletins à envoyer par mail",
        explanation="Chaque étudiant ayant une adresse mail connue de ScoDoc recevra une copie PDF de son bulletin de notes, dans la version choisie.</p><p>"
        + expl_bull,
        choose_mail=True,
    )


"""
from debug import *
from sco_bulletins import *
context = go_dept(app, 'RT')
etudid='EID25013'
etudid='EID27760' # bonh
formsemestre_id = 'SEM27425'
I = formsemestre_bulletinetud_dict(context.Notes, formsemestre_id, etudid, REQUEST=REQUEST)
"""