forked from ScoDoc/ScoDoc
1039 lines
36 KiB
Python
1039 lines
36 KiB
Python
# -*- mode: python -*-
|
|
# -*- coding: utf-8 -*-
|
|
|
|
##############################################################################
|
|
#
|
|
# Gestion scolarite IUT
|
|
#
|
|
# Copyright (c) 1999 - 2024 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 flash, url_for, render_template
|
|
from flask import g, request
|
|
from flask_login import current_user
|
|
|
|
from app import db, log
|
|
from app import models
|
|
from app.formations import edit_matiere
|
|
from app.models import APO_CODE_STR_LEN
|
|
from app.models import Formation, Matiere, Module, UniteEns
|
|
from app.models import FormSemestre, ModuleImpl
|
|
from app.models import ScolarNews
|
|
from app.models.but_refcomp import ApcAppCritique, ApcParcours
|
|
|
|
import app.scodoc.notesdb as ndb
|
|
import app.scodoc.sco_utils as scu
|
|
from app.scodoc.TrivialFormulator import TrivialFormulator
|
|
from app.scodoc.sco_permissions import Permission
|
|
from app.scodoc.sco_exceptions import (
|
|
ScoValueError,
|
|
ScoLockedFormError,
|
|
ScoGenError,
|
|
ScoNonEmptyFormationObject,
|
|
)
|
|
from app.scodoc import codes_cursus
|
|
from app.scodoc import sco_moduleimpl
|
|
|
|
_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",
|
|
"edt_id",
|
|
#'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."
|
|
formation = db.session.get(Formation, args["formation_id"])
|
|
# refuse de créer un module APC avec semestres incohérents:
|
|
if formation.is_apc():
|
|
ue = db.session.get(UniteEns, args["ue_id"])
|
|
if int(args.get("semestre_id", 0)) != ue.semestre_idx:
|
|
raise ScoValueError("Formation incompatible: contacter le support ScoDoc")
|
|
# create
|
|
module = Module.create_from_dict(args)
|
|
db.session.commit()
|
|
log(f"do_module_create: created {module.id} with {args}")
|
|
|
|
# news
|
|
ScolarNews.add(
|
|
typ=ScolarNews.NEWS_FORM,
|
|
obj=formation.id,
|
|
text=f"Modification de la formation {formation.acronyme}",
|
|
)
|
|
formation.invalidate_cached_sems()
|
|
return module.id
|
|
|
|
|
|
def module_create(
|
|
matiere_id=None, module_type=None, semestre_id=None, formation_id=None
|
|
):
|
|
"""Formulaire de création d'un module
|
|
Si matiere_id est spécifié, le module sera créé dans cette matière (cas normal).
|
|
Sinon, donne le choix de l'UE de rattachement et utilise la première
|
|
matière de cette UE (si elle n'existe pas, la crée).
|
|
"""
|
|
return module_edit(
|
|
create=True,
|
|
matiere_id=matiere_id,
|
|
module_type=module_type,
|
|
semestre_id=semestre_id,
|
|
formation_id=formation_id,
|
|
)
|
|
|
|
|
|
def can_delete_module(module):
|
|
"True si le module n'est pas utilisée dans des formsemestre"
|
|
return len(module.modimpls.all()) == 0
|
|
|
|
|
|
def do_module_delete(oid):
|
|
"delete module"
|
|
module = Module.query.get_or_404(oid)
|
|
mod = module_list({"module_id": oid})[0] # sco7
|
|
if module_is_locked(module.id):
|
|
raise ScoLockedFormError()
|
|
if not can_delete_module(module):
|
|
raise ScoNonEmptyFormationObject(
|
|
"Module",
|
|
msg=module.titre,
|
|
dest_url=url_for(
|
|
"notes.ue_table",
|
|
scodoc_dept=g.scodoc_dept,
|
|
formation_id=module.formation_id,
|
|
semestre_idx=module.ue.semestre_idx,
|
|
),
|
|
)
|
|
|
|
# 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"""
|
|
<h3>Destruction du module impossible car il est utilisé dans des
|
|
semestres existants !</h3>
|
|
<p class="help">Il faut d'abord supprimer le semestre (ou en retirer
|
|
ce module).
|
|
Mais il est peut être préférable de laisser ce programme intact et
|
|
d'en créer une nouvelle version pour la modifier sans affecter
|
|
les semestres déjà en place.
|
|
</p>
|
|
<a href="{url_for('notes.ue_table', scodoc_dept=g.scodoc_dept,
|
|
formation_id=mod["formation_id"])}">reprendre</a>
|
|
"""
|
|
raise ScoGenError(err_page)
|
|
# delete
|
|
cnx = ndb.GetDBConnexion()
|
|
_moduleEditor.delete(cnx, oid)
|
|
|
|
# news
|
|
formation = module.formation
|
|
ScolarNews.add(
|
|
typ=ScolarNews.NEWS_FORM,
|
|
obj=mod["formation_id"],
|
|
text=f"Modification de la formation {formation.acronyme}",
|
|
)
|
|
formation.invalidate_cached_sems()
|
|
|
|
|
|
def module_delete(module_id=None):
|
|
"""Delete a module"""
|
|
module = Module.query.get_or_404(module_id)
|
|
mod = module_list(args={"module_id": module_id})[0] # sco7
|
|
|
|
if not can_delete_module(module):
|
|
raise ScoNonEmptyFormationObject(
|
|
"Module",
|
|
msg=module.titre,
|
|
dest_url=url_for(
|
|
"notes.ue_table",
|
|
scodoc_dept=g.scodoc_dept,
|
|
formation_id=module.formation_id,
|
|
semestre_idx=module.ue.semestre_idx,
|
|
),
|
|
)
|
|
|
|
H = [
|
|
f"""<h2>Suppression du module {module.titre or "<em>sans titre</em>"}
|
|
({module.code})</h2>""",
|
|
]
|
|
|
|
dest_url = url_for(
|
|
"notes.ue_table",
|
|
scodoc_dept=g.scodoc_dept,
|
|
formation_id=module.formation_id,
|
|
semestre_idx=module.ue.semestre_idx,
|
|
)
|
|
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 render_template(
|
|
"sco_page_dept.j2",
|
|
title="Suppression d'un module",
|
|
content="\n".join(H) + tf[1],
|
|
)
|
|
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"
|
|
# 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:
|
|
vals = vals.copy()
|
|
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)
|
|
db.session.get(Formation, 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"
|
|
modules = module_list(args={"code": code, "formation_id": formation_id})
|
|
if module_id: # edition: supprime le module en cours
|
|
modules = [m for m in modules if m["module_id"] != module_id]
|
|
|
|
return len(modules) == 0
|
|
|
|
|
|
def module_edit(
|
|
module_id=None,
|
|
create=False,
|
|
matiere_id=None,
|
|
module_type=None,
|
|
semestre_id=None,
|
|
formation_id=None,
|
|
):
|
|
"""Formulaire édition ou création module.
|
|
Si create, création nouveau module.
|
|
Si matiere_id est spécifié, le module sera créé dans cette matière (cas normal).
|
|
Sinon, donne le choix de l'UE de rattachement et utilise la première matière
|
|
de cette UE (si elle n'existe pas, la crée).
|
|
"""
|
|
from app.scodoc import sco_tag_module
|
|
|
|
# --- Détermination de la formation
|
|
orig_semestre_idx = semestre_id
|
|
ue = None
|
|
if create:
|
|
if matiere_id:
|
|
matiere = Matiere.query.get_or_404(matiere_id)
|
|
ue = matiere.ue
|
|
formation = ue.formation
|
|
orig_semestre_idx = ue.semestre_idx if semestre_id is None else semestre_id
|
|
else:
|
|
formation = Formation.query.get_or_404(formation_id)
|
|
module = None
|
|
unlocked = True
|
|
else:
|
|
if not module_id:
|
|
raise ValueError("missing module_id !")
|
|
module = models.Module.query.get_or_404(module_id)
|
|
ue = module.ue
|
|
module_dict = module.to_dict()
|
|
formation = module.formation
|
|
unlocked = not module_is_locked(module_id)
|
|
|
|
parcours = codes_cursus.get_cursus_from_code(formation.type_parcours)
|
|
is_apc = parcours.APC_SAE # BUT
|
|
if not create:
|
|
orig_semestre_idx = module.ue.semestre_idx if is_apc else module.semestre_id
|
|
if orig_semestre_idx is None:
|
|
orig_semestre_idx = 1
|
|
# il y a-t-il des modimpls ?
|
|
in_use = (module is not None) and (len(module.modimpls.all()) > 0)
|
|
matieres = Matiere.query.filter(
|
|
Matiere.ue_id == UniteEns.id, UniteEns.formation_id == formation.id
|
|
).order_by(UniteEns.semestre_idx, UniteEns.numero, Matiere.numero)
|
|
if in_use:
|
|
# restreint aux matières du même semestre
|
|
matieres = matieres.filter(UniteEns.semestre_idx == module.ue.semestre_idx)
|
|
|
|
if is_apc:
|
|
# ne conserve que la 1ere matière de chaque UE,
|
|
# et celle à laquelle ce module est rattaché
|
|
matieres = [
|
|
mat
|
|
for mat in matieres
|
|
if (module and module.matiere and (module.matiere.id == mat.id))
|
|
or (mat.id == mat.ue.matieres.first().id)
|
|
]
|
|
mat_names = [
|
|
"S%s / %s" % (mat.ue.semestre_idx, mat.ue.acronyme) for mat in matieres
|
|
]
|
|
else:
|
|
mat_names = ["%s / %s" % (mat.ue.acronyme, mat.titre or "") for mat in matieres]
|
|
|
|
if module: # edition
|
|
ue_mat_ids = ["%s!%s" % (mat.ue.id, mat.id) for mat in matieres]
|
|
module_dict["ue_matiere_id"] = "%s!%s" % (
|
|
module_dict["ue_id"],
|
|
module_dict["matiere_id"],
|
|
)
|
|
|
|
semestres_indices = list(range(1, parcours.NB_SEM + 1))
|
|
# Toutes les UEs de la formation (tout parcours):
|
|
ues = formation.ues.order_by(
|
|
UniteEns.semestre_idx, UniteEns.numero, UniteEns.acronyme
|
|
)
|
|
if is_apc and create and semestre_id is not None:
|
|
ues = ues.filter_by(semestre_idx=semestre_id)
|
|
# L'UE de rattachement par défaut: 1ere du semestre
|
|
ue_default = (
|
|
formation.ues.filter_by(semestre_idx=orig_semestre_idx)
|
|
.order_by(UniteEns.numero, UniteEns.acronyme)
|
|
.first()
|
|
)
|
|
|
|
# --- Titre de la page
|
|
if create:
|
|
if is_apc and module_type is not None:
|
|
object_name = scu.MODULE_TYPE_NAMES[module_type]
|
|
else:
|
|
object_name = "Module"
|
|
page_title = f"Création {object_name}"
|
|
if matiere_id:
|
|
title = f"""Création {object_name} dans la matière
|
|
{matiere.titre},
|
|
(UE {ue.acronyme}), semestre {ue.semestre_idx}
|
|
"""
|
|
else:
|
|
title = f"""Création {object_name} dans la formation
|
|
{formation.acronyme}"""
|
|
else:
|
|
page_title = f"Modification du module {module.code or module.titre or ''}"
|
|
title = f"""Modification du module {module.code or ''} {module.titre or ''}
|
|
(formation {formation.acronyme}, version {formation.version})
|
|
"""
|
|
|
|
H = [
|
|
f"""<h2>{title}</h2>""",
|
|
render_template(
|
|
"scodoc/help/modules.j2",
|
|
is_apc=is_apc,
|
|
semestre_id=semestre_id,
|
|
formsemestres=(
|
|
FormSemestre.query.filter(
|
|
ModuleImpl.formsemestre_id == FormSemestre.id,
|
|
ModuleImpl.module_id == module_id,
|
|
)
|
|
.order_by(FormSemestre.date_debut)
|
|
.all()
|
|
if not create
|
|
else None
|
|
),
|
|
create=create,
|
|
),
|
|
]
|
|
if not unlocked:
|
|
H.append(
|
|
"""<div class="ue_warning"><span>Formation verrouillée, seuls
|
|
certains éléments peuvent être modifiés</span></div>"""
|
|
)
|
|
if is_apc:
|
|
module_types = scu.ModuleType # tous les types
|
|
else:
|
|
# ne propose pas SAE et Ressources, sauf si déjà de ce type...
|
|
module_types = set(scu.ModuleType) - {
|
|
scu.ModuleType.RESSOURCE,
|
|
scu.ModuleType.SAE,
|
|
}
|
|
if module:
|
|
module_types |= {
|
|
(
|
|
scu.ModuleType(module.module_type)
|
|
if module.module_type
|
|
else scu.ModuleType.STANDARD
|
|
)
|
|
}
|
|
# Numéro du module
|
|
# cherche le numero adéquat (pour placer le module en fin de liste)
|
|
if module:
|
|
default_num = module.numero
|
|
else:
|
|
modules = formation.modules.all()
|
|
if modules:
|
|
default_num = max(m.numero or 0 for m in modules) + 10
|
|
else:
|
|
default_num = 10
|
|
|
|
descr = [
|
|
(
|
|
"code",
|
|
{
|
|
"size": 10,
|
|
"explanation": """code du module (issu du programme, exemple M1203,
|
|
R2.01, ou SAÉ 3.4. 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 if module else None
|
|
),
|
|
},
|
|
),
|
|
(
|
|
"titre",
|
|
{
|
|
"size": 64,
|
|
"explanation": """nom du module. Exemple:
|
|
<em>Introduction à la démarche ergonomique</em>""",
|
|
},
|
|
),
|
|
(
|
|
"abbrev",
|
|
{
|
|
"size": 32,
|
|
"explanation": """(optionnel) nom abrégé pour bulletins.
|
|
Exemple: <em>Intro. à l'ergonomie</em>""",
|
|
},
|
|
),
|
|
(
|
|
"module_type",
|
|
{
|
|
"input_type": "menu",
|
|
"title": "Type",
|
|
"explanation": "",
|
|
"labels": [x.name.capitalize() for x in module_types],
|
|
"allowed_values": [str(int(x)) for x in module_types],
|
|
"enabled": unlocked,
|
|
},
|
|
),
|
|
(
|
|
"heures_cours",
|
|
{
|
|
"title": "Heures cours :",
|
|
"size": 4,
|
|
"type": "float",
|
|
"min_value": 0,
|
|
"explanation": "nombre d'heures de cours (optionnel)",
|
|
},
|
|
),
|
|
(
|
|
"heures_td",
|
|
{
|
|
"title": "Heures de TD :",
|
|
"size": 4,
|
|
"type": "float",
|
|
"min_value": 0,
|
|
"explanation": "nombre d'heures de Travaux Dirigés (optionnel)",
|
|
},
|
|
),
|
|
(
|
|
"heures_tp",
|
|
{
|
|
"title": "Heures de TP :",
|
|
"size": 4,
|
|
"type": "float",
|
|
"min_value": 0,
|
|
"explanation": "nombre d'heures de Travaux Pratiques (optionnel)",
|
|
},
|
|
),
|
|
]
|
|
if is_apc:
|
|
if module:
|
|
coefs_lst = module.ue_coefs_list()
|
|
if coefs_lst:
|
|
coefs_descr_txt = ", ".join(
|
|
[f"{ue.acronyme}: {c}" for (ue, c) in coefs_lst]
|
|
)
|
|
else:
|
|
coefs_descr_txt = """<span class="missing_value">non définis</span>"""
|
|
descr += [
|
|
(
|
|
"ue_coefs",
|
|
{
|
|
"readonly": True,
|
|
"title": "Coefficients vers les UE ",
|
|
"default": coefs_descr_txt,
|
|
"explanation": """ <br>(passer par la page d'édition de la
|
|
formation pour modifier les coefficients)""",
|
|
},
|
|
)
|
|
]
|
|
else:
|
|
descr += [
|
|
(
|
|
"sep_ue_coefs",
|
|
{
|
|
"input_type": "separator",
|
|
"title": """
|
|
<div>(<em>les coefficients vers les UE se fixent sur la page dédiée</em>)
|
|
</div>""",
|
|
},
|
|
),
|
|
]
|
|
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",
|
|
"default": formation.id,
|
|
},
|
|
),
|
|
(
|
|
"semestre_id",
|
|
{
|
|
"input_type": "hidden",
|
|
"default": orig_semestre_idx,
|
|
},
|
|
),
|
|
]
|
|
if module:
|
|
descr += [
|
|
("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 notamment pour les malus"
|
|
+ (
|
|
" (module utilisé, ne peut pas être changé de semestre)"
|
|
if in_use
|
|
else ""
|
|
)
|
|
)
|
|
if is_apc
|
|
else "un module appartient à une seule matière."
|
|
),
|
|
"labels": mat_names,
|
|
"allowed_values": ue_mat_ids,
|
|
"enabled": unlocked,
|
|
},
|
|
),
|
|
]
|
|
else: # Création
|
|
if matiere_id:
|
|
descr += [
|
|
("ue_id", {"default": ue.id, "input_type": "hidden"}),
|
|
("matiere_id", {"default": matiere_id, "input_type": "hidden"}),
|
|
]
|
|
else:
|
|
# choix de l'UE de rattachement
|
|
descr += [
|
|
(
|
|
"ue_id",
|
|
{
|
|
"input_type": "menu",
|
|
"type": "int",
|
|
"title": "UE de rattachement",
|
|
"explanation": "utilisée notamment pour les malus",
|
|
"labels": [
|
|
f"S{u.semestre_idx if u.semestre_idx is not None else '.'} / {u.acronyme} {u.titre}"
|
|
for u in ues
|
|
],
|
|
"allowed_values": [u.id for u in ues],
|
|
"default": ue_default.id if ue_default is not None else "",
|
|
},
|
|
),
|
|
]
|
|
|
|
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": f"{parcours.SESSION_NAME} de début du module dans la formation standard",
|
|
"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 (ce code est propre à chaque établissement, se rapprocher
|
|
du référent Apogée).
|
|
""",
|
|
"validator": lambda val, _: len(val) < APO_CODE_STR_LEN,
|
|
},
|
|
),
|
|
(
|
|
"numero",
|
|
{
|
|
"title": "Numéro",
|
|
"size": 4,
|
|
"explanation": "numéro (1, 2, 3, 4, ...) pour ordre d'affichage",
|
|
"type": "int",
|
|
"default": default_num,
|
|
"allow_null": True,
|
|
},
|
|
),
|
|
]
|
|
|
|
if is_apc:
|
|
# Choix des parcours
|
|
ref_comp = formation.referentiel_competence
|
|
if ref_comp:
|
|
descr += [
|
|
(
|
|
"parcours",
|
|
{
|
|
"input_type": "checkbox",
|
|
"vertical": True,
|
|
"dom_id": "tf_module_parcours",
|
|
"labels": [
|
|
f" {parcour.libelle} (<b>{parcour.code}</b>)"
|
|
for parcour in ref_comp.parcours
|
|
]
|
|
+ [" Tous (tronc commun)"],
|
|
"allowed_values": [
|
|
str(parcour.id) for parcour in ref_comp.parcours
|
|
]
|
|
+ ["-1"],
|
|
"explanation": """Parcours dans lesquels est utilisé ce module (inutile
|
|
hors BUT, pour les modules standards et dans les UEs de bonus).
|
|
<br>
|
|
Attention: si le module ne doit pas avoir les mêmes coefficients suivant
|
|
le parcours, il faut en créer plusieurs versions, car dans ScoDoc chaque
|
|
module a ses coefficients.
|
|
""",
|
|
},
|
|
)
|
|
]
|
|
if module:
|
|
module_dict["parcours"] = [
|
|
str(parcour.id) for parcour in module.parcours
|
|
]
|
|
module_dict["app_critiques"] = [
|
|
str(app_crit.id) for app_crit in module.app_critiques
|
|
]
|
|
# Choix des Apprentissages Critiques
|
|
if ue is not None:
|
|
annee = f"BUT{(orig_semestre_idx+1)//2}"
|
|
app_critiques = ApcAppCritique.app_critiques_ref_comp(ref_comp, annee)
|
|
if ue.niveau_competence is not None:
|
|
descr += [
|
|
(
|
|
"app_critiques",
|
|
{
|
|
"title": "Apprentissages Critiques",
|
|
"input_type": "checkbox",
|
|
"vertical": True,
|
|
"dom_id": "tf_module_app_critiques",
|
|
"labels": [
|
|
f"{app_crit.code} {app_crit.libelle}"
|
|
for app_crit in app_critiques
|
|
],
|
|
"allowed_values": [
|
|
str(app_crit.id) for app_crit in app_critiques
|
|
],
|
|
"html_data": [],
|
|
"explanation": """Apprentissages Critiques liés à ce module.
|
|
(si vous changez le semestre, revenez ensuite sur cette page
|
|
pour associer les AC.)
|
|
""",
|
|
},
|
|
)
|
|
]
|
|
else:
|
|
if module.ue.type == codes_cursus.UE_STANDARD:
|
|
descr += [
|
|
(
|
|
"app_critiques",
|
|
{
|
|
"input_type": "separator",
|
|
"title": f"""<span class="fontred">{scu.EMO_WARNING }
|
|
L'UE <a class="stdlink" href="{
|
|
url_for("notes.ue_edit", scodoc_dept=g.scodoc_dept, ue_id=ue.id)
|
|
}">{ue.acronyme} {ue.titre or ''}</a>
|
|
n'est pas associée à un niveau de compétences
|
|
</span>""",
|
|
},
|
|
)
|
|
]
|
|
else:
|
|
descr += [
|
|
(
|
|
"parcours",
|
|
{
|
|
"input_type": "separator",
|
|
"title": f"""<span class="fontred">{scu.EMO_WARNING }
|
|
Pas de parcours:
|
|
<a class="stdlink" href="{ url_for('notes.refcomp_assoc_formation',
|
|
scodoc_dept=g.scodoc_dept, formation_id=formation.id)
|
|
}">associer un référentiel de compétence</a>
|
|
</span>""",
|
|
},
|
|
)
|
|
]
|
|
# force module semestre_idx to its UE
|
|
if module:
|
|
if (not module.ue) or (module.ue.semestre_idx is None):
|
|
# Filet de sécurité si jamais l'UE n'a pas non plus de semestre:
|
|
module_dict["semestre_id"] = 1
|
|
else:
|
|
module_dict["semestre_id"] = module.ue.semestre_idx
|
|
tags = module.tags if module else []
|
|
tf = TrivialFormulator(
|
|
request.base_url,
|
|
scu.get_request_args(),
|
|
descr,
|
|
html_foot_markup=(
|
|
f"""<div class="scobox sco_tag_module_edit"><span
|
|
class="sco_tag_edit"><textarea data-module_id="{module_id}" class="module_tag_editor"
|
|
>{
|
|
','.join(t.title for t in tags)
|
|
}</textarea></span></div>
|
|
"""
|
|
if not create
|
|
else ""
|
|
),
|
|
initvalues=module_dict if module else {},
|
|
submitlabel="Modifier ce module" if module else "Créer ce module",
|
|
cancelbutton="Annuler",
|
|
)
|
|
#
|
|
if tf[0] == 0:
|
|
return render_template(
|
|
"sco_page_dept.j2",
|
|
title=page_title,
|
|
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",
|
|
"js/module_edit.js",
|
|
],
|
|
content="\n".join(H)
|
|
+ tf[1]
|
|
+ (
|
|
f"""
|
|
<form action="module_clone" class="clone_form" method="post">
|
|
<input type="hidden" name="module_id" value="{module_id}">
|
|
<button type="submit">Créer une copie de ce module</button>
|
|
</form>
|
|
"""
|
|
if not create
|
|
else ""
|
|
),
|
|
)
|
|
if tf[0] == -1:
|
|
return flask.redirect(
|
|
url_for(
|
|
"notes.ue_table",
|
|
scodoc_dept=g.scodoc_dept,
|
|
formation_id=formation.id,
|
|
semestre_idx=orig_semestre_idx,
|
|
)
|
|
)
|
|
if isinstance(tf[2]["numero"], str):
|
|
tf[2]["numero"] = tf[2]["numero"].strip()
|
|
if not isinstance(tf[2]["numero"], int) and not tf[2]["numero"]:
|
|
tf[2]["numero"] = tf[2]["numero"] or default_num
|
|
# Les parcours sont affectés ensuite
|
|
form_parcours = tf[2].pop("parcours", [])
|
|
if create:
|
|
if not matiere_id:
|
|
# formulaire avec choix UE de rattachement
|
|
ue = db.session.get(UniteEns, tf[2]["ue_id"])
|
|
if ue is None:
|
|
raise ValueError("UE invalide")
|
|
matiere = ue.matieres.first()
|
|
if matiere:
|
|
tf[2]["matiere_id"] = matiere.id
|
|
else:
|
|
matiere = Matiere.create_from_dict(
|
|
{
|
|
"ue_id": ue.id,
|
|
"titre": ue.titre or "",
|
|
"numero": 1,
|
|
}
|
|
)
|
|
tf[2]["matiere_id"] = matiere.id
|
|
|
|
tf[2]["semestre_id"] = ue.semestre_idx
|
|
module_id = do_module_create(tf[2])
|
|
module = db.session.get(Module, module_id)
|
|
else: # EDITION MODULE
|
|
# l'UE de rattachement peut changer
|
|
tf[2]["ue_id"], tf[2]["matiere_id"] = tf[2]["ue_matiere_id"].split("!")
|
|
x, y = tf[2]["ue_matiere_id"].split("!")
|
|
tf[2]["ue_id"] = int(x)
|
|
tf[2]["matiere_id"] = int(y)
|
|
old_ue_id = module.ue.id
|
|
new_ue_id = tf[2]["ue_id"]
|
|
if (old_ue_id != new_ue_id) and in_use:
|
|
new_ue = UniteEns.query.get_or_404(new_ue_id)
|
|
if new_ue.semestre_idx != module.ue.semestre_idx:
|
|
# pas changer de semestre un module utilisé !
|
|
raise ScoValueError(
|
|
"Module utilisé: il ne peut pas être changé de semestre !"
|
|
)
|
|
# En APC, force le semestre égal à celui de l'UE
|
|
if is_apc:
|
|
selected_ue = db.session.get(UniteEns, tf[2]["ue_id"])
|
|
if selected_ue is None:
|
|
raise ValueError("UE invalide")
|
|
tf[2]["semestre_id"] = selected_ue.semestre_idx
|
|
if not tf[2].get("code"):
|
|
raise ScoValueError("Le code du module doit être spécifié.")
|
|
# Check unicité code module dans la formation
|
|
# ??? TODO
|
|
#
|
|
do_module_edit(tf[2])
|
|
# Modifie les parcours
|
|
if form_parcours is not None and formation.referentiel_competence:
|
|
if "-1" in form_parcours: # "tous"
|
|
module.parcours = formation.referentiel_competence.parcours.all()
|
|
else:
|
|
module.parcours = [
|
|
db.session.get(ApcParcours, int(parcour_id_str))
|
|
for parcour_id_str in form_parcours
|
|
]
|
|
# Modifie les AC
|
|
if "app_critiques" in tf[2]:
|
|
module.app_critiques = [
|
|
db.session.get(ApcAppCritique, int(ac_id_str))
|
|
for ac_id_str in tf[2]["app_critiques"]
|
|
]
|
|
db.session.add(module)
|
|
db.session.commit()
|
|
return flask.redirect(
|
|
url_for(
|
|
"notes.ue_table",
|
|
scodoc_dept=g.scodoc_dept,
|
|
formation_id=formation.id,
|
|
semestre_idx=tf[2]["semestre_id"] if is_apc else 1,
|
|
)
|
|
)
|
|
|
|
|
|
def module_table(formation_id):
|
|
"""Liste des modules de la formation
|
|
(XXX inutile ou a revoir)
|
|
"""
|
|
if not formation_id:
|
|
raise ScoValueError("invalid formation !")
|
|
formation: Formation = Formation.query.get_or_404(formation_id)
|
|
H = [
|
|
f"""<h2>Listes des modules dans la formation {formation.titre} ({formation.acronyme}</h2>
|
|
<ul class="notes_module_list">
|
|
""",
|
|
]
|
|
editable = current_user.has_permission(Permission.EditFormation)
|
|
|
|
for module_dict in module_list(args={"formation_id": formation_id}):
|
|
H.append('<li class="notes_module_list">%s' % module_dict)
|
|
if editable:
|
|
H.append(
|
|
'<a href="module_edit?module_id=%(module_id)s">modifier</a>'
|
|
% module_dict
|
|
)
|
|
H.append(
|
|
'<a href="module_delete?module_id=%(module_id)s">supprimer</a>'
|
|
% module_dict
|
|
)
|
|
H.append("</li>")
|
|
H.append("</ul>")
|
|
return render_template(
|
|
"sco_page_dept.j2",
|
|
title=f"Liste des modules de {formation.titre}",
|
|
content="\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 formation_add_malus_modules(
|
|
formation_id: int, semestre_id: int = None, titre=None, redirect=True
|
|
):
|
|
"""Création d'un module de "malus" dans chaque UE d'une formation"""
|
|
|
|
formation = Formation.query.get_or_404(formation_id)
|
|
|
|
nb = 0
|
|
ues = formation.ues
|
|
if semestre_id is not None:
|
|
ues = ues.filter_by(semestre_idx=semestre_id)
|
|
for ue in ues:
|
|
if ue.type == codes_cursus.UE_STANDARD:
|
|
if ue_add_malus_module(ue, titre=titre) != None:
|
|
nb += 1
|
|
|
|
flash(f"Modules de malus ajoutés dans {nb} UEs du S{semestre_id}")
|
|
|
|
formation.invalidate_cached_sems()
|
|
|
|
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: UniteEns, titre=None, code=None) -> int:
|
|
"""Add a malus module in this ue.
|
|
If already exists, do nothing.
|
|
Returns id of malus module.
|
|
"""
|
|
modules_malus = [m for m in ue.modules if m.module_type == scu.ModuleType.MALUS]
|
|
if len(modules_malus) > 0:
|
|
return None # déjà existant
|
|
|
|
titre = titre or f"Malus {ue.acronyme}"
|
|
code = code or f"MALUS{ue.numero}"
|
|
|
|
# Tout module doit avoir un semestre_id (indice 1, 2, ...)
|
|
if ue.semestre_idx is None:
|
|
semestre_ids = sorted(list(set([m.semestre_id for m in ue.modules])))
|
|
if len(semestre_ids) > 0:
|
|
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 si l'UE n'a pas de numéro de semestre et ne comporte pas d'autres modules"
|
|
)
|
|
else:
|
|
semestre_id = ue.semestre_idx
|
|
|
|
# Matiere pour placer le module malus
|
|
titre_matiere_malus = "Malus"
|
|
|
|
matieres_malus = [mat for mat in ue.matieres if mat.titre == titre_matiere_malus]
|
|
if len(matieres_malus) > 0:
|
|
# matière Malus déjà existante, l'utilise
|
|
matiere = matieres_malus[0]
|
|
else:
|
|
if ue.matieres.count() > 0:
|
|
numero = max([(mat.numero or 0) for mat in ue.matieres]) + 10
|
|
else:
|
|
numero = 0
|
|
matiere = Matiere(ue_id=ue.id, titre=titre_matiere_malus, numero=numero)
|
|
db.session.add(matiere)
|
|
|
|
module = Module(
|
|
titre=titre,
|
|
code=code,
|
|
coefficient=0.0,
|
|
ue=ue,
|
|
matiere=matiere,
|
|
formation=ue.formation,
|
|
semestre_id=semestre_id,
|
|
module_type=scu.ModuleType.MALUS,
|
|
)
|
|
db.session.add(module)
|
|
db.session.commit()
|
|
|
|
return module.id
|