forked from ScoDoc/ScoDoc
1501 lines
52 KiB
Python
1501 lines
52 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 UE"""
|
|
|
|
import re
|
|
|
|
import sqlalchemy as sa
|
|
import flask
|
|
from flask import flash, render_template, url_for
|
|
from flask import g, request
|
|
from flask_login import current_user
|
|
|
|
from app import db, log
|
|
from app.but import apc_edit_ue
|
|
from app.models import APO_CODE_STR_LEN, SHORT_STR_LEN
|
|
from app.models import (
|
|
Formation,
|
|
FormSemestre,
|
|
FormSemestreUEComputationExpr,
|
|
FormSemestreUECoef,
|
|
Matiere,
|
|
Module,
|
|
UniteEns,
|
|
)
|
|
from app.models import ApcValidationRCUE, ScolarFormSemestreValidation, ScolarEvent
|
|
from app.models import ScolarNews
|
|
import app.scodoc.sco_utils as scu
|
|
from app.scodoc.sco_utils import ModuleType
|
|
from app.scodoc.TrivialFormulator import TrivialFormulator
|
|
from app.scodoc.sco_permissions import Permission
|
|
from app.scodoc.sco_exceptions import (
|
|
ScoValueError,
|
|
ScoLockedFormError,
|
|
ScoNonEmptyFormationObject,
|
|
)
|
|
|
|
from app.scodoc import codes_cursus
|
|
from app.scodoc import sco_edit_apc
|
|
from app.scodoc import sco_groups
|
|
from app.scodoc import sco_moduleimpl
|
|
|
|
|
|
def do_ue_create(args, allow_empty_ue_code=False) -> UniteEns:
|
|
"create an ue"
|
|
# check duplicates
|
|
ues = UniteEns.query.filter_by(
|
|
formation_id=args["formation_id"], acronyme=args["acronyme"]
|
|
).all()
|
|
if ues:
|
|
raise ScoValueError(
|
|
f"""Acronyme d'UE "{args['acronyme']}" déjà utilisé !
|
|
(chaque UE doit avoir un acronyme unique dans la formation)"""
|
|
)
|
|
if "ue_code" not in args or args["ue_code"] is None or not args["ue_code"].strip():
|
|
if allow_empty_ue_code:
|
|
args["ue_code"] = ""
|
|
else:
|
|
# évite les conflits: génère nouveau ue_code
|
|
while True:
|
|
cursor = db.session.execute(sa.text("select notes_newid_ucod();"))
|
|
code = cursor.fetchone()[0]
|
|
if UniteEns.query.filter_by(ue_code=code).count() == 0:
|
|
break
|
|
args["ue_code"] = code
|
|
|
|
# last checks
|
|
if not args.get("acronyme"):
|
|
raise ScoValueError("acronyme vide")
|
|
args["coefficient"] = args.get("coefficient", None)
|
|
if args["coefficient"] == "":
|
|
args["coefficient"] = None
|
|
|
|
# create
|
|
ue = UniteEns.create_from_dict(args)
|
|
db.session.commit()
|
|
log(f"do_ue_create: created {ue} with {args}")
|
|
|
|
# caches
|
|
ue.formation.invalidate_module_coefs()
|
|
ue.formation.invalidate_cached_sems()
|
|
# news
|
|
ScolarNews.add(
|
|
typ=ScolarNews.NEWS_FORM,
|
|
obj=args["formation_id"],
|
|
text=f"Modification de la formation {ue.formation.acronyme}",
|
|
)
|
|
|
|
return ue
|
|
|
|
|
|
def do_ue_delete(ue: UniteEns, delete_validations=False, force=False):
|
|
"""delete UE and attached matieres (but not modules).
|
|
Si force, pas de confirmation dialog et pas de redirect
|
|
"""
|
|
formation: Formation = ue.formation
|
|
semestre_idx = ue.semestre_idx
|
|
if not ue.can_be_deleted():
|
|
raise ScoNonEmptyFormationObject(
|
|
f"UE (id={ue.id}, dud)",
|
|
msg=f"{ue.titre or ''} ({ue.acronyme})",
|
|
dest_url=url_for(
|
|
"notes.ue_table",
|
|
scodoc_dept=g.scodoc_dept,
|
|
formation_id=formation.id,
|
|
semestre_idx=semestre_idx,
|
|
),
|
|
)
|
|
|
|
log(f"do_ue_delete: ue_id={ue.id}, delete_validations={delete_validations}")
|
|
|
|
# Il y a-t-il des etudiants ayant validé cette UE ?
|
|
# si oui, propose de supprimer les validations
|
|
validations_ue = ScolarFormSemestreValidation.query.filter_by(ue_id=ue.id).all()
|
|
validations_rcue = ApcValidationRCUE.query.filter(
|
|
(ApcValidationRCUE.ue1_id == ue.id) | (ApcValidationRCUE.ue2_id == ue.id)
|
|
).all()
|
|
if (
|
|
(len(validations_ue) > 0 or len(validations_rcue) > 0)
|
|
and not delete_validations
|
|
and not force
|
|
):
|
|
return scu.confirm_dialog(
|
|
f"""<p>Des étudiants ont une décision de jury sur l'UE {ue.acronyme} ({ue.titre})</p>
|
|
<p>Si vous supprimez cette UE, ces décisions vont être supprimées !</p>""",
|
|
dest_url="",
|
|
target_variable="delete_validations",
|
|
cancel_url=url_for(
|
|
"notes.ue_table",
|
|
scodoc_dept=g.scodoc_dept,
|
|
formation_id=formation.id,
|
|
semestre_idx=semestre_idx,
|
|
),
|
|
parameters={"ue_id": ue.id, "dialog_confirmed": 1},
|
|
)
|
|
if delete_validations:
|
|
log(f"deleting all validations of UE {ue.id}")
|
|
for v in validations_ue:
|
|
db.session.delete(v)
|
|
for v in validations_rcue:
|
|
db.session.delete(v)
|
|
|
|
# delete old formulas
|
|
formulas = FormSemestreUEComputationExpr.query.filter_by(ue_id=ue.id).all()
|
|
for formula in formulas:
|
|
db.session.delete(formula)
|
|
|
|
# delete all matieres in this UE
|
|
for mat in Matiere.query.filter_by(ue_id=ue.id):
|
|
db.session.delete(mat)
|
|
|
|
# delete uecoefs
|
|
for uecoef in FormSemestreUECoef.query.filter_by(ue_id=ue.id):
|
|
db.session.delete(uecoef)
|
|
# delete events
|
|
for event in ScolarEvent.query.filter_by(ue_id=ue.id):
|
|
db.session.delete(event)
|
|
db.session.flush()
|
|
|
|
db.session.delete(ue)
|
|
db.session.commit()
|
|
|
|
# cas compliqué, mais rarement utilisé: acceptable de tout invalider
|
|
formation.invalidate_module_coefs()
|
|
# -> invalide aussi les formsemestres
|
|
# news
|
|
ScolarNews.add(
|
|
typ=ScolarNews.NEWS_FORM,
|
|
obj=formation.id,
|
|
text=f"Modification de la formation {formation.acronyme}",
|
|
)
|
|
#
|
|
if not force:
|
|
return flask.redirect(
|
|
url_for(
|
|
"notes.ue_table",
|
|
scodoc_dept=g.scodoc_dept,
|
|
formation_id=formation.id,
|
|
semestre_idx=semestre_idx,
|
|
)
|
|
)
|
|
return None
|
|
|
|
|
|
def ue_create(formation_id=None, default_semestre_idx=None):
|
|
"""Formulaire création d'une UE"""
|
|
return ue_edit(
|
|
create=True,
|
|
formation_id=formation_id,
|
|
default_semestre_idx=default_semestre_idx,
|
|
)
|
|
|
|
|
|
def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=None):
|
|
"""Formulaire modification ou création d'une UE"""
|
|
create = int(create)
|
|
if not create:
|
|
ue: UniteEns = UniteEns.query.get_or_404(ue_id)
|
|
ue_dict = ue.to_dict()
|
|
formation_id = ue.formation_id
|
|
title = f"Modification de l'UE {ue.acronyme} {ue.titre}"
|
|
initvalues = ue_dict
|
|
submitlabel = "Modifier les valeurs"
|
|
can_change_semestre_id = (
|
|
(ue.modules.count() == 0) or (ue.semestre_idx is None)
|
|
) and ue.niveau_competence is None
|
|
else:
|
|
ue = None
|
|
title = "Création d'une UE"
|
|
|
|
exp = re.compile(r"UCOD(\d+)$")
|
|
matches = {exp.match(u.ue_code) for u in UniteEns.query if exp.match(u.ue_code)}
|
|
max_code = (
|
|
max(int(match.group(1)) for match in matches if match) if matches else 0
|
|
)
|
|
proposed_code = f"UCOD{max_code+1}"
|
|
|
|
initvalues = {
|
|
"semestre_idx": default_semestre_idx,
|
|
"color": ue_guess_color_default(formation_id, default_semestre_idx),
|
|
"coef_rcue": 1.0,
|
|
"ue_code": proposed_code,
|
|
}
|
|
submitlabel = "Créer cette UE"
|
|
can_change_semestre_id = True
|
|
formation = db.session.get(Formation, formation_id)
|
|
if not formation:
|
|
raise ScoValueError(f"Formation inexistante ! (id={formation_id})")
|
|
cursus = formation.get_cursus()
|
|
is_apc = cursus.APC_SAE
|
|
semestres_indices = list(range(1, cursus.NB_SEM + 1))
|
|
|
|
ue_types = cursus.ALLOWED_UE_TYPES
|
|
ue_types.sort()
|
|
ue_types_names = [codes_cursus.UE_TYPE_NAME[k] for k in ue_types]
|
|
ue_types = [str(x) for x in ue_types]
|
|
|
|
form_descr = [
|
|
("ue_id", {"input_type": "hidden"}),
|
|
("create", {"input_type": "hidden", "default": create}),
|
|
("formation_id", {"input_type": "hidden", "default": formation_id}),
|
|
("titre", {"size": 48, "explanation": "nom de l'UE"}),
|
|
("acronyme", {"size": 12, "explanation": "abbréviation", "allow_null": False}),
|
|
(
|
|
"numero",
|
|
{
|
|
"size": 4,
|
|
"explanation": "numéro (1,2,3,4) de l'UE pour l'ordre d'affichage",
|
|
"type": "int",
|
|
},
|
|
),
|
|
]
|
|
if can_change_semestre_id:
|
|
form_descr += [
|
|
(
|
|
"semestre_idx",
|
|
{
|
|
"input_type": "menu",
|
|
"type": "int",
|
|
"allow_null": False,
|
|
"title": cursus.SESSION_NAME.capitalize(),
|
|
"explanation": f"{cursus.SESSION_NAME} de l'UE dans la formation",
|
|
"labels": ["non spécifié"] + [str(x) for x in semestres_indices],
|
|
"allowed_values": [""] + semestres_indices,
|
|
},
|
|
),
|
|
]
|
|
else:
|
|
form_descr += [
|
|
("semestre_idx", {"default": ue.semestre_idx, "input_type": "hidden"}),
|
|
]
|
|
form_descr += [
|
|
(
|
|
"type",
|
|
{
|
|
"explanation": "type d'UE",
|
|
"input_type": "menu",
|
|
"allowed_values": ue_types,
|
|
"labels": ue_types_names,
|
|
},
|
|
),
|
|
(
|
|
"ects",
|
|
{
|
|
"size": 4,
|
|
"type": "float",
|
|
"min_value": 0,
|
|
"max_value": 1000,
|
|
"title": "ECTS",
|
|
"explanation": "nombre de crédits ECTS (indiquer 0 si UE bonus)"
|
|
+ (
|
|
". (si les ECTS dépendent du parcours, voir plus bas.)"
|
|
if is_apc
|
|
else ""
|
|
),
|
|
"allow_null": not is_apc, # ects requis en APC
|
|
},
|
|
),
|
|
]
|
|
if is_apc: # coef pour la moyenne RCUE
|
|
form_descr.append(
|
|
(
|
|
"coef_rcue",
|
|
{
|
|
"size": 4,
|
|
"type": "float",
|
|
"min_value": 0,
|
|
"title": "Coef. RCUE",
|
|
"explanation": """pondération utilisée pour le calcul de la moyenne du RCUE.
|
|
Laisser à 1, sauf si votre établissement a explicitement décidé de pondérations.
|
|
""",
|
|
"defaut": 1.0,
|
|
"allow_null": False,
|
|
"enabled": is_apc,
|
|
},
|
|
)
|
|
)
|
|
else: # non APC, coef d'UE
|
|
form_descr.append(
|
|
(
|
|
"coefficient",
|
|
{
|
|
"size": 4,
|
|
"type": "float",
|
|
"min_value": 0,
|
|
"title": "Coefficient",
|
|
"explanation": """les coefficients d'UE ne sont utilisés que
|
|
lorsque l'option <em>Utiliser les coefficients d'UE pour calculer
|
|
la moyenne générale</em> est activée. Par défaut, le coefficient
|
|
d'une UE est simplement la somme des coefficients des modules dans
|
|
lesquels l'étudiant a des notes.
|
|
Jamais utilisé en BUT.
|
|
""",
|
|
"enabled": not is_apc,
|
|
},
|
|
)
|
|
)
|
|
form_descr += [
|
|
(
|
|
"ue_code",
|
|
{
|
|
"size": 12,
|
|
"title": "Code UE",
|
|
"max_length": SHORT_STR_LEN,
|
|
"explanation": """code interne (non vide). Toutes les UE partageant le même code
|
|
(et le même code de formation) sont compatibles (compensation de semestres, capitalisation d'UE).
|
|
Voir liste ci-dessous.""",
|
|
"allow_null": False,
|
|
},
|
|
),
|
|
(
|
|
"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""",
|
|
"max_length": APO_CODE_STR_LEN,
|
|
},
|
|
),
|
|
]
|
|
if is_apc:
|
|
form_descr += [
|
|
(
|
|
"code_apogee_rcue",
|
|
{
|
|
"title": "Code Apogée du RCUE",
|
|
"size": 25,
|
|
"explanation": "(optionnel) code(s) élément pédagogique Apogée du RCUE",
|
|
"max_length": APO_CODE_STR_LEN,
|
|
},
|
|
),
|
|
]
|
|
form_descr += [
|
|
(
|
|
"is_external",
|
|
{
|
|
"input_type": "boolcheckbox",
|
|
"title": "UE externe",
|
|
"readonly": not create, # ne permet pas de transformer une UE existante en externe
|
|
"explanation": """réservé pour les capitalisations d'UEs
|
|
effectuées à l'extérieur de l'établissement""",
|
|
},
|
|
),
|
|
(
|
|
"color",
|
|
{
|
|
"input_type": "color",
|
|
"title": "Couleur",
|
|
"explanation": "pour affichages",
|
|
},
|
|
),
|
|
]
|
|
if create and not cursus.UE_IS_MODULE and not is_apc:
|
|
form_descr.append(
|
|
(
|
|
"create_matiere",
|
|
{
|
|
"input_type": "boolcheckbox",
|
|
"default": True,
|
|
"title": "Créer matière identique",
|
|
"explanation": """créer immédiatement une matière dans cette UE
|
|
(utile si on n'utilise pas de matières)""",
|
|
},
|
|
)
|
|
)
|
|
tf = TrivialFormulator(
|
|
request.base_url,
|
|
scu.get_request_args(),
|
|
form_descr,
|
|
initvalues=initvalues,
|
|
submitlabel=submitlabel,
|
|
cancelbutton="Revenir à la formation",
|
|
)
|
|
if tf[0] == 0:
|
|
ue_parcours_div = ""
|
|
if ue and is_apc:
|
|
ue_parcours_div = apc_edit_ue.form_ue_choix_parcours(ue)
|
|
if ue and ue.modules.count() and ue.semestre_idx is not None:
|
|
modules_div = f"""<div class="scobox" id="ue_list_modules">
|
|
<div><b>{ue.modules.count()} modules sont rattachés
|
|
à cette UE</b> du semestre S{ue.semestre_idx},
|
|
elle ne peut donc pas être changée de semestre.</div>
|
|
<ul>"""
|
|
for m in ue.modules:
|
|
modules_div += f"""<li><a class="stdlink" href="{url_for(
|
|
"notes.module_edit",
|
|
scodoc_dept=g.scodoc_dept, module_id=m.id)
|
|
}">{m.code} {m.titre or "sans titre"}</a></li>"""
|
|
modules_div += """</ul></div>"""
|
|
else:
|
|
modules_div = ""
|
|
if ue:
|
|
clone_form = f"""
|
|
<form action="ue_clone" class="clone_form" method="post">
|
|
<input type="hidden" name="ue_id" value="{ue.id}">
|
|
<button type="submit">Créer une copie de cette UE</button>
|
|
</form>
|
|
"""
|
|
else:
|
|
clone_form = ""
|
|
|
|
return render_template(
|
|
"sco_page_dept.j2",
|
|
title=title,
|
|
javascripts=["js/edit_ue.js"],
|
|
content=f"""
|
|
<h2>{title}, (formation {formation.acronyme}, version {formation.version})</h2>
|
|
<p class="help">Les UEs sont des groupes de modules dans une formation donnée,
|
|
utilisés pour la validation (on calcule des moyennes par UE et applique des
|
|
seuils ("barres")).
|
|
</p>
|
|
|
|
<p class="help">Note: sauf exception, l'UE n'a pas de coefficient associé.
|
|
Seuls les <em>modules</em> ont des coefficients.
|
|
</p>
|
|
|
|
<div class="scobox">
|
|
<div class="scobox-title">
|
|
Édition de l'UE {('du semestre S'+str(ue.semestre_idx)) if is_apc and ue else ''}
|
|
</div>
|
|
{tf[1]}
|
|
</div>
|
|
{clone_form}
|
|
{ue_parcours_div}
|
|
{modules_div}
|
|
|
|
<div id="bonus_description" class="scobox"></div>
|
|
<div id="ue_list_code" class="sco_box sco_green_bg"></div>
|
|
""",
|
|
)
|
|
elif tf[0] == 1:
|
|
if create:
|
|
if not tf[2]["ue_code"]:
|
|
del tf[2]["ue_code"]
|
|
if not tf[2]["numero"]:
|
|
# numero regroupant par semestre ou année:
|
|
tf[2]["numero"] = next_ue_numero(
|
|
formation_id, int(tf[2]["semestre_idx"])
|
|
)
|
|
ue = do_ue_create(tf[2])
|
|
matiere_id = None
|
|
if is_apc or cursus.UE_IS_MODULE or tf[2]["create_matiere"]:
|
|
# rappel: en APC, toutes les UE ont une matière, créée ici
|
|
# (inutilisée mais à laquelle les modules sont rattachés)
|
|
matiere = Matiere.create_from_dict(
|
|
{"ue_id": ue.id, "titre": tf[2]["titre"], "numero": 1}
|
|
)
|
|
matiere_id = matiere.id
|
|
if cursus.UE_IS_MODULE:
|
|
# dans ce mode, crée un (unique) module dans l'UE:
|
|
_ = Module.create_from_dict(
|
|
{
|
|
"titre": tf[2]["titre"],
|
|
"code": tf[2]["acronyme"],
|
|
# tous les modules auront coef 1, et on utilisera les ECTS:
|
|
"coefficient": 1.0,
|
|
"ue_id": ue.id,
|
|
"matiere_id": matiere_id,
|
|
"formation_id": formation_id,
|
|
"semestre_id": tf[2]["semestre_idx"],
|
|
},
|
|
)
|
|
db.session.commit()
|
|
flash(f"UE créée (code {ue.ue_code})")
|
|
else:
|
|
if not tf[2]["numero"]:
|
|
tf[2]["numero"] = 0
|
|
do_ue_edit(tf[2])
|
|
flash("UE modifiée")
|
|
|
|
if tf[2]:
|
|
dest_semestre_idx = tf[2]["semestre_idx"]
|
|
elif ue:
|
|
dest_semestre_idx = ue.semestre_idx
|
|
elif default_semestre_idx:
|
|
dest_semestre_idx = default_semestre_idx
|
|
elif "semestre_idx" in request.form:
|
|
dest_semestre_idx = request.form["semestre_idx"]
|
|
else:
|
|
dest_semestre_idx = 1
|
|
|
|
return flask.redirect(
|
|
url_for(
|
|
"notes.ue_table",
|
|
scodoc_dept=g.scodoc_dept,
|
|
formation_id=formation_id,
|
|
semestre_idx=dest_semestre_idx,
|
|
)
|
|
)
|
|
|
|
|
|
def next_ue_numero(formation_id, semestre_id=None) -> int:
|
|
"""Numero d'une nouvelle UE dans cette formation.
|
|
Si le semestre est specifie, cherche les UE ayant des modules de ce semestre
|
|
"""
|
|
formation = db.session.get(Formation, formation_id)
|
|
ues = formation.ues.all()
|
|
if not ues:
|
|
return 0
|
|
if semestre_id is None:
|
|
return ues[-1].numero + 1000
|
|
# Avec semestre: (prend le semestre du 1er module de l'UE)
|
|
ue_list_semestre = [ue for ue in ues if ue.get_semestre_id() == semestre_id]
|
|
if ue_list_semestre:
|
|
return ue_list_semestre[-1].numero + 10
|
|
return ues[-1].numero + 1000
|
|
|
|
|
|
def ue_delete(ue_id=None, delete_validations=False, dialog_confirmed=False):
|
|
"""Delete an UE"""
|
|
ue = UniteEns.query.get_or_404(ue_id)
|
|
if ue.modules.all():
|
|
raise ScoValueError(
|
|
f"""Suppression de l'UE {ue.titre} impossible car
|
|
des modules (ou SAÉ ou ressources) lui sont rattachés.""",
|
|
dest_url=url_for(
|
|
"notes.ue_table",
|
|
scodoc_dept=g.scodoc_dept,
|
|
formation_id=ue.formation.id,
|
|
semestre_idx=ue.semestre_idx,
|
|
),
|
|
)
|
|
if not ue.can_be_deleted():
|
|
raise ScoNonEmptyFormationObject(
|
|
"UE",
|
|
msg=f"{ue.titre or ''} ({ue.acronyme})",
|
|
dest_url=url_for(
|
|
"notes.ue_table",
|
|
scodoc_dept=g.scodoc_dept,
|
|
formation_id=ue.formation_id,
|
|
semestre_idx=ue.semestre_idx,
|
|
),
|
|
)
|
|
|
|
if not dialog_confirmed:
|
|
return scu.confirm_dialog(
|
|
f"<h2>Suppression de l'UE {ue.titre or ''} ({ue.acronyme})</h2>",
|
|
dest_url="",
|
|
parameters={"ue_id": ue.id},
|
|
cancel_url=url_for(
|
|
"notes.ue_table",
|
|
scodoc_dept=g.scodoc_dept,
|
|
formation_id=ue.formation_id,
|
|
semestre_idx=ue.semestre_idx,
|
|
),
|
|
)
|
|
|
|
return do_ue_delete(ue, delete_validations=delete_validations)
|
|
|
|
|
|
def ue_table(formation_id=None, semestre_idx=1, msg=""):
|
|
"""Page affiochage ou édition d'une formation
|
|
avec UEs, matières et module,
|
|
et liens pour éditer si non verrouillée et permission.
|
|
"""
|
|
from app.scodoc import sco_formsemestre_validation
|
|
|
|
formation = Formation.get_formation(formation_id)
|
|
parcours = formation.get_cursus()
|
|
is_apc = parcours.APC_SAE
|
|
if semestre_idx == "all" or semestre_idx == "":
|
|
semestre_idx = None
|
|
else:
|
|
semestre_idx = int(semestre_idx)
|
|
show_tags = scu.to_bool(request.args.get("show_tags", 0))
|
|
locked = formation.has_locked_sems(semestre_idx)
|
|
semestre_ids = range(1, parcours.NB_SEM + 1)
|
|
|
|
ues = (
|
|
formation.ues.filter_by(is_external=False)
|
|
.order_by(UniteEns.semestre_idx, UniteEns.numero)
|
|
.all()
|
|
)
|
|
|
|
# safety check: renumérote les ue s'il en manque ou s'il y a des ex-aequo.
|
|
# cela facilite le travail de la passerelle !
|
|
numeros = {ue.numero for ue in ues}
|
|
if (None in numeros) or len(numeros) < len(ues):
|
|
scu.objects_renumber(db, ues)
|
|
|
|
ues_externes = UniteEns.query.filter_by(
|
|
formation_id=formation_id, is_external=True
|
|
).all()
|
|
# liste ordonnée des formsemestres de cette formation:
|
|
formsemestres = sorted(
|
|
FormSemestre.query.filter_by(formation_id=formation_id).all(),
|
|
key=lambda s: s.sort_key(),
|
|
reverse=True,
|
|
)
|
|
|
|
if is_apc:
|
|
# Pour faciliter la transition des anciens programmes non APC
|
|
for ue in ues:
|
|
ue.guess_semestre_idx()
|
|
# vérifie qu'on a bien au moins une matière dans chaque UE
|
|
if ue.matieres.count() < 1:
|
|
mat = Matiere(ue_id=ue.id)
|
|
db.session.add(mat)
|
|
# donne des couleurs aux UEs crées avant
|
|
colorie_anciennes_ues(ues)
|
|
db.session.commit()
|
|
|
|
# tri par semestre et numero:
|
|
ues.sort(key=lambda u: (u.get_semestre_id(), u.numero))
|
|
ues_externes.sort(key=lambda u: (u.get_semestre_id(), u.numero))
|
|
# Codes dupliqués (pour aider l'utilisateur)
|
|
seen = set()
|
|
duplicated_codes = {
|
|
ue.ue_code for ue in ues if ue.ue_code in seen or seen.add(ue.ue_code)
|
|
}
|
|
ues_with_duplicated_code = [ue for ue in ues if ue.ue_code in duplicated_codes]
|
|
|
|
has_perm_change = current_user.has_permission(Permission.EditFormation)
|
|
# editable = (not locked) and has_perm_change
|
|
# On autorise maintenant la modification des formations qui ont
|
|
# des semestres verrouillés, sauf si cela affect les notes passées
|
|
# (verrouillées):
|
|
# - pas de modif des modules utilisés dans des semestres verrouillés
|
|
# - pas de changement des codes d'UE utilisés dans des semestres verrouillés
|
|
editable = has_perm_change
|
|
tag_editable = (
|
|
current_user.has_permission(Permission.EditFormationTags) or has_perm_change
|
|
)
|
|
if locked:
|
|
lockicon = scu.icontag("lock32_img", title="verrouillé")
|
|
else:
|
|
lockicon = ""
|
|
|
|
arrow_up, arrow_down, arrow_none = sco_groups.get_arrow_icons_tags()
|
|
delete_icon = scu.icontag(
|
|
"delete_small_img", title="Supprimer (module inutilisé)", alt="supprimer"
|
|
)
|
|
delete_disabled_icon = scu.icontag(
|
|
"delete_small_dis_img", title="Suppression impossible (module utilisé)"
|
|
)
|
|
H = [
|
|
f"""<h2>{formation.html()} {lockicon}
|
|
</h2>
|
|
""",
|
|
]
|
|
if locked:
|
|
H.append(
|
|
"""<p class="help">Cette formation est verrouillée car
|
|
des semestres verrouillés s'y réferent.
|
|
Si vous souhaitez modifier cette formation (par exemple pour y ajouter un module),
|
|
vous devez:
|
|
</p>
|
|
<ul class="help">
|
|
<li>soit créer une nouvelle version de cette formation pour pouvoir l'éditer
|
|
librement (vous pouvez passer par la fonction "Associer à une nouvelle version
|
|
du programme" (menu "Semestre") si vous avez un semestre en cours);
|
|
</li>
|
|
<li>soit déverrouiller le ou les semestres qui s'y réfèrent (attention, en
|
|
principe ces semestres sont archivés et ne devraient pas être modifiés).
|
|
</li>
|
|
</ul>"""
|
|
)
|
|
if msg:
|
|
H.append('<p class="msg">' + msg + "</p>")
|
|
|
|
if ues_with_duplicated_code:
|
|
H.append(
|
|
f"""<div class="ue_warning"><span>Attention: plusieurs UEs de cette
|
|
formation ont le même code : <tt>{
|
|
', '.join([
|
|
'<a class="stdlink" href="' + url_for( "notes.ue_edit",
|
|
scodoc_dept=g.scodoc_dept, ue_id=ue.id )
|
|
+ '">' + ue.acronyme + " (code " + ue.ue_code + ")</a>"
|
|
for ue in ues_with_duplicated_code ])
|
|
}</tt>.
|
|
Il faut corriger cela, sinon les capitalisations et ECTS seront
|
|
erronés !</span></div>"""
|
|
)
|
|
|
|
# Description de la formation
|
|
H.append(
|
|
render_template(
|
|
"pn/form_descr.j2",
|
|
formation=formation,
|
|
parcours=parcours,
|
|
editable=editable,
|
|
)
|
|
)
|
|
|
|
# Formation APC (BUT) ?
|
|
if is_apc:
|
|
lock_info = (
|
|
"""<span class="lock_info">verrouillé (voir liste des semestres utilisateurs
|
|
en bas de page)</span>
|
|
"""
|
|
if locked
|
|
else ""
|
|
)
|
|
H.append(
|
|
f"""<div class="formation_apc_infos">
|
|
<div class="ue_list_tit">Formation par compétences (BUT)
|
|
- {_html_select_semestre_idx(formation_id, semestre_ids, semestre_idx)}
|
|
</form>
|
|
{lock_info}
|
|
</div>
|
|
"""
|
|
)
|
|
if formation.referentiel_competence is None:
|
|
descr_refcomp = ""
|
|
msg_refcomp = "associer à un référentiel de compétences"
|
|
else:
|
|
descr_refcomp = f"""Référentiel de compétences:
|
|
<a href="{url_for('notes.refcomp_show',
|
|
scodoc_dept=g.scodoc_dept, refcomp_id=formation.referentiel_competence.id)}"
|
|
class="stdlink">
|
|
{formation.referentiel_competence.get_title()}
|
|
</a> """
|
|
msg_refcomp = "changer"
|
|
H.append(f"""<ul><li>{descr_refcomp}""")
|
|
if current_user.has_permission(Permission.EditFormation):
|
|
if (
|
|
formation.referentiel_competence is None
|
|
or formation.formsemestres.count() == 0
|
|
):
|
|
H.append(
|
|
f"""<a class="stdlink" href="{url_for('notes.refcomp_assoc_formation',
|
|
scodoc_dept=g.scodoc_dept, formation_id=formation_id)
|
|
}">{msg_refcomp}</a>"""
|
|
)
|
|
elif formation.referentiel_competence is not None:
|
|
H.append("""(non modifiable car utilisé par des semestres)""")
|
|
H.append("</li>")
|
|
if formation.referentiel_competence is not None:
|
|
H.append(
|
|
"""<li>Parcours, compétences et UEs :
|
|
<div class="formation_parcs">
|
|
"""
|
|
)
|
|
for parc in formation.referentiel_competence.parcours:
|
|
H.append(
|
|
f"""<div><a href="{url_for("notes.parcour_formation",
|
|
scodoc_dept=g.scodoc_dept, formation_id=formation.id, parcour_id=parc.id )
|
|
}">{parc.code}</a></div>"""
|
|
)
|
|
H.append("""</div></li>""")
|
|
|
|
H.append(
|
|
f"""
|
|
<li> <a class="stdlink" href="{
|
|
url_for('notes.edit_modules_ue_coefs',
|
|
scodoc_dept=g.scodoc_dept, formation_id=formation_id, semestre_idx=semestre_idx)
|
|
}">{'Visualiser' if locked else 'Éditer'} les coefficients des ressources et SAÉs</a>
|
|
</li>
|
|
</ul>
|
|
"""
|
|
)
|
|
# Description des UE/matières/modules
|
|
H.append(
|
|
f"""
|
|
<div class="formation_ue_list">
|
|
<div class="ue_list_tit">Formation (programme pédagogique):</div>
|
|
<form>
|
|
<input type="checkbox" class="sco_tag_checkbox"
|
|
{'checked' if show_tags else ''}
|
|
> Montrer les tags des modules voire en ajouter
|
|
<i>(ceux correspondant aux titres des compétences étant ajoutés par défaut)</i>
|
|
</input>
|
|
</form>
|
|
"""
|
|
)
|
|
if is_apc:
|
|
H.append(
|
|
sco_edit_apc.html_edit_formation_apc(
|
|
formation,
|
|
semestre_idx=semestre_idx,
|
|
editable=editable,
|
|
tag_editable=tag_editable,
|
|
)
|
|
)
|
|
else:
|
|
H.append('<div class="formation_classic_infos">')
|
|
H.append(
|
|
_ue_table_ues(
|
|
parcours,
|
|
ues,
|
|
editable,
|
|
tag_editable,
|
|
has_perm_change,
|
|
arrow_up,
|
|
arrow_down,
|
|
arrow_none,
|
|
delete_icon,
|
|
delete_disabled_icon,
|
|
)
|
|
)
|
|
if editable:
|
|
H.append(
|
|
f"""<ul>
|
|
<li><a class="stdlink" href="{
|
|
url_for('notes.ue_create', scodoc_dept=g.scodoc_dept, formation_id=formation_id)
|
|
}">Ajouter une UE</a>
|
|
</li>
|
|
<li><a href="{
|
|
url_for('notes.formation_add_malus_modules',
|
|
scodoc_dept=g.scodoc_dept, formation_id=formation_id)
|
|
}" class="stdlink">Ajouter des modules de malus dans chaque UE</a>
|
|
</li>
|
|
</ul>
|
|
"""
|
|
)
|
|
H.append("</div>")
|
|
H.append("</div>") # formation_ue_list
|
|
|
|
if ues_externes:
|
|
H.append(
|
|
f"""
|
|
<div class="formation_ue_list formation_ue_list_externes">
|
|
<div class="ue_list_tit">UE externes déclarées (pour information):
|
|
</div>
|
|
{_ue_table_ues(
|
|
parcours,
|
|
ues_externes,
|
|
editable,
|
|
tag_editable,
|
|
has_perm_change,
|
|
arrow_up,
|
|
arrow_down,
|
|
arrow_none,
|
|
delete_icon,
|
|
delete_disabled_icon,
|
|
)}
|
|
</div>
|
|
"""
|
|
)
|
|
H.append(
|
|
"""<div class="scobox formation-actions">
|
|
<ul>"""
|
|
)
|
|
if has_perm_change:
|
|
H.append(
|
|
f"""
|
|
<li><a class="stdlink" href="{
|
|
url_for('notes.formsemestre_associate_new_version',
|
|
scodoc_dept=g.scodoc_dept, formation_id=formation_id
|
|
)
|
|
}">Créer une nouvelle version de la formation</a> (copie non verrouillée)
|
|
</li>
|
|
|
|
"""
|
|
)
|
|
if formsemestres:
|
|
H.append(
|
|
f"""
|
|
<li><a class="stdlink" href="{
|
|
url_for('notes.formation_delete',
|
|
scodoc_dept=g.scodoc_dept, formation_id=formation_id
|
|
)
|
|
}">Supprimer cette formation</a> (pas encore utilisée par des semestres)
|
|
</li>
|
|
"""
|
|
)
|
|
H.append(
|
|
f"""
|
|
<li><a class="stdlink" href="{
|
|
url_for('notes.formation_table_recap', scodoc_dept=g.scodoc_dept,
|
|
formation_id=formation_id)
|
|
}">Table récapitulative de la formation</a>
|
|
</li>
|
|
<li><a class="stdlink" href="{
|
|
url_for('notes.formation_tag_modules_by_type', scodoc_dept=g.scodoc_dept,
|
|
formation_id=formation_id,
|
|
semestre_idx=1 if semestre_idx is None else semestre_idx
|
|
)
|
|
}">Tagguer tous les modules par leur type</a> (tag <tt>res</tt>, <tt>sae</tt>).
|
|
</li>
|
|
<li><a class="stdlink" href="{
|
|
url_for('notes.formation_export', scodoc_dept=g.scodoc_dept,
|
|
formation_id=formation_id, fmt='xml')
|
|
}">Export XML de la formation</a> ou
|
|
<a class="stdlink" href="{
|
|
url_for('notes.formation_export', scodoc_dept=g.scodoc_dept,
|
|
formation_id=formation_id, fmt='xml', export_codes_apo=0)
|
|
}">sans codes Apogée</a>
|
|
(permet de l'enregistrer pour l'échanger avec un autre site)
|
|
</li>
|
|
|
|
<li><a class="stdlink" href="{
|
|
url_for('notes.formation_export', scodoc_dept=g.scodoc_dept,
|
|
formation_id=formation_id, fmt='json')
|
|
}">Export JSON de la formation</a>
|
|
</li>
|
|
|
|
<li><a class="stdlink" href="{
|
|
url_for('notes.module_table', scodoc_dept=g.scodoc_dept,
|
|
formation_id=formation_id)
|
|
}">Liste détaillée des modules de la formation</a> (debug)
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
"""
|
|
)
|
|
if has_perm_change or current_user.has_permission(Permission.EditFormSemestre):
|
|
H.append("""<div class="scobox">""")
|
|
if has_perm_change and len(formsemestres):
|
|
H.append(
|
|
"""
|
|
<div class="scobox-title">
|
|
<a name="sems">Semestres ou sessions de cette formation</a>
|
|
</div>
|
|
"""
|
|
)
|
|
|
|
H.append("<ul>")
|
|
for formsemestre in formsemestres:
|
|
H.append(f"""<li>{formsemestre.html_link_status()}""")
|
|
if not formsemestre.etat:
|
|
H.append(" [verrouillé]")
|
|
else:
|
|
H.append(
|
|
f""" <a class="stdlink"
|
|
href="{url_for("notes.formsemestre_editwithmodules",
|
|
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id
|
|
)}">Modifier</a>"""
|
|
)
|
|
H.append("</li>")
|
|
H.append("</ul>")
|
|
|
|
if current_user.has_permission(Permission.EditFormSemestre):
|
|
H.append(
|
|
f"""<ul>
|
|
<li><b><a class="stdlink" href="{
|
|
url_for('notes.formsemestre_createwithmodules', scodoc_dept=g.scodoc_dept,
|
|
formation_id=formation_id, semestre_id=1)
|
|
}">Mettre en place un nouveau semestre de formation {formation.acronyme}</a></b>
|
|
</li>
|
|
</ul>"""
|
|
)
|
|
H.append("</div>")
|
|
|
|
warn, _ = sco_formsemestre_validation.check_formation_ues(formation)
|
|
H.append(warn)
|
|
|
|
titre = f"Programme {formation.acronyme} v{formation.version}"
|
|
return render_template(
|
|
"sco_page_dept.j2",
|
|
content="".join(H),
|
|
title=titre,
|
|
page_title=titre,
|
|
cssstyles=["libjs/jQuery-tagEditor/jquery.tag-editor.css", "css/ue_table.css"],
|
|
javascripts=[
|
|
"libjs/jinplace-1.2.1.min.js",
|
|
"js/ue_list.js",
|
|
"js/edit_ue.js",
|
|
"libjs/jQuery-tagEditor/jquery.tag-editor.min.js",
|
|
"libjs/jQuery-tagEditor/jquery.caret.min.js",
|
|
"js/module_tag_editor.js",
|
|
],
|
|
)
|
|
|
|
|
|
def _html_select_semestre_idx(formation_id, semestre_ids, semestre_idx):
|
|
htm = """<form method="get">Semestre:
|
|
<select onchange="this.form.submit()" name="semestre_idx" id="semestre_idx" >
|
|
"""
|
|
for i in list(semestre_ids) + ["all"]:
|
|
if i == "all":
|
|
label = "tous"
|
|
else:
|
|
label = f"S{i}"
|
|
htm += f"""<option value="{i}" {
|
|
'selected'
|
|
if (semestre_idx == i)
|
|
or (i == "all" and semestre_idx is None)
|
|
else ''
|
|
}>{label}</option>
|
|
"""
|
|
|
|
htm += f"""
|
|
</select>
|
|
<input type="hidden" name="formation_id" value="{formation_id}"></input>
|
|
<input type="hidden" name="show_tags" value="0"></input>
|
|
</form>"""
|
|
return htm
|
|
|
|
|
|
def _ue_table_ues(
|
|
parcours,
|
|
ues: list[UniteEns],
|
|
editable,
|
|
tag_editable,
|
|
has_perm_change,
|
|
arrow_up,
|
|
arrow_down,
|
|
arrow_none,
|
|
delete_icon,
|
|
delete_disabled_icon,
|
|
) -> str:
|
|
"""Édition de programme: liste des UEs (avec leurs matières et modules).
|
|
Pour les formations classiques (non APC/BUT)
|
|
"""
|
|
H = []
|
|
cur_ue_semestre_id = None
|
|
iue = 0
|
|
for ue in ues:
|
|
ects_str = "" if ue.ects is None else f", {ue.ects:g} ECTS"
|
|
klass = "span_apo_edit" if editable else ""
|
|
edit_url = url_for(
|
|
"apiweb.ue_set_code_apogee",
|
|
scodoc_dept=g.scodoc_dept,
|
|
ue_id=ue.id,
|
|
)
|
|
code_apogee_str = f""", Apo: <span
|
|
class="{klass}" data-url="{edit_url}" id="{ue.id}"
|
|
data-placeholder="{scu.APO_MISSING_CODE_STR}">{
|
|
ue.code_apogee or ""
|
|
}</span>"""
|
|
|
|
if cur_ue_semestre_id != ue.get_semestre_id():
|
|
cur_ue_semestre_id = ue.get_semestre_id()
|
|
if ue.semestre_idx == codes_cursus.UE_SEM_DEFAULT:
|
|
lab = "Pas d'indication de semestre:"
|
|
else:
|
|
lab = f"""Semestre {ue.get_semestre_id()}:"""
|
|
H.append(
|
|
f'<div class="ue_list_div"><div class="ue_list_tit_sem">{lab}</div>'
|
|
)
|
|
H.append('<ul class="notes_ue_list">')
|
|
H.append('<li class="notes_ue_list">')
|
|
if iue != 0 and editable:
|
|
H.append(
|
|
f"""<a href="{
|
|
url_for( 'notes.ue_move',
|
|
scodoc_dept=g.scodoc_dept, ue_id=ue.id, after=0)}"
|
|
class="aud">{arrow_up}</a>"""
|
|
)
|
|
else:
|
|
H.append(arrow_none)
|
|
if iue < len(ues) - 1 and editable:
|
|
H.append(
|
|
f"""<a href="{
|
|
url_for( 'notes.ue_move',
|
|
scodoc_dept=g.scodoc_dept, ue_id=ue.id, after=1)}"
|
|
class="aud">{arrow_down}</a>"""
|
|
)
|
|
else:
|
|
H.append(arrow_none)
|
|
acro_titre = ue.acronyme
|
|
if ue.titre != ue.acronyme:
|
|
acro_titre += " " + (ue.titre or "")
|
|
H.append(
|
|
f"""{acro_titre} <span class="ue_code">(code {ue.ue_code}{ects_str}, coef. {
|
|
(ue.coefficient or 0):3.2f}{code_apogee_str})</span>
|
|
<span class="ue_coef"></span>
|
|
"""
|
|
)
|
|
if ue.type != codes_cursus.UE_STANDARD:
|
|
H.append(
|
|
f"""<span class="ue_type">{codes_cursus.UE_TYPE_NAME[ue.type]}</span>"""
|
|
)
|
|
if ue.is_external:
|
|
# Cas spécial: si l'UE externe a plus d'un module, c'est peut être une UE
|
|
# qui a été déclarée externe par erreur (ou suite à un bug d'import/export xml)
|
|
# Dans ce cas, propose de changer le type (même si verrouillée)
|
|
if ue.modules.count() > 1:
|
|
H.append('<span class="ue_is_external">')
|
|
if has_perm_change:
|
|
H.append(
|
|
f"""<a class="stdlink" href="{
|
|
url_for("notes.ue_set_internal",
|
|
scodoc_dept=g.scodoc_dept, ue_id=ue.id)
|
|
}">transformer en UE ordinaire</a> """
|
|
)
|
|
H.append("</span>")
|
|
ue_locked, ue_locked_reason = ue.is_locked()
|
|
ue_editable = editable and not ue_locked
|
|
if ue_editable:
|
|
H.append(
|
|
f"""<a class="stdlink" href="{
|
|
url_for("notes.ue_edit", scodoc_dept=g.scodoc_dept, ue_id=ue.id)
|
|
}">modifier</a>"""
|
|
)
|
|
else:
|
|
H.append(
|
|
f'<span class="locked fontred">[verrouillée: {ue_locked_reason}]</span>'
|
|
)
|
|
H.append(
|
|
_ue_table_matieres(
|
|
parcours,
|
|
ue,
|
|
editable,
|
|
tag_editable,
|
|
arrow_up,
|
|
arrow_down,
|
|
arrow_none,
|
|
delete_icon,
|
|
delete_disabled_icon,
|
|
)
|
|
)
|
|
if (iue >= len(ues) - 1) or (
|
|
ue.get_semestre_id() != ues[iue + 1].get_semestre_id()
|
|
):
|
|
H.append(
|
|
f"""</ul><ul><li><a href="{
|
|
url_for('notes.ue_create', scodoc_dept=g.scodoc_dept,
|
|
formation_id=ue.formation_id, semestre_idx=ue.get_semestre_id())
|
|
}">Ajouter une UE dans le semestre {ue.get_semestre_id() or ''}</a></li></ul>
|
|
</div>
|
|
"""
|
|
)
|
|
iue += 1
|
|
|
|
return "\n".join(H)
|
|
|
|
|
|
def _ue_table_matieres(
|
|
parcours,
|
|
ue: UniteEns,
|
|
editable,
|
|
tag_editable,
|
|
arrow_up,
|
|
arrow_down,
|
|
arrow_none,
|
|
delete_icon,
|
|
delete_disabled_icon,
|
|
):
|
|
"""Édition de programme: liste des matières (et leurs modules) d'une UE."""
|
|
H = []
|
|
if not parcours.UE_IS_MODULE:
|
|
H.append('<ul class="notes_matiere_list">')
|
|
matieres = ue.matieres.all()
|
|
for mat in matieres:
|
|
if not parcours.UE_IS_MODULE:
|
|
H.append('<li class="notes_matiere_list">')
|
|
if editable and not mat.is_locked():
|
|
H.append(
|
|
f"""<a class="stdlink" href="{
|
|
url_for("notes.matiere_edit",
|
|
scodoc_dept=g.scodoc_dept, matiere_id=mat.id)
|
|
}">
|
|
"""
|
|
)
|
|
H.append(f"{mat.titre or 'sans titre'}")
|
|
if editable and not mat.is_locked():
|
|
H.append("</a>")
|
|
|
|
modules = mat.modules.all()
|
|
H.append(
|
|
_ue_table_modules(
|
|
parcours,
|
|
ue,
|
|
mat,
|
|
modules,
|
|
editable,
|
|
tag_editable,
|
|
arrow_up,
|
|
arrow_down,
|
|
arrow_none,
|
|
delete_icon,
|
|
delete_disabled_icon,
|
|
)
|
|
)
|
|
if not parcours.UE_IS_MODULE:
|
|
H.append("</li>")
|
|
if not matieres:
|
|
H.append("<li>Aucune matière dans cette UE ! ")
|
|
if editable:
|
|
H.append(
|
|
f"""<a class="stdlink" href="{
|
|
url_for('notes.ue_delete', scodoc_dept=g.scodoc_dept, ue_id=ue.id)
|
|
}">supprimer l'UE</a>"""
|
|
)
|
|
H.append("</li>")
|
|
if editable and not parcours.UE_IS_MODULE:
|
|
H.append(
|
|
f"""<li><a class="stdlink" href="{
|
|
url_for("notes.matiere_create", scodoc_dept=g.scodoc_dept, ue_id=ue.id)
|
|
}">créer une matière</a>
|
|
</li>"""
|
|
)
|
|
if not parcours.UE_IS_MODULE:
|
|
H.append("</ul>")
|
|
return "\n".join(H)
|
|
|
|
|
|
def _ue_table_modules(
|
|
parcours,
|
|
ue: UniteEns,
|
|
mat: Matiere,
|
|
modules: list[Module],
|
|
editable,
|
|
tag_editable,
|
|
arrow_up,
|
|
arrow_down,
|
|
arrow_none,
|
|
delete_icon,
|
|
delete_disabled_icon,
|
|
add_suppress_link=True, # lien "supprimer cette matière"
|
|
empty_list_msg="Aucun élément dans cette matière",
|
|
create_element_msg="créer un module",
|
|
):
|
|
"""Édition de programme: liste des modules d'une matière d'une UE"""
|
|
H = ['<ul class="notes_module_list">']
|
|
im = 0
|
|
for mod in modules:
|
|
nb_moduleimpls = mod.modimpls.count()
|
|
klass = "notes_module_list"
|
|
if mod.module_type == ModuleType.MALUS:
|
|
klass += " module_malus"
|
|
H.append(f'<li class="{klass}">')
|
|
|
|
H.append('<span class="notes_module_list_buts">')
|
|
if im != 0 and editable:
|
|
H.append(
|
|
f"""<a href="module_move?module_id={mod.id}&after=0" class="aud">{arrow_up}</a>"""
|
|
)
|
|
else:
|
|
H.append(arrow_none)
|
|
if im < len(modules) - 1 and editable:
|
|
H.append(
|
|
f"""<a href="module_move?module_id={mod.id}&after=1" class="aud">{arrow_down}</a>"""
|
|
)
|
|
else:
|
|
H.append(arrow_none)
|
|
im += 1
|
|
icon = delete_icon if nb_moduleimpls == 0 and editable else delete_disabled_icon
|
|
H.append(
|
|
f"""<a class="smallbutton" href="{
|
|
url_for("notes.module_delete", scodoc_dept=g.scodoc_dept, module_id=mod.id)
|
|
}">{icon}</a>"""
|
|
)
|
|
H.append("</span>")
|
|
|
|
mod_editable = editable
|
|
if mod_editable:
|
|
H.append(
|
|
f"""<a class="discretelink" title="Modifier le module numéro %(numero)s, utilisé par
|
|
{nb_moduleimpls} sessions" href="{
|
|
url_for("notes.module_edit", scodoc_dept=g.scodoc_dept, module_id=mod.id)
|
|
}">"""
|
|
)
|
|
if mod.module_type not in (scu.ModuleType.STANDARD, scu.ModuleType.MALUS):
|
|
H.append(
|
|
f"""<span class="invalid-module-type">{scu.EMO_WARNING} type incompatible </span>"""
|
|
)
|
|
H.append(
|
|
f"""<span class="formation_module_tit">{scu.join_words(mod.code, mod.titre)}</span>"""
|
|
)
|
|
if mod_editable:
|
|
H.append("</a>")
|
|
heures = (
|
|
f"""{mod.heures_cours or 0}/{mod.heures_td or 0}/{mod.heures_tp or 0}, """
|
|
if (mod.heures_cours or mod.heures_td or mod.heures_tp)
|
|
else ""
|
|
)
|
|
heurescoef = f"""{heures}coef. {mod.coefficient}"""
|
|
edit_url = url_for(
|
|
"apiweb.formation_module_set_code_apogee",
|
|
scodoc_dept=g.scodoc_dept,
|
|
module_id=mod.id,
|
|
)
|
|
heurescoef += f""", Apo: <span
|
|
class="{'span_apo_edit' if editable else ''}"
|
|
data-url="{edit_url}" id="{mod.id}"
|
|
data-placeholder="{scu.APO_MISSING_CODE_STR}">{
|
|
mod.code_apogee or ""
|
|
}</span>"""
|
|
tag_cls = "module_tag_editor" if tag_editable else "module_tag_editor_ro"
|
|
tag_edit = f"""<span class="sco_tag_edit">
|
|
<form>
|
|
<textarea data-module_id="{mod.id}" class="{tag_cls}">{
|
|
",".join([ tag.title for tag in mod.tags ])
|
|
}</textarea>
|
|
</form>
|
|
</span>"""
|
|
if ue.semestre_idx is not None and mod.semestre_id != ue.semestre_idx:
|
|
warning_semestre = ' <span class="red">incohérent ?</span>'
|
|
else:
|
|
warning_semestre = ""
|
|
H.append(
|
|
f""" {parcours.SESSION_NAME} {mod.semestre_id}{warning_semestre}
|
|
{heurescoef}{tag_edit}"""
|
|
)
|
|
H.append("</li>")
|
|
if not modules:
|
|
H.append(f"<li>{empty_list_msg} ! ")
|
|
if editable and add_suppress_link:
|
|
H.append(
|
|
f"""<a class="stdlink" href="{
|
|
url_for("notes.matiere_delete",
|
|
scodoc_dept=g.scodoc_dept, matiere_id=mat.id)}"
|
|
>la supprimer</a>
|
|
"""
|
|
)
|
|
H.append("</li>")
|
|
if editable: # and ((not parcours.UE_IS_MODULE) or len(Modlist) == 0):
|
|
H.append(
|
|
f"""<li> <a class="stdlink" href="{
|
|
url_for("notes.module_create",
|
|
scodoc_dept=g.scodoc_dept, matiere_id=mat.id)}"
|
|
>{create_element_msg}</a></li>
|
|
"""
|
|
)
|
|
H.append("</ul>")
|
|
return "\n".join(H)
|
|
|
|
|
|
def ue_sharing_code(ue_code: str = "", ue_id: int = None, hide_ue_id: int = None):
|
|
"""HTML list of UE sharing this code
|
|
Either ue_code or ue_id may be specified.
|
|
hide_ue_id spécifie un id à retirer de la liste.
|
|
"""
|
|
if ue_id is not None:
|
|
ue = UniteEns.query.get_or_404(ue_id)
|
|
if not ue_code:
|
|
ue_code = ue.ue_code
|
|
formation_code = ue.formation.formation_code
|
|
# UE du même code, code formation et departement:
|
|
q_ues = (
|
|
UniteEns.query.filter_by(ue_code=ue_code)
|
|
.join(UniteEns.formation)
|
|
.filter_by(dept_id=g.scodoc_dept_id, formation_code=formation_code)
|
|
)
|
|
else:
|
|
# Toutes les UE du departement avec ce code:
|
|
q_ues = (
|
|
UniteEns.query.filter_by(ue_code=ue_code)
|
|
.join(UniteEns.formation)
|
|
.filter_by(dept_id=g.scodoc_dept_id)
|
|
)
|
|
|
|
if hide_ue_id is not None: # enlève l'ue de depart
|
|
q_ues = q_ues.filter(UniteEns.id != hide_ue_id)
|
|
|
|
ues = q_ues.all()
|
|
msg = " dans les formations du département "
|
|
if not ues:
|
|
if ue_id is not None:
|
|
return f"""<span class="ue_share">Seule UE avec code {
|
|
ue_code if ue_code is not None else '-'}{msg}</span>"""
|
|
else:
|
|
return f"""<span class="ue_share">Aucune UE avec code {
|
|
ue_code if ue_code is not None else '-'}{msg}</span>"""
|
|
H = []
|
|
if ue_id:
|
|
H.append(
|
|
f"""<span class="ue_share">Pour information, autres UEs avec le code {
|
|
ue_code if ue_code is not None else '-'}{msg}:</span>"""
|
|
)
|
|
else:
|
|
H.append(
|
|
f"""<span class="ue_share">UE avec le code {
|
|
ue_code if ue_code is not None else '-'}{msg}:</span>"""
|
|
)
|
|
H.append("<ul>")
|
|
for ue in ues:
|
|
H.append(
|
|
f"""<li>{ue.acronyme} ({ue.titre or ''}) dans
|
|
<a class="stdlink" href="{
|
|
url_for("notes.ue_table",
|
|
scodoc_dept=g.scodoc_dept, formation_id=ue.formation.id)}"
|
|
>{ue.formation.acronyme} ({ue.formation.titre})</a>, version {ue.formation.version}
|
|
</li>
|
|
"""
|
|
)
|
|
H.append("</ul>")
|
|
return "\n".join(H)
|
|
|
|
|
|
def do_ue_edit(args, bypass_lock=False, dont_invalidate_cache=False):
|
|
"edit an UE"
|
|
# check
|
|
ue_id = args["ue_id"]
|
|
ue = UniteEns.get_ue(ue_id)
|
|
if not bypass_lock:
|
|
ue_locked, ue_locked_reason = ue.is_locked()
|
|
if ue_locked:
|
|
raise ScoLockedFormError(msg=f"UE verrouillée: {ue_locked_reason}")
|
|
# check: acronyme unique dans cette formation
|
|
if "acronyme" in args:
|
|
new_acro = args["acronyme"]
|
|
ues = UniteEns.query.filter_by(
|
|
formation_id=ue.formation_id, acronyme=new_acro
|
|
).all()
|
|
if ues and ues[0].id != ue_id:
|
|
raise ScoValueError(
|
|
f"""Acronyme d'UE "{args['acronyme']}" déjà utilisé !
|
|
(chaque UE doit avoir un acronyme unique dans la formation.)"""
|
|
)
|
|
# On ne peut pas supprimer le code UE:
|
|
if "ue_code" in args and not args["ue_code"]:
|
|
del args["ue_code"]
|
|
|
|
ue.from_dict(args)
|
|
db.session.commit()
|
|
if not dont_invalidate_cache:
|
|
# Invalide les semestres utilisant cette formation
|
|
# ainsi que les poids et coefs
|
|
ue.formation.invalidate_module_coefs()
|
|
|
|
|
|
UE_PALETTE = [
|
|
"#B80004", # rouge
|
|
"#F97B3D", # Orange Crayola
|
|
"#FEB40B", # Honey Yellow
|
|
"#80CB3F", # Yellow Green
|
|
"#05162E", # Oxford Blue
|
|
"#548687", # Steel Teal
|
|
"#444054", # Independence
|
|
"#889696", # Spanish Gray
|
|
"#0CA4A5", # Viridian Green
|
|
]
|
|
|
|
|
|
def colorie_anciennes_ues(ues: list[UniteEns]) -> None:
|
|
"""Avant ScoDoc 9.2, les ue n'avaient pas de couleurs
|
|
Met des défauts raisonnables
|
|
"""
|
|
nb_colors = len(UE_PALETTE)
|
|
index = 0
|
|
last_sem_idx = 0
|
|
for ue in ues:
|
|
if ue.semestre_idx != last_sem_idx:
|
|
index = 0
|
|
last_sem_idx = ue.semestre_idx
|
|
if ue.color is None:
|
|
ue.color = UE_PALETTE[index % nb_colors]
|
|
index += 1
|
|
db.session.add(ue)
|
|
|
|
|
|
def ue_guess_color_default(formation_id: int, default_semestre_idx: int) -> str:
|
|
"""Un code couleur pour une nouvelle UE dans ce semestre"""
|
|
nb_colors = len(UE_PALETTE)
|
|
# UE existantes dans ce semestre:
|
|
nb_ues = UniteEns.query.filter(
|
|
UniteEns.formation_id == formation_id,
|
|
UniteEns.semestre_idx == default_semestre_idx,
|
|
).count()
|
|
index = nb_ues
|
|
return UE_PALETTE[index % nb_colors]
|