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

##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2023 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
#
##############################################################################

"""Semestres: validation semestre et UE dans parcours
"""
import time

import flask
from flask import url_for, flash, g, request
from flask_login import current_user
import sqlalchemy as sa

from app.models.etudiants import Identite
import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu
from app import db, log

from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat
from app.models import Formation, FormSemestre, UniteEns, ScolarNews
from app.models.notes import etud_has_notes_attente
from app.models.validations import (
    ScolarAutorisationInscription,
    ScolarFormSemestreValidation,
)
from app.models.but_validations import ApcValidationRCUE, ApcValidationAnnee
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.scolog import logdb
from app.scodoc.codes_cursus import *
from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message

from app.scodoc import html_sco_header
from app.scodoc import sco_assiduites
from app.scodoc import codes_cursus
from app.scodoc import sco_cache
from app.scodoc import sco_edit_ue
from app.scodoc import sco_etud
from app.scodoc import sco_formsemestre
from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_cursus
from app.scodoc import sco_cursus_dut
from app.scodoc.sco_cursus_dut import etud_est_inscrit_ue
from app.scodoc import sco_photos
from app.scodoc import sco_preferences
from app.scodoc import sco_pv_dict
from app.scodoc.sco_permissions import Permission


# ------------------------------------------------------------------------------------
def formsemestre_validation_etud_form(
    formsemestre_id=None,  # required
    etudid=None,  # one of etudid or etud_index is required
    etud_index=None,
    check=0,  # opt: si true, propose juste une relecture du parcours
    dest_url=None,
    sortcol=None,
    read_only=True,
):
    """Formulaire de validation des décisions de jury"""
    formsemestre: FormSemestre = FormSemestre.query.filter_by(
        id=formsemestre_id, dept_id=g.scodoc_dept_id
    ).first_or_404()
    etud = Identite.get_etud(etudid)

    nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
    T = nt.get_table_moyennes_triees()
    if not etudid and etud_index is None:
        raise ValueError("formsemestre_validation_etud_form: missing argument etudid")
    if etud_index is not None:
        etud_index = int(etud_index)
        # cherche l'etudid correspondant
        if etud_index < 0 or etud_index >= len(T):
            raise ValueError(
                "formsemestre_validation_etud_form: invalid etud_index value"
            )
        etudid = T[etud_index][-1]
    else:
        # cherche index pour liens navigation
        etud_index = len(T) - 1
        while etud_index >= 0 and T[etud_index][-1] != etudid:
            etud_index -= 1
        if etud_index < 0:
            raise ValueError(
                "formsemestre_validation_etud_form: can't retreive etud_index !"
            )
    # prev, next pour liens navigation
    etud_index_next = etud_index + 1
    if etud_index_next >= len(T):
        etud_index_next = None
    etud_index_prev = etud_index - 1
    if etud_index_prev < 0:
        etud_index_prev = None
    if read_only:
        check = True

    etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
    Se = sco_cursus.get_situation_etud_cursus(etud, formsemestre_id)
    if not Se.sem["etat"]:
        raise ScoValueError("validation: semestre verrouille")

    url_tableau = url_for(
        "notes.formsemestre_recapcomplet",
        scodoc_dept=g.scodoc_dept,
        mode_jury=1,
        formsemestre_id=formsemestre_id,
        selected_etudid=etudid,  # va a la bonne ligne
    )

    H = [
        html_sco_header.sco_header(
            page_title=f"Parcours {etud['nomprenom']}",
            javascripts=["js/recap_parcours.js"],
        )
    ]

    # Navigation suivant/precedent
    if etud_index_prev is not None:
        etud_prev = Identite.get_etud(T[etud_index_prev][-1])
        url_prev = url_for(
            "notes.formsemestre_validation_etud_form",
            scodoc_dept=g.scodoc_dept,
            formsemestre_id=formsemestre_id,
            etud_index=etud_index_prev,
        )
    else:
        url_prev = None
    if etud_index_next is not None:
        etud_next = Identite.get_etud(T[etud_index_next][-1])
        url_next = url_for(
            "notes.formsemestre_validation_etud_form",
            scodoc_dept=g.scodoc_dept,
            formsemestre_id=formsemestre_id,
            etud_index=etud_index_next,
        )
    else:
        url_next = None
    footer = ["""<div class="jury_footer"><span>"""]
    if url_prev:
        footer.append(
            f'< <a class="stdlink" href="{url_prev}">{etud_prev.nomprenom}</a>'
        )
    footer.append(
        f"""</span><span><a class="stdlink" href="{url_tableau}">retour à la liste</a></span><span>"""
    )
    if url_next:
        footer.append(
            f'<a class="stdlink" href="{url_next}">{etud_next.nomprenom}</a> >'
        )
    footer.append("</span></div>")

    footer.append(html_sco_header.sco_footer())

    H.append('<table style="width: 100%"><tr><td>')
    if not check:
        H.append(
            '<h2 class="formsemestre">%s: validation %s%s</h2>Parcours: %s'
            % (
                etud["nomprenom"],
                Se.parcours.SESSION_NAME_A,
                Se.parcours.SESSION_NAME,
                Se.get_cursus_descr(),
            )
        )
    else:
        H.append(
            '<h2 class="formsemestre">Parcours de %s</h2>%s'
            % (etud["nomprenom"], Se.get_cursus_descr())
        )

    H.append(
        '</td><td style="text-align: right;"><a href="%s">%s</a></td></tr></table>'
        % (
            url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid),
            sco_photos.etud_photo_html(etud, title="fiche de %s" % etud["nom"]),
        )
    )

    etud_etat = nt.get_etud_etat(etudid)
    if etud_etat == scu.DEMISSION:
        H.append('<div class="ue_warning"><span>Etudiant démissionnaire</span></div>')
    if etud_etat == scu.DEF:
        H.append('<div class="ue_warning"><span>Etudiant défaillant</span></div>')
    if etud_etat != scu.INSCRIT:
        H.append(
            f"""
            <div class="warning">
            Impossible de statuer sur cet étudiant:
                il est démissionnaire ou défaillant (voir <a href="{
                    url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid)
                }">sa fiche</a>)
            </div>
            """
        )
        return "\n".join(H + footer)

    H.append(
        formsemestre_recap_parcours_table(
            Se, etudid, with_links=(check and not read_only)
        )
    )
    if check:
        if not dest_url:
            dest_url = url_tableau
        H.append(f'<ul><li><a href="{dest_url}">Continuer</a></li></ul>')

        return "\n".join(H + footer)

    decision_jury = Se.nt.get_etud_decision_sem(etudid)

    # Bloque si note en attente
    if etud_has_notes_attente(etudid, formsemestre_id):
        H.append(
            tf_error_message(
                f"""Impossible de statuer sur cet étudiant: il a des notes en
                attente dans des évaluations de ce semestre (voir <a href="{
                    url_for( "notes.formsemestre_status",
                    scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id)
                }">tableau de bord</a>)
                """
            )
        )
        return "\n".join(H + footer)

    # Infos si pas de semestre précédent
    if not Se.prev:
        if Se.sem["semestre_id"] == 1:
            H.append("<p>Premier semestre (pas de précédent)</p>")
        else:
            H.append("<p>Pas de semestre précédent !</p>")
    else:
        if not Se.prev_decision:
            H.append(
                tf_error_message(
                    f"""Le jury n'a pas statué sur le semestre précédent ! (<a href="{
                    url_for("notes.formsemestre_validation_etud_form",
                        scodoc_dept=g.scodoc_dept,
                        formsemestre_id=Se.prev["formsemestre_id"],
                        etudid=etudid)
                    }">le faire maintenant</a>)
                    """
                )
            )
            if decision_jury:
                H.append(
                    f"""<a href="{
                    url_for("notes.formsemestre_validation_suppress_etud",
                        scodoc_dept=g.scodoc_dept,
                        etudid=etudid, formsemestre_id=formsemestre_id
                    )
                    }" class="stdlink">Supprimer décision existante</a>
                    """
                )
            H.append(html_sco_header.sco_footer())
            return "\n".join(H)

    # Infos sur decisions déjà saisies
    if decision_jury:
        if decision_jury["assidu"]:
            ass = "assidu"
        else:
            ass = "non assidu"
        H.append("<p>Décision existante du %(event_date)s: %(code)s" % decision_jury)
        H.append(" (%s)" % ass)
        autorisations = ScolarAutorisationInscription.query.filter_by(
            etudid=etudid, origin_formsemestre_id=formsemestre_id
        ).all()
        if autorisations:
            H.append(". Autorisé%s à s'inscrire en " % etud["ne"])
            H.append(", ".join([f"S{aut.semestre_id}" for aut in autorisations]) + ".")
        H.append("</p>")

    # Cas particulier pour ATJ: corriger precedent avant de continuer
    if Se.prev_decision and Se.prev_decision["code"] == ATJ:
        H.append(
            """<div class="sfv_warning"><p>La décision du semestre précédent est en
        <b>attente</b> à cause d\'un <b>problème d\'assiduité<b>.</p>
        <p>Vous devez la corriger avant de continuer ce jury. Soit vous considérez que le
        problème d'assiduité n'est pas réglé et choisissez de ne pas valider le semestre
        précédent (échec), soit vous entrez une décision sans prendre en compte
        l'assiduité.</p>
        <form method="get" action="formsemestre_validation_etud_form">
        <input type="submit" value="Statuer sur le semestre précédent"/>
        <input type="hidden" name="formsemestre_id" value="%s"/>
        <input type="hidden" name="etudid" value="%s"/>
        <input type="hidden" name="desturl" value="formsemestre_validation_etud_form?etudid=%s&formsemestre_id=%s"/>
        """
            % (Se.prev["formsemestre_id"], etudid, etudid, formsemestre_id)
        )
        if sortcol:
            H.append('<input type="hidden" name="sortcol" value="%s"/>' % sortcol)
        H.append("</form></div>")

        H.append(html_sco_header.sco_footer())
        return "\n".join(H)

    # Explication sur barres actuelles
    H.append('<p class="sfv_explication">L\'étudiant ')
    if Se.barre_moy_ok:
        H.append("a la moyenne générale, ")
    else:
        H.append("<b>n'a pas</b> la moyenne générale, ")

    H.append(Se.barres_ue_diag)  # eg 'les UEs sont au dessus des barres'

    if (not Se.barre_moy_ok) and Se.can_compensate_with_prev:
        H.append(", et ce semestre peut se <b>compenser</b> avec le précédent")
    H.append(".</p>")

    # Décisions possibles
    rows_assidu = decisions_possible_rows(
        Se, True, subtitle="Étudiant assidu:", trclass="sfv_ass"
    )
    rows_non_assidu = decisions_possible_rows(
        Se, False, subtitle="Si problème d'assiduité:", trclass="sfv_pbass"
    )
    # s'il y a des decisions recommandees issues des regles:
    if rows_assidu or rows_non_assidu:
        H.append(
            """<form method="get" action="formsemestre_validation_etud" id="formvalid" class="sfv_decisions">
        <input type="hidden" name="etudid" value="%s"/>
        <input type="hidden" name="formsemestre_id" value="%s"/>"""
            % (etudid, formsemestre_id)
        )
        if dest_url:
            H.append('<input type="hidden" name="desturl" value="%s"/>' % dest_url)
        if sortcol:
            H.append('<input type="hidden" name="sortcol" value="%s"/>' % sortcol)

        H.append('<h3 class="sfv">Décisions <em>recommandées</em> :</h3>')
        H.append("<table>")
        H.append(rows_assidu)
        if rows_non_assidu:
            H.append("<tr><td>&nbsp;</td></tr>")  # spacer
            H.append(rows_non_assidu)

        H.append("</table>")
        H.append(
            '<p><br></p><input type="submit" value="Valider ce choix" disabled="1" id="subut"/>'
        )
        H.append("</form>")

    H.append(form_decision_manuelle(Se, formsemestre_id, etudid))

    H.append(
        f"""<div class="link_defaillance">Ou <a class="stdlink" href="{
            url_for("scolar.form_def", scodoc_dept=g.scodoc_dept, etudid=etudid,
                    formsemestre_id=formsemestre_id)
            }">déclarer l'étudiant comme défaillant dans ce semestre</a></div>"""
    )

    H.append('<p style="font-size: 50%;">Formation ')
    if Se.sem["gestion_semestrielle"]:
        H.append("avec semestres décalés</p>")
    else:
        H.append("sans semestres décalés</p>")

    return "".join(H + footer)


def formsemestre_validation_etud(
    formsemestre_id=None,  # required
    etudid=None,  # required
    codechoice=None,  # required
    desturl="",
    sortcol=None,
):
    """Enregistre validation"""
    etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
    Se = sco_cursus.get_situation_etud_cursus(etud, formsemestre_id)
    # retrouve la decision correspondant au code:
    choices = Se.get_possible_choices(assiduite=True)
    choices += Se.get_possible_choices(assiduite=False)
    selected_choice = None
    for choice in choices:
        if choice.codechoice == codechoice:
            selected_choice = choice
            break
    if not selected_choice:
        raise ValueError(f"code choix invalide ! ({codechoice})")
    #
    Se.valide_decision(selected_choice)  # enregistre
    return _redirect_valid_choice(
        formsemestre_id, etudid, Se, selected_choice, desturl, sortcol
    )


def formsemestre_validation_etud_manu(
    formsemestre_id=None,  # required
    etudid=None,  # required
    code_etat="",
    new_code_prev="",
    devenir="",  # required (la decision manuelle)
    assidu=False,
    desturl="",
    sortcol=None,
    redirect=True,
):
    """Enregistre validation"""
    if assidu:
        assidu = True
    etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
    Se = sco_cursus.get_situation_etud_cursus(etud, formsemestre_id)
    if code_etat in Se.parcours.UNUSED_CODES:
        raise ScoValueError("code decision invalide dans ce parcours")
    # Si code ADC, extrait le semestre utilisé:
    if code_etat[:3] == ADC:
        formsemestre_id_utilise_pour_compenser = code_etat.split("_")[1]
        if not formsemestre_id_utilise_pour_compenser:
            formsemestre_id_utilise_pour_compenser = (
                None  # compense avec semestre hors ScoDoc
            )
        code_etat = ADC
    else:
        formsemestre_id_utilise_pour_compenser = None

    # Construit le choix correspondant:
    choice = sco_cursus_dut.DecisionSem(
        code_etat=code_etat,
        new_code_prev=new_code_prev,
        devenir=devenir,
        assiduite=assidu,
        formsemestre_id_utilise_pour_compenser=formsemestre_id_utilise_pour_compenser,
    )
    #
    Se.valide_decision(choice)  # enregistre
    if redirect:
        return _redirect_valid_choice(
            formsemestre_id, etudid, Se, choice, desturl, sortcol
        )


def _redirect_valid_choice(formsemestre_id, etudid, Se, choice, desturl, sortcol):
    adr = "formsemestre_validation_etud_form?formsemestre_id=%s&etudid=%s&check=1" % (
        formsemestre_id,
        etudid,
    )
    if sortcol:
        adr += "&sortcol=" + str(sortcol)
    # if desturl:
    #    desturl += "&desturl=" + desturl
    return flask.redirect(adr)
    # Si le precedent a été modifié, demande relecture du parcours.
    # sinon  renvoie au listing general,


def _dispcode(c):
    if not c:
        return ""
    return c


def decisions_possible_rows(Se, assiduite, subtitle="", trclass=""):
    "Liste HTML des decisions possibles"
    choices = Se.get_possible_choices(assiduite=assiduite)
    if not choices:
        return ""
    TitlePrev = ""
    if Se.prev:
        if Se.prev["semestre_id"] >= 0:
            TitlePrev = "%s%d" % (Se.parcours.SESSION_ABBRV, Se.prev["semestre_id"])
        else:
            TitlePrev = "Prec."

    if Se.sem["semestre_id"] >= 0:
        TitleCur = "%s%d" % (Se.parcours.SESSION_ABBRV, Se.sem["semestre_id"])
    else:
        TitleCur = Se.parcours.SESSION_NAME

    H = [
        '<tr class="%s titles"><th class="sfv_subtitle">%s</em></th>'
        % (trclass, subtitle)
    ]
    if Se.prev:
        H.append("<th>Code %s</th>" % TitlePrev)
    H.append("<th>Code %s</th><th>Devenir</th></tr>" % TitleCur)
    for ch in choices:
        H.append(
            """<tr class="%s"><td title="règle %s"><input type="radio" name="codechoice" value="%s" onClick="document.getElementById('subut').disabled=false;">"""
            % (trclass, ch.rule_id, ch.codechoice)
        )
        H.append("%s </input></td>" % ch.explication)
        if Se.prev:
            H.append('<td class="centercell">%s</td>' % _dispcode(ch.new_code_prev))
        H.append(
            '<td class="centercell">%s</td><td>%s</td>'
            % (_dispcode(ch.code_etat), Se.explique_devenir(ch.devenir))
        )
        H.append("</tr>")

    return "\n".join(H)


def formsemestre_recap_parcours_table(
    situation_etud_cursus: sco_cursus_dut.SituationEtudCursus,
    etudid,
    with_links=False,
    with_all_columns=True,
    a_url="",
    sem_info=None,
    show_details=False,
):
    """Tableau HTML recap parcours
    Si with_links, ajoute liens pour modifier decisions (colonne de droite)
    sem_info = { formsemestre_id : txt } permet d'ajouter des informations associées à chaque semestre
    with_all_columns: si faux, pas de colonne "assiduité".
    """
    sem_info = sem_info or {}
    H = []
    linktmpl = '<span onclick="toggle_vis(this);" class="toggle_sem sem_%%s">%s</span>'
    minuslink = linktmpl % scu.icontag("minus_img", border="0", alt="-")
    pluslink = linktmpl % scu.icontag("plus_img", border="0", alt="+")
    if show_details:
        sd = " recap_show_details"
        plusminus = minuslink
    else:
        sd = " recap_hide_details"
        plusminus = pluslink
    H.append(
        f"""<table class="recap_parcours{sd}">
    <tr>
        <th><span onclick="toggle_all_sems(this);"
            title="Ouvrir/fermer tous les semestres">{
            scu.icontag("plus18_img", width=18, height=18, border=0, title="", alt="+")
        }</span></th>
        <th></th>
        <th>Semestre</th>
        <th>Etat</th>
        <th>Abs</th>
    """
    )
    # titres des UE
    H.append("<th></th>" * situation_etud_cursus.nb_max_ue)
    #
    if with_links:
        H.append("<th></th>")
    H.append("<th></th></tr>")

    num_sem = 0
    for sem in situation_etud_cursus.get_semestres():
        is_prev = situation_etud_cursus.prev and (
            situation_etud_cursus.prev["formsemestre_id"] == sem["formsemestre_id"]
        )
        is_cur = situation_etud_cursus.formsemestre_id == sem["formsemestre_id"]
        num_sem += 1

        dpv = sco_pv_dict.dict_pvjury(sem["formsemestre_id"], etudids=[etudid])
        pv = dpv["decisions"][0]
        decision_sem = pv["decision_sem"]
        decisions_ue = pv["decisions_ue"]
        if with_all_columns and decision_sem and not decision_sem["assidu"]:
            ass = " (non ass.)"
        else:
            ass = ""

        formsemestre = db.session.get(FormSemestre, sem["formsemestre_id"])
        nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
        if is_cur:
            type_sem = "*"  # now unused
            class_sem = "sem_courant"
        elif is_prev:
            type_sem = "p"
            class_sem = "sem_precedent"
        else:
            type_sem = ""
            class_sem = "sem_autre"
        if sem["formation_code"] != situation_etud_cursus.formation.formation_code:
            class_sem += " sem_autre_formation"
        if sem["bul_bgcolor"]:
            bgcolor = sem["bul_bgcolor"]
        else:
            bgcolor = "background-color: rgb(255,255,240)"
        # 1ere ligne: titre sem, decision, acronymes UE
        H.append('<tr class="%s rcp_l1 sem_%s">' % (class_sem, sem["formsemestre_id"]))
        if is_cur:
            pm = ""
        elif is_prev:
            pm = minuslink % sem["formsemestre_id"]
        else:
            pm = plusminus % sem["formsemestre_id"]

        inscr = formsemestre.etuds_inscriptions.get(etudid)
        parcours_name = ""
        if inscr and nt.is_apc:
            if inscr.parcour:
                parcours_name = (
                    f' <span class="code_parcours">{inscr.parcour.code}</span>'
                )
            else:
                # si l'étudiant n'est pas inscrit à un parcours mais que le semestre a plus d'UE
                # signale un éventuel problème:
                if len(nt.formsemestre.get_ues()) > len(
                    nt.etud_ues_ids(etudid)
                ):  # XXX sans dispenses
                    parcours_name = f"""
                    <span class="code_parcours no_parcours">{scu.EMO_WARNING}&nbsp;pas de parcours
                    </span>"""

        H.append(
            f"""
        <td class="rcp_type_sem" style="background-color:{bgcolor};">{num_sem}{pm}</td>
        <td class="datedebut">{sem['mois_debut']}</td>
        <td class="rcp_titre_sem"><a class="formsemestre_status_link"
        href="{a_url}formsemestre_bulletinetud?formsemestre_id={formsemestre.id}&etudid={etudid}"
        title="Bulletin de notes">{formsemestre.titre_annee()}{parcours_name}</a>
        """
        )
        if nt.is_apc:
            H.append(
                f"""<a class="stdlink jury_link" title="Validations du semestre BUT"
                href="{ url_for("notes.formsemestre_validation_but",
                scodoc_dept=g.scodoc_dept,
                formsemestre_id=formsemestre.id,
                etudid=etudid,
                )
            }">jury</a>"""
            )
        H.append("""</td>""")

        if nt.is_apc:
            H.append('<td class="rcp_but">BUT</td>')
        elif decision_sem:
            H.append(
                f"""<td class="rcp_dec">{
                    decision_sem["code"]}</td>"""
            )
        else:
            H.append("<td><em>en cours</em></td>")
        H.append(f"""<td class="rcp_nonass">{ass}</td>""")  # abs
        # acronymes UEs auxquelles l'étudiant est inscrit (ou capitalisé)
        ues = list(nt.etud_ues(etudid))  # nb: en BUT, les UE "dispensées" sont incluses
        cnx = ndb.GetDBConnexion()
        etud_ue_status = {ue.id: nt.get_etud_ue_status(etudid, ue.id) for ue in ues}
        if not nt.is_apc:
            # formations classiques: filtre UE sur inscriptions (et garde UE capitalisées)
            ues = [
                ue
                for ue in ues
                if etud_est_inscrit_ue(cnx, etudid, sem["formsemestre_id"], ue.id)
                or etud_ue_status[ue.id]["is_capitalized"]
            ]

        for ue in ues:
            H.append(f"""<td class="ue_acro"><span>{ue.acronyme}</span></td>""")
        if len(ues) < situation_etud_cursus.nb_max_ue:
            H.append(
                f"""<td colspan="{situation_etud_cursus.nb_max_ue - len(ues)}"></td>"""
            )
        # indique le semestre compensé par celui ci:
        if decision_sem and decision_sem["compense_formsemestre_id"]:
            csem = sco_formsemestre.get_formsemestre(
                decision_sem["compense_formsemestre_id"]
            )
            H.append(f"""<td><em>compense S{csem["semestre_id"]}</em></td>""")
        else:
            H.append("<td></td>")
        if with_links:
            H.append("<td></td>")
        H.append("</tr>")
        # 2eme ligne: notes
        H.append(f"""<tr class="{class_sem} rcp_l2 sem_{sem["formsemestre_id"]}">""")
        H.append(
            f"""<td class="rcp_type_sem"
                style="background-color:{bgcolor};">&nbsp;</td>"""
        )
        if is_prev:
            default_sem_info = '<span class="fontred">[sem. précédent]</span>'
        else:
            default_sem_info = ""
        if not sem["etat"]:  # locked
            lockicon = scu.icontag("lock32_img", title="verrouillé", border="0")
            default_sem_info += lockicon
        if sem["formation_code"] != situation_etud_cursus.formation.formation_code:
            default_sem_info += f"""Autre formation: {sem["formation_code"]}"""
        H.append(
            '<td class="datefin">%s</td><td class="sem_info">%s</td>'
            % (sem["mois_fin"], sem_info.get(sem["formsemestre_id"], default_sem_info))
        )
        # Moy Gen (sous le code decision)
        H.append(
            f"""<td class="rcp_moy">{scu.fmt_note(nt.get_etud_moy_gen(etudid))}</td>"""
        )
        # Absences (nb d'abs non just. dans ce semestre)
        nbabs, nbabsjust = sco_assiduites.get_assiduites_count(etudid, sem)
        H.append(f"""<td class="rcp_abs">{nbabs - nbabsjust}</td>""")

        # UEs
        for ue in ues:
            if decisions_ue and ue.id in decisions_ue:
                code = decisions_ue[ue.id]["code"]
            else:
                code = ""
            ue_status = etud_ue_status[ue.id]
            moy_ue = ue_status["moy"] if ue_status else ""
            explanation_ue = []  # list of strings
            if code == ADM:
                class_ue = "ue_adm"
            elif code == CMP:
                class_ue = "ue_cmp"
            else:
                class_ue = "ue"
            if ue_status and ue_status["is_external"]:  # validation externe
                explanation_ue.append("UE externe.")

            if ue_status and ue_status["is_capitalized"]:
                class_ue += " ue_capitalized"
                explanation_ue.append(
                    f"""Capitalisée le {ue_status["event_date"] or "?"}."""
                )
            # Dispense BUT ?
            if (etudid, ue.id) in nt.dispense_ues:
                moy_ue_txt = "❎" if (ue_status and ue_status["is_capitalized"]) else "⭕"
                explanation_ue.append("non inscrit (dispense)")
            else:
                moy_ue_txt = scu.fmt_note(moy_ue)
            H.append(
                f"""<td class="{class_ue}" title="{
                    " ".join(explanation_ue)
                }">{moy_ue_txt}</td>"""
            )
        if len(ues) < situation_etud_cursus.nb_max_ue:
            H.append(
                f"""<td colspan="{situation_etud_cursus.nb_max_ue - len(ues)}"></td>"""
            )

        H.append("<td></td>")
        if with_links:
            H.append(
                '<td><a href="%sformsemestre_validation_etud_form?formsemestre_id=%s&etudid=%s">modifier</a></td>'
                % (a_url, sem["formsemestre_id"], etudid)
            )

        H.append("</tr>")
        # 3eme ligne: ECTS
        if (
            sco_preferences.get_preference("bul_show_ects", sem["formsemestre_id"])
            or nt.parcours.ECTS_ONLY
        ):
            etud_ects_infos = nt.get_etud_ects_pot(etudid)  # ECTS potentiels
            H.append(
                f"""<tr class="{class_sem} rcp_l2 sem_{sem["formsemestre_id"]}">
                <td class="rcp_type_sem" style="background-color:{bgcolor};">&nbsp;</td>
                <td></td>"""
            )
            # Total ECTS (affiché sous la moyenne générale)
            H.append(
                f"""<td class="sem_ects_tit"><a title="crédit acquis">ECTS:</a></td>
                <td class="sem_ects">{pv.get("sum_ects",0):2.2g} / {etud_ects_infos["ects_total"]:2.2g}</td>
                <td class="rcp_abs"></td>
                """
            )
            # ECTS validables dans chaque UE
            for ue in ues:
                ue_status = nt.get_etud_ue_status(etudid, ue.id)
                if ue_status:
                    ects = ue_status["ects"]
                    ects_pot = ue_status["ects_pot"]
                    H.append(
                        f"""<td class="ue"
                        title="{ects:2.2g}/{ects_pot:2.2g} ECTS">{ects:2.2g}</td>"""
                    )
                else:
                    H.append("""<td class="ue"></td>""")
            H.append("<td></td></tr>")

    H.append("</table>")
    return "\n".join(H)


def form_decision_manuelle(Se, formsemestre_id, etudid, desturl="", sortcol=None):
    """Formulaire pour saisie décision manuelle"""
    H = [
        """
    <script type="text/javascript">
    function IsEmpty(aTextField) {
    if ((aTextField.value.length==0) || (aTextField.value==null)) {
        return true;
     } else { return false; }
    }
    function check_sfv_form() {
    if (IsEmpty(document.forms.formvalidmanu.code_etat)) {
       alert('Choisir un code semestre !');
       return false;
    }
    return true;
    }
    </script>

    <form method="get" action="formsemestre_validation_etud_manu" name="formvalidmanu" id="formvalidmanu" class="sfv_decisions sfv_decisions_manuelles" onsubmit="return check_sfv_form()">
    <input type="hidden" name="etudid" value="%s"/>
    <input type="hidden" name="formsemestre_id" value="%s"/>
    """
        % (etudid, formsemestre_id)
    ]
    if desturl:
        H.append('<input type="hidden" name="desturl" value="%s"/>' % desturl)
    if sortcol:
        H.append('<input type="hidden" name="sortcol" value="%s"/>' % sortcol)

    H.append(
        '<h3 class="sfv">Décisions manuelles : <em>(vérifiez bien votre choix !)</em></h3><table>'
    )

    # Choix code semestre:
    codes = sorted(codes_cursus.CODES_JURY_SEM)
    # fortuitement, cet ordre convient bien !

    H.append(
        '<tr><td>Code semestre: </td><td><select name="code_etat"><option value="" selected>Choisir...</option>'
    )
    for cod in codes:
        if cod in Se.parcours.UNUSED_CODES:
            continue
        if cod != ADC:
            H.append(
                '<option value="%s">%s (code %s)</option>'
                % (cod, codes_cursus.CODES_EXPL[cod], cod)
            )
        elif Se.sem["gestion_compensation"]:
            # traitement spécial pour ADC (compensation)
            # ne propose que les semestres avec lesquels on peut compenser
            # le code transmis est ADC_formsemestre_id
            # on propose aussi une compensation sans utiliser de semestre, pour les cas ou le semestre
            # précédent n'est pas géré dans ScoDoc (code ADC_)
            # log(str(Se.sems))
            for sem in Se.sems:
                if sem["can_compensate"]:
                    H.append(
                        '<option value="%s_%s">Admis par compensation avec S%s (%s)</option>'
                        % (
                            cod,
                            sem["formsemestre_id"],
                            sem["semestre_id"],
                            sem["date_debut"],
                        )
                    )
            if Se.could_be_compensated():
                H.append(
                    '<option value="ADC_">Admis par compensation (avec un semestre hors ScoDoc)</option>'
                )
    H.append("</select></td></tr>")

    # Choix code semestre precedent:
    if Se.prev:
        H.append(
            '<tr><td>Code semestre précédent: </td><td><select name="new_code_prev"><option value="">Choisir une décision...</option>'
        )
        for cod in codes:
            if cod == ADC:  # ne propose pas ce choix
                continue
            if Se.prev_decision and cod == Se.prev_decision["code"]:
                sel = "selected"
            else:
                sel = ""
            H.append(
                '<option value="%s" %s>%s (code %s)</option>'
                % (cod, sel, codes_cursus.CODES_EXPL[cod], cod)
            )
        H.append("</select></td></tr>")

    # Choix code devenir
    codes = list(codes_cursus.DEVENIR_EXPL.keys())
    codes.sort()  # fortuitement, cet ordre convient aussi bien !

    if Se.sem["semestre_id"] == -1:
        allowed_codes = codes_cursus.DEVENIRS_MONO
    else:
        allowed_codes = set(codes_cursus.DEVENIRS_STD)
        # semestres decales ?
        if Se.sem["gestion_semestrielle"]:
            allowed_codes = allowed_codes.union(codes_cursus.DEVENIRS_DEC)
        # n'autorise les codes NEXT2 que si semestres décalés et s'il ne manque qu'un semestre avant le n+2
        if Se.can_jump_to_next2():
            allowed_codes = allowed_codes.union(codes_cursus.DEVENIRS_NEXT2)

    H.append(
        '<tr><td>Devenir: </td><td><select name="devenir"><option value="" selected>Choisir...</option>'
    )
    for cod in codes:
        if cod in allowed_codes:  # or Se.sem['gestion_semestrielle'] == '1'
            H.append('<option value="%s">%s</option>' % (cod, Se.explique_devenir(cod)))
    H.append("</select></td></tr>")

    H.append(
        '<tr><td><input type="checkbox" name="assidu" checked="checked">assidu</input></td></tr>'
    )

    H.append(
        """</table>
    <input type="submit" name="formvalidmanu_submit" value="Valider décision manuelle"/>
    <span style="padding-left: 5em;"><a href="formsemestre_validation_suppress_etud?etudid=%s&formsemestre_id=%s" class="stdlink">Supprimer décision existante</a></span>
    </form>
    """
        % (etudid, formsemestre_id)
    )
    return "\n".join(H)


# -----------
def formsemestre_validation_auto(formsemestre_id):
    "Formulaire saisie automatisee des decisions d'un semestre"
    H = [
        html_sco_header.html_sem_header("Saisie automatique des décisions du semestre"),
        f"""
    <ul>
    <li>Seuls les étudiants qui obtiennent le semestre seront affectés (code ADM, moyenne générale et
    toutes les barres, semestre précédent validé);</li>
    <li>le semestre précédent, s'il y en a un, doit avoir été validé;</li>
    <li>les décisions du semestre précédent ne seront pas modifiées;</li>
    <li>l'assiduité n'est <b>pas</b> prise en compte;</li>
    <li>les étudiants avec des notes en attente sont ignorés.</li>
    </ul>
    <p>Il est donc vivement conseillé de relire soigneusement les décisions à l'issue
    de cette procédure !</p>
    <form action="do_formsemestre_validation_auto">
    <input type="hidden" name="formsemestre_id" value="{formsemestre_id}"/>
    <input type="submit" value="Calculer automatiquement ces décisions"/>
    <p><em>Le calcul prend quelques minutes, soyez patients !</em></p>
    </form>
    """,
        html_sco_header.sco_footer(),
    ]
    return "\n".join(H)


def do_formsemestre_validation_auto(formsemestre_id):
    "Saisie automatisee des decisions d'un semestre"
    sem = sco_formsemestre.get_formsemestre(formsemestre_id)
    next_semestre_id = sem["semestre_id"] + 1
    formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
    nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
    etudids = nt.get_etudids()
    nb_valid = 0
    conflicts = []  # liste des etudiants avec decision differente déjà saisie
    with sco_cache.DeferredSemCacheManager():
        for etudid in etudids:
            etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
            Se = sco_cursus.get_situation_etud_cursus(etud, formsemestre_id)
            ins = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(
                {"etudid": etudid, "formsemestre_id": formsemestre_id}
            )[0]

            # Conditions pour validation automatique:
            if ins["etat"] == scu.INSCRIT and (
                (
                    (not Se.prev)
                    or (
                        Se.prev_decision and Se.prev_decision["code"] in (ADM, ADC, ADJ)
                    )
                )
                and Se.barre_moy_ok
                and Se.barres_ue_ok
                and not etud_has_notes_attente(etudid, formsemestre_id)
            ):
                # check: s'il existe une decision ou autorisation et qu'elles sont differentes,
                # warning (et ne fait rien)
                decision_sem = nt.get_etud_decision_sem(etudid)
                ok = True
                if decision_sem and decision_sem["code"] != ADM:
                    ok = False
                    conflicts.append(etud)
                autorisations = ScolarAutorisationInscription.query.filter_by(
                    etudid=etudid, origin_formsemestre_id=formsemestre_id
                ).all()
                if len(autorisations) != 0:
                    if (
                        len(autorisations) > 1
                        or autorisations[0].semestre_id != next_semestre_id
                    ):
                        if ok:
                            conflicts.append(etud)
                            ok = False

                # ok, valide !
                if ok:
                    formsemestre_validation_etud_manu(
                        formsemestre_id,
                        etudid,
                        code_etat=ADM,
                        devenir="NEXT",
                        assidu=True,
                        redirect=False,
                    )
                    nb_valid += 1
    log(
        f"do_formsemestre_validation_auto: {nb_valid} validations, {len(conflicts)} conflicts"
    )
    ScolarNews.add(
        typ=ScolarNews.NEWS_JURY,
        obj=formsemestre.id,
        text=f"""Calcul jury automatique du semestre {formsemestre.html_link_status()
                                                      } ({nb_valid} décisions)""",
        url=url_for(
            "notes.formsemestre_status",
            scodoc_dept=g.scodoc_dept,
            formsemestre_id=formsemestre.id,
        ),
    )
    H = [
        f"""{html_sco_header.sco_header(page_title="Saisie automatique")}
        <h2>Saisie automatique des décisions du semestre {formsemestre.titre_annee()}</h2>
    <p>Opération effectuée.</p>
    <p>{nb_valid} étudiants validés sur {len(etudids)}</p>
    """
    ]
    if conflicts:
        H.append(
            f"""<p><b>Attention:</b> {len(conflicts)} étudiants non modifiés
            car décisions différentes déja saisies :
            <ul>"""
        )
        for etud in conflicts:
            H.append(
                f"""<li><a href="{
                    url_for('notes.formsemestre_validation_etud_form',
                    scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id,
                    etudid=etud["etudid"], check=1)
                }">{etud["nomprenom"]}</li>"""
            )
        H.append("</ul>")
    H.append(
        f"""<a href="{url_for('notes.formsemestre_recapcomplet',
        scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, mode_jury=1)
        }">continuer</a>"""
    )
    H.append(html_sco_header.sco_footer())
    return "\n".join(H)


def formsemestre_validation_suppress_etud(formsemestre_id, etudid):
    """Suppression des décisions de jury pour un étudiant/formsemestre.
    Efface toutes les décisions enregistrées concernant ce formsemestre et cet étudiant:
    code semestre, UEs, autorisations d'inscription
    """
    log(f"formsemestre_validation_suppress_etud( {formsemestre_id}, {etudid})")

    # Validations jury  classiques (semestres, UEs, autorisations)
    for v in ScolarFormSemestreValidation.query.filter_by(
        etudid=etudid, formsemestre_id=formsemestre_id
    ):
        db.session.delete(v)
    for v in ScolarAutorisationInscription.query.filter_by(
        etudid=etudid, origin_formsemestre_id=formsemestre_id
    ):
        db.session.delete(v)
    # Validations jury spécifiques BUT
    for v in ApcValidationRCUE.query.filter_by(
        etudid=etudid, formsemestre_id=formsemestre_id
    ):
        db.session.delete(v)
    for v in ApcValidationAnnee.query.filter_by(
        etudid=etudid, formsemestre_id=formsemestre_id
    ):
        db.session.delete(v)

    db.session.commit()

    sem = sco_formsemestre.get_formsemestre(formsemestre_id)
    _invalidate_etud_formation_caches(
        etudid, sem["formation_id"]
    )  # > suppr. decision jury (peut affecter de plusieurs semestres utilisant UE capitalisée)


def formsemestre_validate_previous_ue(formsemestre: FormSemestre, etud: Identite):
    """Form. saisie UE validée hors ScoDoc
    (pour étudiants arrivant avec un UE antérieurement validée).
    """
    formation: Formation = formsemestre.formation

    # Toutes les UEs non bonus de cette formation sont présentées
    # avec indice de semestre <= semestre courant ou NULL
    ues = formation.ues.filter(
        UniteEns.type != UE_SPORT,
        db.or_(
            UniteEns.semestre_idx == None,
            UniteEns.semestre_idx <= formsemestre.semestre_id,
        ),
    ).order_by(UniteEns.semestre_idx, UniteEns.numero)

    ue_names = ["Choisir..."] + [
        f"""{('S'+str(ue.semestre_idx)+' : ') if ue.semestre_idx is not None else ''
           }{ue.acronyme} {ue.titre} ({ue.ue_code or ""})"""
        for ue in ues
    ]
    ue_ids = [""] + [ue.id for ue in ues]
    form_descr = [
        ("etudid", {"input_type": "hidden"}),
        ("formsemestre_id", {"input_type": "hidden"}),
        (
            "ue_id",
            {
                "input_type": "menu",
                "title": "Unité d'Enseignement (UE)",
                "allow_null": False,
                "allowed_values": ue_ids,
                "labels": ue_names,
            },
        ),
    ]
    if not formation.is_apc():
        form_descr.append(
            (
                "semestre_id",
                {
                    "input_type": "menu",
                    "title": "Indice du semestre",
                    "explanation": "Facultatif: indice du semestre dans la formation",
                    "allow_null": True,
                    "allowed_values": [""] + [x for x in range(11)],
                    "labels": ["-"] + list(range(11)),
                },
            )
        )
    ue_codes = sorted(codes_cursus.CODES_JURY_UE)
    form_descr += [
        (
            "date",
            {
                "input_type": "date",
                "size": 9,
                "explanation": "j/m/a",
                "default": time.strftime("%d/%m/%Y"),
            },
        ),
        (
            "moy_ue",
            {
                "type": "float",
                "allow_null": False,
                "min_value": 0,
                "max_value": 20,
                "title": "Moyenne (/20) obtenue dans cette UE:",
            },
        ),
        (
            "code_jury",
            {
                "input_type": "menu",
                "title": "Code jury",
                "explanation": " code donné par le jury (ADM si validée normalement)",
                "allow_null": True,
                "allowed_values": [""] + ue_codes,
                "labels": ["-"] + ue_codes,
                "default": ADM,
            },
        ),
    ]
    tf = TrivialFormulator(
        request.base_url,
        scu.get_request_args(),
        form_descr,
        cancelbutton="Revenir au bulletin",
        submitlabel="Enregistrer validation d'UE",
    )
    if tf[0] == 0:
        return f"""
    {html_sco_header.sco_header(
        page_title="Validation UE antérieure",
        javascripts=["js/validate_previous_ue.js"],
        cssstyles=["css/jury_delete_manual.css"],
        etudid=etud.id,
        formsemestre_id=formsemestre.id,
    )}
    <h2 class="formsemestre">Gestion des validations d'UEs antérieures
    de {etud.html_link_fiche()}
    </h2>

    <p class="help">Utiliser cette page pour enregistrer des UEs validées antérieurement,
    <em>dans un semestre hors ScoDoc</em>.</p>
    <p class="expl"><b>Les UE validées dans ScoDoc sont
    automatiquement prises en compte</b>.
    </p>
    <p>Cette page est surtout utile  pour les étudiants ayant
    suivi un début de cursus dans <b>un autre établissement</b>, ou qui
    ont suivi une UE à l'étranger ou dans un semestre géré <b>sans ScoDoc</b>.
    </>
    <p>Il est aussi nécessaire de valider les UEs antérieures en cas de changement
    de référentiel de compétence en cours de cursus (par exemple si un étudiant redouble et
    que le programme change de référentiel entre temps).
    </p>
    <p>Pour les semestres précédents gérés avec ScoDoc, passer par la page jury normale.
    </p>
    <p>Notez que l'UE est validée, avec enregistrement immédiat de la décision et
    l'attribution des ECTS si le code jury est validant (ADM).
    </p>
    <p>On ne peut valider ici que les UEs du cursus <b>{formation.titre}</b></p>

    {_get_etud_ue_cap_html(etud, formsemestre)}

    <div class="sco_box">
        <div class="sco_box_title">
        Enregistrer une UE antérieure
        </div>
        {tf[1]}
    </div>
    <div id="ue_list_code" class="sco_box sco_green_bg">
        <!-- filled by ue_sharing_code -->
    </div>
    {check_formation_ues(formation.id)[0]}
    {html_sco_header.sco_footer()}
    """

    dest_url = url_for(
        "notes.formsemestre_validate_previous_ue",
        scodoc_dept=g.scodoc_dept,
        formsemestre_id=formsemestre.id,
        etudid=etud.id,
    )
    if tf[0] == -1:
        return flask.redirect(
            url_for(
                "notes.formsemestre_bulletinetud",
                scodoc_dept=g.scodoc_dept,
                formsemestre_id=formsemestre.id,
                etudid=etud.id,
            )
        )
    if tf[2].get("semestre_id"):
        semestre_id = int(tf[2]["semestre_id"])
    else:
        semestre_id = None

    if tf[2]["code_jury"] not in CODES_JURY_UE:
        flash("Code UE invalide")
        return flask.redirect(dest_url)
    do_formsemestre_validate_previous_ue(
        formsemestre,
        etud.id,
        tf[2]["ue_id"],
        tf[2]["moy_ue"],
        tf[2]["date"],
        code=tf[2]["code_jury"],
        semestre_id=semestre_id,
    )
    flash("Validation d'UE enregistrée")
    return flask.redirect(dest_url)


def _get_etud_ue_cap_html(etud: Identite, formsemestre: FormSemestre) -> str:
    """HTML listant les validations d'UEs pour cet étudiant dans des formations de même
    code que celle du formsemestre indiqué.
    """
    validations: list[ScolarFormSemestreValidation] = (
        ScolarFormSemestreValidation.query.filter_by(etudid=etud.id)
        .join(UniteEns)
        .join(Formation)
        .filter_by(formation_code=formsemestre.formation.formation_code)
        .order_by(
            sa.desc(UniteEns.semestre_idx),
            UniteEns.acronyme,
            sa.desc(ScolarFormSemestreValidation.event_date),
        )
        .all()
    )

    if not validations:
        return ""
    H = [
        f"""<div class="sco_box sco_lightgreen_bg ue_list_etud_validations">
        <div class="sco_box_title">Validations d'UEs dans cette formation</div>
        <div class="help">Liste de toutes les UEs validées par {etud.html_link_fiche()},
        sur des semestres ou déclarées comme "antérieures" (externes).
        </div>
        <ul class="liste_validations">"""
    ]
    for validation in validations:
        if validation.formsemestre_id is None:
            origine = " enregistrée d'un parcours antérieur (hors ScoDoc)"
        else:
            origine = f", du semestre {formsemestre.html_link_status()}"
        if validation.semestre_id is not None:
            origine += f" (<b>S{validation.semestre_id}</b>)"
        H.append(f"""<li>{validation.html()}""")
        if (validation.formsemestre and validation.formsemestre.can_edit_jury()) or (
            current_user and current_user.has_permission(Permission.EtudInscrit)
        ):
            H.append(
                f"""
                <form class="inline-form">
                    <button
                    data-v_id="{validation.id}" data-type="validation_ue" data-etudid="{etud.id}"
                    >effacer</button>
                </form>
                """,
            )
        else:
            H.append(scu.icontag("lock_img", border="0", title="Semestre verrouillé"))
        H.append("</li>")
    H.append("</ul></div>")
    return "\n".join(H)


def do_formsemestre_validate_previous_ue(
    formsemestre: FormSemestre,
    etudid,
    ue_id,
    moy_ue,
    date,
    code=ADM,
    semestre_id=None,
    ue_coefficient=None,
):
    """Enregistre (ou modifie) validation d'UE (obtenue hors ScoDoc).
    Si le coefficient est spécifié, modifie le coefficient de
    cette UE (utile seulement pour les semestres extérieurs).
    """
    nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
    ue: UniteEns = UniteEns.query.get_or_404(ue_id)

    cnx = ndb.GetDBConnexion()
    if ue_coefficient is not None:
        sco_formsemestre.do_formsemestre_uecoef_edit_or_create(
            cnx, formsemestre.id, ue_id, ue_coefficient
        )
    else:
        sco_formsemestre.do_formsemestre_uecoef_delete(cnx, formsemestre.id, ue_id)
    sco_cursus_dut.do_formsemestre_validate_ue(
        cnx,
        nt,
        formsemestre.id,  # "importe" cette UE dans le semestre (new 3/2015)
        etudid,
        ue_id,
        code,
        moy_ue=moy_ue,
        date=date,
        semestre_id=semestre_id,
        is_external=True,
    )

    logdb(
        cnx,
        method="formsemestre_validate_previous_ue",
        etudid=etudid,
        msg=f"Validation UE prec. {ue_id} {ue.acronyme}: {code}",
        commit=False,
    )
    _invalidate_etud_formation_caches(etudid, formsemestre.formation_id)
    cnx.commit()


def _invalidate_etud_formation_caches(etudid, formation_id):
    "Invalide tous les semestres de cette formation où l'etudiant est inscrit..."
    r = ndb.SimpleDictFetch(
        """SELECT sem.id
        FROM notes_formsemestre sem, notes_formsemestre_inscription i
        WHERE sem.formation_id = %(formation_id)s
        AND i.formsemestre_id = sem.id
        AND i.etudid = %(etudid)s
        """,
        {"etudid": etudid, "formation_id": formation_id},
    )
    for fsid in [s["id"] for s in r]:
        sco_cache.invalidate_formsemestre(
            formsemestre_id=fsid
        )  # > modif decision UE (inval tous semestres avec cet etudiant, ok mais conservatif)


def check_formation_ues(formation_id):
    """Verifie que les UE d'une formation sont chacune utilisée dans un seul semestre_id
    Si ce n'est pas le cas, c'est probablement (mais pas forcément) une erreur de
    définition du programme: cette fonction retourne un bout de HTML
    à afficher pour prévenir l'utilisateur, ou '' si tout est ok.
    """
    ues = sco_edit_ue.ue_list({"formation_id": formation_id})
    ue_multiples = {}  # { ue_id : [ liste des formsemestre ] }
    for ue in ues:
        # formsemestres utilisant cette ue ?
        sems = ndb.SimpleDictFetch(
            """SELECT DISTINCT sem.id AS formsemestre_id, sem.*
             FROM notes_formsemestre sem, notes_modules mod, notes_moduleimpl mi
             WHERE sem.formation_id = %(formation_id)s
             AND mod.id = mi.module_id
             AND mi.formsemestre_id = sem.id
             AND mod.ue_id = %(ue_id)s
             """,
            {"ue_id": ue["ue_id"], "formation_id": formation_id},
        )
        semestre_ids = set([x["semestre_id"] for x in sems])
        if (
            len(semestre_ids) > 1
        ):  # plusieurs semestres d'indices differents dans le cursus
            ue_multiples[ue["ue_id"]] = sems

    if not ue_multiples:
        return "", {}
    # Genere message HTML:
    H = [
        """<div class="ue_warning"><span>Attention:</span> les UE suivantes de cette formation
        sont utilisées dans des
        semestres de rangs différents (eg S1 et S3). <br>Cela peut engendrer des problèmes pour
        la capitalisation des UE. Il serait préférable d'essayer de rectifier cette situation:
        soit modifier le programme de la formation (définir des UE dans chaque semestre),
        soit veiller à saisir le bon indice de semestre dans le menu lors de la validation d'une
        UE extérieure.
        <ul>
        """
    ]
    for ue in ues:
        if ue["ue_id"] in ue_multiples:
            sems = [
                sco_formsemestre.get_formsemestre(x["formsemestre_id"])
                for x in ue_multiples[ue["ue_id"]]
            ]
            slist = ", ".join(
                [
                    """%(titreannee)s (<em>semestre <b class="fontred">%(semestre_id)s</b></em>)"""
                    % s
                    for s in sems
                ]
            )
            H.append("<li><b>%s</b> : %s</li>" % (ue["acronyme"], slist))
    H.append("</ul></div>")

    return "\n".join(H), ue_multiples