# -*- mode: python -*- # -*- coding: utf-8 -*- ############################################################################## # # Gestion scolarite IUT # # Copyright (c) 1999 - 2022 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 # ############################################################################## """Ajout/Modification/Suppression modules (portage from DTML) """ import flask from flask import url_for, render_template from flask import g, request from flask_login import current_user from app.models import APO_CODE_STR_LEN from app.models import Matiere, Module, UniteEns import app.scodoc.notesdb as ndb import app.scodoc.sco_utils as scu from app.scodoc.sco_utils import ModuleType from app import log from app import models from app.models import Formation from app.scodoc.TrivialFormulator import TrivialFormulator from app.scodoc.sco_permissions import Permission from app.scodoc.sco_exceptions import ScoValueError, ScoLockedFormError, ScoGenError from app.scodoc import html_sco_header from app.scodoc import sco_codes_parcours from app.scodoc import sco_edit_matiere from app.scodoc import sco_moduleimpl from app.scodoc import sco_news _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 }, ) def module_list(*args, **kw): "list modules" cnx = ndb.GetDBConnexion() return _moduleEditor.list(cnx, *args, **kw) def do_module_create(args) -> int: "Create a module. Returns id of new object." # create from app.scodoc import sco_formations cnx = ndb.GetDBConnexion() r = _moduleEditor.create(cnx, args) # news F = sco_formations.formation_list(args={"formation_id": args["formation_id"]})[0] sco_news.add( typ=sco_news.NEWS_FORM, object=args["formation_id"], text="Modification de la formation %(acronyme)s" % F, max_frequency=3, ) return r def module_create(matiere_id=None, module_type=None, semestre_id=None): """Création d'un module""" from app.scodoc import sco_formations from app.scodoc import sco_edit_ue matiere = Matiere.query.get_or_404(matiere_id) if matiere is None: raise ScoValueError("invalid matiere !") ue = matiere.ue parcours = ue.formation.get_parcours() is_apc = parcours.APC_SAE ues = ue.formation.ues.order_by( UniteEns.semestre_idx, UniteEns.numero, UniteEns.acronyme ).all() # cherche le numero adéquat (pour placer le module en fin de liste) modules = matiere.ue.formation.modules.all() if modules: default_num = max([m.numero or 0 for m in modules]) + 10 else: default_num = 10 if is_apc and module_type is not None: object_name = scu.MODULE_TYPE_NAMES[module_type] else: object_name = "Module" H = [ html_sco_header.sco_header(page_title=f"Création {object_name}"), ] if is_apc: H += [ f"""

Création {object_name} dans la formation {ue.formation.acronyme}

""" ] else: H += [ f"""

Création {object_name} dans la matière {matiere.titre}, (UE {ue.acronyme})

""" ] H += [ render_template( "scodoc/help/modules.html", is_apc=is_apc, ue=ue, semestre_id=semestre_id, ) ] descr = [ ( "code", { "size": 10, "explanation": "code du module, ressource ou SAÉ. Exemple M1203, R2.01, ou SAÉ 3.4. Ce code doit être unique dans la formation.", "allow_null": False, "validator": lambda val, field, formation_id=ue.formation_id: check_module_code_unicity( val, field, formation_id ), }, ), ( "titre", { "size": 30, "explanation": "nom du module. Exemple: Introduction à la démarche ergonomique", }, ), ( "abbrev", { "size": 20, "explanation": "nom abrégé (pour les bulletins). Exemple: Intro. à l'ergonomie", }, ), ] semestres_indices = list(range(1, parcours.NB_SEM + 1)) if is_apc: # BUT: choix de l'UE de rattachement (qui donnera le semestre) descr += [ ( "ue_id", { "input_type": "menu", "type": "int", "title": "UE de rattachement", "explanation": "utilisée pour la présentation dans certains documents", "labels": [f"{u.acronyme} {u.titre}" for u in ues], "allowed_values": [u.id for u in ues], }, ), ] else: # Formations classiques: choix du semestre descr += [ ( "semestre_id", { "input_type": "menu", "type": "int", "title": parcours.SESSION_NAME.capitalize(), "explanation": "%s du module" % parcours.SESSION_NAME, "labels": [str(x) for x in semestres_indices], "allowed_values": semestres_indices, }, ), ] descr += [ ( "module_type", { "input_type": "menu", "title": "Type", "explanation": "", "labels": [x.name.capitalize() for x in scu.ModuleType], "allowed_values": [str(int(x)) for x in scu.ModuleType], }, ), ( "heures_cours", { "title": "Heures de cours", "size": 4, "type": "float", "explanation": "nombre d'heures de cours (optionnel)", }, ), ( "heures_td", { "title": "Heures de TD", "size": 4, "type": "float", "explanation": "nombre d'heures de Travaux Dirigés (optionnel)", }, ), ( "heures_tp", { "title": "Heures de TP", "size": 4, "type": "float", "explanation": "nombre d'heures de Travaux Pratiques (optionnel)", }, ), ] if is_apc: descr += [ ( "sep_ue_coefs", { "input_type": "separator", "title": """
(les coefficients vers les UE se fixent sur la page dédiée)
""", }, ), ] else: descr += [ ( "coefficient", { "size": 4, "type": "float", "explanation": "coefficient dans la formation (PPN)", "allow_null": False, }, ), ] descr += [ # ('ects', { 'size' : 4, 'type' : 'float', 'title' : 'ECTS', 'explanation' : 'nombre de crédits ECTS (inutilisés: les crédits sont associés aux UE)' }), ("formation_id", {"default": ue.formation_id, "input_type": "hidden"}), ("ue_id", {"default": ue.id, "input_type": "hidden"}), ("matiere_id", {"default": matiere.id, "input_type": "hidden"}), ( "code_apogee", { "title": "Code Apogée", "size": 25, "explanation": "(optionnel) code élément pédagogique Apogée ou liste de codes ELP séparés par des virgules", }, ), ( "numero", { "size": 2, "explanation": "numéro (1,2,3,4...) pour ordre d'affichage", "type": "int", "default": default_num, }, ), ] args = scu.get_request_args() tf = TrivialFormulator( request.base_url, args, descr, submitlabel="Créer ce module", ) if tf[0] == 0: return "\n".join(H) + tf[1] + html_sco_header.sco_footer() else: if is_apc: # BUT: l'UE indique le semestre selected_ue = UniteEns.query.get(tf[2]["ue_id"]) if selected_ue is None: raise ValueError("UE invalide") tf[2]["semestre_id"] = selected_ue.semestre_idx _ = do_module_create(tf[2]) return flask.redirect( url_for( "notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=ue.formation_id, semestre_idx=tf[2]["semestre_id"], ) ) def do_module_delete(oid): "delete module" from app.scodoc import sco_formations mod = module_list({"module_id": oid})[0] if module_is_locked(mod["module_id"]): raise ScoLockedFormError() # S'il y a des moduleimpls, on ne peut pas detruire le module ! mods = sco_moduleimpl.moduleimpl_list(module_id=oid) if mods: err_page = f"""

Destruction du module impossible car il est utilisé dans des semestres existants !

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.

reprendre """ raise ScoGenError(err_page) # delete cnx = ndb.GetDBConnexion() _moduleEditor.delete(cnx, oid) # news F = sco_formations.formation_list(args={"formation_id": mod["formation_id"]})[0] sco_news.add( typ=sco_news.NEWS_FORM, object=mod["formation_id"], text="Modification de la formation %(acronyme)s" % F, max_frequency=3, ) def module_delete(module_id=None): """Delete a module""" if not module_id: raise ScoValueError("invalid module !") modules = module_list(args={"module_id": module_id}) if not modules: raise ScoValueError("Module inexistant !") mod = modules[0] H = [ html_sco_header.sco_header(page_title="Suppression d'un module"), """

Suppression du module %(titre)s (%(code)s)

""" % mod, ] dest_url = url_for( "notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=str(mod["formation_id"]), ) tf = TrivialFormulator( request.base_url, scu.get_request_args(), (("module_id", {"input_type": "hidden"}),), initvalues=mod, submitlabel="Confirmer la suppression", cancelbutton="Annuler", ) if tf[0] == 0: return "\n".join(H) + tf[1] + html_sco_header.sco_footer() elif tf[0] == -1: return flask.redirect(dest_url) else: do_module_delete(module_id) return flask.redirect(dest_url) def do_module_edit(vals: dict) -> None: "edit a module" from app.scodoc import sco_edit_formation # check mod = module_list({"module_id": vals["module_id"]})[0] if 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 vals: del vals[f] # edit cnx = ndb.GetDBConnexion() _moduleEditor.edit(cnx, vals) Formation.query.get(mod["formation_id"]).invalidate_cached_sems() def check_module_code_unicity(code, field, formation_id, module_id=None): "true si code module unique dans la formation" Mods = module_list(args={"code": code, "formation_id": formation_id}) if module_id: # edition: supprime le module en cours Mods = [m for m in Mods if m["module_id"] != module_id] return len(Mods) == 0 def module_edit(module_id=None): """Edit a module""" from app.scodoc import sco_formations from app.scodoc import sco_tag_module if not module_id: raise ScoValueError("invalid module !") modules = module_list(args={"module_id": module_id}) if not modules: raise ScoValueError("invalid module !") module = modules[0] a_module = models.Module.query.get(module_id) unlocked = not module_is_locked(module_id) formation_id = module["formation_id"] formation = sco_formations.formation_list(args={"formation_id": formation_id})[0] parcours = sco_codes_parcours.get_parcours_from_code(formation["type_parcours"]) is_apc = parcours.APC_SAE ues_matieres = ndb.SimpleDictFetch( """SELECT ue.acronyme, mat.*, mat.id AS matiere_id FROM notes_matieres mat, notes_ue ue WHERE mat.ue_id = ue.id AND ue.formation_id = %(formation_id)s ORDER BY ue.numero, mat.numero """, {"formation_id": formation_id}, ) mat_names = ["%s / %s" % (x["acronyme"], x["titre"]) for x in ues_matieres] ue_mat_ids = ["%s!%s" % (x["ue_id"], x["matiere_id"]) for x in ues_matieres] module["ue_matiere_id"] = "%s!%s" % (module["ue_id"], module["matiere_id"]) semestres_indices = list(range(1, parcours.NB_SEM + 1)) H = [ html_sco_header.sco_header( page_title="Modification du module %(titre)s" % module, cssstyles=["libjs/jQuery-tagEditor/jquery.tag-editor.css"], javascripts=[ "libjs/jQuery-tagEditor/jquery.tag-editor.min.js", "libjs/jQuery-tagEditor/jquery.caret.min.js", "js/module_tag_editor.js", ], ), """

Modification du module %(titre)s""" % module, """ (formation %(acronyme)s, version %(version)s)

""" % formation, render_template("scodoc/help/modules.html", is_apc=is_apc), ] if not unlocked: H.append( """
Formation verrouillée, seuls certains éléments peuvent être modifiés
""" ) descr = [ ( "code", { "size": 10, "explanation": "code du module (doit être unique dans la formation)", "allow_null": False, "validator": lambda val, field, formation_id=formation_id: check_module_code_unicity( val, field, formation_id, module_id=module_id ), }, ), ("titre", {"size": 30, "explanation": "nom du module"}), ("abbrev", {"size": 20, "explanation": "nom abrégé (pour bulletins)"}), ( "module_type", { "input_type": "menu", "title": "Type", "explanation": "", "labels": [x.name.capitalize() for x in scu.ModuleType], "allowed_values": [str(int(x)) for x in scu.ModuleType], "enabled": unlocked, }, ), ( "heures_cours", {"size": 4, "type": "float", "explanation": "nombre d'heures de cours"}, ), ( "heures_td", { "size": 4, "type": "float", "explanation": "nombre d'heures de Travaux Dirigés", }, ), ( "heures_tp", { "size": 4, "type": "float", "explanation": "nombre d'heures de Travaux Pratiques", }, ), ] if is_apc: coefs_descr = a_module.ue_coefs_descr() if coefs_descr: coefs_descr_txt = ", ".join(["%s: %s" % x for x in coefs_descr]) else: coefs_descr_txt = """non définis""" descr += [ ( "ue_coefs", { "readonly": True, "title": "Coefficients vers les UE", "default": coefs_descr_txt, "explanation": "passer par la page d'édition de la formation pour modifier les coefficients", }, ) ] else: # Module classique avec coef scalaire: descr += [ ( "coefficient", { "size": 4, "type": "float", "explanation": "coefficient dans la formation (PPN)", "allow_null": False, "enabled": unlocked, }, ), ] descr += [ ("formation_id", {"input_type": "hidden"}), ("ue_id", {"input_type": "hidden"}), ("module_id", {"input_type": "hidden"}), ( "ue_matiere_id", { "input_type": "menu", "title": "Rattachement :" if is_apc else "Matière :", "explanation": "UE de rattachement, utilisée pour la présentation" if is_apc else "un module appartient à une seule matière.", "labels": mat_names, "allowed_values": ue_mat_ids, "enabled": unlocked, }, ), ] if is_apc: # le semestre du module est toujours celui de son UE descr += [ ( "semestre_id", { "input_type": "hidden", "type": "int", "readonly": True, }, ) ] else: descr += [ ( "semestre_id", { "input_type": "menu", "type": "int", "title": parcours.SESSION_NAME.capitalize(), "explanation": "%s de début du module dans la formation standard" % parcours.SESSION_NAME, "labels": [str(x) for x in semestres_indices], "allowed_values": semestres_indices, "enabled": unlocked, }, ) ] descr += [ ( "code_apogee", { "title": "Code Apogée", "size": 25, "explanation": "(optionnel) code élément pédagogique Apogée ou liste de codes ELP séparés par des virgules", "validator": lambda val, _: len(val) < APO_CODE_STR_LEN, }, ), ( "numero", { "size": 2, "explanation": "numéro (1,2,3,4...) pour ordre d'affichage", "type": "int", }, ), ] # force module semestre_idx to its UE if a_module.ue.semestre_idx: module["semestre_id"] = a_module.ue.semestre_idx # Filet de sécurité si jamais l'UE n'a pas non plus de semestre: if not module["semestre_id"]: module["semestre_id"] = 1 tf = TrivialFormulator( request.base_url, scu.get_request_args(), descr, html_foot_markup="""
""".format( module_id, ",".join(sco_tag_module.module_tag_list(module_id)) ), initvalues=module, submitlabel="Modifier ce module", ) if tf[0] == 0: return "\n".join(H) + tf[1] + html_sco_header.sco_footer() elif tf[0] == -1: return flask.redirect( url_for( "notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=formation_id, semestre_idx=module["semestre_id"], ) ) else: # l'UE peut changer tf[2]["ue_id"], tf[2]["matiere_id"] = tf[2]["ue_matiere_id"].split("!") # En APC, force le semestre égal à celui de l'UE if is_apc: selected_ue = UniteEns.query.get(tf[2]["ue_id"]) if selected_ue is None: raise ValueError("UE invalide") tf[2]["semestre_id"] = selected_ue.semestre_idx # Check unicité code module dans la formation do_module_edit(tf[2]) return flask.redirect( url_for( "notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=formation_id, semestre_idx=tf[2]["semestre_id"], ) ) # Edition en ligne du code Apogee def edit_module_set_code_apogee(id=None, value=None): "Set UE code apogee" module_id = id value = str(value).strip("-_ \t") log("edit_module_set_code_apogee: module_id=%s code_apogee=%s" % (module_id, value)) modules = module_list(args={"module_id": module_id}) if not modules: return "module invalide" # should not occur do_module_edit({"module_id": module_id, "code_apogee": value}) if not value: value = scu.APO_MISSING_CODE_STR return value def module_table(formation_id): """Liste des modules de la formation (XXX inutile ou a revoir) """ from app.scodoc import sco_formations if not formation_id: raise ScoValueError("invalid formation !") F = sco_formations.formation_list(args={"formation_id": formation_id})[0] H = [ html_sco_header.sco_header(page_title="Liste des modules de %(titre)s" % F), """

Listes des modules dans la formation %(titre)s (%(acronyme)s)

""" % F, '") H.append(html_sco_header.sco_footer()) return "\n".join(H) def module_is_locked(module_id): """True if module should not be modified (used in a locked formsemestre) """ r = ndb.SimpleDictFetch( """SELECT mi.id FROM notes_modules mod, notes_formsemestre sem, notes_moduleimpl mi WHERE mi.module_id = mod.id AND mi.formsemestre_id = sem.id AND mi.module_id = %(module_id)s AND sem.etat = false """, {"module_id": module_id}, ) return len(r) > 0 def module_count_moduleimpls(module_id): "Number of moduleimpls using this module" mods = sco_moduleimpl.moduleimpl_list(module_id=module_id) return len(mods) def formation_add_malus_modules(formation_id, titre=None, redirect=True): """Création d'un module de "malus" dans chaque UE d'une formation""" from app.scodoc import sco_edit_ue ues = sco_edit_ue.ue_list(args={"formation_id": formation_id}) for ue in ues: # Un seul module de malus par UE: nb_mod_malus = len( [ mod for mod in module_list(args={"ue_id": ue["ue_id"]}) if mod["module_type"] == ModuleType.MALUS ] ) if nb_mod_malus == 0: ue_add_malus_module(ue["ue_id"], titre=titre) if redirect: return flask.redirect( url_for( "notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=formation_id ) ) def ue_add_malus_module(ue_id, titre=None, code=None): """Add a malus module in this ue""" from app.scodoc import sco_edit_ue ue = sco_edit_ue.ue_list(args={"ue_id": ue_id})[0] if titre is None: titre = "" if code is None: code = "MALUS%d" % ue["numero"] # Tout module doit avoir un semestre_id (indice 1, 2, ...) semestre_ids = sco_edit_ue.ue_list_semestre_ids(ue) if semestre_ids: semestre_id = semestre_ids[0] else: # c'est ennuyeux: dans ce cas, on pourrait demander à indiquer explicitement # le semestre ? ou affecter le malus au semestre 1 ??? raise ScoValueError( "Impossible d'ajouter un malus s'il n'y a pas d'autres modules" ) # Matiere pour placer le module malus Matlist = sco_edit_matiere.matiere_list(args={"ue_id": ue_id}) numero = max([mat["numero"] for mat in Matlist]) + 10 matiere_id = sco_edit_matiere.do_matiere_create( {"ue_id": ue_id, "titre": "Malus", "numero": numero} ) module_id = do_module_create( { "titre": titre, "code": code, "coefficient": 0.0, # unused "ue_id": ue_id, "matiere_id": matiere_id, "formation_id": ue["formation_id"], "semestre_id": semestre_id, "module_type": ModuleType.MALUS, }, ) return module_id