# -*- 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"""
Des étudiants ont une décision de jury sur l'UE {ue.acronyme} ({ue.titre})
Si vous supprimez cette UE, ces décisions vont être supprimées !
""",
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.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 Utiliser les coefficients d'UE pour calculer
la moyenne générale 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"""
{ue.modules.count()} modules sont rattachés
à cette UE du semestre S{ue.semestre_idx},
elle ne peut donc pas être changée de semestre.
"""
else:
modules_div = ""
if ue:
clone_form = f"""
"""
else:
clone_form = ""
return render_template(
"sco_page_dept.j2",
title=title,
javascripts=["js/edit_ue.js"],
content=f"""
{title}, (formation {formation.acronyme}, version {formation.version})
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")).
Note: sauf exception, l'UE n'a pas de coefficient associé.
Seuls les modules ont des coefficients.
Édition de l'UE {('du semestre S'+str(ue.semestre_idx)) if is_apc and ue else ''}
{tf[1]}
{clone_form}
{ue_parcours_div}
{modules_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.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"Suppression de l'UE {ue.titre or ''} ({ue.acronyme})
",
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"""{formation.html()} {lockicon}
""",
]
if locked:
H.append(
"""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:
- 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);
- 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).
"""
)
if msg:
H.append('' + msg + "
")
if ues_with_duplicated_code:
H.append(
f"""Attention: plusieurs UEs de cette
formation ont le même code : {
', '.join([
'' + ue.acronyme + " (code " + ue.ue_code + ")"
for ue in ues_with_duplicated_code ])
}.
Il faut corriger cela, sinon les capitalisations et ECTS seront
erronés ! """
)
# 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 = (
"""verrouillé (voir liste des semestres utilisateurs
en bas de page)
"""
if locked
else ""
)
H.append(
f"""
Formation par compétences (BUT)
- {_html_select_semestre_idx(formation_id, semestre_ids, semestre_idx)}
{lock_info}
"""
)
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:
{formation.referentiel_competence.get_title()}
"""
msg_refcomp = "changer"
H.append(f"""
- {descr_refcomp}""")
if current_user.has_permission(Permission.EditFormation):
if (
formation.referentiel_competence is None
or formation.formsemestres.count() == 0
):
H.append(
f"""{msg_refcomp}"""
)
elif formation.referentiel_competence is not None:
H.append("""(non modifiable car utilisé par des semestres)""")
H.append("
")
if formation.referentiel_competence is not None:
H.append(
"""- Parcours, compétences et UEs :
""")
H.append(
f"""
- {'Visualiser' if locked else 'Éditer'} les coefficients des ressources et SAÉs
"""
)
# Description des UE/matières/modules
H.append(
f"""
") # formation_ue_list
if ues_externes:
H.append(
f"""
"""
)
H.append(
"""
"""
)
if has_perm_change or current_user.has_permission(Permission.EditFormSemestre):
H.append("""
""")
if has_perm_change and len(formsemestres):
H.append(
"""
"""
)
H.append("
")
for formsemestre in formsemestres:
H.append(f"""- {formsemestre.html_link_status()}""")
if not formsemestre.etat:
H.append(" [verrouillé]")
else:
H.append(
f""" Modifier"""
)
H.append("
")
H.append("
")
if current_user.has_permission(Permission.EditFormSemestre):
H.append(
f"""
"""
)
H.append("
")
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 = """
"""
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:
{
ue.code_apogee or ""
}"""
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'
{lab}
'
)
H.append('
')
H.append('- ')
if iue != 0 and editable:
H.append(
f"""{arrow_up}"""
)
else:
H.append(arrow_none)
if iue < len(ues) - 1 and editable:
H.append(
f"""{arrow_down}"""
)
else:
H.append(arrow_none)
acro_titre = ue.acronyme
if ue.titre != ue.acronyme:
acro_titre += " " + (ue.titre or "")
H.append(
f"""{acro_titre} (code {ue.ue_code}{ects_str}, coef. {
(ue.coefficient or 0):3.2f}{code_apogee_str})
"""
)
if ue.type != codes_cursus.UE_STANDARD:
H.append(
f"""{codes_cursus.UE_TYPE_NAME[ue.type]}"""
)
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('')
if has_perm_change:
H.append(
f"""transformer en UE ordinaire """
)
H.append("")
ue_locked, ue_locked_reason = ue.is_locked()
ue_editable = editable and not ue_locked
if ue_editable:
H.append(
f"""modifier"""
)
else:
H.append(
f'[verrouillée: {ue_locked_reason}]'
)
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"""
"""
)
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('
')
matieres = ue.matieres.all()
for mat in matieres:
if not parcours.UE_IS_MODULE:
H.append('- ')
if editable and not mat.is_locked():
H.append(
f"""
"""
)
H.append(f"{mat.titre or 'sans titre'}")
if editable and not mat.is_locked():
H.append("")
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("
")
if not matieres:
H.append("- Aucune matière dans cette UE ! ")
if editable:
H.append(
f"""supprimer l'UE"""
)
H.append("
")
if editable and not parcours.UE_IS_MODULE:
H.append(
f"""- créer une matière
"""
)
if not parcours.UE_IS_MODULE:
H.append("
")
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 = ['
']
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'- ')
H.append('')
if im != 0 and editable:
H.append(
f"""{arrow_up}"""
)
else:
H.append(arrow_none)
if im < len(modules) - 1 and editable:
H.append(
f"""{arrow_down}"""
)
else:
H.append(arrow_none)
im += 1
icon = delete_icon if nb_moduleimpls == 0 and editable else delete_disabled_icon
H.append(
f"""{icon}"""
)
H.append("")
mod_editable = editable
if mod_editable:
H.append(
f""""""
)
if mod.module_type not in (scu.ModuleType.STANDARD, scu.ModuleType.MALUS):
H.append(
f"""{scu.EMO_WARNING} type incompatible """
)
H.append(
f"""{scu.join_words(mod.code, mod.titre)}"""
)
if mod_editable:
H.append("")
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: {
mod.code_apogee or ""
}"""
tag_cls = "module_tag_editor" if tag_editable else "module_tag_editor_ro"
tag_edit = f"""
"""
if ue.semestre_idx is not None and mod.semestre_id != ue.semestre_idx:
warning_semestre = ' incohérent ?'
else:
warning_semestre = ""
H.append(
f""" {parcours.SESSION_NAME} {mod.semestre_id}{warning_semestre}
{heurescoef}{tag_edit}"""
)
H.append("
")
if not modules:
H.append(f"- {empty_list_msg} ! ")
if editable and add_suppress_link:
H.append(
f"""la supprimer
"""
)
H.append("
")
if editable: # and ((not parcours.UE_IS_MODULE) or len(Modlist) == 0):
H.append(
f"""- {create_element_msg}
"""
)
H.append("
")
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.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"""
Seule UE avec code {
ue_code if ue_code is not None else '-'}{msg}"""
else:
return f"""
Aucune UE avec code {
ue_code if ue_code is not None else '-'}{msg}"""
H = []
if ue_id:
H.append(
f"""
Pour information, autres UEs avec le code {
ue_code if ue_code is not None else '-'}{msg}:"""
)
else:
H.append(
f"""
UE avec le code {
ue_code if ue_code is not None else '-'}{msg}:"""
)
H.append("
")
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]