# -*- 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
#
##############################################################################
"""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 Matiere, Module
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.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(matiere_id)
if matiere is None:
raise ScoValueError("invalid matiere !")
ue = matiere.ue
parcours = ue.formation.get_parcours()
is_apc = parcours.APC_SAE
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,
)
]
# cherche le numero adéquat (pour placer le module en fin de liste)
modules = Matiere.query.get(1).modules.all()
if modules:
default_num = max([m.numero for m in modules]) + 10
else:
default_num = 10
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))
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:
module_id = do_module_create(tf[2])
mod = Module.query.get(module_id)
return flask.redirect(
url_for(
"notes.ue_table",
scodoc_dept=g.scodoc_dept,
formation_id=ue.formation_id,
semestre_idx=mod.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(val):
"edit a module"
from app.scodoc import sco_edit_formation
# check
mod = module_list({"module_id": val["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 val:
del val[f]
# edit
cnx = ndb.GetDBConnexion()
_moduleEditor.edit(cnx, val)
sco_edit_formation.invalidate_sems_in_formation(mod["formation_id"])
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))
dest_url = url_for(
"notes.ue_table",
scodoc_dept=g.scodoc_dept,
formation_id=str(formation_id),
)
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" if not is_apc else "hidden",
"title": "Matière",
"explanation": "un module appartient à une seule matière.",
"labels": mat_names,
"allowed_values": ue_mat_ids,
"enabled": unlocked and not is_apc, # pas d'édition des matieres en BUT
},
),
]
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",
},
),
(
"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(dest_url)
else:
# l'UE peut changer
tf[2]["ue_id"], tf[2]["matiere_id"] = tf[2]["ue_matiere_id"].split("!")
# Check unicité code module dans la formation
do_module_edit(tf[2])
return flask.redirect(dest_url)
# 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,
'',
]
editable = current_user.has_permission(Permission.ScoChangeFormation)
for Mod in module_list(args={"formation_id": formation_id}):
H.append('- %s' % Mod)
if editable:
H.append('modifier' % Mod)
H.append(
'supprimer' % Mod
)
H.append("
")
H.append("
")
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