# -*- 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
#
##############################################################################

"""Interface Zope <-> Notes
"""
import time
import datetime
import jaxml
import pprint

from sco_zope import *  # pylint: disable=unused-wildcard-import

# ---------------
import sco_utils as scu
import notesdb as ndb
from notes_log import log, sendAlarm
import scolog
from scolog import logdb
from sco_permissions import (
    ScoView,
    ScoEnsView,
    ScoImplement,
    ScoChangeFormation,
    ScoObservateur,
    ScoEtudInscrit,
    ScoEtudChangeGroups,
    ScoEtudChangeAdr,
    ScoEtudSupprAnnotations,
    ScoEditAllEvals,
    ScoEditAllNotes,
    ScoEditFormationTags,
    ScoEditApo,
    ScoSuperAdmin,
)
from sco_exceptions import ScoValueError, ScoLockedFormError, ScoGenError, AccessDenied

from TrivialFormulator import TrivialFormulator
import htmlutils
import sco_excel

# import notes_users
from gen_tables import GenTable
import sco_cache
import scolars
import sco_news
from sco_news import NEWS_INSCR, NEWS_NOTE, NEWS_FORM, NEWS_SEM, NEWS_MISC

import sco_formsemestre
import sco_formsemestre_edit
import sco_formsemestre_status
import sco_formsemestre_inscriptions
import sco_formsemestre_custommenu
import sco_moduleimpl
import sco_moduleimpl_status
import sco_moduleimpl_inscriptions
import sco_evaluations
import sco_groups
import sco_edit_ue
import sco_edit_formation
import sco_edit_matiere
import sco_edit_module
import sco_tag_module
import sco_bulletins
import sco_bulletins_pdf
import sco_compute_moy
import sco_recapcomplet
import sco_liste_notes
import sco_saisie_notes
import sco_placement
import sco_undo_notes
import sco_formations
import sco_report
import sco_lycee
import sco_poursuite_dut
import pe_view
import sco_debouche
import sco_ue_external
import sco_cost_formation
import sco_formsemestre_validation
import sco_parcours_dut
import sco_codes_parcours
import sco_pvjury
import sco_pvpdf
import sco_prepajury
import sco_inscr_passage
import sco_synchro_etuds
import sco_archives
import sco_apogee_csv
import sco_etape_apogee_view
import sco_apogee_compare
import sco_semset
import sco_export_results
import sco_formsemestre_exterieurs

from sco_pdf import PDFLOCK
import notes_table
from notes_table import NOTES_CACHE_INST, CacheNotesTable
import VERSION

#
# Cache global: chaque instance, repérée par sa connexion db, a un cache
# qui est recréé à la demande
#
CACHE_formsemestre_inscription = {}
CACHE_evaluations = {}

# ---------------


class ZNotes(ObjectManager, PropertyManager, RoleManager, Item, Persistent, Implicit):

    "ZNotes object"

    meta_type = "ZNotes"
    security = ClassSecurityInfo()

    # This is the list of the methods associated to 'tabs' in the ZMI
    # Be aware that The first in the list is the one shown by default, so if
    # the 'View' tab is the first, you will never see your tabs by cliquing
    # on the object.
    manage_options = (
        ({"label": "Contents", "action": "manage_main"},)
        + PropertyManager.manage_options  # add the 'Properties' tab
        + (
            # this line is kept as an example with the files :
            #     dtml/manage_editZScolarForm.dtml
            #     html/ZScolar-edit.stx
            #   {'label': 'Properties', 'action': 'manage_editForm',},
            {"label": "View", "action": "index_html"},
        )
        + Item.manage_options  # add the 'Undo' & 'Owner' tab
        + RoleManager.manage_options  # add the 'Security' tab
    )

    # no permissions, only called from python
    def __init__(self, id, title):
        "initialise a new instance of ZNotes"
        self.id = id
        self.title = title

    # The form used to edit this object
    security.declareProtected(ScoView, "manage_editZNotes")

    def manage_editZNotes(self, title, RESPONSE=None):
        "Changes the instance values"
        self.title = title
        self._p_changed = 1
        return RESPONSE.redirect("manage_editForm")

    def _getNotesCache(self):
        "returns CacheNotesTable instance for us"
        u = self.GetDBConnexionString()  # identifie le dept de facon fiable
        if not NOTES_CACHE_INST.has_key(u):
            log("getNotesCache: creating cache for %s" % u)
            NOTES_CACHE_INST[u] = CacheNotesTable()
        return NOTES_CACHE_INST[u]

    def _inval_cache(
        self, formsemestre_id=None, pdfonly=False, formsemestre_id_list=None
    ):  # >
        "expire cache pour un semestre (ou tous si pas d'argument)"
        if formsemestre_id_list:
            for formsemestre_id in formsemestre_id_list:
                self._getNotesCache().inval_cache(
                    self, formsemestre_id=formsemestre_id, pdfonly=pdfonly
                )
                # Affecte aussi cache inscriptions
                self.get_formsemestre_inscription_cache().inval_cache(
                    key=formsemestre_id
                )  # >
        else:
            self._getNotesCache().inval_cache(
                self, formsemestre_id=formsemestre_id, pdfonly=pdfonly
            )  # >
            # Affecte aussi cache inscriptions
            self.get_formsemestre_inscription_cache().inval_cache(
                key=formsemestre_id
            )  # >

    security.declareProtected(ScoView, "clearcache")

    def clearcache(self, REQUEST=None):
        "Efface les caches de notes (utile pendant developpement slt)"
        log("*** clearcache request")
        # Debugging code: compare results before and after cache reconstruction
        # (_should_ be identicals !)
        # Compare XML representation
        cache = self._getNotesCache()
        formsemestre_ids = cache.get_cached_formsemestre_ids()
        docs_before = []
        for formsemestre_id in formsemestre_ids:
            docs_before.append(
                sco_recapcomplet.do_formsemestre_recapcomplet(
                    self, REQUEST, formsemestre_id, format="xml", xml_nodate=True
                )
            )
        #
        cache.inval_cache(self)  # >
        # Rebuild cache (useful only to debug)
        docs_after = []
        for formsemestre_id in formsemestre_ids:
            docs_after.append(
                sco_recapcomplet.do_formsemestre_recapcomplet(
                    self, REQUEST, formsemestre_id, format="xml", xml_nodate=True
                )
            )
        if docs_before != docs_after:
            log("clearcache: inconsistency !")
            txt = "before=" + repr(docs_before) + "\n\nafter=" + repr(docs_after) + "\n"
            log(txt)
            sendAlarm(self, "clearcache: inconsistency !", txt)

    # --------------------------------------------------------------------
    #
    #    NOTES (top level)
    #
    # --------------------------------------------------------------------
    # XXX essai
    security.declareProtected(ScoView, "gloups")

    def gloups(self, REQUEST):
        "essai gloups"
        return ""
        # return pdfbulletins.essaipdf(REQUEST)
        # return scu.sendPDFFile(REQUEST, pdfbulletins.pdftrombino(0,0), 'toto.pdf' )

    # Python methods:
    security.declareProtected(ScoView, "formsemestre_status")
    formsemestre_status = sco_formsemestre_status.formsemestre_status

    security.declareProtected(ScoImplement, "formsemestre_createwithmodules")
    formsemestre_createwithmodules = (
        sco_formsemestre_edit.formsemestre_createwithmodules
    )

    security.declareProtected(
        ScoView, "formsemestre_editwithmodules"
    )  # controle d'acces specifique pour dir. etud
    formsemestre_editwithmodules = sco_formsemestre_edit.formsemestre_editwithmodules

    security.declareProtected(ScoImplement, "formsemestre_clone")
    formsemestre_clone = sco_formsemestre_edit.formsemestre_clone

    security.declareProtected(ScoChangeFormation, "formsemestre_associate_new_version")
    formsemestre_associate_new_version = (
        sco_formsemestre_edit.formsemestre_associate_new_version
    )

    security.declareProtected(ScoImplement, "formsemestre_delete")
    formsemestre_delete = sco_formsemestre_edit.formsemestre_delete
    security.declareProtected(ScoImplement, "formsemestre_delete2")
    formsemestre_delete2 = sco_formsemestre_edit.formsemestre_delete2

    security.declareProtected(ScoView, "formsemestre_recapcomplet")
    formsemestre_recapcomplet = sco_recapcomplet.formsemestre_recapcomplet

    security.declareProtected(ScoObservateur, "formsemestres_bulletins")
    formsemestres_bulletins = sco_recapcomplet.formsemestres_bulletins

    security.declareProtected(ScoView, "moduleimpl_status")
    moduleimpl_status = sco_moduleimpl_status.moduleimpl_status

    security.declareProtected(ScoView, "formsemestre_description")
    formsemestre_description = sco_formsemestre_status.formsemestre_description

    security.declareProtected(ScoView, "formsemestre_lists")
    formsemestre_lists = sco_formsemestre_status.formsemestre_lists

    security.declareProtected(ScoView, "formsemestre_status_menubar")
    formsemestre_status_menubar = sco_formsemestre_status.formsemestre_status_menubar
    security.declareProtected(ScoChangeFormation, "formation_create")
    formation_create = sco_edit_formation.formation_create
    security.declareProtected(ScoChangeFormation, "formation_delete")
    formation_delete = sco_edit_formation.formation_delete
    security.declareProtected(ScoChangeFormation, "formation_edit")
    formation_edit = sco_edit_formation.formation_edit

    security.declareProtected(ScoView, "formsemestre_bulletinetud")
    formsemestre_bulletinetud = sco_bulletins.formsemestre_bulletinetud

    security.declareProtected(ScoView, "formsemestre_evaluations_cal")
    formsemestre_evaluations_cal = sco_evaluations.formsemestre_evaluations_cal
    security.declareProtected(ScoView, "formsemestre_evaluations_delai_correction")
    formsemestre_evaluations_delai_correction = (
        sco_evaluations.formsemestre_evaluations_delai_correction
    )

    security.declareProtected(ScoView, "module_evaluation_renumber")
    module_evaluation_renumber = sco_evaluations.module_evaluation_renumber
    security.declareProtected(ScoView, "module_evaluation_move")
    module_evaluation_move = sco_evaluations.module_evaluation_move

    security.declareProtected(ScoView, "formsemestre_list_saisies_notes")
    formsemestre_list_saisies_notes = sco_undo_notes.formsemestre_list_saisies_notes

    security.declareProtected(ScoChangeFormation, "ue_create")
    ue_create = sco_edit_ue.ue_create
    security.declareProtected(ScoChangeFormation, "ue_delete")
    ue_delete = sco_edit_ue.ue_delete
    security.declareProtected(ScoChangeFormation, "ue_edit")
    ue_edit = sco_edit_ue.ue_edit
    security.declareProtected(ScoView, "ue_list")
    ue_list = sco_edit_ue.ue_list
    security.declareProtected(ScoView, "ue_sharing_code")
    ue_sharing_code = sco_edit_ue.ue_sharing_code
    security.declareProtected(ScoChangeFormation, "edit_ue_set_code_apogee")
    edit_ue_set_code_apogee = sco_edit_ue.edit_ue_set_code_apogee
    security.declareProtected(ScoView, "formation_table_recap")
    formation_table_recap = sco_edit_ue.formation_table_recap
    security.declareProtected(ScoChangeFormation, "formation_add_malus_modules")
    formation_add_malus_modules = sco_edit_module.formation_add_malus_modules

    security.declareProtected(ScoChangeFormation, "matiere_create")
    matiere_create = sco_edit_matiere.matiere_create
    security.declareProtected(ScoChangeFormation, "matiere_delete")
    matiere_delete = sco_edit_matiere.matiere_delete
    security.declareProtected(ScoChangeFormation, "matiere_edit")
    matiere_edit = sco_edit_matiere.matiere_edit

    security.declareProtected(ScoChangeFormation, "module_create")
    module_create = sco_edit_module.module_create
    security.declareProtected(ScoChangeFormation, "module_delete")
    module_delete = sco_edit_module.module_delete
    security.declareProtected(ScoChangeFormation, "module_edit")
    module_edit = sco_edit_module.module_edit
    security.declareProtected(ScoChangeFormation, "edit_module_set_code_apogee")
    edit_module_set_code_apogee = sco_edit_module.edit_module_set_code_apogee
    security.declareProtected(ScoView, "module_list")
    module_list = sco_edit_module.module_list
    # Tags
    security.declareProtected(ScoView, "module_tag_search")
    module_tag_search = sco_tag_module.module_tag_search
    security.declareProtected(
        ScoView, "module_tag_set"
    )  # should be ScoEditFormationTags, but not present in old installs => check in method
    module_tag_set = sco_tag_module.module_tag_set

    #
    security.declareProtected(ScoView, "index_html")

    def index_html(self, REQUEST=None):
        "Page accueil formations"

        editable = REQUEST.AUTHENTICATED_USER.has_permission(ScoChangeFormation, self)

        H = [
            self.sco_header(REQUEST, page_title="Programmes formations"),
            """<h2>Programmes pédagogiques</h2>
              """,
        ]
        T = sco_formations.formation_list_table(self, REQUEST=REQUEST)

        H.append(T.html())

        if editable:
            H.append(
                """<p><a class="stdlink" href="formation_create">Créer une formation</a></p>
      <p><a class="stdlink" href="formation_import_xml_form">Importer une formation (xml)</a></p>
         <p class="help">Une "formation" est un programme pédagogique structuré en UE, matières et modules. Chaque semestre se réfère à une formation. La modification d'une formation affecte tous les semestres qui s'y réfèrent.</p>
            """
            )

        H.append(self.sco_footer(REQUEST))
        return "\n".join(H)

    # --------------------------------------------------------------------
    #
    #    Notes Methods
    #
    # --------------------------------------------------------------------

    # --- Formations
    _formationEditor = ndb.EditableTable(
        "notes_formations",
        "formation_id",
        (
            "formation_id",
            "acronyme",
            "titre",
            "titre_officiel",
            "version",
            "formation_code",
            "type_parcours",
            "code_specialite",
        ),
        sortkey="acronyme",
    )

    security.declareProtected(ScoChangeFormation, "do_formation_create")

    def do_formation_create(self, args, REQUEST):
        "create a formation"
        cnx = self.GetDBConnexion()
        # check unique acronyme/titre/version
        a = args.copy()
        if a.has_key("formation_id"):
            del a["formation_id"]
        F = self.formation_list(args=a)
        if len(F) > 0:
            log(
                "do_formation_create: error: %d formations matching args=%s"
                % (len(F), a)
            )
            raise ScoValueError("Formation non unique (%s) !" % str(a))
        # Si pas de formation_code, l'enleve (default SQL)
        if args.has_key("formation_code") and not args["formation_code"]:
            del args["formation_code"]
        #
        r = self._formationEditor.create(cnx, args)

        sco_news.add(
            self,
            REQUEST,
            typ=NEWS_FORM,
            text="Création de la formation %(titre)s (%(acronyme)s)" % args,
        )
        return r

    security.declareProtected(ScoChangeFormation, "do_formation_delete")

    def do_formation_delete(self, oid, REQUEST):
        """delete a formation (and all its UE, matieres, modules)
        XXX delete all ues, will break if there are validations ! USE WITH CARE !
        """
        F = self.formation_list(args={"formation_id": oid})[0]
        if self.formation_has_locked_sems(oid):
            raise ScoLockedFormError()
        cnx = self.GetDBConnexion()
        # delete all UE in this formation
        ues = self.do_ue_list({"formation_id": oid})
        for ue in ues:
            self._do_ue_delete(ue["ue_id"], REQUEST=REQUEST, force=True)

        self._formationEditor.delete(cnx, oid)

        # news
        sco_news.add(
            self,
            REQUEST,
            typ=NEWS_FORM,
            object=oid,
            text="Suppression de la formation %(acronyme)s" % F,
        )

    security.declareProtected(ScoView, "formation_list")

    def formation_list(self, format=None, REQUEST=None, formation_id=None, args={}):
        """List formation(s) with given id, or matching args
        (when args is given, formation_id is ignored).
        """
        # logCallStack()
        if not args:
            if formation_id is None:
                args = {}
            else:
                args = {"formation_id": formation_id}
        cnx = self.GetDBConnexion()
        r = self._formationEditor.list(cnx, args=args)
        # log('%d formations found' % len(r))
        return scu.sendResult(REQUEST, r, name="formation", format=format)

    security.declareProtected(ScoView, "formation_export")

    def formation_export(
        self, formation_id, export_ids=False, format=None, REQUEST=None
    ):
        "Export de la formation au format indiqué (xml ou json)"
        return sco_formations.formation_export(
            self, formation_id, export_ids=export_ids, format=format, REQUEST=REQUEST
        )

    security.declareProtected(ScoChangeFormation, "formation_import_xml")

    def formation_import_xml(self, file, REQUEST):
        "import d'une formation en XML"
        log("formation_import_xml")
        doc = file.read()
        return sco_formations.formation_import_xml(self, REQUEST, doc)

    security.declareProtected(ScoChangeFormation, "formation_import_xml_form")

    def formation_import_xml_form(self, REQUEST):
        "form import d'une formation en XML"
        H = [
            self.sco_header(page_title="Import d'une formation", REQUEST=REQUEST),
            """<h2>Import d'une formation</h2>
        <p>Création d'une formation (avec UE, matières, modules)
        à partir un fichier XML (réservé aux utilisateurs avertis)</p>
        """,
        ]
        footer = self.sco_footer(REQUEST)
        tf = TrivialFormulator(
            REQUEST.URL0,
            REQUEST.form,
            (("xmlfile", {"input_type": "file", "title": "Fichier XML", "size": 30}),),
            submitlabel="Importer",
            cancelbutton="Annuler",
        )
        if tf[0] == 0:
            return "\n".join(H) + tf[1] + footer
        elif tf[0] == -1:
            return REQUEST.RESPONSE.redirect(self.NotesURL())
        else:
            formation_id, _, _ = self.formation_import_xml(tf[2]["xmlfile"], REQUEST)

            return (
                "\n".join(H)
                + """<p>Import effectué !</p>
            <p><a class="stdlink" href="ue_list?formation_id=%s">Voir la formation</a></p>"""
                % formation_id
                + footer
            )

    security.declareProtected(ScoChangeFormation, "formation_create_new_version")

    def formation_create_new_version(self, formation_id, redirect=True, REQUEST=None):
        "duplicate formation, with new version number"
        xml = sco_formations.formation_export(
            self, formation_id, export_ids=True, format="xml"
        )
        new_id, modules_old2new, ues_old2new = sco_formations.formation_import_xml(
            self, REQUEST, xml
        )
        # news
        F = self.formation_list(args={"formation_id": new_id})[0]
        sco_news.add(
            self,
            REQUEST,
            typ=NEWS_FORM,
            object=new_id,
            text="Nouvelle version de la formation %(acronyme)s" % F,
        )
        if redirect:
            return REQUEST.RESPONSE.redirect(
                "ue_list?formation_id=" + new_id + "&amp;msg=Nouvelle version !"
            )
        else:
            return new_id, modules_old2new, ues_old2new

    # --- UE
    _ueEditor = ndb.EditableTable(
        "notes_ue",
        "ue_id",
        (
            "ue_id",
            "formation_id",
            "acronyme",
            "numero",
            "titre",
            "type",
            "ue_code",
            "ects",
            "is_external",
            "code_apogee",
            "coefficient",
        ),
        sortkey="numero",
        input_formators={"type": ndb.int_null_is_zero},
        output_formators={
            "numero": ndb.int_null_is_zero,
            "ects": ndb.float_null_is_null,
            "coefficient": ndb.float_null_is_zero,
        },
    )

    security.declareProtected(ScoChangeFormation, "do_ue_create")

    def do_ue_create(self, args, REQUEST):
        "create an ue"
        cnx = self.GetDBConnexion()
        # check duplicates
        ues = self.do_ue_list(
            {"formation_id": args["formation_id"], "acronyme": args["acronyme"]}
        )
        if ues:
            raise ScoValueError('Acronyme d\'UE "%s" déjà utilisé !' % args["acronyme"])
        # create
        r = self._ueEditor.create(cnx, args)

        # news
        F = self.formation_list(args={"formation_id": args["formation_id"]})[0]
        sco_news.add(
            self,
            REQUEST,
            typ=NEWS_FORM,
            object=args["formation_id"],
            text="Modification de la formation %(acronyme)s" % F,
        )
        return r

    def _do_ue_delete(self, ue_id, delete_validations=False, REQUEST=None, force=False):
        "delete UE and attached matieres (but not modules (it should ?))"
        cnx = self.GetDBConnexion()
        log(
            "do_ue_delete: ue_id=%s, delete_validations=%s"
            % (ue_id, delete_validations)
        )
        # check
        ue = self.do_ue_list({"ue_id": ue_id})
        if not ue:
            raise ScoValueError("UE inexistante !")
        ue = ue[0]
        if self.ue_is_locked(ue["ue_id"]):
            raise ScoLockedFormError()
        # Il y a-t-il des etudiants ayant validé cette UE ?
        # si oui, propose de supprimer les validations
        validations = sco_parcours_dut.scolar_formsemestre_validation_list(
            cnx, args={"ue_id": ue_id}
        )
        if validations and not delete_validations and not force:
            return self.confirmDialog(
                "<p>%d étudiants ont validé l'UE %s (%s)</p><p>Si vous supprimez cette UE, ces validations vont être supprimées !</p>"
                % (len(validations), ue["acronyme"], ue["titre"]),
                dest_url="",
                REQUEST=REQUEST,
                target_variable="delete_validations",
                cancel_url="ue_list?formation_id=%s" % ue["formation_id"],
                parameters={"ue_id": ue_id, "dialog_confirmed": 1},
            )
        if delete_validations:
            log("deleting all validations of UE %s" % ue_id)
            ndb.SimpleQuery(
                self,
                "DELETE FROM scolar_formsemestre_validation WHERE ue_id=%(ue_id)s",
                {"ue_id": ue_id},
            )

        # delete all matiere in this UE
        mats = self.do_matiere_list({"ue_id": ue_id})
        for mat in mats:
            self.do_matiere_delete(mat["matiere_id"], REQUEST)
        # delete uecoef and events
        ndb.SimpleQuery(
            self,
            "DELETE FROM notes_formsemestre_uecoef WHERE ue_id=%(ue_id)s",
            {"ue_id": ue_id},
        )
        ndb.SimpleQuery(
            self, "DELETE FROM scolar_events WHERE ue_id=%(ue_id)s", {"ue_id": ue_id}
        )
        cnx = self.GetDBConnexion()
        self._ueEditor.delete(cnx, ue_id)
        self._inval_cache()  # > UE delete + supr. validations associées etudiants (cas compliqué, mais rarement utilisé: acceptable de tout invalider ?)
        # news
        F = self.formation_list(args={"formation_id": ue["formation_id"]})[0]
        sco_news.add(
            self,
            REQUEST,
            typ=NEWS_FORM,
            object=ue["formation_id"],
            text="Modification de la formation %(acronyme)s" % F,
        )
        #
        if not force:
            return REQUEST.RESPONSE.redirect(
                self.NotesURL() + "/ue_list?formation_id=" + str(ue["formation_id"])
            )
        else:
            return None

    security.declareProtected(ScoView, "do_ue_list")

    def do_ue_list(self, *args, **kw):
        "list UEs"
        cnx = self.GetDBConnexion()
        return self._ueEditor.list(cnx, *args, **kw)

    # --- Matieres
    _matiereEditor = ndb.EditableTable(
        "notes_matieres",
        "matiere_id",
        ("matiere_id", "ue_id", "numero", "titre"),
        sortkey="numero",
        output_formators={"numero": ndb.int_null_is_zero},
    )

    security.declareProtected(ScoChangeFormation, "do_matiere_create")

    def do_matiere_create(self, args, REQUEST):
        "create a matiere"
        cnx = self.GetDBConnexion()
        # check
        ue = self.do_ue_list({"ue_id": args["ue_id"]})[0]
        # create matiere
        r = self._matiereEditor.create(cnx, args)

        # news
        F = self.formation_list(args={"formation_id": ue["formation_id"]})[0]
        sco_news.add(
            self,
            REQUEST,
            typ=NEWS_FORM,
            object=ue["formation_id"],
            text="Modification de la formation %(acronyme)s" % F,
        )
        return r

    security.declareProtected(ScoChangeFormation, "do_matiere_delete")

    def do_matiere_delete(self, oid, REQUEST):
        "delete matiere and attached modules"
        cnx = self.GetDBConnexion()
        # check
        mat = self.do_matiere_list({"matiere_id": oid})[0]
        ue = self.do_ue_list({"ue_id": mat["ue_id"]})[0]
        locked = self.matiere_is_locked(mat["matiere_id"])
        if locked:
            log("do_matiere_delete: mat=%s" % mat)
            log("do_matiere_delete: ue=%s" % ue)
            log("do_matiere_delete: locked sems: %s" % locked)
            raise ScoLockedFormError()
        log("do_matiere_delete: matiere_id=%s" % oid)
        # delete all modules in this matiere
        mods = self.do_module_list({"matiere_id": oid})
        for mod in mods:
            self.do_module_delete(mod["module_id"], REQUEST)
        self._matiereEditor.delete(cnx, oid)

        # news
        F = self.formation_list(args={"formation_id": ue["formation_id"]})[0]
        sco_news.add(
            self,
            REQUEST,
            typ=NEWS_FORM,
            object=ue["formation_id"],
            text="Modification de la formation %(acronyme)s" % F,
        )

    security.declareProtected(ScoView, "do_matiere_list")

    def do_matiere_list(self, *args, **kw):
        "list matieres"
        cnx = self.GetDBConnexion()
        return self._matiereEditor.list(cnx, *args, **kw)

    security.declareProtected(ScoChangeFormation, "do_matiere_edit")

    def do_matiere_edit(self, *args, **kw):
        "edit a matiere"
        cnx = self.GetDBConnexion()
        # check
        mat = self.do_matiere_list({"matiere_id": args[0]["matiere_id"]})[0]
        if self.matiere_is_locked(mat["matiere_id"]):
            raise ScoLockedFormError()
        # edit
        self._matiereEditor.edit(cnx, *args, **kw)
        self._inval_cache()  # > modif matiere

    security.declareProtected(ScoView, "do_matiere_formation_id")

    def do_matiere_formation_id(self, matiere_id):
        "get formation_id from matiere"
        cnx = self.GetDBConnexion()
        cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
        cursor.execute(
            "select UE.formation_id from notes_matieres M, notes_ue UE where M.matiere_id = %(matiere_id)s and M.ue_id = UE.ue_id",
            {"matiere_id": matiere_id},
        )
        res = cursor.fetchall()
        return res[0][0]

    # --- Modules
    _moduleEditor = ndb.EditableTable(
        "notes_modules",
        "module_id",
        (
            "module_id",
            "titre",
            "code",
            "abbrev",
            "heures_cours",
            "heures_td",
            "heures_tp",
            "coefficient",
            "ue_id",
            "matiere_id",
            "formation_id",
            "semestre_id",
            "numero",
            "code_apogee",
            "module_type"
            #'ects'
        ),
        sortkey="numero, code, titre",
        output_formators={
            "heures_cours": ndb.float_null_is_zero,
            "heures_td": ndb.float_null_is_zero,
            "heures_tp": ndb.float_null_is_zero,
            "numero": ndb.int_null_is_zero,
            "coefficient": ndb.float_null_is_zero,
            "module_type": ndb.int_null_is_zero
            #'ects' : ndb.float_null_is_null
        },
    )

    security.declareProtected(ScoChangeFormation, "do_module_create")

    def do_module_create(self, args, REQUEST):
        "create a module"
        # create
        cnx = self.GetDBConnexion()
        r = self._moduleEditor.create(cnx, args)

        # news
        F = self.formation_list(args={"formation_id": args["formation_id"]})[0]
        sco_news.add(
            self,
            REQUEST,
            typ=NEWS_FORM,
            object=args["formation_id"],
            text="Modification de la formation %(acronyme)s" % F,
        )
        return r

    security.declareProtected(ScoChangeFormation, "do_module_delete")

    def do_module_delete(self, oid, REQUEST):
        "delete module"
        mod = self.do_module_list({"module_id": oid})[0]
        if self.module_is_locked(mod["module_id"]):
            raise ScoLockedFormError()

        # S'il y a des moduleimpls, on ne peut pas detruire le module !
        mods = sco_moduleimpl.do_moduleimpl_list(self, module_id=oid)
        if mods:
            err_page = self.confirmDialog(
                message="""<h3>Destruction du module impossible car il est utilisé dans des semestres existants !</h3>""",
                helpmsg="""Il faut d'abord supprimer le semestre. Mais il est peut être préférable de laisser ce programme intact et d'en créer une nouvelle version pour la modifier.""",
                dest_url="ue_list",
                parameters={"formation_id": mod["formation_id"]},
                REQUEST=REQUEST,
            )
            raise ScoGenError(err_page)
        # delete
        cnx = self.GetDBConnexion()
        self._moduleEditor.delete(cnx, oid)

        # news
        F = self.formation_list(args={"formation_id": mod["formation_id"]})[0]
        sco_news.add(
            self,
            REQUEST,
            typ=NEWS_FORM,
            object=mod["formation_id"],
            text="Modification de la formation %(acronyme)s" % F,
        )

    security.declareProtected(ScoView, "do_module_list")

    def do_module_list(self, *args, **kw):
        "list modules"
        cnx = self.GetDBConnexion()
        return self._moduleEditor.list(cnx, *args, **kw)

    security.declareProtected(ScoChangeFormation, "do_module_edit")

    def do_module_edit(self, val):
        "edit a module"
        # check
        mod = self.do_module_list({"module_id": val["module_id"]})[0]
        if self.module_is_locked(mod["module_id"]):
            # formation verrouillée: empeche de modifier certains champs:
            protected_fields = ("coefficient", "ue_id", "matiere_id", "semestre_id")
            for f in protected_fields:
                if f in val:
                    del val[f]
        # edit
        cnx = self.GetDBConnexion()
        self._moduleEditor.edit(cnx, val)

        sems = sco_formsemestre.do_formsemestre_list(
            self, args={"formation_id": mod["formation_id"]}
        )
        if sems:
            self._inval_cache(
                formsemestre_id_list=[s["formsemestre_id"] for s in sems]
            )  # > modif module

    #
    security.declareProtected(ScoView, "formation_has_locked_sems")

    def formation_has_locked_sems(self, formation_id):
        "True if there is a locked formsemestre in this formation"
        sems = sco_formsemestre.do_formsemestre_list(
            self, args={"formation_id": formation_id, "etat": "0"}
        )
        return sems

    security.declareProtected(ScoView, "formation_count_sems")

    def formation_count_sems(self, formation_id):
        "Number of formsemestre in this formation (locked or not)"
        sems = sco_formsemestre.do_formsemestre_list(
            self, args={"formation_id": formation_id}
        )
        return len(sems)

    security.declareProtected(ScoView, "module_count_moduleimpls")

    def module_count_moduleimpls(self, module_id):
        "Number of moduleimpls using this module"
        mods = sco_moduleimpl.do_moduleimpl_list(self, module_id=module_id)
        return len(mods)

    security.declareProtected(ScoView, "module_is_locked")

    def module_is_locked(self, module_id):
        """True if UE should not be modified
        (used in a locked formsemestre)
        """
        r = ndb.SimpleDictFetch(
            self,
            """SELECT mi.* from notes_modules mod, notes_formsemestre sem, notes_moduleimpl mi
            WHERE mi.module_id = mod.module_id AND mi.formsemestre_id = sem.formsemestre_id
            AND mi.module_id = %(module_id)s AND sem.etat = 0
            """,
            {"module_id": module_id},
        )
        return len(r) > 0

    security.declareProtected(ScoView, "matiere_is_locked")

    def matiere_is_locked(self, matiere_id):
        """True if matiere should not be modified
        (contains modules used in a locked formsemestre)
        """
        r = ndb.SimpleDictFetch(
            self,
            """SELECT ma.* from notes_matieres ma, notes_modules mod, notes_formsemestre sem, notes_moduleimpl mi
            WHERE ma.matiere_id = mod.matiere_id AND mi.module_id = mod.module_id AND mi.formsemestre_id = sem.formsemestre_id
            AND ma.matiere_id = %(matiere_id)s AND sem.etat = 0
            """,
            {"matiere_id": matiere_id},
        )
        return len(r) > 0

    security.declareProtected(ScoView, "ue_is_locked")

    def ue_is_locked(self, ue_id):
        """True if module should not be modified
        (contains modules used in a locked formsemestre)
        """
        r = ndb.SimpleDictFetch(
            self,
            """SELECT ue.* FROM notes_ue ue, notes_modules mod, notes_formsemestre sem, notes_moduleimpl mi
               WHERE ue.ue_id = mod.ue_id
               AND mi.module_id = mod.module_id AND mi.formsemestre_id = sem.formsemestre_id
               AND ue.ue_id = %(ue_id)s AND sem.etat = 0
            """,
            {"ue_id": ue_id},
        )
        return len(r) > 0

    security.declareProtected(ScoChangeFormation, "module_move")

    def module_move(self, module_id, after=0, REQUEST=None, redirect=1):
        """Move before/after previous one (decrement/increment numero)"""
        module = self.do_module_list({"module_id": module_id})[0]
        redirect = int(redirect)
        after = int(after)  # 0: deplace avant, 1 deplace apres
        if after not in (0, 1):
            raise ValueError('invalid value for "after"')
        formation_id = module["formation_id"]
        others = self.do_module_list({"matiere_id": module["matiere_id"]})
        # log('others=%s' % others)
        if len(others) > 1:
            idx = [p["module_id"] for p in others].index(module_id)
            # log('module_move: after=%s idx=%s' % (after, idx))
            neigh = None  # object to swap with
            if after == 0 and idx > 0:
                neigh = others[idx - 1]
            elif after == 1 and idx < len(others) - 1:
                neigh = others[idx + 1]
            if neigh:  #
                # swap numero between partition and its neighbor
                # log('moving module %s' % module_id)
                cnx = self.GetDBConnexion()
                module["numero"], neigh["numero"] = neigh["numero"], module["numero"]
                if module["numero"] == neigh["numero"]:
                    neigh["numero"] -= 2 * after - 1
                self._moduleEditor.edit(cnx, module)
                self._moduleEditor.edit(cnx, neigh)

        # redirect to ue_list page:
        if redirect:
            return REQUEST.RESPONSE.redirect("ue_list?formation_id=" + formation_id)

    security.declareProtected(ScoChangeFormation, "ue_move")

    def ue_move(self, ue_id, after=0, REQUEST=None, redirect=1):
        """Move UE before/after previous one (decrement/increment numero)"""
        o = self.do_ue_list({"ue_id": ue_id})[0]
        # log('ue_move %s (#%s) after=%s' % (ue_id, o['numero'], after))
        redirect = int(redirect)
        after = int(after)  # 0: deplace avant, 1 deplace apres
        if after not in (0, 1):
            raise ValueError('invalid value for "after"')
        formation_id = o["formation_id"]
        others = self.do_ue_list({"formation_id": formation_id})
        if len(others) > 1:
            idx = [p["ue_id"] for p in others].index(ue_id)
            neigh = None  # object to swap with
            if after == 0 and idx > 0:
                neigh = others[idx - 1]
            elif after == 1 and idx < len(others) - 1:
                neigh = others[idx + 1]
            if neigh:  #
                # swap numero between partition and its neighbor
                # log('moving ue %s (neigh #%s)' % (ue_id, neigh['numero']))
                cnx = self.GetDBConnexion()
                o["numero"], neigh["numero"] = neigh["numero"], o["numero"]
                if o["numero"] == neigh["numero"]:
                    neigh["numero"] -= 2 * after - 1
                self._ueEditor.edit(cnx, o)
                self._ueEditor.edit(cnx, neigh)
        # redirect to ue_list page
        if redirect:
            return REQUEST.RESPONSE.redirect(
                "ue_list?formation_id=" + o["formation_id"]
            )

    # --- Semestres de formation

    security.declareProtected(ScoImplement, "do_formsemestre_create")

    def do_formsemestre_create(self, args, REQUEST, silent=False):
        "create a formsemestre"
        cnx = self.GetDBConnexion()
        formsemestre_id = sco_formsemestre._formsemestreEditor.create(cnx, args)
        if args["etapes"]:
            args["formsemestre_id"] = formsemestre_id
            sco_formsemestre.write_formsemestre_etapes(self, args)
        if args["responsables"]:
            args["formsemestre_id"] = formsemestre_id
            sco_formsemestre.write_formsemestre_responsables(self, args)

        # create default partition
        partition_id = sco_groups.partition_create(
            self, formsemestre_id, default=True, redirect=0, REQUEST=REQUEST
        )
        _group_id = sco_groups.createGroup(
            self, partition_id, default=True, REQUEST=REQUEST
        )

        # news
        if not args.has_key("titre"):
            args["titre"] = "sans titre"
        args["formsemestre_id"] = formsemestre_id
        args["url"] = (
            "Notes/formsemestre_status?formsemestre_id=%(formsemestre_id)s" % args
        )
        if not silent:
            sco_news.add(
                self,
                REQUEST,
                typ=NEWS_SEM,
                text='Création du semestre <a href="%(url)s">%(titre)s</a>' % args,
                url=args["url"],
            )
        return formsemestre_id

    security.declareProtected(ScoView, "formsemestre_list")

    def formsemestre_list(
        self,
        format=None,
        REQUEST=None,
        formsemestre_id=None,
        formation_id=None,
        etape_apo=None,
    ):
        """List formsemestres in given format.
        kw can specify some conditions: examples:
           formsemestre_list( format='json', formation_id='F777', REQUEST=REQUEST)
        """
        # XAPI: new json api
        args = {}
        L = locals()
        for argname in ("formsemestre_id", "formation_id", "etape_apo"):
            if L[argname] is not None:
                args[argname] = L[argname]
        sems = sco_formsemestre.do_formsemestre_list(self, args=args)
        # log('formsemestre_list: format="%s", %s semestres found' % (format,len(sems)))
        return scu.sendResult(REQUEST, sems, name="formsemestre", format=format)

    security.declareProtected(ScoView, "XMLgetFormsemestres")

    def XMLgetFormsemestres(self, etape_apo=None, formsemestre_id=None, REQUEST=None):
        """List all formsemestres matching etape, XML format
        DEPRECATED: use formsemestre_list()
        """
        log("Warning: calling deprecated XMLgetFormsemestres")
        args = {}
        if etape_apo:
            args["etape_apo"] = etape_apo
        if formsemestre_id:
            args["formsemestre_id"] = formsemestre_id
        if REQUEST:
            REQUEST.RESPONSE.setHeader("content-type", scu.XML_MIMETYPE)
        doc = jaxml.XML_document(encoding=scu.SCO_ENCODING)
        doc.formsemestrelist()
        for sem in sco_formsemestre.do_formsemestre_list(self, args=args):
            doc._push()
            doc.formsemestre(sem)
            doc._pop()
        return repr(doc)

    security.declareProtected(ScoImplement, "do_formsemestre_edit")
    do_formsemestre_edit = sco_formsemestre.do_formsemestre_edit

    security.declareProtected(ScoView, "formsemestre_edit_options")
    formsemestre_edit_options = sco_formsemestre_edit.formsemestre_edit_options

    security.declareProtected(ScoView, "formsemestre_change_lock")
    formsemestre_change_lock = sco_formsemestre_edit.formsemestre_change_lock

    security.declareProtected(ScoView, "formsemestre_change_publication_bul")
    formsemestre_change_publication_bul = (
        sco_formsemestre_edit.formsemestre_change_publication_bul
    )

    security.declareProtected(ScoView, "view_formsemestre_by_etape")
    view_formsemestre_by_etape = sco_formsemestre.view_formsemestre_by_etape

    def _check_access_diretud(
        self, formsemestre_id, REQUEST, required_permission=ScoImplement
    ):
        """Check if access granted: responsable or ScoImplement
        Return True|False, HTML_error_page
        """
        authuser = REQUEST.AUTHENTICATED_USER
        sem = sco_formsemestre.get_formsemestre(self, formsemestre_id)
        header = self.sco_header(page_title="Accès interdit", REQUEST=REQUEST)
        footer = self.sco_footer(REQUEST)
        if (str(authuser) not in sem["responsables"]) and not authuser.has_permission(
            required_permission, self
        ):
            return (
                False,
                "\n".join(
                    [
                        header,
                        "<h2>Opération non autorisée pour %s</h2>" % authuser,
                        "<p>Responsable de ce semestre : <b>%s</b></p>"
                        % ", ".join(sem["responsables"]),
                        footer,
                    ]
                ),
            )
        else:
            return True, ""

    security.declareProtected(ScoView, "formsemestre_custommenu_edit")

    def formsemestre_custommenu_edit(self, REQUEST, formsemestre_id):
        "Dialogue modif menu"
        # accessible à tous !
        return sco_formsemestre_custommenu.formsemestre_custommenu_edit(
            self, formsemestre_id, REQUEST=REQUEST
        )

    security.declareProtected(ScoView, "formsemestre_custommenu_html")
    formsemestre_custommenu_html = (
        sco_formsemestre_custommenu.formsemestre_custommenu_html
    )

    security.declareProtected(ScoView, "html_sem_header")

    def html_sem_header(
        self,
        REQUEST,
        title,
        sem=None,
        with_page_header=True,
        with_h2=True,
        page_title=None,
        **args
    ):
        "Titre d'une page semestre avec lien vers tableau de bord"
        # sem now unused and thus optional...
        if with_page_header:
            h = self.sco_header(
                REQUEST, page_title="%s" % (page_title or title), **args
            )
        else:
            h = ""
        if with_h2:
            return h + """<h2 class="formsemestre">%s</h2>""" % (title)
        else:
            return h

    # --- dialogue modif enseignants/moduleimpl
    security.declareProtected(ScoView, "edit_enseignants_form")

    def edit_enseignants_form(self, REQUEST, moduleimpl_id):
        "modif liste enseignants/moduleimpl"
        M, sem = sco_moduleimpl.can_change_ens(self, REQUEST, moduleimpl_id)
        # --
        header = self.html_sem_header(
            REQUEST,
            'Enseignants du <a href="moduleimpl_status?moduleimpl_id=%s">module %s</a>'
            % (moduleimpl_id, M["module"]["titre"]),
            page_title="Enseignants du module %s" % M["module"]["titre"],
            javascripts=["libjs/AutoSuggest.js"],
            cssstyles=["css/autosuggest_inquisitor.css"],
            bodyOnLoad="init_tf_form('')",
        )
        footer = self.sco_footer(REQUEST)

        # Liste des enseignants avec forme pour affichage / saisie avec suggestion
        userlist = self.Users.get_userlist()
        login2display = {}  # user_name : forme pour affichage = "NOM Prenom (login)"
        for u in userlist:
            login2display[u["user_name"]] = u["nomplogin"]
            allowed_user_names = login2display.values()

        H = [
            "<ul><li><b>%s</b> (responsable)</li>"
            % login2display.get(M["responsable_id"], M["responsable_id"])
        ]
        for ens in M["ens"]:
            H.append(
                '<li>%s (<a class="stdlink" href="edit_enseignants_form_delete?moduleimpl_id=%s&amp;ens_id=%s">supprimer</a>)</li>'
                % (
                    login2display.get(ens["ens_id"], ens["ens_id"]),
                    moduleimpl_id,
                    ens["ens_id"],
                )
            )
        H.append("</ul>")
        F = """<p class="help">Les enseignants d'un module ont le droit de
        saisir et modifier toutes les notes des évaluations de ce module.
        </p>
        <p class="help">Pour changer le responsable du module, passez par la
        page "<a class="stdlink" href="formsemestre_editwithmodules?formation_id=%s&amp;formsemestre_id=%s">Modification du semestre</a>", accessible uniquement au responsable de la formation (chef de département)
        </p>
        """ % (
            sem["formation_id"],
            M["formsemestre_id"],
        )

        modform = [
            ("moduleimpl_id", {"input_type": "hidden"}),
            (
                "ens_id",
                {
                    "input_type": "text_suggest",
                    "size": 50,
                    "title": "Ajouter un enseignant",
                    "allowed_values": allowed_user_names,
                    "allow_null": False,
                    "text_suggest_options": {
                        "script": "Users/get_userlist_xml?",
                        "varname": "start",
                        "json": False,
                        "noresults": "Valeur invalide !",
                        "timeout": 60000,
                    },
                },
            ),
        ]
        tf = TrivialFormulator(
            REQUEST.URL0,
            REQUEST.form,
            modform,
            submitlabel="Ajouter enseignant",
            cancelbutton="Annuler",
        )
        if tf[0] == 0:
            return header + "\n".join(H) + tf[1] + F + footer
        elif tf[0] == -1:
            return REQUEST.RESPONSE.redirect(
                "moduleimpl_status?moduleimpl_id=" + moduleimpl_id
            )
        else:
            ens_id = self.Users.get_user_name_from_nomplogin(tf[2]["ens_id"])
            if not ens_id:
                H.append(
                    '<p class="help">Pour ajouter un enseignant, choisissez un nom dans le menu</p>'
                )
            else:
                # et qu'il n'est pas deja:
                if (
                    ens_id in [x["ens_id"] for x in M["ens"]]
                    or ens_id == M["responsable_id"]
                ):
                    H.append(
                        '<p class="help">Enseignant %s déjà dans la liste !</p>'
                        % ens_id
                    )
                else:
                    sco_moduleimpl.do_ens_create(
                        self, {"moduleimpl_id": moduleimpl_id, "ens_id": ens_id}
                    )
                    return REQUEST.RESPONSE.redirect(
                        "edit_enseignants_form?moduleimpl_id=%s" % moduleimpl_id
                    )
            return header + "\n".join(H) + tf[1] + F + footer

    security.declareProtected(ScoView, "edit_moduleimpl_resp")

    def edit_moduleimpl_resp(self, REQUEST, moduleimpl_id):
        """Changement d'un enseignant responsable de module
        Accessible par Admin et dir des etud si flag resp_can_change_ens
        """
        M, sem = sco_moduleimpl.can_change_module_resp(self, REQUEST, moduleimpl_id)
        H = [
            self.html_sem_header(
                REQUEST,
                'Modification du responsable du <a href="moduleimpl_status?moduleimpl_id=%s">module %s</a>'
                % (moduleimpl_id, M["module"]["titre"]),
                sem,
                javascripts=["libjs/AutoSuggest.js"],
                cssstyles=["css/autosuggest_inquisitor.css"],
                bodyOnLoad="init_tf_form('')",
            )
        ]
        help = """<p class="help">Taper le début du nom de l'enseignant.</p>"""
        # Liste des enseignants avec forme pour affichage / saisie avec suggestion
        userlist = self.Users.get_userlist()
        login2display = {}  # user_name : forme pour affichage = "NOM Prenom (login)"
        for u in userlist:
            login2display[u["user_name"]] = u["nomplogin"]
        allowed_user_names = login2display.values()

        initvalues = M
        initvalues["responsable_id"] = login2display.get(
            M["responsable_id"], M["responsable_id"]
        )
        form = [
            ("moduleimpl_id", {"input_type": "hidden"}),
            (
                "responsable_id",
                {
                    "input_type": "text_suggest",
                    "size": 50,
                    "title": "Responsable du module",
                    "allowed_values": allowed_user_names,
                    "allow_null": False,
                    "text_suggest_options": {
                        "script": "Users/get_userlist_xml?",
                        "varname": "start",
                        "json": False,
                        "noresults": "Valeur invalide !",
                        "timeout": 60000,
                    },
                },
            ),
        ]
        tf = TrivialFormulator(
            REQUEST.URL0,
            REQUEST.form,
            form,
            submitlabel="Changer responsable",
            cancelbutton="Annuler",
            initvalues=initvalues,
        )
        if tf[0] == 0:
            return "\n".join(H) + tf[1] + help + self.sco_footer(REQUEST)
        elif tf[0] == -1:
            return REQUEST.RESPONSE.redirect(
                "moduleimpl_status?moduleimpl_id=" + moduleimpl_id
            )
        else:
            responsable_id = self.Users.get_user_name_from_nomplogin(
                tf[2]["responsable_id"]
            )
            if (
                not responsable_id
            ):  # presque impossible: tf verifie les valeurs (mais qui peuvent changer entre temps)
                return REQUEST.RESPONSE.redirect(
                    "moduleimpl_status?moduleimpl_id=" + moduleimpl_id
                )
            sco_moduleimpl.do_moduleimpl_edit(
                self,
                {"moduleimpl_id": moduleimpl_id, "responsable_id": responsable_id},
                formsemestre_id=sem["formsemestre_id"],
            )
            return REQUEST.RESPONSE.redirect(
                "moduleimpl_status?moduleimpl_id="
                + moduleimpl_id
                + "&amp;head_message=responsable%20modifié"
            )

    _expr_help = """<p class="help">Expérimental: formule de calcul de la moyenne %(target)s</p>
        <p class="help">Attention: l'utilisation de formules ralenti considérablement
        les traitements. A utiliser uniquement dans els cas ne pouvant pas être traités autrement.</p>
        <p class="help">Dans la formule, les variables suivantes sont définies:</p>
        <ul class="help">
        <li><tt>moy</tt> la moyenne, calculée selon la règle standard (moyenne pondérée)</li>
        <li><tt>moy_is_valid</tt> vrai si la moyenne est valide (numérique)</li>
        <li><tt>moy_val</tt> la valeur de la moyenne (nombre, valant 0 si invalide)</li>
        <li><tt>notes</tt> vecteur des notes (/20) aux %(objs)s</li>
        <li><tt>coefs</tt> vecteur des coefficients des %(objs)s, les coefs des %(objs)s sans notes (ATT, EXC) étant mis à zéro</li>
        <li><tt>cmask</tt> vecteur de 0/1, 0 si le coef correspondant a été annulé</li>
        <li>Nombre d'absences: <tt>nb_abs</tt>, <tt>nb_abs_just</tt>, <tt>nb_abs_nojust</tt> (en demi-journées)</li>
        </ul>
        <p class="help">Les éléments des vecteurs sont ordonnés dans l'ordre des %(objs)s%(ordre)s.</p>
        <p class="help">Les fonctions suivantes sont utilisables: <tt>abs, cmp, dot, len, map, max, min, pow, reduce, round, sum, ifelse</tt>.</p>
        <p class="help">La notation <tt>V(1,2,3)</tt> représente un vecteur <tt>(1,2,3)</tt>.</p>
        <p class="help"></p>Pour indiquer que la note calculée n'existe pas, utiliser la chaîne <tt>'NA'</tt>.</p>
        <p class="help">Vous pouvez désactiver la formule (et revenir au mode de calcul "classique") 
        en supprimant le texte ou en faisant précéder la première ligne par <tt>#</tt></p>
    """

    security.declareProtected(ScoView, "edit_moduleimpl_expr")

    def edit_moduleimpl_expr(self, REQUEST, moduleimpl_id):
        """Edition formule calcul moyenne module
        Accessible par Admin, dir des etud et responsable module
        """
        M, sem = sco_moduleimpl.can_change_ens(self, REQUEST, moduleimpl_id)
        H = [
            self.html_sem_header(
                REQUEST,
                'Modification règle de calcul du <a href="moduleimpl_status?moduleimpl_id=%s">module %s</a>'
                % (moduleimpl_id, M["module"]["titre"]),
                sem,
            ),
            self._expr_help
            % {
                "target": "du module",
                "objs": "évaluations",
                "ordre": " (le premier élément est la plus ancienne évaluation)",
            },
        ]
        initvalues = M
        form = [
            ("moduleimpl_id", {"input_type": "hidden"}),
            (
                "computation_expr",
                {
                    "title": "Formule de calcul",
                    "input_type": "textarea",
                    "rows": 4,
                    "cols": 60,
                    "explanation": "formule de calcul (expérimental)",
                },
            ),
        ]
        tf = TrivialFormulator(
            REQUEST.URL0,
            REQUEST.form,
            form,
            submitlabel="Modifier formule de calcul",
            cancelbutton="Annuler",
            initvalues=initvalues,
        )
        if tf[0] == 0:
            return "\n".join(H) + tf[1] + self.sco_footer(REQUEST)
        elif tf[0] == -1:
            return REQUEST.RESPONSE.redirect(
                "moduleimpl_status?moduleimpl_id=" + moduleimpl_id
            )
        else:
            sco_moduleimpl.do_moduleimpl_edit(
                self,
                {
                    "moduleimpl_id": moduleimpl_id,
                    "computation_expr": tf[2]["computation_expr"],
                },
                formsemestre_id=sem["formsemestre_id"],
            )
            self._inval_cache(
                formsemestre_id=sem["formsemestre_id"]
            )  # > modif regle calcul
            return REQUEST.RESPONSE.redirect(
                "moduleimpl_status?moduleimpl_id="
                + moduleimpl_id
                + "&amp;head_message=règle%20de%20calcul%20modifiée"
            )

    security.declareProtected(ScoView, "view_module_abs")

    def view_module_abs(self, REQUEST, moduleimpl_id, format="html"):
        """Visualisation des absences a un module"""
        M = sco_moduleimpl.do_moduleimpl_withmodule_list(
            self, moduleimpl_id=moduleimpl_id
        )[0]
        sem = sco_formsemestre.get_formsemestre(self, M["formsemestre_id"])
        debut_sem = ndb.DateDMYtoISO(sem["date_debut"])
        fin_sem = ndb.DateDMYtoISO(sem["date_fin"])
        list_insc = sco_moduleimpl.do_moduleimpl_listeetuds(self, moduleimpl_id)

        T = []
        for etudid in list_insc:
            nb_abs = self.Absences.CountAbs(
                etudid=etudid, debut=debut_sem, fin=fin_sem, moduleimpl_id=moduleimpl_id
            )
            if nb_abs:
                nb_abs_just = self.Absences.CountAbsJust(
                    etudid=etudid,
                    debut=debut_sem,
                    fin=fin_sem,
                    moduleimpl_id=moduleimpl_id,
                )
                etud = self.getEtudInfo(etudid=etudid, filled=True)[0]
                T.append(
                    {
                        "nomprenom": etud["nomprenom"],
                        "just": nb_abs_just,
                        "nojust": nb_abs - nb_abs_just,
                        "total": nb_abs,
                        "_nomprenom_target": "ficheEtud?etudid=%s" % etudid,
                    }
                )

        H = [
            self.html_sem_header(
                REQUEST,
                'Absences du <a href="moduleimpl_status?moduleimpl_id=%s">module %s</a>'
                % (moduleimpl_id, M["module"]["titre"]),
                page_title="Absences du module %s" % (M["module"]["titre"]),
                sem=sem,
            )
        ]
        if not T and format == "html":
            return (
                "\n".join(H)
                + "<p>Aucune absence signalée</p>"
                + self.sco_footer(REQUEST)
            )

        tab = GenTable(
            titles={
                "nomprenom": "Nom",
                "just": "Just.",
                "nojust": "Non Just.",
                "total": "Total",
            },
            columns_ids=("nomprenom", "just", "nojust", "total"),
            rows=T,
            html_class="table_leftalign",
            base_url="%s?moduleimpl_id=%s" % (REQUEST.URL0, moduleimpl_id),
            filename="absmodule_" + scu.make_filename(M["module"]["titre"]),
            caption="Absences dans le module %s" % M["module"]["titre"],
            preferences=self.get_preferences(),
        )

        if format != "html":
            return tab.make_page(self, format=format, REQUEST=REQUEST)

        return "\n".join(H) + tab.html() + self.sco_footer(REQUEST)

    security.declareProtected(ScoView, "edit_ue_expr")

    def edit_ue_expr(self, REQUEST, formsemestre_id, ue_id):
        """Edition formule calcul moyenne UE"""
        # Check access
        sem = sco_formsemestre_edit.can_edit_sem(self, REQUEST, formsemestre_id)
        if not sem:
            raise AccessDenied("vous n'avez pas le droit d'effectuer cette opération")
        cnx = self.GetDBConnexion()
        #
        ue = self.do_ue_list({"ue_id": ue_id})[0]
        H = [
            self.html_sem_header(
                REQUEST,
                "Modification règle de calcul de l'UE %s (%s)"
                % (ue["acronyme"], ue["titre"]),
                sem,
            ),
            self._expr_help % {"target": "de l'UE", "objs": "modules", "ordre": ""},
        ]
        el = sco_compute_moy.formsemestre_ue_computation_expr_list(
            cnx, {"formsemestre_id": formsemestre_id, "ue_id": ue_id}
        )
        if el:
            initvalues = el[0]
        else:
            initvalues = {}
        form = [
            ("ue_id", {"input_type": "hidden"}),
            ("formsemestre_id", {"input_type": "hidden"}),
            (
                "computation_expr",
                {
                    "title": "Formule de calcul",
                    "input_type": "textarea",
                    "rows": 4,
                    "cols": 60,
                    "explanation": "formule de calcul (expérimental)",
                },
            ),
        ]
        tf = TrivialFormulator(
            REQUEST.URL0,
            REQUEST.form,
            form,
            submitlabel="Modifier formule de calcul",
            cancelbutton="Annuler",
            initvalues=initvalues,
        )
        if tf[0] == 0:
            return "\n".join(H) + tf[1] + self.sco_footer(REQUEST)
        elif tf[0] == -1:
            return REQUEST.RESPONSE.redirect(
                "formsemestre_status?formsemestre_id=" + formsemestre_id
            )
        else:
            if el:
                el[0]["computation_expr"] = tf[2]["computation_expr"]
                sco_compute_moy.formsemestre_ue_computation_expr_edit(cnx, el[0])
            else:
                sco_compute_moy.formsemestre_ue_computation_expr_create(cnx, tf[2])

            self._inval_cache(formsemestre_id=formsemestre_id)  # > modif regle calcul
            return REQUEST.RESPONSE.redirect(
                "formsemestre_status?formsemestre_id="
                + formsemestre_id
                + "&amp;head_message=règle%20de%20calcul%20modifiée"
            )

    security.declareProtected(ScoView, "formsemestre_enseignants_list")

    def formsemestre_enseignants_list(self, REQUEST, formsemestre_id, format="html"):
        """Liste les enseignants intervenants dans le semestre (resp. modules et chargés de TD)
        et indique les absences saisies par chacun.
        """
        sem = sco_formsemestre.get_formsemestre(self, formsemestre_id)
        # resp. de modules:
        mods = sco_moduleimpl.do_moduleimpl_withmodule_list(
            self, formsemestre_id=formsemestre_id
        )
        sem_ens = {}
        for mod in mods:
            if not mod["responsable_id"] in sem_ens:
                sem_ens[mod["responsable_id"]] = {"mods": [mod]}
            else:
                sem_ens[mod["responsable_id"]]["mods"].append(mod)
        # charges de TD:
        for mod in mods:
            for ensd in mod["ens"]:
                if not ensd["ens_id"] in sem_ens:
                    sem_ens[ensd["ens_id"]] = {"mods": [mod]}
                else:
                    sem_ens[ensd["ens_id"]]["mods"].append(mod)
        # compte les absences ajoutées par chacun dans tout le semestre
        cnx = self.GetDBConnexion()
        cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
        for ens in sem_ens:
            cursor.execute(
                "select * from scolog L, notes_formsemestre_inscription I where method='AddAbsence' and authenticated_user=%(authenticated_user)s and L.etudid = I.etudid and  I.formsemestre_id=%(formsemestre_id)s and date > %(date_debut)s and date < %(date_fin)s",
                {
                    "authenticated_user": ens,
                    "formsemestre_id": formsemestre_id,
                    "date_debut": ndb.DateDMYtoISO(sem["date_debut"]),
                    "date_fin": ndb.DateDMYtoISO(sem["date_fin"]),
                },
            )

            events = cursor.dictfetchall()
            sem_ens[ens]["nbabsadded"] = len(events)

        # description textuelle des modules
        for ens in sem_ens:
            sem_ens[ens]["descr_mods"] = ", ".join(
                [x["module"]["code"] for x in sem_ens[ens]["mods"]]
            )

        # ajoute infos sur enseignant:
        for ens in sem_ens:
            sem_ens[ens].update(self.Users.user_info(ens))
            if sem_ens[ens]["email"]:
                sem_ens[ens]["_email_target"] = "mailto:%s" % sem_ens[ens]["email"]

        sem_ens_list = sem_ens.values()
        sem_ens_list.sort(lambda x, y: cmp(x["nomprenom"], y["nomprenom"]))

        # --- Generate page with table
        title = "Enseignants de " + sem["titremois"]
        T = GenTable(
            columns_ids=["nom_fmt", "prenom_fmt", "descr_mods", "nbabsadded", "email"],
            titles={
                "nom_fmt": "Nom",
                "prenom_fmt": "Prénom",
                "email": "Mail",
                "descr_mods": "Modules",
                "nbabsadded": "Saisies Abs.",
            },
            rows=sem_ens_list,
            html_sortable=True,
            html_class="table_leftalign",
            filename=scu.make_filename("Enseignants-" + sem["titreannee"]),
            html_title=self.html_sem_header(
                REQUEST, "Enseignants du semestre", sem, with_page_header=False
            ),
            base_url="%s?formsemestre_id=%s" % (REQUEST.URL0, formsemestre_id),
            caption="Tous les enseignants (responsables ou associés aux modules de ce semestre) apparaissent. Le nombre de saisies d'absences est le nombre d'opérations d'ajout effectuées sur ce semestre, sans tenir compte des annulations ou double saisies.",
            preferences=self.get_preferences(formsemestre_id),
        )
        return T.make_page(
            self, page_title=title, title=title, REQUEST=REQUEST, format=format
        )

    security.declareProtected(ScoView, "edit_enseignants_form_delete")

    def edit_enseignants_form_delete(self, REQUEST, moduleimpl_id, ens_id):
        "remove ens"
        M, _ = sco_moduleimpl.can_change_ens(self, REQUEST, moduleimpl_id)
        # search ens_id
        ok = False
        for ens in M["ens"]:
            if ens["ens_id"] == ens_id:
                ok = True
                break
        if not ok:
            raise ScoValueError("invalid ens_id (%s)" % ens_id)
        sco_moduleimpl.do_ens_delete(self, ens["modules_enseignants_id"])
        return REQUEST.RESPONSE.redirect(
            "edit_enseignants_form?moduleimpl_id=%s" % moduleimpl_id
        )

    # --- Gestion des inscriptions aux modules
    _formsemestre_inscriptionEditor = ndb.EditableTable(
        "notes_formsemestre_inscription",
        "formsemestre_inscription_id",
        ("formsemestre_inscription_id", "etudid", "formsemestre_id", "etat", "etape"),
        sortkey="formsemestre_id",
    )

    security.declareProtected(ScoEtudInscrit, "do_formsemestre_inscription_create")

    def do_formsemestre_inscription_create(self, args, REQUEST, method=None):
        "create a formsemestre_inscription (and sco event)"
        cnx = self.GetDBConnexion()
        log("do_formsemestre_inscription_create: args=%s" % str(args))
        sems = sco_formsemestre.do_formsemestre_list(
            self, {"formsemestre_id": args["formsemestre_id"]}
        )
        if len(sems) != 1:
            raise ScoValueError(
                "code de semestre invalide: %s" % args["formsemestre_id"]
            )
        sem = sems[0]
        # check lock
        if sem["etat"] != "1":
            raise ScoValueError("inscription: semestre verrouille")
        #
        r = self._formsemestre_inscriptionEditor.create(cnx, args)
        # Evenement
        scolars.scolar_events_create(
            cnx,
            args={
                "etudid": args["etudid"],
                "event_date": time.strftime("%d/%m/%Y"),
                "formsemestre_id": args["formsemestre_id"],
                "event_type": "INSCRIPTION",
            },
        )
        # Log etudiant
        logdb(
            REQUEST,
            cnx,
            method=method,
            etudid=args["etudid"],
            msg="inscription en semestre %s" % args["formsemestre_id"],
            commit=False,
        )
        #
        self._inval_cache(
            formsemestre_id=args["formsemestre_id"]
        )  # > inscription au semestre
        return r

    security.declareProtected(ScoImplement, "do_formsemestre_inscription_delete")

    def do_formsemestre_inscription_delete(self, oid, formsemestre_id=None):
        "delete formsemestre_inscription"
        cnx = self.GetDBConnexion()
        self._formsemestre_inscriptionEditor.delete(cnx, oid)

        self._inval_cache(
            formsemestre_id=formsemestre_id
        )  # > desinscription du semestre

    security.declareProtected(ScoView, "do_formsemestre_inscription_list")

    def do_formsemestre_inscription_list(self, *args, **kw):
        "list formsemestre_inscriptions"
        cnx = self.GetDBConnexion()
        return self._formsemestre_inscriptionEditor.list(cnx, *args, **kw)

    security.declareProtected(ScoView, "do_formsemestre_inscription_listinscrits")

    def do_formsemestre_inscription_listinscrits(self, formsemestre_id):
        """Liste les inscrits (état I) à ce semestre et cache le résultat"""
        cache = self.get_formsemestre_inscription_cache()
        r = cache.get(formsemestre_id)
        if r != None:
            return r
        # retreive list
        r = self.do_formsemestre_inscription_list(
            args={"formsemestre_id": formsemestre_id, "etat": "I"}
        )
        cache.set(formsemestre_id, r)
        return r

    security.declareProtected(ScoImplement, "do_formsemestre_inscription_edit")

    def do_formsemestre_inscription_edit(self, args=None, formsemestre_id=None):
        "edit a formsemestre_inscription"
        cnx = self.GetDBConnexion()
        self._formsemestre_inscriptionEditor.edit(cnx, args)
        self._inval_cache(
            formsemestre_id=formsemestre_id
        )  # > modif inscription semestre (demission ?)

    # Cache inscriptions semestres
    def get_formsemestre_inscription_cache(self):
        u = self.GetDBConnexionString()
        if CACHE_formsemestre_inscription.has_key(u):
            return CACHE_formsemestre_inscription[u]
        else:
            log("get_formsemestre_inscription_cache: new simpleCache")
            CACHE_formsemestre_inscription[u] = sco_cache.simpleCache()
            return CACHE_formsemestre_inscription[u]

    security.declareProtected(ScoImplement, "formsemestre_desinscription")

    def formsemestre_desinscription(
        self, etudid, formsemestre_id, REQUEST=None, dialog_confirmed=False
    ):
        """desinscrit l'etudiant de ce semestre (et donc de tous les modules).
        A n'utiliser qu'en cas d'erreur de saisie.
        S'il s'agit d'un semestre extérieur et qu'il n'y a plus d'inscrit,
        le semestre sera supprimé.
        """
        sem = sco_formsemestre.get_formsemestre(self, formsemestre_id)
        # -- check lock
        if sem["etat"] != "1":
            raise ScoValueError("desinscription impossible: semestre verrouille")

        # -- Si décisions de jury, désinscription interdite
        nt = self._getNotesCache().get_NotesTable(self, formsemestre_id)
        if nt.etud_has_decision(etudid):
            raise ScoValueError(
                """Désinscription impossible: l'étudiant a une décision de jury 
                (la supprimer avant si nécessaire: 
                <a href="formsemestre_validation_suppress_etud?etudid=%s&amp;formsemestre_id=%s">
                supprimer décision jury</a>
                )
                """
                % (etudid, formsemestre_id)
            )
        if not dialog_confirmed:
            etud = self.getEtudInfo(etudid=etudid, filled=1)[0]
            if sem["modalite"] != "EXT":
                msg_ext = """
                <p>%s sera désinscrit de tous les modules du semestre %s (%s - %s).</p>
                <p>Cette opération ne doit être utilisée que pour corriger une <b>erreur</b> !
                Un étudiant réellement inscrit doit le rester, le faire éventuellement <b>démissionner<b>.
                </p>
                """ % (
                    etud["nomprenom"],
                    sem["titre_num"],
                    sem["date_debut"],
                    sem["date_fin"],
                )
            else:  # semestre extérieur
                msg_ext = """
                <p>%s sera désinscrit du semestre extérieur %s (%s - %s).</p>
                """ % (
                    etud["nomprenom"],
                    sem["titre_num"],
                    sem["date_debut"],
                    sem["date_fin"],
                )
                inscrits = self.do_formsemestre_inscription_list(
                    args={"formsemestre_id": formsemestre_id}
                )
                nbinscrits = len(inscrits)
                if nbinscrits <= 1:
                    msg_ext = """<p class="warning">Attention: le semestre extérieur sera supprimé
                    car il n'a pas d'autre étudiant inscrit.
                    </p>
                    """
            return self.confirmDialog(
                """<h2>Confirmer la demande de desinscription ?</h2>""" + msg_ext,
                dest_url="",
                REQUEST=REQUEST,
                cancel_url="formsemestre_status?formsemestre_id=%s" % formsemestre_id,
                parameters={"etudid": etudid, "formsemestre_id": formsemestre_id},
            )

        self.do_formsemestre_desinscription(etudid, formsemestre_id, REQUEST=REQUEST)

        return (
            self.sco_header(REQUEST)
            + '<p>Etudiant désinscrit !</p><p><a class="stdlink" href="%s/ficheEtud?etudid=%s">retour à la fiche</a>'
            % (self.ScoURL(), etudid)
            + self.sco_footer(REQUEST)
        )

    security.declareProtected(ScoImplement, "do_formsemestre_desinscription")

    def do_formsemestre_desinscription(self, etudid, formsemestre_id, REQUEST=None):
        """Désinscription d'un étudiant.
        Si semestre extérieur et dernier inscrit, suppression de ce semestre.
        """
        sem = sco_formsemestre.get_formsemestre(self, formsemestre_id)
        # -- check lock
        if sem["etat"] != "1":
            raise ScoValueError("desinscription impossible: semestre verrouille")

        # -- Si decisions de jury, desinscription interdite
        nt = self._getNotesCache().get_NotesTable(self, formsemestre_id)
        if nt.etud_has_decision(etudid):
            raise ScoValueError(
                "desinscription impossible: l'étudiant a une décision de jury (la supprimer avant si nécessaire)"
            )

        insem = self.do_formsemestre_inscription_list(
            args={"formsemestre_id": formsemestre_id, "etudid": etudid}
        )
        if not insem:
            raise ScoValueError("%s n'est pas inscrit au semestre !" % etudid)
        insem = insem[0]
        # -- desinscription de tous les modules
        cnx = self.GetDBConnexion()
        cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
        cursor.execute(
            "select moduleimpl_inscription_id from notes_moduleimpl_inscription Im, notes_moduleimpl M  where Im.etudid=%(etudid)s and Im.moduleimpl_id = M.moduleimpl_id and M.formsemestre_id = %(formsemestre_id)s",
            {"etudid": etudid, "formsemestre_id": formsemestre_id},
        )
        res = cursor.fetchall()
        moduleimpl_inscription_ids = [x[0] for x in res]
        for moduleimpl_inscription_id in moduleimpl_inscription_ids:
            sco_moduleimpl.do_moduleimpl_inscription_delete(
                self, moduleimpl_inscription_id, formsemestre_id=formsemestre_id
            )
        # -- desincription du semestre
        self.do_formsemestre_inscription_delete(
            insem["formsemestre_inscription_id"], formsemestre_id=formsemestre_id
        )
        # --- Semestre extérieur
        if sem["modalite"] == "EXT":
            inscrits = self.do_formsemestre_inscription_list(
                args={"formsemestre_id": formsemestre_id}
            )
            nbinscrits = len(inscrits)
            if nbinscrits == 0:
                log(
                    "do_formsemestre_desinscription: suppression du semestre extérieur %s"
                    % formsemestre_id
                )
                sco_formsemestre_edit.do_formsemestre_delete(
                    self, formsemestre_id, REQUEST=REQUEST
                )

        if REQUEST:
            logdb(
                REQUEST,
                cnx,
                method="formsemestre_desinscription",
                etudid=etudid,
                msg="desinscription semestre %s" % formsemestre_id,
                commit=False,
            )

    security.declareProtected(ScoEtudInscrit, "etud_desinscrit_ue")

    def etud_desinscrit_ue(self, etudid, formsemestre_id, ue_id, REQUEST=None):
        """Desinscrit l'etudiant de tous les modules de cette UE dans ce semestre."""
        sco_moduleimpl_inscriptions.do_etud_desinscrit_ue(
            self, etudid, formsemestre_id, ue_id, REQUEST=REQUEST
        )
        return REQUEST.RESPONSE.redirect(
            self.ScoURL()
            + "/Notes/moduleimpl_inscriptions_stats?formsemestre_id="
            + formsemestre_id
        )

    security.declareProtected(ScoEtudInscrit, "etud_inscrit_ue")

    def etud_inscrit_ue(self, etudid, formsemestre_id, ue_id, REQUEST=None):
        """Inscrit l'etudiant de tous les modules de cette UE dans ce semestre."""
        sco_moduleimpl_inscriptions.do_etud_inscrit_ue(
            self, etudid, formsemestre_id, ue_id, REQUEST=REQUEST
        )
        return REQUEST.RESPONSE.redirect(
            self.ScoURL()
            + "/Notes/moduleimpl_inscriptions_stats?formsemestre_id="
            + formsemestre_id
        )

    # --- Inscriptions
    security.declareProtected(
        ScoEtudInscrit, "formsemestre_inscription_with_modules_form"
    )
    formsemestre_inscription_with_modules_form = (
        sco_formsemestre_inscriptions.formsemestre_inscription_with_modules_form
    )

    security.declareProtected(
        ScoEtudInscrit, "formsemestre_inscription_with_modules_etud"
    )
    formsemestre_inscription_with_modules_etud = (
        sco_formsemestre_inscriptions.formsemestre_inscription_with_modules_etud
    )

    security.declareProtected(ScoEtudInscrit, "formsemestre_inscription_with_modules")
    formsemestre_inscription_with_modules = (
        sco_formsemestre_inscriptions.formsemestre_inscription_with_modules
    )

    security.declareProtected(ScoEtudInscrit, "formsemestre_inscription_option")
    formsemestre_inscription_option = (
        sco_formsemestre_inscriptions.formsemestre_inscription_option
    )

    security.declareProtected(ScoEtudInscrit, "do_moduleimpl_incription_options")
    do_moduleimpl_incription_options = (
        sco_formsemestre_inscriptions.do_moduleimpl_incription_options
    )

    security.declareProtected(ScoView, "formsemestre_inscrits_ailleurs")
    formsemestre_inscrits_ailleurs = (
        sco_formsemestre_inscriptions.formsemestre_inscrits_ailleurs
    )

    security.declareProtected(ScoEtudInscrit, "moduleimpl_inscriptions_edit")
    moduleimpl_inscriptions_edit = (
        sco_moduleimpl_inscriptions.moduleimpl_inscriptions_edit
    )

    security.declareProtected(ScoView, "moduleimpl_inscriptions_stats")
    moduleimpl_inscriptions_stats = (
        sco_moduleimpl_inscriptions.moduleimpl_inscriptions_stats
    )

    # --- Evaluations
    _evaluationEditor = ndb.EditableTable(
        "notes_evaluation",
        "evaluation_id",
        (
            "evaluation_id",
            "moduleimpl_id",
            "jour",
            "heure_debut",
            "heure_fin",
            "description",
            "note_max",
            "coefficient",
            "visibulletin",
            "publish_incomplete",
            "evaluation_type",
            "numero",
        ),
        sortkey="numero desc, jour desc, heure_debut desc",  # plus recente d'abord
        output_formators={
            "jour": ndb.DateISOtoDMY,
            "visibulletin": str,
            "publish_incomplete": str,
            "numero": ndb.int_null_is_zero,
        },
        input_formators={
            "jour": ndb.DateDMYtoISO,
            "heure_debut": ndb.TimetoISO8601,  # converti par do_evaluation_list
            "heure_fin": ndb.TimetoISO8601,  # converti par do_evaluation_list
            "visibulletin": int,
            "publish_incomplete": int,
        },
    )

    def _evaluation_check_write_access(self, REQUEST, moduleimpl_id=None):
        """Vérifie que l'on a le droit de modifier, créer ou détruire une
        évaluation dans ce module.
        Sinon, lance une exception.
        (nb: n'implique pas le droit de saisir ou modifier des notes)
        """
        # acces pour resp. moduleimpl et resp. form semestre (dir etud)
        if moduleimpl_id is None:
            raise ValueError("no moduleimpl specified")  # bug
        authuser = REQUEST.AUTHENTICATED_USER
        uid = str(authuser)
        M = sco_moduleimpl.do_moduleimpl_list(self, moduleimpl_id=moduleimpl_id)[0]
        sem = sco_formsemestre.get_formsemestre(self, M["formsemestre_id"])

        if (
            (not authuser.has_permission(ScoEditAllEvals, self))
            and uid != M["responsable_id"]
            and uid not in sem["responsables"]
        ):
            if sem["ens_can_edit_eval"]:
                for ens in M["ens"]:
                    if ens["ens_id"] == uid:
                        return  # ok
            raise AccessDenied("Modification évaluation impossible pour %s" % (uid,))

    security.declareProtected(ScoEnsView, "do_evaluation_create")

    def do_evaluation_create(
        self,
        moduleimpl_id=None,
        jour=None,
        heure_debut=None,
        heure_fin=None,
        description=None,
        note_max=None,
        coefficient=None,
        visibulletin=None,
        publish_incomplete=None,
        evaluation_type=None,
        numero=None,
        REQUEST=None,
        **kw
    ):
        """Create an evaluation"""
        args = locals()
        log("do_evaluation_create: args=" + str(args))
        self._evaluation_check_write_access(REQUEST, moduleimpl_id=moduleimpl_id)
        self._check_evaluation_args(args)
        # Check numeros
        sco_evaluations.module_evaluation_renumber(
            self, moduleimpl_id, REQUEST=REQUEST, only_if_unumbered=True
        )
        if not "numero" in args or args["numero"] is None:
            n = None
            # determine le numero avec la date
            # Liste des eval existantes triees par date, la plus ancienne en tete
            ModEvals = self.do_evaluation_list(
                args={"moduleimpl_id": moduleimpl_id},
                sortkey="jour asc, heure_debut asc",
            )
            if args["jour"]:
                next_eval = None
                t = (
                    ndb.DateDMYtoISO(args["jour"]),
                    ndb.TimetoISO8601(args["heure_debut"]),
                )
                for e in ModEvals:
                    if (
                        ndb.DateDMYtoISO(e["jour"]),
                        ndb.TimetoISO8601(e["heure_debut"]),
                    ) > t:
                        next_eval = e
                        break
                if next_eval:
                    n = sco_evaluations.module_evaluation_insert_before(
                        self, ModEvals, next_eval, REQUEST
                    )
                else:
                    n = None  # a placer en fin
            if n is None:  # pas de date ou en fin:
                if ModEvals:
                    log(pprint.pformat(ModEvals[-1]))
                    n = ModEvals[-1]["numero"] + 1
                else:
                    n = 0  # the only one
            # log("creating with numero n=%d" % n)
            args["numero"] = n

        #
        cnx = self.GetDBConnexion()
        r = self._evaluationEditor.create(cnx, args)

        # news
        M = sco_moduleimpl.do_moduleimpl_list(self, moduleimpl_id=moduleimpl_id)[0]
        mod = self.do_module_list(args={"module_id": M["module_id"]})[0]
        mod["moduleimpl_id"] = M["moduleimpl_id"]
        mod["url"] = "Notes/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % mod
        sco_news.add(
            self,
            REQUEST,
            typ=NEWS_NOTE,
            object=moduleimpl_id,
            text='Création d\'une évaluation dans <a href="%(url)s">%(titre)s</a>'
            % mod,
            url=mod["url"],
        )

        return r

    def _check_evaluation_args(self, args):
        "Check coefficient, dates and duration, raises exception if invalid"
        moduleimpl_id = args["moduleimpl_id"]
        # check bareme
        note_max = args.get("note_max", None)
        if note_max is None:
            raise ScoValueError("missing note_max")
        try:
            note_max = float(note_max)
        except ValueError:
            raise ScoValueError("Invalid note_max value")
        if note_max < 0:
            raise ScoValueError("Invalid note_max value (must be positive or null)")
        # check coefficient
        coef = args.get("coefficient", None)
        if coef is None:
            raise ScoValueError("missing coefficient")
        try:
            coef = float(coef)
        except ValueError:
            raise ScoValueError("Invalid coefficient value")
        if coef < 0:
            raise ScoValueError("Invalid coefficient value (must be positive or null)")
        # check date
        jour = args.get("jour", None)
        args["jour"] = jour
        if jour:
            M = sco_moduleimpl.do_moduleimpl_list(self, moduleimpl_id=moduleimpl_id)[0]
            sem = sco_formsemestre.get_formsemestre(self, M["formsemestre_id"])
            d, m, y = [int(x) for x in sem["date_debut"].split("/")]
            date_debut = datetime.date(y, m, d)
            d, m, y = [int(x) for x in sem["date_fin"].split("/")]
            date_fin = datetime.date(y, m, d)
            # passe par ndb.DateDMYtoISO pour avoir date pivot
            y, m, d = [int(x) for x in ndb.DateDMYtoISO(jour).split("-")]
            jour = datetime.date(y, m, d)
            if (jour > date_fin) or (jour < date_debut):
                raise ScoValueError(
                    "La date de l'évaluation (%s/%s/%s) n'est pas dans le semestre !"
                    % (d, m, y)
                )
        heure_debut = args.get("heure_debut", None)
        args["heure_debut"] = heure_debut
        heure_fin = args.get("heure_fin", None)
        args["heure_fin"] = heure_fin
        if jour and ((not heure_debut) or (not heure_fin)):
            raise ScoValueError("Les heures doivent être précisées")
        d = ndb.TimeDuration(heure_debut, heure_fin)
        if d and ((d < 0) or (d > 60 * 12)):
            raise ScoValueError("Heures de l'évaluation incohérentes !")

    security.declareProtected(ScoEnsView, "evaluation_delete")

    def evaluation_delete(self, REQUEST, evaluation_id):
        """Form delete evaluation"""
        El = self.do_evaluation_list(args={"evaluation_id": evaluation_id})
        if not El:
            raise ValueError("Evalution inexistante ! (%s)" % evaluation_id)
        E = El[0]
        M = sco_moduleimpl.do_moduleimpl_list(self, moduleimpl_id=E["moduleimpl_id"])[0]
        Mod = self.do_module_list(args={"module_id": M["module_id"]})[0]
        tit = "Suppression de l'évaluation %(description)s (%(jour)s)" % E
        etat = sco_evaluations.do_evaluation_etat(self, evaluation_id)
        H = [
            self.html_sem_header(REQUEST, tit, with_h2=False),
            """<h2 class="formsemestre">Module <tt>%(code)s</tt> %(titre)s</h2>"""
            % Mod,
            """<h3>%s</h3>""" % tit,
            """<p class="help">Opération <span class="redboldtext">irréversible</span>. Si vous supprimez l'évaluation, vous ne pourrez pas retrouver les notes associées.</p>""",
        ]
        warning = False
        if etat["nb_notes_total"]:
            warning = True
            nb_desinscrits = etat["nb_notes_total"] - etat["nb_notes"]
            H.append(
                """<div class="ue_warning"><span>Il y a %s notes"""
                % etat["nb_notes_total"]
            )
            if nb_desinscrits:
                H.append(
                    """ (dont %s d'étudiants qui ne sont plus inscrits)"""
                    % nb_desinscrits
                )
            H.append(""" dans l'évaluation</span>""")
            if etat["nb_notes"] == 0:
                H.append(
                    """<p>Vous pouvez quand même supprimer l'évaluation, les notes des étudiants désincrits seront effacées.</p>"""
                )

        if etat["nb_notes"]:
            H.append(
                """<p>Suppression impossible (effacer les notes d'abord)</p><p><a class="stdlink" href="moduleimpl_status?moduleimpl_id=%s">retour au tableau de bord du module</a></p></div>"""
                % E["moduleimpl_id"]
            )
            return "\n".join(H) + self.sco_footer(REQUEST)
        if warning:
            H.append("""</div>""")

        tf = TrivialFormulator(
            REQUEST.URL0,
            REQUEST.form,
            (("evaluation_id", {"input_type": "hidden"}),),
            initvalues=E,
            submitlabel="Confirmer la suppression",
            cancelbutton="Annuler",
        )
        if tf[0] == 0:
            return "\n".join(H) + tf[1] + self.sco_footer(REQUEST)
        elif tf[0] == -1:
            return REQUEST.RESPONSE.redirect(
                self.ScoURL()
                + "/Notes/moduleimpl_status?moduleimpl_id="
                + E["moduleimpl_id"]
            )
        else:
            sco_evaluations.do_evaluation_delete(self, REQUEST, E["evaluation_id"])
            return (
                "\n".join(H)
                + """<p>OK, évaluation supprimée.</p>
            <p><a class="stdlink" href="%s">Continuer</a></p>"""
                % (
                    self.ScoURL()
                    + "/Notes/moduleimpl_status?moduleimpl_id="
                    + E["moduleimpl_id"]
                )
                + self.sco_footer(REQUEST)
            )

    security.declareProtected(ScoView, "do_evaluation_list")

    def do_evaluation_list(self, args, sortkey=None):
        """List evaluations, sorted by numero (or most recent date first).

        Ajoute les champs:
        'duree' : '2h30'
        'matin' : 1 (commence avant 12:00) ou 0
        'apresmidi' : 1 (termine après 12:00) ou 0
        'descrheure' : ' de 15h00 à 16h30'
        """
        cnx = self.GetDBConnexion()
        evals = self._evaluationEditor.list(cnx, args, sortkey=sortkey)
        # calcule duree (chaine de car.) de chaque evaluation et ajoute jouriso, matin, apresmidi
        for e in evals:
            heure_debut_dt = e["heure_debut"] or datetime.time(
                8, 00
            )  # au cas ou pas d'heure (note externe?)
            heure_fin_dt = e["heure_fin"] or datetime.time(8, 00)
            e["heure_debut"] = ndb.TimefromISO8601(e["heure_debut"])
            e["heure_fin"] = ndb.TimefromISO8601(e["heure_fin"])
            e["jouriso"] = ndb.DateDMYtoISO(e["jour"])
            heure_debut, heure_fin = e["heure_debut"], e["heure_fin"]
            d = ndb.TimeDuration(heure_debut, heure_fin)
            if d is not None:
                m = d % 60
                e["duree"] = "%dh" % (d / 60)
                if m != 0:
                    e["duree"] += "%02d" % m
            else:
                e["duree"] = ""
            if heure_debut and (not heure_fin or heure_fin == heure_debut):
                e["descrheure"] = " à " + heure_debut
            elif heure_debut and heure_fin:
                e["descrheure"] = " de %s à %s" % (heure_debut, heure_fin)
            else:
                e["descrheure"] = ""
            # matin, apresmidi: utile pour se referer aux absences:
            if heure_debut_dt < datetime.time(12, 00):
                e["matin"] = 1
            else:
                e["matin"] = 0
            if heure_fin_dt > datetime.time(12, 00):
                e["apresmidi"] = 1
            else:
                e["apresmidi"] = 0

        return evals

    security.declareProtected(ScoView, "do_evaluation_list_in_formsemestre")

    def do_evaluation_list_in_formsemestre(self, formsemestre_id):
        "list evaluations in this formsemestre"
        mods = sco_moduleimpl.do_moduleimpl_list(self, formsemestre_id=formsemestre_id)
        evals = []
        for mod in mods:
            evals += self.do_evaluation_list(
                args={"moduleimpl_id": mod["moduleimpl_id"]}
            )
        return evals

    security.declareProtected(ScoEnsView, "do_evaluation_edit")

    def do_evaluation_edit(self, REQUEST, args):
        "edit a evaluation"
        evaluation_id = args["evaluation_id"]
        the_evals = self.do_evaluation_list({"evaluation_id": evaluation_id})
        if not the_evals:
            raise ValueError("evaluation inexistante !")
        moduleimpl_id = the_evals[0]["moduleimpl_id"]
        args["moduleimpl_id"] = moduleimpl_id
        self._check_evaluation_args(args)
        self._evaluation_check_write_access(REQUEST, moduleimpl_id=moduleimpl_id)
        cnx = self.GetDBConnexion()
        self._evaluationEditor.edit(cnx, args)
        # inval cache pour ce semestre
        M = sco_moduleimpl.do_moduleimpl_list(self, moduleimpl_id=moduleimpl_id)[0]
        self._inval_cache(
            formsemestre_id=M["formsemestre_id"]
        )  # > evaluation_edit (coef, ...)

    security.declareProtected(ScoEnsView, "evaluation_edit")

    def evaluation_edit(self, evaluation_id, REQUEST):
        "form edit evaluation"
        return sco_evaluations.evaluation_create_form(
            self, evaluation_id=evaluation_id, REQUEST=REQUEST, edit=True
        )

    security.declareProtected(ScoEnsView, "evaluation_create")

    def evaluation_create(self, moduleimpl_id, REQUEST):
        "form create evaluation"
        return sco_evaluations.evaluation_create_form(
            self, moduleimpl_id=moduleimpl_id, REQUEST=REQUEST, edit=False
        )

    security.declareProtected(ScoView, "evaluation_listenotes")

    def evaluation_listenotes(self, REQUEST=None):
        """Affichage des notes d'une évaluation"""
        if REQUEST.form.get("format", "html") == "html":
            H = self.sco_header(
                REQUEST,
                cssstyles=["css/verticalhisto.css"],
                javascripts=["js/etud_info.js"],
                init_qtip=True,
            )
            F = self.sco_footer(REQUEST)
        else:
            H, F = "", ""
        B = self.do_evaluation_listenotes(REQUEST)
        return H + B + F

    security.declareProtected(ScoView, "do_evaluation_listenotes")
    do_evaluation_listenotes = sco_liste_notes.do_evaluation_listenotes

    security.declareProtected(ScoView, "evaluation_list_operations")
    evaluation_list_operations = sco_undo_notes.evaluation_list_operations

    security.declareProtected(ScoView, "evaluation_check_absences_html")
    evaluation_check_absences_html = sco_liste_notes.evaluation_check_absences_html

    security.declareProtected(ScoView, "formsemestre_check_absences_html")
    formsemestre_check_absences_html = sco_liste_notes.formsemestre_check_absences_html

    # --- Placement des étudiants pour l'évaluation
    security.declareProtected(ScoEnsView, "placement_eval_selectetuds")
    placement_eval_selectetuds = sco_placement.placement_eval_selectetuds

    security.declareProtected(ScoEnsView, "do_placement")
    do_placement = sco_placement.do_placement

    # --- Saisie des notes
    security.declareProtected(ScoEnsView, "saisie_notes_tableur")
    saisie_notes_tableur = sco_saisie_notes.saisie_notes_tableur

    security.declareProtected(ScoEnsView, "feuille_saisie_notes")
    feuille_saisie_notes = sco_saisie_notes.feuille_saisie_notes

    security.declareProtected(ScoEnsView, "saisie_notes")
    saisie_notes = sco_saisie_notes.saisie_notes

    security.declareProtected(ScoEnsView, "save_note")
    save_note = sco_saisie_notes.save_note

    security.declareProtected(ScoEnsView, "do_evaluation_set_missing")
    do_evaluation_set_missing = sco_saisie_notes.do_evaluation_set_missing

    security.declareProtected(ScoView, "evaluation_suppress_alln")
    evaluation_suppress_alln = sco_saisie_notes.evaluation_suppress_alln

    security.declareProtected(ScoEditAllNotes, "dummy_ScoEditAllNotes")

    def dummy_ScoEditAllNotes(self):
        "dummy method, necessary to declare permission ScoEditAllNotes"
        return True

    security.declareProtected(ScoEditAllEvals, "dummy_ScoEditAllEvals")

    def dummy_ScoEditAllEvals(self):
        "dummy method, necessary to declare permission ScoEditAllEvals"
        return True

    security.declareProtected(ScoSuperAdmin, "dummy_ScoSuperAdmin")

    def dummy_ScoSuperAdmin(self):
        "dummy method, necessary to declare permission ScoSuperAdmin"
        return True

    security.declareProtected(ScoEtudChangeGroups, "dummy_ScoEtudChangeGroups")

    def dummy_ScoEtudChangeGroups(self):
        "dummy method, necessary to declare permission ScoEtudChangeGroups"
        return True

    security.declareProtected(ScoEtudSupprAnnotations, "dummy_ScoEtudSupprAnnotations")

    def dummy_ScoEtudSupprAnnotations(self):
        "dummy method, necessary to declare permission ScoEtudSupprAnnotations"
        return True

    security.declareProtected(ScoEditFormationTags, "dummy_ScoEditFormationTags")

    def dummy_ScoEditFormationTags(self):
        "dummy method, necessary to declare permission ScoEditFormationTags"
        return True

    # cache notes evaluations
    def get_evaluations_cache(self):
        u = self.GetDBConnexionString()
        if CACHE_evaluations.has_key(u):
            return CACHE_evaluations[u]
        else:
            log("get_evaluations_cache: new simpleCache")
            CACHE_evaluations[u] = sco_cache.simpleCache()
            return CACHE_evaluations[u]

    def _notes_getall(
        self, evaluation_id, table="notes_notes", filter_suppressed=True, by_uid=None
    ):
        """get tt les notes pour une evaluation: { etudid : { 'value' : value, 'date' : date ... }}
        Attention: inclue aussi les notes des étudiants qui ne sont plus inscrits au module.
        """
        # log('_notes_getall( e=%s fs=%s )' % (evaluation_id, filter_suppressed))
        do_cache = (
            filter_suppressed and table == "notes_notes" and (by_uid is None)
        )  # pas de cache pour (rares) appels via undo_notes ou specifiant un enseignant
        if do_cache:
            cache = self.get_evaluations_cache()
            r = cache.get(evaluation_id)
            if r != None:
                return r
        cnx = self.GetDBConnexion()
        cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
        cond = " where evaluation_id=%(evaluation_id)s"
        if by_uid:
            cond += " and uid=%(by_uid)s"

        cursor.execute(
            "select * from " + table + cond,
            {"evaluation_id": evaluation_id, "by_uid": by_uid},
        )
        res = cursor.dictfetchall()
        d = {}
        if filter_suppressed:
            for x in res:
                if x["value"] != scu.NOTES_SUPPRESS:
                    d[x["etudid"]] = x
        else:
            for x in res:
                d[x["etudid"]] = x
        if do_cache:
            cache.set(evaluation_id, d)
        return d

    # --- Bulletins
    security.declareProtected(ScoView, "formsemestre_bulletins_pdf")

    def formsemestre_bulletins_pdf(
        self, formsemestre_id, REQUEST, version="selectedevals"
    ):
        "Publie les bulletins dans un classeur PDF"
        pdfdoc, filename = sco_bulletins_pdf.get_formsemestre_bulletins_pdf(
            self, formsemestre_id, REQUEST, version=version
        )
        return scu.sendPDFFile(REQUEST, pdfdoc, filename)

    security.declareProtected(ScoView, "etud_bulletins_pdf")

    def etud_bulletins_pdf(self, etudid, REQUEST, version="selectedevals"):
        "Publie tous les bulletins d'un etudiants dans un classeur PDF"
        pdfdoc, filename = sco_bulletins_pdf.get_etud_bulletins_pdf(
            self, etudid, REQUEST, version=version
        )
        return scu.sendPDFFile(REQUEST, pdfdoc, filename)

    security.declareProtected(ScoView, "formsemestre_bulletins_pdf_choice")
    formsemestre_bulletins_pdf_choice = sco_bulletins.formsemestre_bulletins_pdf_choice

    security.declareProtected(ScoView, "formsemestre_bulletins_mailetuds_choice")
    formsemestre_bulletins_mailetuds_choice = (
        sco_bulletins.formsemestre_bulletins_mailetuds_choice
    )

    security.declareProtected(ScoView, "formsemestre_bulletins_mailetuds")

    def formsemestre_bulletins_mailetuds(
        self,
        formsemestre_id,
        REQUEST,
        version="long",
        dialog_confirmed=False,
        prefer_mail_perso=0,
    ):
        "envoi a chaque etudiant (inscrit et ayant un mail) son bulletin"
        prefer_mail_perso = int(prefer_mail_perso)
        nt = self._getNotesCache().get_NotesTable(
            self, formsemestre_id
        )  # > get_etudids
        etudids = nt.get_etudids()
        #
        if not sco_bulletins.can_send_bulletin_by_mail(self, formsemestre_id, REQUEST):
            raise AccessDenied("vous n'avez pas le droit d'envoyer les bulletins")
        # Confirmation dialog
        if not dialog_confirmed:
            return self.confirmDialog(
                "<h2>Envoyer les %d bulletins par e-mail aux étudiants ?"
                % len(etudids),
                dest_url="",
                REQUEST=REQUEST,
                cancel_url="formsemestre_status?formsemestre_id=%s" % formsemestre_id,
                parameters={
                    "version": version,
                    "formsemestre_id": formsemestre_id,
                    "prefer_mail_perso": prefer_mail_perso,
                },
            )

        # Make each bulletin
        nb_send = 0
        for etudid in etudids:
            h, _ = sco_bulletins.do_formsemestre_bulletinetud(
                self,
                formsemestre_id,
                etudid,
                version=version,
                prefer_mail_perso=prefer_mail_perso,
                format="pdfmail",
                nohtml=True,
                REQUEST=REQUEST,
            )
            if h:
                nb_send += 1
        #
        return (
            self.sco_header(REQUEST)
            + '<p>%d bulletins sur %d envoyés par mail !</p><p><a class="stdlink" href="formsemestre_status?formsemestre_id=%s">continuer</a></p>'
            % (nb_send, len(etudids), formsemestre_id)
            + self.sco_footer(REQUEST)
        )

    security.declareProtected(ScoView, "external_ue_create_form")
    external_ue_create_form = sco_ue_external.external_ue_create_form

    security.declareProtected(ScoEnsView, "appreciation_add_form")

    def appreciation_add_form(
        self,
        etudid=None,
        formsemestre_id=None,
        id=None,  # si id, edit
        suppress=False,  # si true, supress id
        REQUEST=None,
    ):
        "form ajout ou edition d'une appreciation"
        cnx = self.GetDBConnexion()
        authuser = REQUEST.AUTHENTICATED_USER
        if id:  # edit mode
            apps = scolars.appreciations_list(cnx, args={"id": id})
            if not apps:
                raise ScoValueError("id d'appreciation invalide !")
            app = apps[0]
            formsemestre_id = app["formsemestre_id"]
            etudid = app["etudid"]
        if REQUEST.form.has_key("edit"):
            edit = int(REQUEST.form["edit"])
        elif id:
            edit = 1
        else:
            edit = 0
        sem = sco_formsemestre.get_formsemestre(self, formsemestre_id)
        # check custom access permission
        can_edit_app = (str(authuser) in sem["responsables"]) or (
            authuser.has_permission(ScoEtudInscrit, self)
        )
        if not can_edit_app:
            raise AccessDenied("vous n'avez pas le droit d'ajouter une appreciation")
        #
        bull_url = "formsemestre_bulletinetud?formsemestre_id=%s&amp;etudid=%s" % (
            formsemestre_id,
            etudid,
        )
        if suppress:
            scolars.appreciations_delete(cnx, id)
            logdb(REQUEST, cnx, method="appreciation_suppress", etudid=etudid, msg="")
            return REQUEST.RESPONSE.redirect(bull_url)
        #
        etud = self.getEtudInfo(etudid=etudid, filled=1)[0]
        if id:
            a = "Edition"
        else:
            a = "Ajout"
        H = [
            self.sco_header(REQUEST)
            + "<h2>%s d'une appréciation sur %s</h2>" % (a, etud["nomprenom"])
        ]
        F = self.sco_footer(REQUEST)
        descr = [
            ("edit", {"input_type": "hidden", "default": edit}),
            ("etudid", {"input_type": "hidden"}),
            ("formsemestre_id", {"input_type": "hidden"}),
            ("id", {"input_type": "hidden"}),
            ("comment", {"title": "", "input_type": "textarea", "rows": 4, "cols": 60}),
        ]
        if id:
            initvalues = {
                "etudid": etudid,
                "formsemestre_id": formsemestre_id,
                "comment": app["comment"],
            }
        else:
            initvalues = {}
        tf = TrivialFormulator(
            REQUEST.URL0,
            REQUEST.form,
            descr,
            initvalues=initvalues,
            cancelbutton="Annuler",
            submitlabel="Ajouter appréciation",
        )
        if tf[0] == 0:
            return "\n".join(H) + "\n" + tf[1] + F
        elif tf[0] == -1:
            return REQUEST.RESPONSE.redirect(bull_url)
        else:
            args = {
                "etudid": etudid,
                "formsemestre_id": formsemestre_id,
                "author": str(authuser),
                "comment": tf[2]["comment"],
                "zope_authenticated_user": str(authuser),
                "zope_remote_addr": REQUEST.REMOTE_ADDR,
            }
            if edit:
                args["id"] = id
                scolars.appreciations_edit(cnx, args)
            else:  # nouvelle
                scolars.appreciations_create(cnx, args, has_uniq_values=False)
            # log
            logdb(
                REQUEST,
                cnx,
                method="appreciation_add",
                etudid=etudid,
                msg=tf[2]["comment"],
            )
            # ennuyeux mais necessaire (pour le PDF seulement)
            self._inval_cache(
                pdfonly=True, formsemestre_id=formsemestre_id
            )  # > appreciation_add
            return REQUEST.RESPONSE.redirect(bull_url)

    def _can_edit_pv(self, REQUEST, formsemestre_id):
        "Vrai si utilisateur peut editer un PV de jury de ce semestre"

        sem = sco_formsemestre.get_formsemestre(self, formsemestre_id)
        if self._is_chef_or_diretud(REQUEST, sem):
            return True
        # Autorise les secrétariats, repérés via la permission ScoEtudChangeAdr
        # (ceci nous évite d'ajouter une permission Zope aux installations existantes)
        authuser = REQUEST.AUTHENTICATED_USER
        return authuser.has_permission(ScoEtudChangeAdr, self)

    # --- FORMULAIRE POUR VALIDATION DES UE ET SEMESTRES
    def _can_validate_sem(self, REQUEST, formsemestre_id):
        "Vrai si utilisateur peut saisir decision de jury dans ce semestre"
        sem = sco_formsemestre.get_formsemestre(self, formsemestre_id)
        if sem["etat"] != "1":
            return False  # semestre verrouillé

        return self._is_chef_or_diretud(REQUEST, sem)

    def _is_chef_or_diretud(self, REQUEST, sem):
        "Vrai si utilisateur est admin, chef dept ou responsable du semestre"
        authuser = REQUEST.AUTHENTICATED_USER
        if authuser.has_permission(ScoImplement, self):
            return True  # admin, chef dept
        uid = str(authuser)
        if uid in sem["responsables"]:
            return True

        return False

    security.declareProtected(ScoView, "formsemestre_validation_etud_form")

    def formsemestre_validation_etud_form(
        self,
        formsemestre_id,
        etudid=None,
        etud_index=None,
        check=0,
        desturl="",
        sortcol=None,
        REQUEST=None,
    ):
        "Formulaire choix jury pour un étudiant"
        readonly = not self._can_validate_sem(REQUEST, formsemestre_id)
        return sco_formsemestre_validation.formsemestre_validation_etud_form(
            self,
            formsemestre_id,
            etudid=etudid,
            etud_index=etud_index,
            check=check,
            readonly=readonly,
            desturl=desturl,
            sortcol=sortcol,
            REQUEST=REQUEST,
        )

    security.declareProtected(ScoView, "formsemestre_validation_etud")

    def formsemestre_validation_etud(
        self,
        formsemestre_id,
        etudid=None,
        codechoice=None,
        desturl="",
        sortcol=None,
        REQUEST=None,
    ):
        "Enregistre choix jury pour un étudiant"
        if not self._can_validate_sem(REQUEST, formsemestre_id):
            return self.confirmDialog(
                message="<p>Opération non autorisée pour %s</h2>"
                % REQUEST.AUTHENTICATED_USER,
                dest_url=self.ScoURL(),
                REQUEST=REQUEST,
            )

        return sco_formsemestre_validation.formsemestre_validation_etud(
            self,
            formsemestre_id,
            etudid=etudid,
            codechoice=codechoice,
            desturl=desturl,
            sortcol=sortcol,
            REQUEST=REQUEST,
        )

    security.declareProtected(ScoView, "formsemestre_validation_etud_manu")

    def formsemestre_validation_etud_manu(
        self,
        formsemestre_id,
        etudid=None,
        code_etat="",
        new_code_prev="",
        devenir="",
        assidu=False,
        desturl="",
        sortcol=None,
        REQUEST=None,
    ):
        "Enregistre choix jury pour un étudiant"
        if not self._can_validate_sem(REQUEST, formsemestre_id):
            return self.confirmDialog(
                message="<p>Opération non autorisée pour %s</h2>"
                % REQUEST.AUTHENTICATED_USER,
                dest_url=self.ScoURL(),
                REQUEST=REQUEST,
            )

        return sco_formsemestre_validation.formsemestre_validation_etud_manu(
            self,
            formsemestre_id,
            etudid=etudid,
            code_etat=code_etat,
            new_code_prev=new_code_prev,
            devenir=devenir,
            assidu=assidu,
            desturl=desturl,
            sortcol=sortcol,
            REQUEST=REQUEST,
        )

    security.declareProtected(ScoView, "formsemestre_validate_previous_ue")

    def formsemestre_validate_previous_ue(
        self, formsemestre_id, etudid=None, REQUEST=None
    ):
        "Form. saisie UE validée hors ScoDoc "
        if not self._can_validate_sem(REQUEST, formsemestre_id):
            return self.confirmDialog(
                message="<p>Opération non autorisée pour %s</h2>"
                % REQUEST.AUTHENTICATED_USER,
                dest_url=self.ScoURL(),
                REQUEST=REQUEST,
            )
        return sco_formsemestre_validation.formsemestre_validate_previous_ue(
            self, formsemestre_id, etudid, REQUEST=REQUEST
        )

    security.declareProtected(ScoView, "formsemestre_ext_create_form")
    formsemestre_ext_create_form = (
        sco_formsemestre_exterieurs.formsemestre_ext_create_form
    )

    security.declareProtected(ScoView, "formsemestre_ext_edit_ue_validations")

    def formsemestre_ext_edit_ue_validations(
        self, formsemestre_id, etudid=None, REQUEST=None
    ):
        "Form. edition UE semestre extérieur"
        if not self._can_validate_sem(REQUEST, formsemestre_id):
            return self.confirmDialog(
                message="<p>Opération non autorisée pour %s</h2>"
                % REQUEST.AUTHENTICATED_USER,
                dest_url=self.ScoURL(),
                REQUEST=REQUEST,
            )
        return sco_formsemestre_exterieurs.formsemestre_ext_edit_ue_validations(
            self, formsemestre_id, etudid, REQUEST=REQUEST
        )

    security.declareProtected(ScoView, "get_etud_ue_cap_html")
    get_etud_ue_cap_html = sco_formsemestre_validation.get_etud_ue_cap_html

    security.declareProtected(ScoView, "etud_ue_suppress_validation")

    def etud_ue_suppress_validation(self, etudid, formsemestre_id, ue_id, REQUEST=None):
        """Suppress a validation (ue_id, etudid) and redirect to formsemestre"""
        if not self._can_validate_sem(REQUEST, formsemestre_id):
            return self.confirmDialog(
                message="<p>Opération non autorisée pour %s</h2>"
                % REQUEST.AUTHENTICATED_USER,
                dest_url=self.ScoURL(),
                REQUEST=REQUEST,
            )
        return sco_formsemestre_validation.etud_ue_suppress_validation(
            self, etudid, formsemestre_id, ue_id, REQUEST=REQUEST
        )

    security.declareProtected(ScoView, "formsemestre_validation_auto")

    def formsemestre_validation_auto(self, formsemestre_id, REQUEST):
        "Formulaire saisie automatisee des decisions d'un semestre"
        if not self._can_validate_sem(REQUEST, formsemestre_id):
            return self.confirmDialog(
                message="<p>Opération non autorisée pour %s</h2>"
                % REQUEST.AUTHENTICATED_USER,
                dest_url=self.ScoURL(),
                REQUEST=REQUEST,
            )

        return sco_formsemestre_validation.formsemestre_validation_auto(
            self, formsemestre_id, REQUEST
        )

    security.declareProtected(ScoView, "do_formsemestre_validation_auto")

    def do_formsemestre_validation_auto(self, formsemestre_id, REQUEST):
        "Formulaire saisie automatisee des decisions d'un semestre"
        if not self._can_validate_sem(REQUEST, formsemestre_id):
            return self.confirmDialog(
                message="<p>Opération non autorisée pour %s</h2>"
                % REQUEST.AUTHENTICATED_USER,
                dest_url=self.ScoURL(),
                REQUEST=REQUEST,
            )

        return sco_formsemestre_validation.do_formsemestre_validation_auto(
            self, formsemestre_id, REQUEST
        )

    security.declareProtected(ScoView, "formsemestre_fix_validation_ues")

    def formsemestre_fix_validation_ues(self, formsemestre_id, REQUEST=None):
        "Verif/reparation codes UE"
        if not self._can_validate_sem(REQUEST, formsemestre_id):
            return self.confirmDialog(
                message="<p>Opération non autorisée pour %s</h2>"
                % REQUEST.AUTHENTICATED_USER,
                dest_url=self.ScoURL(),
                REQUEST=REQUEST,
            )

        return sco_formsemestre_validation.formsemestre_fix_validation_ues(
            self, formsemestre_id, REQUEST
        )

    security.declareProtected(ScoView, "formsemestre_validation_suppress_etud")

    def formsemestre_validation_suppress_etud(
        self, formsemestre_id, etudid, REQUEST=None, dialog_confirmed=False
    ):
        """Suppression des decisions de jury pour un etudiant."""
        if not self._can_validate_sem(REQUEST, formsemestre_id):
            return self.confirmDialog(
                message="<p>Opération non autorisée pour %s</h2>"
                % REQUEST.AUTHENTICATED_USER,
                dest_url=self.ScoURL(),
                REQUEST=REQUEST,
            )
        if not dialog_confirmed:
            sem = sco_formsemestre.get_formsemestre(self, formsemestre_id)
            etud = self.getEtudInfo(etudid=etudid, filled=1)[0]
            nt = self._getNotesCache().get_NotesTable(
                self, formsemestre_id
            )  # > get_etud_decision_sem
            decision_jury = nt.get_etud_decision_sem(etudid)
            if decision_jury:
                existing = (
                    "<p>Décision existante: %(code)s du %(event_date)s</p>"
                    % decision_jury
                )
            else:
                existing = ""
            return self.confirmDialog(
                """<h2>Confirmer la suppression des décisions du semestre %s (%s - %s) pour %s ?</h2>%s
                <p>Cette opération est irréversible.
                </p>
                """
                % (
                    sem["titre_num"],
                    sem["date_debut"],
                    sem["date_fin"],
                    etud["nomprenom"],
                    existing,
                ),
                OK="Supprimer",
                dest_url="",
                REQUEST=REQUEST,
                cancel_url="formsemestre_validation_etud_form?formsemestre_id=%s&amp;etudid=%s"
                % (formsemestre_id, etudid),
                parameters={"etudid": etudid, "formsemestre_id": formsemestre_id},
            )

        sco_formsemestre_validation.formsemestre_validation_suppress_etud(
            self, formsemestre_id, etudid
        )
        return REQUEST.RESPONSE.redirect(
            self.ScoURL()
            + "/Notes/formsemestre_validation_etud_form?formsemestre_id=%s&amp;etudid=%s&amp;head_message=Décision%%20supprimée"
            % (formsemestre_id, etudid)
        )

    # ------------- PV de JURY et archives
    security.declareProtected(ScoView, "formsemestre_pvjury")
    formsemestre_pvjury = sco_pvjury.formsemestre_pvjury

    security.declareProtected(ScoView, "formsemestre_lettres_individuelles")
    formsemestre_lettres_individuelles = sco_pvjury.formsemestre_lettres_individuelles
    security.declareProtected(ScoView, "formsemestre_pvjury_pdf")
    formsemestre_pvjury_pdf = sco_pvjury.formsemestre_pvjury_pdf

    security.declareProtected(ScoView, "feuille_preparation_jury")
    feuille_preparation_jury = sco_prepajury.feuille_preparation_jury

    security.declareProtected(ScoView, "formsemestre_archive")
    formsemestre_archive = sco_archives.formsemestre_archive

    security.declareProtected(ScoView, "formsemestre_delete_archive")
    formsemestre_delete_archive = sco_archives.formsemestre_delete_archive

    security.declareProtected(ScoView, "formsemestre_list_archives")
    formsemestre_list_archives = sco_archives.formsemestre_list_archives

    security.declareProtected(ScoView, "formsemestre_get_archived_file")
    formsemestre_get_archived_file = sco_archives.formsemestre_get_archived_file

    security.declareProtected(ScoEditApo, "view_apo_csv")
    view_apo_csv = sco_etape_apogee_view.view_apo_csv

    security.declareProtected(ScoEditApo, "view_apo_csv_store")
    view_apo_csv_store = sco_etape_apogee_view.view_apo_csv_store

    security.declareProtected(ScoEditApo, "view_apo_csv_download_and_store")
    view_apo_csv_download_and_store = (
        sco_etape_apogee_view.view_apo_csv_download_and_store
    )

    security.declareProtected(ScoEditApo, "view_apo_csv_delete")
    view_apo_csv_delete = sco_etape_apogee_view.view_apo_csv_delete

    security.declareProtected(ScoEditApo, "view_scodoc_etuds")
    view_scodoc_etuds = sco_etape_apogee_view.view_scodoc_etuds

    security.declareProtected(ScoEditApo, "view_apo_etuds")
    view_apo_etuds = sco_etape_apogee_view.view_apo_etuds

    security.declareProtected(ScoEditApo, "apo_semset_maq_status")
    apo_semset_maq_status = sco_etape_apogee_view.apo_semset_maq_status

    security.declareProtected(ScoEditApo, "apo_csv_export_results")
    apo_csv_export_results = sco_etape_apogee_view.apo_csv_export_results

    # sco_semset
    security.declareProtected(ScoEditApo, "semset_page")
    semset_page = sco_semset.semset_page

    security.declareProtected(ScoEditApo, "do_semset_create")
    do_semset_create = sco_semset.do_semset_create

    security.declareProtected(ScoEditApo, "do_semset_delete")
    do_semset_delete = sco_semset.do_semset_delete

    security.declareProtected(ScoEditApo, "edit_semset_set_title")
    edit_semset_set_title = sco_semset.edit_semset_set_title

    security.declareProtected(ScoEditApo, "do_semset_add_sem")
    do_semset_add_sem = sco_semset.do_semset_add_sem

    security.declareProtected(ScoEditApo, "do_semset_remove_sem")
    do_semset_remove_sem = sco_semset.do_semset_remove_sem

    # sco_export_result
    security.declareProtected(ScoEditApo, "scodoc_table_results")
    scodoc_table_results = sco_export_results.scodoc_table_results

    security.declareProtected(ScoView, "apo_compare_csv_form")
    apo_compare_csv_form = sco_apogee_compare.apo_compare_csv_form

    security.declareProtected(ScoView, "apo_compare_csv")
    apo_compare_csv = sco_apogee_compare.apo_compare_csv

    # ------------- INSCRIPTIONS: PASSAGE D'UN SEMESTRE A UN AUTRE
    security.declareProtected(ScoEtudInscrit, "formsemestre_inscr_passage")
    formsemestre_inscr_passage = sco_inscr_passage.formsemestre_inscr_passage

    security.declareProtected(ScoView, "formsemestre_synchro_etuds")
    formsemestre_synchro_etuds = sco_synchro_etuds.formsemestre_synchro_etuds

    # ------------- RAPPORTS STATISTIQUES
    security.declareProtected(ScoView, "formsemestre_report_counts")
    formsemestre_report_counts = sco_report.formsemestre_report_counts

    security.declareProtected(ScoView, "formsemestre_suivi_cohorte")
    formsemestre_suivi_cohorte = sco_report.formsemestre_suivi_cohorte

    security.declareProtected(ScoView, "formsemestre_suivi_parcours")
    formsemestre_suivi_parcours = sco_report.formsemestre_suivi_parcours

    security.declareProtected(ScoView, "formsemestre_etuds_lycees")
    formsemestre_etuds_lycees = sco_lycee.formsemestre_etuds_lycees

    security.declareProtected(ScoView, "scodoc_table_etuds_lycees")
    scodoc_table_etuds_lycees = sco_lycee.scodoc_table_etuds_lycees

    security.declareProtected(ScoView, "formsemestre_graph_parcours")
    formsemestre_graph_parcours = sco_report.formsemestre_graph_parcours

    security.declareProtected(ScoView, "formsemestre_poursuite_report")
    formsemestre_poursuite_report = sco_poursuite_dut.formsemestre_poursuite_report

    security.declareProtected(ScoView, "pe_view_sem_recap")
    pe_view_sem_recap = pe_view.pe_view_sem_recap

    security.declareProtected(ScoView, "report_debouche_date")
    report_debouche_date = sco_debouche.report_debouche_date

    security.declareProtected(ScoView, "formsemestre_estim_cost")
    formsemestre_estim_cost = sco_cost_formation.formsemestre_estim_cost

    # --------------------------------------------------------------------
    # DEBUG
    security.declareProtected(ScoView, "check_sem_integrity")

    def check_sem_integrity(self, formsemestre_id, REQUEST):
        """Debug.
        Check that ue and module formations are consistents
        """
        sem = sco_formsemestre.get_formsemestre(self, formsemestre_id)

        modimpls = sco_moduleimpl.do_moduleimpl_list(
            self, formsemestre_id=formsemestre_id
        )
        bad_ue = []
        bad_sem = []
        for modimpl in modimpls:
            mod = self.do_module_list({"module_id": modimpl["module_id"]})[0]
            ue = self.do_ue_list({"ue_id": mod["ue_id"]})[0]
            if ue["formation_id"] != mod["formation_id"]:
                modimpl["mod"] = mod
                modimpl["ue"] = ue
                bad_ue.append(modimpl)
            if sem["formation_id"] != mod["formation_id"]:
                bad_sem.append(modimpl)
                modimpl["mod"] = mod

        return (
            self.sco_header(REQUEST=REQUEST)
            + "<p>formation_id=%s" % sem["formation_id"]
            + "<h2>Inconsistent UE/MOD:</h2>"
            + "<br/>".join([str(x) for x in bad_ue])
            + "<h2>Inconsistent SEM/MOD:</h2>"
            + "<br/>".join([str(x) for x in bad_sem])
            + self.sco_footer(REQUEST)
        )

    security.declareProtected(ScoView, "check_form_integrity")

    def check_form_integrity(self, formation_id, fix=False, REQUEST=None):
        "debug"
        log("check_form_integrity: formation_id=%s  fix=%s" % (formation_id, fix))
        ues = self.do_ue_list(args={"formation_id": formation_id})
        bad = []
        for ue in ues:
            mats = self.do_matiere_list(args={"ue_id": ue["ue_id"]})
            for mat in mats:
                mods = self.do_module_list({"matiere_id": mat["matiere_id"]})
                for mod in mods:
                    if mod["ue_id"] != ue["ue_id"]:
                        if fix:
                            # fix mod.ue_id
                            log(
                                "fix: mod.ue_id = %s (was %s)"
                                % (ue["ue_id"], mod["ue_id"])
                            )
                            mod["ue_id"] = ue["ue_id"]
                            self.do_module_edit(mod)
                        bad.append(mod)
                    if mod["formation_id"] != formation_id:
                        bad.append(mod)
        if bad:
            txth = "<br/>".join([str(x) for x in bad])
            txt = "\n".join([str(x) for x in bad])
            log(
                "check_form_integrity: formation_id=%s\ninconsistencies:" % formation_id
            )
            log(txt)
            # Notify by e-mail
            sendAlarm(self, "Notes: formation incoherente !", txt)
        else:
            txth = "OK"
            log("ok")
        return self.sco_header(REQUEST=REQUEST) + txth + self.sco_footer(REQUEST)

    security.declareProtected(ScoView, "check_formsemestre_integrity")

    def check_formsemestre_integrity(self, formsemestre_id, REQUEST=None):
        "debug"
        log("check_formsemestre_integrity: formsemestre_id=%s" % (formsemestre_id))
        # verifie que tous les moduleimpl d'un formsemestre
        # se réfèrent à un module dont l'UE appartient a la même formation
        # Ancien bug: les ue_id étaient mal copiés lors des création de versions
        # de formations
        diag = []

        Mlist = sco_moduleimpl.do_moduleimpl_withmodule_list(
            self, formsemestre_id=formsemestre_id
        )
        for mod in Mlist:
            if mod["module"]["ue_id"] != mod["matiere"]["ue_id"]:
                diag.append(
                    "moduleimpl %s: module.ue_id=%s != matiere.ue_id=%s"
                    % (
                        mod["moduleimpl_id"],
                        mod["module"]["ue_id"],
                        mod["matiere"]["ue_id"],
                    )
                )
            if mod["ue"]["formation_id"] != mod["module"]["formation_id"]:
                diag.append(
                    "moduleimpl %s: ue.formation_id=%s != mod.formation_id=%s"
                    % (
                        mod["moduleimpl_id"],
                        mod["ue"]["formation_id"],
                        mod["module"]["formation_id"],
                    )
                )
        if diag:
            sendAlarm(
                self,
                "Notes: formation incoherente dans semestre %s !" % formsemestre_id,
                "\n".join(diag),
            )
            log("check_formsemestre_integrity: formsemestre_id=%s" % formsemestre_id)
            log("inconsistencies:\n" + "\n".join(diag))
        else:
            diag = ["OK"]
            log("ok")
        return (
            self.sco_header(REQUEST=REQUEST)
            + "<br/>".join(diag)
            + self.sco_footer(REQUEST)
        )

    security.declareProtected(ScoView, "check_integrity_all")

    def check_integrity_all(self, REQUEST=None):
        "debug: verifie tous les semestres et tt les formations"
        # formations
        for F in self.formation_list():
            self.check_form_integrity(F["formation_id"], REQUEST=REQUEST)
        # semestres
        for sem in sco_formsemestre.do_formsemestre_list(self):
            self.check_formsemestre_integrity(sem["formsemestre_id"], REQUEST=REQUEST)
        return (
            self.sco_header(REQUEST=REQUEST)
            + "<p>empty page: see logs and mails</p>"
            + self.sco_footer(REQUEST)
        )

    # --------------------------------------------------------------------
    #     Support for legacy ScoDoc 7 API
    # --------------------------------------------------------------------
    security.declareProtected(ScoView, "do_moduleimpl_list")
    do_moduleimpl_list = sco_moduleimpl.do_moduleimpl_list

    security.declareProtected(ScoView, "do_moduleimpl_withmodule_list")
    do_moduleimpl_withmodule_list = sco_moduleimpl.do_moduleimpl_withmodule_list
