# -*- 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
#
##############################################################################
"""Form choix modules / responsables et creation formsemestre
"""
import flask
from flask import flash, g, request, redirect, render_template, url_for
from flask_login import current_user
import sqlalchemy as sa
from app import db
from app.auth.models import User
from app.models import APO_CODE_STR_LEN, SHORT_STR_LEN
from app.models import (
ApcValidationAnnee,
ApcValidationRCUE,
Evaluation,
FormationModalite,
FormSemestreUECoef,
Module,
ModuleImpl,
ScoDocSiteConfig,
ScolarAutorisationInscription,
ScolarFormSemestreValidation,
ScolarNews,
UniteEns,
)
from app.models.formations import Formation
from app.models.formsemestre import FormSemestre, FormSemestreEtape
from app.models.but_refcomp import ApcParcours
import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu
from app.scodoc import sco_cache
from app.scodoc import sco_groups
from app import log
from app.scodoc.TrivialFormulator import TrivialFormulator
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_vdi import ApoEtapeVDI
from app.scodoc import codes_cursus
from app.scodoc import sco_formsemestre
from app.scodoc import sco_groups_copy
from app.scodoc import sco_moduleimpl
from app.scodoc import sco_permissions_check
from app.scodoc import sco_portal_apogee
from app.scodoc import sco_preferences
from app.scodoc import sco_users
def _default_sem_title(formation):
"""Default title for a semestre in formation"""
return formation.acronyme
def formsemestre_createwithmodules():
"""Page création d'un semestre"""
H = ["""
Mise en place d'un semestre de formation
"""]
r = do_formsemestre_createwithmodules()
if not isinstance(r, str):
return r # response redirect
H.append(r)
return render_template(
"sco_page_dept.j2",
title="Création d'un semestre",
content="\n".join(H),
javascripts=["libjs/AutoSuggest.js", "js/formsemestre_edit.js"],
cssstyles=["css/autosuggest_inquisitor.css"],
)
def formsemestre_editwithmodules(formsemestre_id: int):
"""Page modification semestre"""
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
H = []
if not formsemestre.etat:
H.append(
f"""
{scu.icontag(
"lock_img", border="0", title="Semestre verrouillé")
}Ce semestre est verrouillé.
"""
)
else:
r = do_formsemestre_createwithmodules(edit=True, formsemestre=formsemestre)
if isinstance(r, str):
H.append(r)
else:
return r # response redirect
vals = scu.get_request_args()
if not vals.get("tf_submitted", False):
H.append(
"""
Seuls les modules cochés font partie de ce semestre.
Pour les retirer, les décocher et appuyer sur le bouton "modifier".
Attention : s'il y a déjà des évaluations dans un module,
il ne peut pas être supprimé !
Les modules ont toujours un responsable.
Par défaut, c'est le directeur des études.
Un semestre ne peut comporter qu'une seule UE "bonus
sport/culture"
" + "\n".join(H),
)
def can_edit_sem(formsemestre_id: int = None, sem=None):
"""Return sem if user can edit it, False otherwise"""
sem = sem or sco_formsemestre.get_formsemestre(formsemestre_id)
if not current_user.has_permission(Permission.EditFormSemestre): # pas chef
if not sem["resp_can_edit"] or current_user.id not in sem["responsables"]:
return False
return sem
RESP_FIELDS = [
"responsable_id",
"responsable_id2",
"responsable_id3",
"responsable_id4",
]
def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = None):
"Form choix modules / responsables et création formsemestre"
vals = scu.get_request_args()
# Fonction accessible à tous, contrôle d'acces à la main:
if not current_user.has_permission(Permission.EditFormSemestre):
if not edit:
# il faut EditFormSemestre pour créer un semestre
raise AccessDenied("vous n'avez pas le droit d'effectuer cette opération")
else:
if not formsemestre.resp_can_edit or current_user.id not in (
u.id for u in formsemestre.responsables
):
raise AccessDenied(
"vous n'avez pas le droit d'effectuer cette opération"
)
# Liste des enseignants avec form pour affichage / saisie avec suggestion
# attention: il faut prendre ici tous les utilisateurs, même inactifs, car
# les responsables de modules d'anciens semestres peuvent ne plus être actifs.
# Mais la suggestion utilise get_user_list_xml() qui ne suggérera que les actifs.
user_list = sco_users.get_user_list(with_inactives=True)
uid2display = {} # user_name : forme pour affichage = "NOM Prenom (login)"
for u in user_list:
uid2display[u.id] = u.get_nomplogin()
allowed_user_names = list(uid2display.values()) + [""]
#
if formsemestre:
formation = formsemestre.formation
else:
formation_id = int(vals["formation_id"])
formation = Formation.get_or_404(formation_id)
is_apc = formation.is_apc()
if not edit:
initvalues = {"titre": _default_sem_title(formation)}
try:
semestre_id = int(vals["semestre_id"])
except ValueError as exc:
raise ScoValueError("valeur invalide pour l'indice de semestre") from exc
module_ids_set = set()
else:
# setup form init values
initvalues = formsemestre.to_dict()
semestre_id = formsemestre.semestre_id
# add associated modules to tf-checked:
module_ids_existing = [modimpl.module.id for modimpl in formsemestre.modimpls]
module_ids_set = set(module_ids_existing)
initvalues["tf-checked"] = ["MI" + str(x) for x in module_ids_existing]
for modimpl in formsemestre.modimpls:
initvalues[f"MI{modimpl.module.id}"] = uid2display.get(
modimpl.responsable_id,
f"inconnu numéro {modimpl.responsable_id} resp. de {modimpl.id} !",
)
for index, resp in enumerate(formsemestre.responsables):
initvalues[RESP_FIELDS[index]] = uid2display.get(resp.id)
group_tous = formsemestre.get_default_group()
if group_tous:
initvalues["edt_promo_id"] = group_tous.edt_id or ""
# Liste des ID de semestres
if formation.type_parcours is not None:
parcours = codes_cursus.get_cursus_from_code(formation.type_parcours)
nb_sem = parcours.NB_SEM
else:
nb_sem = 10 # fallback, max 10 semestres
if nb_sem == 1:
semestre_id_list = [-1]
else:
if edit and is_apc:
# en APC, ne permet pas de changer de semestre
semestre_id_list = [formsemestre.semestre_id]
else:
semestre_id_list = list(range(1, nb_sem + 1))
if not is_apc:
# propose "pas de semestre" seulement en classique
semestre_id_list.insert(0, -1)
semestre_id_labels = []
for sid in semestre_id_list:
if sid == -1:
semestre_id_labels.append("pas de semestres")
else:
semestre_id_labels.append(f"S{sid}")
# Liste des modules dans cette formation
if is_apc:
# BUT: trie par type (res, sae), parcours, numéro
modules = sorted(
formation.modules,
key=lambda m: m.sort_key_apc(),
)
else:
modules = (
Module.query.filter(
Module.formation_id == formation.id, UniteEns.id == Module.ue_id
)
.order_by(Module.module_type, UniteEns.numero, Module.numero)
.all()
)
# Elimine les ressources et SAE sauf si déjà dans le semestre
modules = [
m
for m in modules
if (m.module_type not in (scu.ModuleType.RESSOURCE, scu.ModuleType.SAE))
or m.id in module_ids_set
]
# Pour regroupement des modules par semestres:
semestre_ids = sorted({mod.semestre_id for mod in modules})
# Liste des modalités
modalites_abbrv_titres = {
modalite.modalite: modalite.titre
for modalite in FormationModalite.query.order_by(FormationModalite.numero)
}
#
modform = [
("formsemestre_id", {"input_type": "hidden"}),
("formation_id", {"input_type": "hidden", "default": formation.id}),
(
"date_debut",
{
"title": "Date de début", # j/m/a
"input_type": "datedmy",
"explanation": "j/m/a",
"size": 9,
"allow_null": False,
},
),
(
"date_fin",
{
"title": "Date de fin", # j/m/a
"input_type": "datedmy",
"explanation": "j/m/a",
"size": 9,
"allow_null": False,
},
),
*[
(
field,
{
"input_type": "text_suggest",
"size": 50,
"title": (
"(Co-)Directeur(s) des études"
if index
else "Directeur des études"
),
"explanation": (
"(facultatif) taper le début du nom et choisir dans le menu"
if index
else "(obligatoire) taper le début du nom et choisir dans le menu"
),
"allowed_values": allowed_user_names,
"allow_null": index, # > 0, # il faut au moins un responsable de semestre
"text_suggest_options": {
"script": url_for(
"users.get_user_list_xml", scodoc_dept=g.scodoc_dept
)
+ "?", # "Users/get_user_list_xml?",
"varname": "start",
"json": False,
"noresults": "Valeur invalide !",
"timeout": 60000,
},
},
)
for index, field in enumerate(RESP_FIELDS)
],
(
"titre",
{
"size": 40,
"title": "Nom de ce semestre",
"explanation": f"""n'indiquez pas les dates, ni le semestre, ni la modalité dans
le titre: ils seront automatiquement ajoutés """,
"allow_null": False,
},
),
(
"modalite",
{
"input_type": "menu",
"title": "Modalité",
"allowed_values": modalites_abbrv_titres.keys(),
"labels": modalites_abbrv_titres.values(),
},
),
(
"semestre_id",
{
"input_type": "menu",
"title": "Semestre dans la formation",
"allowed_values": semestre_id_list,
"labels": semestre_id_labels,
"explanation": (
"en BUT, on ne peut pas modifier le semestre après création"
if is_apc
else ""
),
"attributes": ['onchange="change_semestre_id();"'] if is_apc else "",
},
),
(
"capacite_accueil",
{
"title": "Capacité d'accueil",
"size": 4,
"explanation": "nombre max d'inscrits (hors démissionnaires). Laisser vide si pas de limite.",
"type": "int",
"allow_null": True,
},
),
]
etapes = sco_portal_apogee.get_etapes_apogee_dept()
# Propose les etapes renvoyées par le portail
# et ajoute les étapes du semestre qui ne sont pas dans la liste
# (soit la liste a changé, soit l'étape a été ajoutée manuellement)
etapes_set = {et[0] for et in etapes}
if edit:
for etape_vdi in formsemestre.etapes_apo_vdi():
if etape_vdi.etape not in etapes_set:
etapes.append((etape_vdi.etape, "inconnue"))
modform.append(
(
"elt_help_apo",
{
"title": "Codes Apogée nécessaires pour inscrire les étudiants et exporter les notes en fin de semestre:",
"input_type": "separator",
},
)
)
mf_manual = {
"size": 12,
"template": '
%(label)s
%(elem)s',
"validator": lambda val, _: len(val) < APO_CODE_STR_LEN,
}
if etapes:
mf = {
"input_type": "menu",
"allowed_values": [""] + [e[0] for e in etapes],
"labels": ["(aucune)"] + ["%s (%s)" % (e[1], e[0]) for e in etapes],
"template": '
Sélectionner les modules, leurs responsables et les étudiants
à inscrire:
"""
else:
formtit = """
Sélectionner les modules et leurs responsables
Si vous avez des parcours (options), dans un premier temps
ne sélectionnez que les modules du tronc commun, puis après inscriptions,
revenez ajouter les modules de parcours en sélectionnant les groupes d'étudiants
à y inscrire.
"""
modform += [
(
"gestion_compensation_lst",
{
"input_type": "checkbox",
"title": "Jurys",
"allowed_values": ["X"],
"explanation": "proposer compensations de semestres (parcours DUT)",
"labels": [""],
},
),
(
"gestion_semestrielle_lst",
{
"input_type": "checkbox",
"title": "",
"allowed_values": ["X"],
"explanation": "formation semestrialisée (jurys avec semestres décalés)",
"labels": [""],
},
),
]
if current_user.has_permission(Permission.EditFormSemestre):
modform += [
(
"resp_can_edit",
{
"input_type": "boolcheckbox",
"title": "Autorisations",
"explanation": "Autoriser le directeur des études à modifier ce semestre",
},
)
]
modform += [
(
"resp_can_change_ens",
{
"input_type": "boolcheckbox",
"title": "",
"explanation": "Autoriser le directeur des études à modifier les enseignants",
},
),
(
"ens_can_edit_eval",
{
"input_type": "boolcheckbox",
"title": "",
"explanation": """Autoriser tous les enseignants associés
à un module à y créer des évaluations""",
},
),
(
"bul_bgcolor",
{
"size": 8,
"title": "Couleur fond des bulletins",
"explanation": "version web seulement (ex: #ffeeee)",
"validator": lambda val, _: len(val) < SHORT_STR_LEN,
},
),
(
"bul_publish_xml_lst",
{
"input_type": "checkbox",
"title": "Publication",
"allowed_values": ["X"],
"explanation": "publier le bulletin sur la passerelle étudiants",
"labels": [""],
},
),
(
"block_moyennes",
{
"input_type": "boolcheckbox",
"title": "Bloquer moyennes",
"explanation": "empêcher le calcul des moyennes d'UE et générale (en BUT, empèche sa prise en compte dans les jurys annuels)",
},
),
(
"block_moyenne_generale",
{
"input_type": "boolcheckbox",
"title": "Pas de moyenne générale",
"explanation": "ne pas calculer la moyenne générale indicative (en BUT)",
},
),
]
# Choix des parcours
if is_apc:
ref_comp = formation.referentiel_competence
if ref_comp:
modform += [
(
"parcours",
{
"input_type": "checkbox",
"vertical": True,
"dom_id": "tf_module_parcours",
"labels": [parcour.libelle for parcour in ref_comp.parcours],
"allowed_values": [
str(parcour.id) for parcour in ref_comp.parcours
],
"explanation": """Parcours proposés dans ce semestre.
Cocher tous les parcours est exactement équivalent à n'en cocher aucun:
par exemple, pour un semestre de "tronc commun", on peut ne pas indiquer de parcours.
Si aucun parcours n'est coché, toutes les UEs du
programme seront donc considérées, quel que soit leur parcours.
""",
},
)
]
if edit:
initvalues["parcours"] = [
str(parcour.id) for parcour in formsemestre.get_parcours_apc()
]
else:
modform += [
(
"parcours",
{
"input_type": "separator",
"title": f"""{scu.EMO_WARNING }
Pas de parcours:
vérifier la formation""",
},
)
]
# Choix des modules
modform += [
(
"sep",
{
"input_type": "separator",
"title": "",
"template": f"{formtit}
",
},
),
]
nbmod = 0
for semestre_id in semestre_ids:
if is_apc:
# pour restreindre l'édition aux modules du semestre sélectionné
tr_class = f'class="sem{semestre_id}"'
else:
tr_class = ""
if edit:
templ_sep = f"""
%(label)s
Responsable
Inscrire
"""
else:
templ_sep = (
f"""
%(label)s
Responsable
"""
)
modform.append(
(
"sep",
{
"input_type": "separator",
"title": f"Semestre {semestre_id}",
"template": templ_sep,
},
)
)
for mod in modules:
if mod.semestre_id == semestre_id and (
(not edit) # creation => tous modules
or (not is_apc) # pas BUT, on peut mixer les semestres
or (semestre_id == formsemestre.semestre_id) # module du semestre
or (mod.id in module_ids_set) # module déjà présent
):
nbmod += 1
if edit:
select_name = f"{mod.id}!group_id"
def opt_selected(gid):
if gid == vals.get(select_name):
return "selected"
return ""
if mod.id in module_ids_set:
# pas de menu inscription si le module est déjà présent
disabled = "disabled"
else:
disabled = ""
fcg = f'"
itemtemplate = f"""
%(label)s
%(elem)s
{fcg}
"""
else:
itemtemplate = f"""
%(label)s
%(elem)s
"""
modform.append(
(
"MI" + str(mod.id),
{
"input_type": "text_suggest",
"size": 50,
"withcheckbox": True,
"title": f"""{mod.code or ""} {mod.titre or ""}""",
"allowed_values": allowed_user_names,
"template": itemtemplate,
"text_suggest_options": {
"script": url_for(
"users.get_user_list_xml", scodoc_dept=g.scodoc_dept
)
+ "?",
"varname": "start",
"json": False,
"noresults": "Valeur invalide !",
"timeout": 60000,
},
},
)
)
if nbmod == 0:
modform.append(
(
"sep",
{
"input_type": "separator",
"title": "aucun module dans cette formation !!!",
},
)
)
if edit:
submitlabel = "Modifier ce semestre"
else:
submitlabel = "Créer ce semestre de formation"
#
# Etapes:
if edit:
n = 1
for etape_vdi in formsemestre.etapes_apo_vdi():
initvalues["etape_apo" + str(n)] = etape_vdi.etape
initvalues["vdi_apo" + str(n)] = etape_vdi.vdi
n += 1
#
initvalues["gestion_compensation"] = initvalues.get("gestion_compensation", False)
if initvalues["gestion_compensation"]:
initvalues["gestion_compensation_lst"] = ["X"]
else:
initvalues["gestion_compensation_lst"] = []
if vals.get("tf_submitted", False) and "gestion_compensation_lst" not in vals:
vals["gestion_compensation_lst"] = []
initvalues["gestion_semestrielle"] = initvalues.get("gestion_semestrielle", False)
if initvalues["gestion_semestrielle"]:
initvalues["gestion_semestrielle_lst"] = ["X"]
else:
initvalues["gestion_semestrielle_lst"] = []
if vals.get("tf_submitted", False) and "gestion_semestrielle_lst" not in vals:
vals["gestion_semestrielle_lst"] = []
initvalues["bul_hide_xml"] = initvalues.get("bul_hide_xml", False)
if not initvalues["bul_hide_xml"]:
initvalues["bul_publish_xml_lst"] = ["X"]
else:
initvalues["bul_publish_xml_lst"] = []
if vals.get("tf_submitted", False) and "bul_publish_xml_lst" not in vals:
vals["bul_publish_xml_lst"] = []
#
tf = TrivialFormulator(
request.base_url,
vals,
modform,
submitlabel=submitlabel,
cancelbutton="Annuler",
top_buttons=True,
initvalues=initvalues,
)
msg = ""
if tf[0] == 1:
# convert and check dates
tf[2]["date_debut"] = scu.convert_fr_date(tf[2]["date_debut"])
tf[2]["date_fin"] = scu.convert_fr_date(tf[2]["date_fin"])
if tf[2]["date_debut"] > tf[2]["date_fin"]:
msg = """
Dates de début et fin incompatibles !
"""
if (
sco_preferences.get_preference("always_require_apo_sem_codes")
and not any(
tf[2]["etape_apo" + str(n)] for n in range(0, scu.EDIT_NB_ETAPES + 1)
)
# n'impose pas d'Apo pour les sem. extérieurs
and ((formsemestre is None) or formsemestre.modalite != "EXT")
):
msg = '
Code étape Apogée manquant
'
# check capacité accueil si indiquée
if edit and isinstance(tf[2]["capacite_accueil"], int):
new_capacite_accueil = tf[2]["capacite_accueil"]
inscriptions = formsemestre.get_inscrits(etats={scu.INSCRIT, scu.DEF})
if len(inscriptions) > new_capacite_accueil:
msg = f"""
Capacité d'accueil insuffisante
(il y a {len(inscriptions)} inscrits non démissionaires)
{msg}
{tf[1]}
"""
if tf[0] == -1: # Annulation
if formsemestre:
return redirect(
url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
)
)
return redirect(url_for("notes.index_html", scodoc_dept=g.scodoc_dept))
# Edition ou modification du semestre
tf[2]["gestion_compensation"] = bool(tf[2]["gestion_compensation_lst"])
tf[2]["gestion_semestrielle"] = bool(tf[2]["gestion_semestrielle_lst"])
tf[2]["bul_hide_xml"] = not bool(tf[2]["bul_publish_xml_lst"])
_remap_resp_modimpls(tf[2])
if "parcours" in tf[2]:
tf[2]["parcours"] = [
db.session.get(ApcParcours, int(parcour_id_str))
for parcour_id_str in tf[2]["parcours"]
]
# Modules sélectionnés:
# (retire le "MI" du début du nom de champs)
module_ids_checked = [int(x[2:]) for x in tf[2]["tf-checked"]]
_formsemestre_check_ue_bonus_unicity(module_ids_checked)
ok = False
if not edit:
if is_apc:
_formsemestre_check_module_list(module_ids_checked, tf[2]["semestre_id"])
# création du semestre
formsemestre = FormSemestre.create_formsemestre(tf[2])
# création des modules
for module_id in module_ids_checked:
modargs = {
"module_id": module_id,
"formsemestre_id": formsemestre.id,
"responsable_id": tf[2][f"MI{module_id}"],
}
_ = ModuleImpl.create_from_dict(modargs)
else:
# Modification du semestre:
# on doit creer les modules nouvellement selectionnés
# modifier ceux à modifier, et DETRUIRE ceux qui ne sont plus selectionnés.
# Note: la destruction échouera s'il y a des objets dépendants
# (eg des évaluations définies)
module_ids_tocreate = [
x for x in module_ids_checked if not x in module_ids_existing
]
if is_apc:
_formsemestre_check_module_list(module_ids_tocreate, tf[2]["semestre_id"])
# modules existants à modifier
module_ids_toedit = [x for x in module_ids_checked if x in module_ids_existing]
# modules à détruire
module_ids_todelete = [
x for x in module_ids_existing if not x in module_ids_checked
]
#
formsemestre.from_dict(tf[2])
sco_cache.invalidate_formsemestre(formsemestre.id)
#
msg = []
for module_id in module_ids_tocreate:
modargs = {
"module_id": module_id,
"formsemestre_id": formsemestre.id,
"responsable_id": tf[2]["MI" + str(module_id)],
}
modimpl = ModuleImpl.create_from_dict(modargs)
assert modimpl.module_id == module_id
mod = modimpl.module
msg += [f"""création de {mod.code or "?"} ({mod.titre or "?"})"""]
# INSCRIPTIONS DES ETUDIANTS
group_id = tf[2][f"{module_id}!group_id"]
log(f"""inscription module: {module_id}!group_id = '{group_id}'""")
if group_id:
etudids = [x["etudid"] for x in sco_groups.get_group_members(group_id)]
log(
f"""inscription module:module_id={module_id},moduleimpl_id={
modimpl.id}: {etudids}"""
)
sco_moduleimpl.do_moduleimpl_inscrit_etuds(
modimpl.id,
formsemestre.id,
etudids,
)
msg += [
f"""inscription de {len(etudids)} étudiants au module {
mod.code or "(module sans code)"}"""
]
else:
log(
f"""inscription module:module_id={module_id},moduleimpl_id={
modimpl.id}: aucun etudiant inscrit"""
)
#
ok, diag = formsemestre_delete_moduleimpls(formsemestre, module_ids_todelete)
msg += diag
for module_id in module_ids_toedit:
modimpl = formsemestre.modimpls.filter_by(module_id=module_id).first()
if not modimpl:
raise ScoValueError("Module introuvable ! Veuillez recharger la page.")
modimpl.responsable_id = tf[2]["MI" + str(module_id)]
db.session.add(modimpl)
db.session.commit()
# --- Etapes
_set_apo_etapes(formsemestre, tf[2], etapes)
# --- id edt du groupe par défault
if "edt_promo_id" in tf[2]:
group_tous = formsemestre.get_default_group()
if group_tous:
group_tous.edt_id = tf[2]["edt_promo_id"]
db.session.add(group_tous)
db.session.add(formsemestre)
db.session.commit()
sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre.id)
# --- Crée ou met à jour les groupes de parcours BUT
formsemestre.setup_parcours_groups()
# peut être nécessaire dans certains cas:
formsemestre.update_inscriptions_parcours_from_groups()
# --- Fin
if edit:
log(f"""formsemestre_edit: {formsemestre}""")
if msg:
return f"""
Attention !
{"
".join(msg)}
{"
Modification effectuée
" if ok
else "
Modules non modifiés
"
}
retour au tableau de bord
"""
flash("Semestre modifié")
return flask.redirect(
url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
check_parcours=0,
)
)
flash("Nouveau semestre créé")
return flask.redirect(
url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
)
)
def _remap_resp_modimpls(args: dict):
"remap les identifiants de responsables de modules"
for field in RESP_FIELDS:
resp = User.get_user_from_nomplogin(args[field])
args[field] = resp.id if resp else -1
args["responsables"] = []
for field in RESP_FIELDS:
if args[field]:
args["responsables"].append(args[field])
for module_id in args["tf-checked"]:
mod_resp = User.get_user_from_nomplogin(args[module_id])
if mod_resp is None:
# Si un module n'a pas de responsable (ou inconnu),
# l'affecte au 1er directeur des etudes:
mod_resp_id = args["responsable_id"]
else:
mod_resp_id = mod_resp.id
args[module_id] = mod_resp_id
def _set_apo_etapes(formsemestre: FormSemestre, args: dict, etapes: list[str]):
"""Affecte les étapes Apo du semestre,
à partir de args["etape_apo"] et args["vdi_apo]
"""
# menus => case supplementaire pour saisie manuelle, indicée 0
start_i = 0 if etapes else 1
apo_etapes_vdi = []
for n in range(start_i, scu.EDIT_NB_ETAPES + 1):
apo_etapes_vdi.append(
ApoEtapeVDI(etape=args["etape_apo" + str(n)], vdi=args["vdi_apo" + str(n)])
)
# uniques:
apo_etapes_vdi_uniq = {str(e): e for e in apo_etapes_vdi if str(e)}.values()
formsemestre.etapes = []
db.session.add(formsemestre)
db.session.flush()
formsemestre.etapes = [
FormSemestreEtape.create_from_apovdi(formsemestre.id, etape_vdi)
for etape_vdi in apo_etapes_vdi_uniq
]
db.session.add(formsemestre)
db.session.flush()
log(f"setting etapes: {formsemestre}: {formsemestre.etapes}")
def _formsemestre_check_module_list(module_ids, semestre_idx):
"""En APC: Vérifie que tous les modules de la liste
sont dans le semestre indiqué.
Sinon, raise ScoValueError.
"""
# vérification de la cohérence / modules / semestre
mod_sems_idx = {
Module.get_or_404(module_id).ue.semestre_idx for module_id in module_ids
}
if mod_sems_idx and mod_sems_idx != {semestre_idx}:
modules = [Module.get_or_404(module_id) for module_id in module_ids]
log(
f"""_formsemestre_check_module_list:
{chr(10).join( str(module) + " " + str(module.ue) for module in modules )}
"""
)
for module in modules:
log(
f"{module.code}\tsemestre_id={module.semestre_id}\tue.semestre_idx={module.ue.semestre_idx}"
)
raise ScoValueError(
f"Les modules sélectionnés ne sont pas tous dans le semestre choisi (S{semestre_idx}) !",
dest_url="javascript:history.back();",
)
def _formsemestre_check_ue_bonus_unicity(module_ids):
"""Vérifie qu'il n'y a qu'une seule UE bonus associée aux modules choisis"""
ues = [Module.get_or_404(module_id).ue for module_id in module_ids]
ues_bonus = {ue.id for ue in ues if ue.type == codes_cursus.UE_SPORT}
if len(ues_bonus) > 1:
raise ScoValueError(
"""Les modules de bonus sélectionnés ne sont pas tous dans la même UE bonus.
Changez la sélection ou modifiez la structure du programme de formation.""",
dest_url="javascript:history.back();",
)
def formsemestre_delete_moduleimpls(
formsemestre: FormSemestre, module_ids_to_del: list[int]
) -> tuple[bool, list[str]]:
"""Delete moduleimpls
module_ids_to_del: list of module_id (warning: not moduleimpl)
Moduleimpls must have no associated evaluations.
"""
ok = True
msg = []
for module_id in module_ids_to_del:
module = db.session.get(Module, module_id)
if module is None:
continue # ignore invalid ids
modimpls = ModuleImpl.query.filter_by(
formsemestre_id=formsemestre.id, module_id=module_id
)
for modimpl in modimpls:
nb_evals = modimpl.evaluations.count()
if nb_evals > 0:
msg += [
f"""impossible de supprimer {module.code} ({module.titre or ""})
car il y a {nb_evals} évaluations définies
(supprimez-les d\'abord)"""
]
ok = False
else:
msg += [f"""suppression de {module.code} ({module.titre or ""})"""]
db.session.delete(modimpl)
if ok:
db.session.commit()
else:
db.session.rollback()
return ok, msg
def formsemestre_clone(formsemestre_id):
"""
Formulaire clonage d'un semestre
"""
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
# Liste des enseignants avec forme pour affichage / saisie avec suggestion
user_list = sco_users.get_user_list()
uid2display = {} # user_name : forme pour affichage = "NOM Prenom (login)"
for u in user_list:
uid2display[u.id] = u.get_nomplogin()
allowed_user_names = list(uid2display.values()) + [""]
initvalues = {
"formsemestre_id": sem["formsemestre_id"],
"responsable_id": uid2display.get(
sem["responsables"][0], sem["responsables"][0]
),
}
H = [
"""
Cette opération duplique un semestre:
on reprend les mêmes modules et responsables.
Aucun étudiant n'est inscrit.
""",
]
descr = [
("formsemestre_id", {"input_type": "hidden"}),
(
"date_debut",
{
"title": "Date de début", # j/m/a
"input_type": "datedmy",
"explanation": "j/m/a",
"size": 9,
"allow_null": False,
},
),
(
"date_fin",
{
"title": "Date de fin", # j/m/a
"input_type": "datedmy",
"explanation": "j/m/a",
"size": 9,
"allow_null": False,
},
),
(
"responsable_id",
{
"input_type": "text_suggest",
"size": 50,
"title": "Directeur des études",
"explanation": "taper le début du nom et choisir dans le menu",
"allowed_values": allowed_user_names,
"allow_null": False,
"text_suggest_options": {
"script": url_for(
"users.get_user_list_xml", scodoc_dept=g.scodoc_dept
)
+ "?",
"varname": "start",
"json": False,
"noresults": "Valeur invalide !",
"timeout": 60000,
},
},
),
(
"clone_evaluations",
{
"title": "Copier aussi les évaluations",
"input_type": "boolcheckbox",
"explanation": "copie toutes les évaluations, sans les dates (ni les notes!)",
},
),
(
"clone_partitions",
{
"title": "Copier aussi les partitions",
"input_type": "boolcheckbox",
"explanation": "copie toutes les partitions (sans les étudiants!)",
},
),
]
tf = TrivialFormulator(
request.base_url,
scu.get_request_args(),
descr,
submitlabel="Dupliquer ce semestre",
cancelbutton="Annuler",
initvalues=initvalues,
)
msg = ""
if tf[0] == 1:
# check dates
if ndb.DateDMYtoISO(tf[2]["date_debut"]) > ndb.DateDMYtoISO(tf[2]["date_fin"]):
msg = '
Dates de début et fin incompatibles !
'
if tf[0] == 0 or msg:
return render_template(
"sco_page.j2",
javascripts=["libjs/AutoSuggest.js"],
cssstyles=["css/autosuggest_inquisitor.css"],
title="Copie du semestre",
content="".join(H) + msg + tf[1],
)
elif tf[0] == -1: # cancel
return flask.redirect(
url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
)
)
else:
resp = User.get_user_from_nomplogin(tf[2]["responsable_id"])
if not resp:
raise ScoValueError("id responsable invalide")
new_formsemestre_id = do_formsemestre_clone(
formsemestre_id,
resp,
tf[2]["date_debut"],
tf[2]["date_fin"],
clone_evaluations=tf[2]["clone_evaluations"],
clone_partitions=tf[2]["clone_partitions"],
)
flash("Nouveau semestre créé")
return flask.redirect(
url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=new_formsemestre_id,
)
)
def do_formsemestre_clone(
orig_formsemestre_id,
responsable: User, # new resp.
date_debut,
date_fin, # 'dd/mm/yyyy'
clone_evaluations=False,
clone_partitions=False,
):
"""Clone a semestre: make copy, same modules, same options, same resps, same partitions.
Clone description.
New dates, responsable_id
"""
log(f"do_formsemestre_clone: {orig_formsemestre_id}")
formsemestre_orig: FormSemestre = FormSemestre.get_or_404(orig_formsemestre_id)
# 1- create sem
args = formsemestre_orig.to_dict()
del args["formsemestre_id"]
del args["id"]
del args["parcours"] # copiés ensuite
args["responsables"] = [responsable]
args["date_debut"] = date_debut
args["date_fin"] = date_fin
args["etat"] = 1 # non verrouillé
formsemestre = FormSemestre.create_formsemestre(args)
log(f"created formsemestre {formsemestre}")
# 2- create moduleimpls
modimpl_orig: ModuleImpl
for modimpl_orig in formsemestre_orig.modimpls:
assert isinstance(modimpl_orig, ModuleImpl)
assert isinstance(modimpl_orig.id, int)
log(f"cloning {modimpl_orig}")
args = modimpl_orig.to_dict(with_module=False)
args["formsemestre_id"] = formsemestre.id
modimpl_new = ModuleImpl.create_from_dict(args)
log(f"created ModuleImpl from {args}")
db.session.flush()
# copy enseignants
for ens in modimpl_orig.enseignants:
modimpl_new.enseignants.append(ens)
db.session.add(modimpl_new)
db.session.flush()
log(f"new moduleimpl.id = {modimpl_new.id}")
# optionally, copy evaluations
if clone_evaluations:
e: Evaluation
for e in Evaluation.query.filter_by(moduleimpl_id=modimpl_orig.id):
log(f"cloning evaluation {e.id}")
# copie en enlevant la date
args = dict(e.__dict__)
args.pop("_sa_instance_state")
args.pop("id")
args.pop("date_debut", None)
args.pop("date_fin", None)
args["moduleimpl_id"] = modimpl_new.id
new_eval = Evaluation(**args)
db.session.add(new_eval)
db.session.commit()
# Copie les poids APC de l'évaluation
new_eval.set_ue_poids_dict(e.get_ue_poids_dict())
db.session.commit()
if clone_evaluations:
flash(
"Attention: les évaluations n'ont plus de dates: n'oubliez pas de les indiquer"
)
# 3- copy uecoefs
for ue_coef in FormSemestreUECoef.query.filter_by(
formsemestre_id=formsemestre_orig.id
):
new_ue_coef = FormSemestreUECoef(
formsemestre_id=formsemestre.id,
ue_id=ue_coef.ue_id,
coefficient=ue_coef.coefficient,
)
db.session.add(new_ue_coef)
db.session.flush()
# NB: don't copy notes_formsemestre_custommenu (usually specific)
# 4- Copy new style preferences
prefs = sco_preferences.SemPreferences(orig_formsemestre_id)
if orig_formsemestre_id in prefs.base_prefs.prefs:
for pname in prefs.base_prefs.prefs[orig_formsemestre_id]:
if not prefs.is_global(pname):
pvalue = prefs[pname]
try:
prefs.base_prefs.set(formsemestre.id, pname, pvalue)
except ValueError:
log(
f"""do_formsemestre_clone: ignoring old preference {
pname}={pvalue} for {formsemestre}"""
)
# 5- Copie les parcours
formsemestre.parcours = formsemestre_orig.parcours
# 6- Copy description
if formsemestre_orig.description:
formsemestre.description = formsemestre_orig.description.clone()
db.session.add(formsemestre)
db.session.commit()
# 7- Copy partitions and groups
if clone_partitions:
sco_groups_copy.clone_partitions_and_groups(
orig_formsemestre_id, formsemestre.id
)
return formsemestre.id
def formsemestre_delete(formsemestre_id: int) -> str | flask.Response:
"""Delete a formsemestre (affiche avertissements)"""
formsemestre: FormSemestre = FormSemestre.get_formsemestre(formsemestre_id)
H = [
"""
Suppression du semestre
Attention !
A n'utiliser qu'en cas d'erreur lors de la saisie d'une formation. Normalement,
un semestre ne doit jamais être supprimé
(on perd la mémoire des notes et de tous les événements liés à ce semestre !).
Tous les modules de ce semestre seront supprimés.
Ceci n'est possible que si :
aucune décision de jury n'a été entrée dans ce semestre;
et aucun étudiant de ce semestre ne le compense avec un autre semestre.
""",
dest_url="",
cancel_url=url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
),
parameters={"formsemestre_id": formsemestre.id},
)
# Bon, s'il le faut...
do_formsemestre_delete(formsemestre.id)
flash("Semestre supprimé !")
return flask.redirect(url_for("scolar.index_html", scodoc_dept=g.scodoc_dept))
def formsemestre_has_decisions_or_compensations(
formsemestre: FormSemestre,
) -> tuple[bool, str]:
"""True if decision de jury (sem. UE, RCUE, année) émanant de ce semestre
ou compensation de ce semestre par d'autres semestres
ou autorisations de passage.
"""
# Validations de semestre ou d'UEs
nb_validations = ScolarFormSemestreValidation.query.filter_by(
formsemestre_id=formsemestre.id
).count()
if nb_validations:
return True, f"{nb_validations} validations de semestre ou d'UE"
nb_validations = ScolarFormSemestreValidation.query.filter_by(
compense_formsemestre_id=formsemestre.id
).count()
if nb_validations:
return True, f"{nb_validations} compensations utilisées dans d'autres semestres"
# Autorisations d'inscription:
nb_validations = ScolarAutorisationInscription.query.filter_by(
origin_formsemestre_id=formsemestre.id
).count()
if nb_validations:
return (
True,
f"{nb_validations} autorisations d'inscriptions émanant de ce semestre",
)
# Validations d'années BUT
nb_validations = ApcValidationAnnee.query.filter_by(
formsemestre_id=formsemestre.id
).count()
if nb_validations:
return True, f"{nb_validations} validations d'année BUT utilisant ce semestre"
# Validations de RCUEs
nb_validations = ApcValidationRCUE.query.filter_by(
formsemestre_id=formsemestre.id
).count()
if nb_validations:
return True, f"{nb_validations} validations de RCUE utilisant ce semestre"
return False, ""
def do_formsemestre_delete(formsemestre_id: int):
"""delete formsemestre, and all its moduleimpls.
No checks, no warnings: erase all !
"""
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
sco_cache.EvaluationCache.invalidate_sem(formsemestre.id)
titre_sem = formsemestre.titre_annee()
# --- Destruction des modules de ce semestre
for modimpl in formsemestre.modimpls:
# evaluations
for e in modimpl.evaluations:
db.session.execute(
sa.text(
"""DELETE FROM notes_notes WHERE evaluation_id=:evaluation_id"""
),
{"evaluation_id": e.id},
)
db.session.execute(
sa.text(
"""DELETE FROM notes_notes_log WHERE evaluation_id=:evaluation_id"""
),
{"evaluation_id": e.id},
)
db.session.delete(e)
db.session.delete(modimpl)
# --- Desinscription des etudiants
db.session.execute(
sa.text(
"DELETE FROM notes_formsemestre_inscription WHERE formsemestre_id=:formsemestre_id"
),
{"formsemestre_id": formsemestre_id},
)
# --- Suppression des evenements
db.session.execute(
sa.text("DELETE FROM scolar_events WHERE formsemestre_id=:formsemestre_id"),
{"formsemestre_id": formsemestre_id},
)
# --- Suppression des appreciations
db.session.execute(
sa.text(
"DELETE FROM notes_appreciations WHERE formsemestre_id=:formsemestre_id"
),
{"formsemestre_id": formsemestre_id},
)
# --- Supression des validations (!!!)
db.session.execute(
sa.text(
"DELETE FROM scolar_formsemestre_validation WHERE formsemestre_id=:formsemestre_id"
),
{"formsemestre_id": formsemestre_id},
)
# --- Supression des references a ce semestre dans les compensations:
db.session.execute(
sa.text(
"""UPDATE scolar_formsemestre_validation
SET compense_formsemestre_id=NULL
WHERE compense_formsemestre_id=:formsemestre_id"""
),
{"formsemestre_id": formsemestre_id},
)
# --- Suppression des autorisations
db.session.execute(
sa.text(
"DELETE FROM scolar_autorisation_inscription WHERE origin_formsemestre_id=:formsemestre_id"
),
{"formsemestre_id": formsemestre_id},
)
# --- Suppression des coefs d'UE capitalisées
db.session.execute(
sa.text(
"DELETE FROM notes_formsemestre_uecoef WHERE formsemestre_id=:formsemestre_id"
),
{"formsemestre_id": formsemestre_id},
)
# --- Suppression des item du menu custom
db.session.execute(
sa.text(
"DELETE FROM notes_formsemestre_custommenu WHERE formsemestre_id=:formsemestre_id"
),
{"formsemestre_id": formsemestre_id},
)
# --- Suppression des formules
db.session.execute(
sa.text(
"DELETE FROM notes_formsemestre_ue_computation_expr WHERE formsemestre_id=:formsemestre_id"
),
{"formsemestre_id": formsemestre_id},
)
# --- Suppression des preferences
db.session.execute(
sa.text("DELETE FROM sco_prefs WHERE formsemestre_id=:formsemestre_id"),
{"formsemestre_id": formsemestre_id},
)
# --- Suppression des groupes et partitions
db.session.execute(
sa.text(
"""
DELETE FROM group_membership
WHERE group_id IN
(SELECT gm.group_id FROM group_membership gm, partition p, group_descr gd
WHERE gm.group_id = gd.id AND gd.partition_id = p.id
AND p.formsemestre_id=:formsemestre_id)
"""
),
{"formsemestre_id": formsemestre_id},
)
db.session.execute(
sa.text(
"""
DELETE FROM group_descr
WHERE id IN
(SELECT gd.id FROM group_descr gd, partition p
WHERE gd.partition_id = p.id
AND p.formsemestre_id=:formsemestre_id)
"""
),
{"formsemestre_id": formsemestre_id},
)
db.session.execute(
sa.text("DELETE FROM partition WHERE formsemestre_id=:formsemestre_id"),
{"formsemestre_id": formsemestre_id},
)
# --- Responsables
db.session.execute(
sa.text(
"DELETE FROM notes_formsemestre_responsables WHERE formsemestre_id=:formsemestre_id"
),
{"formsemestre_id": formsemestre_id},
)
# --- Etapes
db.session.execute(
sa.text(
"DELETE FROM notes_formsemestre_etapes WHERE formsemestre_id=:formsemestre_id"
),
{"formsemestre_id": formsemestre_id},
)
# --- SemSets
db.session.execute(
sa.text(
"DELETE FROM notes_semset_formsemestre WHERE formsemestre_id=:formsemestre_id"
),
{"formsemestre_id": formsemestre_id},
)
# --- Dispenses d'UE
db.session.execute(
sa.text("""DELETE FROM "dispenseUE" WHERE formsemestre_id=:formsemestre_id"""),
{"formsemestre_id": formsemestre_id},
)
# --- Destruction du semestre
db.session.delete(formsemestre)
# news
ScolarNews.add(
typ=ScolarNews.NEWS_SEM,
obj=formsemestre_id,
text=f"Suppression du semestre {titre_sem}",
max_frequency=0,
)
# ---------------------------------------------------------------------------------------
def formsemestre_edit_options(formsemestre_id):
"""dialog to change formsemestre options
(accessible par EditFormSemestre ou dir. etudes)
"""
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
ok, err = sco_permissions_check.check_access_diretud(formsemestre)
if not ok:
return err
return sco_preferences.SemPreferences(formsemestre_id).edit(
categories=["bul", "bul_but_pdf"]
)
def formsemestre_change_publication_bul(formsemestre_id, dialog_confirmed=False):
"""Change état publication bulletins sur portail"""
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
ok, err = sco_permissions_check.check_access_diretud(formsemestre)
if not ok:
return err
etat = not formsemestre.bul_hide_xml
status_url = url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
)
if not dialog_confirmed:
msg = "non" if etat else ""
return scu.confirm_dialog(
f"
Confirmer la {msg} publication des bulletins ?
",
help_msg="""Il est parfois utile de désactiver la diffusion des bulletins,
par exemple pendant la tenue d'un jury ou avant harmonisation des notes.
Ce réglage n'a d'effet que si votre établissement a interfacé ScoDoc avec
une passerelle étudiant.
""",
dest_url="",
cancel_url=status_url,
parameters={"bul_hide_xml": etat, "formsemestre_id": formsemestre_id},
)
formsemestre.bul_hide_xml = etat
db.session.add(formsemestre)
db.session.commit()
log(f"formsemestre_change_publication_bul: {formsemestre} -> {etat}")
return flask.redirect(status_url)
def formsemestre_edit_uecoefs(formsemestre_id, err_ue_id=None):
"""Changement manuel des coefficients des UE capitalisées."""
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
ok, err = sco_permissions_check.check_access_diretud(formsemestre)
if not ok:
return err
help_msg = """
Seuls les modules ont un coefficient. Cependant, il est nécessaire d'affecter un coefficient aux UE capitalisée pour pouvoir les prendre en compte dans la moyenne générale.
ScoDoc calcule normalement le coefficient d'une UE comme la somme des
coefficients des modules qui la composent.
Dans certains cas, on n'a pas les mêmes modules dans le semestre antérieur
(capitalisé) et dans le semestre courant, et le coefficient d'UE est alors variable.
Il est alors possible de forcer la valeur du coefficient d'UE.
Indiquez "auto" (ou laisser vide) pour que ScoDoc calcule automatiquement le coefficient,
ou bien entrez une valeur (nombre réel).
Dans le doute, si le mode auto n'est pas applicable et que tous les étudiants sont inscrits aux mêmes modules de ce semestre, prenez comme coefficient la somme indiquée.
Sinon, référez vous au programme pédagogique. Les lignes en rouge
sont à changer.
Les coefficients indiqués ici ne s'appliquent que pour le traitement des UE capitalisées.
"""
H = [
help_msg,
]
#
ues, modimpls = _get_sem_ues_modimpls(formsemestre)
sum_coefs_by_ue_id = {}
for ue in ues:
sum_coefs_by_ue_id[ue.id] = sum(
modimpl.module.coefficient or 0.0
for modimpl in modimpls
if modimpl.module.ue_id == ue.id
)
cnx = ndb.GetDBConnexion()
initvalues = {"formsemestre_id": formsemestre_id}
form = [("formsemestre_id", {"input_type": "hidden"})]
for ue in ues:
coefs = sco_formsemestre.formsemestre_uecoef_list(
cnx, args={"formsemestre_id": formsemestre_id, "ue_id": ue.id}
)
if coefs:
initvalues["ue_" + str(ue.id)] = coefs[0]["coefficient"]
else:
initvalues["ue_" + str(ue.id)] = "auto"
descr = {
"size": 10,
"title": ue.acronyme,
"explanation": f"somme coefs modules = {sum_coefs_by_ue_id[ue.id]}",
}
if ue.id == err_ue_id:
descr["dom_id"] = "erroneous_ue"
form.append(("ue_" + str(ue.id), descr))
tf = TrivialFormulator(
request.base_url,
scu.get_request_args(),
form,
submitlabel="Changer les coefficients",
cancelbutton="Annuler",
initvalues=initvalues,
)
if tf[0] == 0:
return render_template(
"sco_page.j2",
title="Coefficients des UE du semestre",
content="
Coefficients des UE du semestre
" + "\n".join(H) + tf[1],
)
elif tf[0] == -1:
return redirect(
url_for(
"notes.formsemestre_editwithmodules",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
)
)
else:
# change values
# 1- supprime les coef qui ne sont plus forcés
# 2- modifie ou cree les coefs
ue_deleted = []
ue_modified: list[tuple[UniteEns, float]] = []
msg = []
for ue in ues:
val = tf[2]["ue_" + str(ue.id)]
coefs = sco_formsemestre.formsemestre_uecoef_list(
cnx, args={"formsemestre_id": formsemestre_id, "ue_id": ue.id}
)
if val == "" or val == "auto":
# supprime ce coef (il sera donc calculé automatiquement)
if coefs:
ue_deleted.append(ue)
else:
try:
val = float(val)
if (not coefs) or (coefs[0]["coefficient"] != val):
ue_modified.append((ue, val))
except ValueError:
ok = False
msg.append(
f"valeur invalide ({val}) pour le coefficient de l'UE {ue.acronyme}"
)
if not ok:
render_template(
"sco_page.j2",
title="Coefficients des UE du semestre",
content="
Coefficients des UE du semestre
"
+ "\n".join(H)
+ "
%s
" % "
".join(msg)
+ tf[1],
)
# apply modifications
for ue, val in ue_modified:
sco_formsemestre.do_formsemestre_uecoef_edit_or_create(
cnx, formsemestre_id, ue.id, val
)
for ue in ue_deleted:
sco_formsemestre.do_formsemestre_uecoef_delete(cnx, formsemestre_id, ue.id)
if ue_modified or ue_deleted:
message = ["""
Modification effectuées
"""]
if ue_modified:
message.append("""
Coefs modifiés dans les UE:
""")
for ue, val in ue_modified:
message.append(f"
{ue.acronyme} : {val}
")
message.append("
")
if ue_deleted:
message.append("""
Coefs supprimés dans les UE:
""")
for ue in ue_deleted:
message.append(f"
{ue.acronyme}
")
message.append("
")
sco_cache.invalidate_formsemestre(
formsemestre_id=formsemestre_id
) # > modif coef UE cap (modifs notes de _certains_ etudiants)
else:
message = ["""
Aucune modification
"""]
return render_template(
"sco_page.j2",
title="Coefficients des UE du semestre",
content=f"""
""",
)
def _get_sem_ues_modimpls(
formsemestre: FormSemestre,
) -> tuple[list[UniteEns], list[ModuleImpl]]:
"""Get liste des UE du semestre (à partir des moduleimpls)
(utilisé quand on ne peut pas construire nt et faire nt.get_ues_stat_dict())
"""
uedict = {}
modimpls = formsemestre.modimpls.all()
for modimpl in modimpls:
if not modimpl.module.ue_id in uedict:
uedict[modimpl.module.ue.id] = modimpl.module.ue
ues = list(uedict.values())
ues.sort(key=lambda u: u.numero)
return ues, modimpls
# ----- identification externe des sessions (pour SOJA et autres logiciels)
def get_formsemestre_session_id(sem, code_specialite, parcours):
"""Identifiant de session pour ce semestre
Obsolete: vooir FormSemestre.session_id() #sco7
"""
imputation_dept = sco_preferences.get_preference(
"ImputationDept", sem["formsemestre_id"]
)
if not imputation_dept:
imputation_dept = sco_preferences.get_preference("DeptName") or ""
imputation_dept = imputation_dept.upper()
parcours_type = parcours.NAME
modalite = sem["modalite"]
modalite = (
(modalite or "").replace("FAP", "FA").replace("APP", "FA")
) # exception pour code Apprentissage
if sem["semestre_id"] > 0:
decale = scu.sem_decale_str(sem)
semestre_id = "S%d" % sem["semestre_id"] + decale
else:
semestre_id = code_specialite
annee_sco = str(scu.annee_scolaire_debut(sem["annee_debut"], sem["mois_debut_ord"]))
return scu.sanitize_string(
"-".join(
(imputation_dept, parcours_type, modalite, semestre_id or "", annee_sco)
)
)