Compare commits
24 Commits
Author | SHA1 | Date | |
---|---|---|---|
3b984ea823 | |||
abd8bf484d | |||
8feff2b182 | |||
0b5b0f8079 | |||
9e7b169998 | |||
c7c6b49373 | |||
d84f101b36 | |||
29defb6f00 | |||
1dfba157c2 | |||
056be2e152 | |||
2efdfa16c6 | |||
da472668b5 | |||
a9fd1296bc | |||
f601ef4710 | |||
bd610c70cf | |||
908af14e5a | |||
f7f55c2494 | |||
820e42ac28 | |||
ca1ac28f2e | |||
d5a5c66390 | |||
4717361f8b | |||
f10dd46abb | |||
8e932a8e0b | |||
00efa7bcad |
@ -30,7 +30,7 @@ from app.models import (
|
||||
Module,
|
||||
UniteEns,
|
||||
)
|
||||
from app.scodoc import sco_formations
|
||||
from app.formations import formation_io
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
|
||||
|
||||
@ -141,7 +141,7 @@ def formation_export_by_formation_id(formation_id: int, export_ids=False):
|
||||
formation = query.first_or_404(formation_id)
|
||||
app.set_sco_dept(formation.departement.acronym)
|
||||
try:
|
||||
data = sco_formations.formation_export(formation_id, export_ids)
|
||||
data = formation_io.formation_export(formation_id, export_ids)
|
||||
except ValueError:
|
||||
return json_error(500, message="Erreur inconnue")
|
||||
|
||||
|
@ -60,18 +60,16 @@ def decisions_jury(formsemestre_id: int):
|
||||
/formsemestre/1/decisions_jury
|
||||
"""
|
||||
# APC, pair:
|
||||
formsemestre: FormSemestre = db.session.get(FormSemestre, formsemestre_id)
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
if formsemestre is None:
|
||||
return json_error(
|
||||
404,
|
||||
message="formsemestre inconnu",
|
||||
)
|
||||
if formsemestre.formation.is_apc():
|
||||
app.set_sco_dept(formsemestre.departement.acronym)
|
||||
rows = jury_but_results.get_jury_but_results(formsemestre)
|
||||
return rows
|
||||
else:
|
||||
raise ScoException("non implemente")
|
||||
|
||||
app.set_sco_dept(formsemestre.departement.acronym)
|
||||
rows = jury_but_results.get_jury_but_results(formsemestre)
|
||||
return rows
|
||||
|
||||
|
||||
def _news_delete_jury_etud(etud: Identite, detail: str = ""):
|
||||
|
@ -15,7 +15,9 @@ from app.models import Departement, Identite, Admission
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
|
||||
|
||||
def get_etud(etudid=None, nip=None, ine=None) -> models.Identite:
|
||||
def get_etud(
|
||||
etudid: int | None = None, nip: str | None = None, ine: str | None = None
|
||||
) -> models.Identite:
|
||||
"""
|
||||
L'instance d'étudiant la plus récente en fonction de l'etudid,
|
||||
ou du code nip ou code ine.
|
||||
@ -29,6 +31,13 @@ def get_etud(etudid=None, nip=None, ine=None) -> models.Identite:
|
||||
allowed_depts = current_user.get_depts_with_permission(Permission.ScoView)
|
||||
|
||||
if etudid is not None:
|
||||
try:
|
||||
etudid = int(etudid)
|
||||
except ValueError:
|
||||
return json_error(
|
||||
404,
|
||||
message="etudid invalide",
|
||||
)
|
||||
query: Identite = Identite.query.filter_by(id=etudid)
|
||||
elif nip is not None:
|
||||
query = Identite.query.filter_by(code_nip=nip)
|
||||
|
@ -499,7 +499,13 @@ class BulletinBUT:
|
||||
etud, res.ressources, version=version
|
||||
),
|
||||
"saes": self.etud_mods_results(etud, res.saes, version=version),
|
||||
"ues_capitalisees": self.etud_ues_capitalisees(etud),
|
||||
"ues_capitalisees": (
|
||||
self.etud_ues_capitalisees(etud)
|
||||
if sco_preferences.get_preference(
|
||||
"bul_show_ue_cap_but", formsemestre.id
|
||||
)
|
||||
else {}
|
||||
),
|
||||
"semestre": semestre_infos,
|
||||
},
|
||||
)
|
||||
|
@ -16,8 +16,11 @@ from app.scodoc import sco_pv_dict
|
||||
|
||||
|
||||
def get_jury_but_results(formsemestre: FormSemestre) -> list[dict]:
|
||||
"""Liste des résultats jury BUT sous forme de dict, pour API"""
|
||||
if formsemestre.formation.referentiel_competence is None:
|
||||
"""Liste des résultats jury BUT ou classique sous forme de dict, pour API"""
|
||||
if (
|
||||
formsemestre.formation.is_apc()
|
||||
and formsemestre.formation.referentiel_competence is None
|
||||
):
|
||||
# pas de ref. comp., donc pas de decisions de jury (ne lance pas d'exception)
|
||||
return []
|
||||
dpv = sco_pv_dict.dict_pvjury(formsemestre.id)
|
||||
@ -30,7 +33,7 @@ def get_jury_but_results(formsemestre: FormSemestre) -> list[dict]:
|
||||
def _get_jury_but_etud_result(
|
||||
formsemestre: FormSemestre, dpv: dict, etudid: int
|
||||
) -> dict:
|
||||
"""Résultats de jury d'un étudiant sur un semestre pair de BUT"""
|
||||
"""Résultats de jury d'un étudiant sur un semestre, sous forme de dict"""
|
||||
etud = Identite.get_etud(etudid)
|
||||
dec_etud = dpv["decisions_dict"][etudid]
|
||||
if formsemestre.formation.is_apc():
|
||||
@ -56,16 +59,20 @@ def _get_jury_but_etud_result(
|
||||
rcue_dict = {
|
||||
"ue_1": {
|
||||
"ue_id": rcue.ue_1.id,
|
||||
"moy": None
|
||||
if (dec_ue1.moy_ue is None or np.isnan(dec_ue1.moy_ue))
|
||||
else dec_ue1.moy_ue,
|
||||
"moy": (
|
||||
None
|
||||
if (dec_ue1.moy_ue is None or np.isnan(dec_ue1.moy_ue))
|
||||
else dec_ue1.moy_ue
|
||||
),
|
||||
"code": dec_ue1.code_valide,
|
||||
},
|
||||
"ue_2": {
|
||||
"ue_id": rcue.ue_2.id,
|
||||
"moy": None
|
||||
if (dec_ue2.moy_ue is None or np.isnan(dec_ue2.moy_ue))
|
||||
else dec_ue2.moy_ue,
|
||||
"moy": (
|
||||
None
|
||||
if (dec_ue2.moy_ue is None or np.isnan(dec_ue2.moy_ue))
|
||||
else dec_ue2.moy_ue
|
||||
),
|
||||
"code": dec_ue2.code_valide,
|
||||
},
|
||||
"moy": rcue.moy_rcue,
|
||||
@ -89,6 +96,15 @@ def _get_jury_but_etud_result(
|
||||
row["semestre"] = {"code": dec_etud["decision_sem"].get("code")}
|
||||
else:
|
||||
row["semestre"] = {} # APC, ...
|
||||
# --- L'année (BUT)
|
||||
if deca and deca.validation:
|
||||
row["annee"] = {
|
||||
"code": deca.validation.code,
|
||||
"ordre": deca.validation.ordre,
|
||||
"annee_scolaire": deca.validation.annee_scolaire,
|
||||
}
|
||||
else:
|
||||
row["annee"] = {}
|
||||
# --- Autorisations
|
||||
row["autorisations"] = dec_etud["autorisations"]
|
||||
return row
|
||||
|
@ -774,6 +774,34 @@ class BonusIUTRennes1(BonusSportAdditif):
|
||||
"""Calcul bonus optionnels (sport, langue vivante, engagement étudiant),
|
||||
règle IUT de l'Université de Rennes 1 (Lannion, Rennes, St Brieuc, St Malo).
|
||||
|
||||
<div><b>Pour les semestres à partir de sept. 2024:</b></div>
|
||||
<p>
|
||||
Article 5 : Bonification des activités hors programmes nationaux :
|
||||
</p>
|
||||
<p>
|
||||
Pour calculer la moyenne M de chaque UE des semestres pairs, il convient
|
||||
d'ajouter la validation de note obtenue qui s'effectuera suivant la
|
||||
formule :
|
||||
<pre>
|
||||
M = Mo + ((S-10)/10)/(Nombre d'UE du semestre)
|
||||
</pre>
|
||||
où Mo est la moyenne de l'UE avant prise en compte de la note et S est
|
||||
la note finale attribuée par l'activité bonifiée (si S > 10).
|
||||
</p>
|
||||
<p>
|
||||
Sont éligibles à cette bonification une activité sportive dans le cadre
|
||||
universitaire, une langue vivante optionnelle et l'engagement étudiant
|
||||
(dans le cadre défini par la commission de la formation et de la vie
|
||||
universitaire (CFVE) de l'Université de Rennes annexe 3).
|
||||
</p>
|
||||
<p>
|
||||
Pour l'étudiant pouvant prétendre à plusieurs bonifications, seule la
|
||||
meilleure des notes sera prise en compte. Toutefois, une bonification
|
||||
liée à l'engagement est cumulable avec une autre bonification.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div><b>Pour les semestres avant 2024:</b></div>
|
||||
<ul>
|
||||
<li>Les étudiants peuvent suivre un ou plusieurs activités optionnelles notées
|
||||
dans les semestres pairs.<br>
|
||||
@ -796,17 +824,26 @@ class BonusIUTRennes1(BonusSportAdditif):
|
||||
|
||||
# S'applique aussi en classic, sur la moy. gen.
|
||||
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
|
||||
"""calcul du bonus"""
|
||||
"""calcul du bonus, avec changement de règle en sept 2024"""
|
||||
# Nouvelle règle à partir de sept. 2024
|
||||
# Prend la note de chaque modimpl, sans considération d'UE
|
||||
if len(sem_modimpl_moys_inscrits.shape) > 2: # apc
|
||||
sem_modimpl_moys_inscrits = sem_modimpl_moys_inscrits[:, :, 0]
|
||||
# ici sem_modimpl_moys_inscrits est nb_etuds x nb_mods_bonus, en APC et en classic
|
||||
note_bonus_max = np.max(sem_modimpl_moys_inscrits, axis=1) # 1d, nb_etuds
|
||||
nb_ues = len(self.formsemestre.get_ues(with_sport=False))
|
||||
|
||||
if nb_ues == 0:
|
||||
self.bonus_ues = pd.DataFrame()
|
||||
self.bonus_moy_gen = pd.Series()
|
||||
return
|
||||
proportion_point = (
|
||||
1 / (10.0 * nb_ues)
|
||||
if self.formsemestre.date_debut > datetime.date(2024, 8, 1)
|
||||
else self.proportion_point
|
||||
)
|
||||
bonus_moy_arr = np.where(
|
||||
note_bonus_max > self.seuil_moy_gen,
|
||||
(note_bonus_max - self.seuil_moy_gen) * self.proportion_point,
|
||||
(note_bonus_max - self.seuil_moy_gen) * proportion_point,
|
||||
0.0,
|
||||
)
|
||||
# Seuil: bonus dans [min, max] (défaut [0,20])
|
||||
|
@ -36,7 +36,7 @@ def send_email(
|
||||
sender: str,
|
||||
recipients: list,
|
||||
text_body: str,
|
||||
html_body="",
|
||||
html_body: str | None = None,
|
||||
bcc=(),
|
||||
attachments=(),
|
||||
):
|
||||
@ -56,7 +56,6 @@ def send_email(
|
||||
msg.attach(
|
||||
attachment["filename"], attachment["mimetype"], attachment["data"]
|
||||
)
|
||||
|
||||
send_message(msg)
|
||||
|
||||
|
||||
|
1
app/formations/__init__.py
Normal file
1
app/formations/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# Fonctions et vues sur les formations ScoDoc
|
@ -33,6 +33,7 @@ from flask import flash, g, url_for, render_template, request
|
||||
import sqlalchemy
|
||||
|
||||
from app import db
|
||||
from app.formations import edit_ue
|
||||
from app.models import SHORT_STR_LEN
|
||||
from app.models.formations import Formation
|
||||
from app.models.modules import Module
|
||||
@ -45,7 +46,6 @@ from app.scodoc.sco_exceptions import ScoValueError, ScoNonEmptyFormationObject
|
||||
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import codes_cursus
|
||||
from app.scodoc import sco_edit_ue
|
||||
|
||||
|
||||
def formation_delete(formation_id=None, dialog_confirmed=False):
|
||||
@ -123,7 +123,7 @@ def do_formation_delete(formation_id):
|
||||
db.session.flush()
|
||||
# Suppression des UEs
|
||||
for ue in formation.ues:
|
||||
sco_edit_ue.do_ue_delete(ue, force=True)
|
||||
edit_ue.do_ue_delete(ue, force=True)
|
||||
|
||||
db.session.delete(formation)
|
||||
|
@ -25,16 +25,14 @@
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
"""Ajout/Modification/Supression matieres
|
||||
(portage from DTML)
|
||||
"""Ajout/Modification/Suppression matieres
|
||||
"""
|
||||
import flask
|
||||
from flask import g, render_template, request, url_for
|
||||
from flask import flash, g, render_template, request, url_for
|
||||
|
||||
from app import db, log
|
||||
from app.models import Formation, Matiere, UniteEns, ScolarNews
|
||||
from app.models import Matiere, UniteEns
|
||||
|
||||
import app.scodoc.notesdb as ndb
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
||||
from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message
|
||||
@ -44,59 +42,9 @@ from app.scodoc.sco_exceptions import (
|
||||
ScoNonEmptyFormationObject,
|
||||
)
|
||||
|
||||
_matiereEditor = ndb.EditableTable(
|
||||
"notes_matieres",
|
||||
"matiere_id",
|
||||
("matiere_id", "ue_id", "numero", "titre"),
|
||||
sortkey="numero",
|
||||
output_formators={"numero": ndb.int_null_is_zero},
|
||||
)
|
||||
|
||||
|
||||
def matiere_list(*args, **kw):
|
||||
"list matieres"
|
||||
cnx = ndb.GetDBConnexion()
|
||||
return _matiereEditor.list(cnx, *args, **kw)
|
||||
|
||||
|
||||
def do_matiere_edit(*args, **kw):
|
||||
"edit a matiere"
|
||||
from app.scodoc import sco_edit_ue
|
||||
|
||||
cnx = ndb.GetDBConnexion()
|
||||
# check
|
||||
mat = matiere_list({"matiere_id": args[0]["matiere_id"]})[0]
|
||||
if matiere_is_locked(mat["matiere_id"]):
|
||||
raise ScoLockedFormError()
|
||||
# edit
|
||||
_matiereEditor.edit(cnx, *args, **kw)
|
||||
formation_id = sco_edit_ue.ue_list({"ue_id": mat["ue_id"]})[0]["formation_id"]
|
||||
db.session.get(Formation, formation_id).invalidate_cached_sems()
|
||||
|
||||
|
||||
def do_matiere_create(args):
|
||||
"create a matiere"
|
||||
from app.scodoc import sco_edit_ue
|
||||
|
||||
cnx = ndb.GetDBConnexion()
|
||||
# check
|
||||
ue = sco_edit_ue.ue_list({"ue_id": args["ue_id"]})[0]
|
||||
# create matiere
|
||||
r = _matiereEditor.create(cnx, args)
|
||||
|
||||
# news
|
||||
formation = db.session.get(Formation, ue["formation_id"])
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_FORM,
|
||||
obj=ue["formation_id"],
|
||||
text=f"Modification de la formation {formation.acronyme}",
|
||||
)
|
||||
formation.invalidate_cached_sems()
|
||||
return r
|
||||
|
||||
|
||||
def matiere_create(ue_id=None):
|
||||
"""Creation d'une matiere"""
|
||||
"""Formulaire création d'une matiere"""
|
||||
ue: UniteEns = UniteEns.query.get_or_404(ue_id)
|
||||
default_numero = max([mat.numero for mat in ue.matieres] or [9]) + 1
|
||||
H = [
|
||||
@ -153,8 +101,8 @@ associé.
|
||||
if tf[0] == -1:
|
||||
return flask.redirect(dest_url)
|
||||
# check unicity
|
||||
mats = matiere_list(args={"ue_id": ue_id, "titre": tf[2]["titre"]})
|
||||
if mats:
|
||||
nb_mats = Matiere.query.filter_by(ue_id=ue_id, titre=tf[2]["titre"]).count()
|
||||
if nb_mats:
|
||||
return render_template(
|
||||
"sco_page.j2",
|
||||
title="Création d'une matière",
|
||||
@ -164,57 +112,14 @@ associé.
|
||||
+ tf[1]
|
||||
),
|
||||
)
|
||||
_ = do_matiere_create(tf[2])
|
||||
Matiere.create_from_dict(tf[2])
|
||||
return flask.redirect(dest_url)
|
||||
|
||||
|
||||
def can_delete_matiere(matiere: Matiere) -> tuple[bool, str]:
|
||||
"True si la matiere n'est pas utilisée dans des formsemestre"
|
||||
locked = matiere_is_locked(matiere.id)
|
||||
if locked:
|
||||
return False
|
||||
if any(m.modimpls.all() for m in matiere.modules):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def do_matiere_delete(oid):
|
||||
"delete matiere and attached modules"
|
||||
from app.scodoc import sco_edit_ue
|
||||
from app.scodoc import sco_edit_module
|
||||
|
||||
cnx = ndb.GetDBConnexion()
|
||||
# check
|
||||
matiere = Matiere.query.get_or_404(oid)
|
||||
mat = matiere_list({"matiere_id": oid})[0] # compat sco7
|
||||
ue = sco_edit_ue.ue_list({"ue_id": mat["ue_id"]})[0]
|
||||
if not can_delete_matiere(matiere):
|
||||
# il y a au moins un modimpl dans un module de cette matière
|
||||
raise ScoNonEmptyFormationObject("Matière", matiere.titre)
|
||||
|
||||
log("do_matiere_delete: matiere_id=%s" % matiere.id)
|
||||
# delete all modules in this matiere
|
||||
mods = sco_edit_module.module_list({"matiere_id": matiere.id})
|
||||
for mod in mods:
|
||||
sco_edit_module.do_module_delete(mod["module_id"])
|
||||
_matiereEditor.delete(cnx, oid)
|
||||
|
||||
# news
|
||||
formation = db.session.get(Formation, ue["formation_id"])
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_FORM,
|
||||
obj=ue["formation_id"],
|
||||
text=f"Modification de la formation {formation.acronyme}",
|
||||
)
|
||||
formation.invalidate_cached_sems()
|
||||
|
||||
|
||||
def matiere_delete(matiere_id=None):
|
||||
"""Delete matière"""
|
||||
from app.scodoc import sco_edit_ue
|
||||
|
||||
matiere = Matiere.query.get_or_404(matiere_id)
|
||||
if not can_delete_matiere(matiere):
|
||||
"""Form delete matière"""
|
||||
matiere = Matiere.get_instance(matiere_id)
|
||||
if not matiere.can_be_deleted():
|
||||
# il y a au moins un modimpl dans un module de cette matière
|
||||
raise ScoNonEmptyFormationObject(
|
||||
"Matière",
|
||||
@ -227,22 +132,20 @@ def matiere_delete(matiere_id=None):
|
||||
),
|
||||
)
|
||||
|
||||
mat = matiere_list(args={"matiere_id": matiere_id})[0]
|
||||
UE = sco_edit_ue.ue_list(args={"ue_id": mat["ue_id"]})[0]
|
||||
H = [
|
||||
"<h2>Suppression de la matière %(titre)s" % mat,
|
||||
" dans l'UE (%(acronyme)s))</h2>" % UE,
|
||||
f"""<h2>Suppression de la matière {matiere.titre}
|
||||
dans l'UE {matiere.ue.acronyme}</h2>""",
|
||||
]
|
||||
dest_url = url_for(
|
||||
"notes.ue_table",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formation_id=str(UE["formation_id"]),
|
||||
formation_id=matiere.ue.formation_id,
|
||||
)
|
||||
tf = TrivialFormulator(
|
||||
request.base_url,
|
||||
scu.get_request_args(),
|
||||
(("matiere_id", {"input_type": "hidden"}),),
|
||||
initvalues=mat,
|
||||
initvalues=matiere.to_dict(),
|
||||
submitlabel="Confirmer la suppression",
|
||||
cancelbutton="Annuler",
|
||||
)
|
||||
@ -255,29 +158,23 @@ def matiere_delete(matiere_id=None):
|
||||
if tf[0] == -1:
|
||||
return flask.redirect(dest_url)
|
||||
|
||||
do_matiere_delete(matiere_id)
|
||||
matiere.delete()
|
||||
return flask.redirect(dest_url)
|
||||
|
||||
|
||||
def matiere_edit(matiere_id=None):
|
||||
"""Edit matiere"""
|
||||
from app.scodoc import sco_edit_ue
|
||||
|
||||
F = matiere_list(args={"matiere_id": matiere_id})
|
||||
if not F:
|
||||
raise ScoValueError("Matière inexistante !")
|
||||
F = F[0]
|
||||
ues = sco_edit_ue.ue_list(args={"ue_id": F["ue_id"]})
|
||||
if not ues:
|
||||
raise ScoValueError("UE inexistante !")
|
||||
ue = ues[0]
|
||||
formation: Formation = Formation.query.get_or_404(ue["formation_id"])
|
||||
ues = sco_edit_ue.ue_list(args={"formation_id": ue["formation_id"]})
|
||||
ue_names = ["%(acronyme)s (%(titre)s)" % u for u in ues]
|
||||
ue_ids = [u["ue_id"] for u in ues]
|
||||
"""Form edit matiere"""
|
||||
matiere: Matiere = Matiere.get_instance(matiere_id)
|
||||
if matiere.is_locked():
|
||||
raise ScoLockedFormError()
|
||||
ue = matiere.ue
|
||||
formation = ue.formation
|
||||
ues = matiere.ue.formation.ues
|
||||
ue_names = [f"{u.acronyme} ({u.titre or ''})" for u in ues]
|
||||
ue_ids = [u.id for u in ues]
|
||||
H = [
|
||||
"""<h2>Modification de la matière %(titre)s""" % F,
|
||||
f"""(formation ({formation.acronyme}, version {formation.version})</h2>""",
|
||||
f"""<h2>Modification de la matière {matiere.titre or 'sans titre'}
|
||||
(formation ({formation.acronyme}, version {formation.version})</h2>""",
|
||||
]
|
||||
help_msg = """<p class="help">Les matières sont des groupes de modules dans une UE
|
||||
d'une formation donnée. Les matières servent surtout pour la
|
||||
@ -317,14 +214,14 @@ associé.
|
||||
},
|
||||
),
|
||||
),
|
||||
initvalues=F,
|
||||
initvalues=matiere.to_dict(),
|
||||
submitlabel="Modifier les valeurs",
|
||||
)
|
||||
|
||||
dest_url = url_for(
|
||||
"notes.ue_table",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formation_id=str(ue["formation_id"]),
|
||||
formation_id=formation.id,
|
||||
)
|
||||
if tf[0] == 0:
|
||||
return render_template(
|
||||
@ -336,8 +233,8 @@ associé.
|
||||
return flask.redirect(dest_url)
|
||||
else:
|
||||
# check unicity
|
||||
mats = matiere_list(args={"ue_id": tf[2]["ue_id"], "titre": tf[2]["titre"]})
|
||||
if len(mats) > 1 or (len(mats) == 1 and mats[0]["matiere_id"] != matiere_id):
|
||||
mats = Matiere.query.filter_by(ue_id=tf[2]["ue_id"], titre=tf[2]["titre"]).all()
|
||||
if len(mats) > 1 or (len(mats) == 1 and mats[0].id != matiere_id):
|
||||
return render_template(
|
||||
"sco_page.j2",
|
||||
title="Modification d'une matière",
|
||||
@ -348,32 +245,18 @@ associé.
|
||||
),
|
||||
)
|
||||
|
||||
modif = False
|
||||
# changement d'UE ?
|
||||
if tf[2]["ue_id"] != F["ue_id"]:
|
||||
log("attaching mat %s to new UE %s" % (matiere_id, tf[2]["ue_id"]))
|
||||
ndb.SimpleQuery(
|
||||
"UPDATE notes_modules SET ue_id = %(ue_id)s WHERE matiere_id=%(matiere_id)s",
|
||||
{"ue_id": tf[2]["ue_id"], "matiere_id": matiere_id},
|
||||
)
|
||||
|
||||
do_matiere_edit(tf[2])
|
||||
|
||||
if tf[2]["ue_id"] != ue.id:
|
||||
log(f"attaching mat {matiere_id} to new UE id={tf[2]['ue_id']}")
|
||||
new_ue = UniteEns.get_ue(tf[2]["ue_id"])
|
||||
if new_ue.formation_id != formation.id:
|
||||
raise ScoValueError("UE does not belong to the same formation")
|
||||
matiere.ue = new_ue
|
||||
modif = True
|
||||
modif |= matiere.from_dict(tf[2])
|
||||
if modif:
|
||||
db.session.commit()
|
||||
matiere.ue.formation.invalidate_cached_sems()
|
||||
flash("Matière modifiée", "info")
|
||||
return flask.redirect(dest_url)
|
||||
|
||||
|
||||
def matiere_is_locked(matiere_id):
|
||||
"""True if matiere should not be modified
|
||||
(contains modules used in a locked formsemestre)
|
||||
"""
|
||||
r = ndb.SimpleDictFetch(
|
||||
"""SELECT ma.id
|
||||
FROM notes_matieres ma, notes_modules mod, notes_formsemestre sem, notes_moduleimpl mi
|
||||
WHERE ma.id = mod.matiere_id
|
||||
AND mi.module_id = mod.id
|
||||
AND mi.formsemestre_id = sem.id
|
||||
AND ma.id = %(matiere_id)s
|
||||
AND sem.etat = false
|
||||
""",
|
||||
{"matiere_id": matiere_id},
|
||||
)
|
||||
return len(r) > 0
|
@ -33,168 +33,28 @@ from flask import flash, url_for, render_template
|
||||
from flask import g, request
|
||||
from flask_login import current_user
|
||||
|
||||
from app import db, log
|
||||
from app import db
|
||||
from app import models
|
||||
from app.models import APO_CODE_STR_LEN
|
||||
from app.models import Formation, Matiere, Module, UniteEns
|
||||
from app.models import FormSemestre, ModuleImpl
|
||||
from app.models import ScolarNews
|
||||
from app.models.but_refcomp import ApcAppCritique, ApcParcours
|
||||
|
||||
import app.scodoc.notesdb as ndb
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc.TrivialFormulator import TrivialFormulator
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc.sco_exceptions import (
|
||||
ScoValueError,
|
||||
ScoLockedFormError,
|
||||
ScoGenError,
|
||||
ScoNonEmptyFormationObject,
|
||||
)
|
||||
from app.scodoc import codes_cursus
|
||||
from app.scodoc import sco_edit_matiere
|
||||
from app.scodoc import sco_moduleimpl
|
||||
|
||||
_moduleEditor = ndb.EditableTable(
|
||||
"notes_modules",
|
||||
"module_id",
|
||||
(
|
||||
"module_id",
|
||||
"titre",
|
||||
"code",
|
||||
"abbrev",
|
||||
"heures_cours",
|
||||
"heures_td",
|
||||
"heures_tp",
|
||||
"coefficient",
|
||||
"ue_id",
|
||||
"matiere_id",
|
||||
"formation_id",
|
||||
"semestre_id",
|
||||
"numero",
|
||||
"code_apogee",
|
||||
"module_type",
|
||||
"edt_id",
|
||||
#'ects'
|
||||
),
|
||||
sortkey="numero, code, titre",
|
||||
output_formators={
|
||||
"heures_cours": ndb.float_null_is_zero,
|
||||
"heures_td": ndb.float_null_is_zero,
|
||||
"heures_tp": ndb.float_null_is_zero,
|
||||
"numero": ndb.int_null_is_zero,
|
||||
"coefficient": ndb.float_null_is_zero,
|
||||
"module_type": ndb.int_null_is_zero,
|
||||
#'ects' : ndb.float_null_is_null
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def module_list(*args, **kw):
|
||||
"list modules"
|
||||
cnx = ndb.GetDBConnexion()
|
||||
return _moduleEditor.list(cnx, *args, **kw)
|
||||
|
||||
|
||||
def do_module_create(args) -> int:
|
||||
"Create a module. Returns id of new object."
|
||||
formation = db.session.get(Formation, args["formation_id"])
|
||||
# refuse de créer un module APC avec semestres incohérents:
|
||||
if formation.is_apc():
|
||||
ue = db.session.get(UniteEns, args["ue_id"])
|
||||
if int(args.get("semestre_id", 0)) != ue.semestre_idx:
|
||||
raise ScoValueError("Formation incompatible: contacter le support ScoDoc")
|
||||
# create
|
||||
module = Module.create_from_dict(args)
|
||||
db.session.commit()
|
||||
log(f"do_module_create: created {module.id} with {args}")
|
||||
|
||||
# news
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_FORM,
|
||||
obj=formation.id,
|
||||
text=f"Modification de la formation {formation.acronyme}",
|
||||
)
|
||||
formation.invalidate_cached_sems()
|
||||
return module.id
|
||||
|
||||
|
||||
def module_create(
|
||||
matiere_id=None, module_type=None, semestre_id=None, formation_id=None
|
||||
):
|
||||
"""Formulaire de création d'un module
|
||||
Si matiere_id est spécifié, le module sera créé dans cette matière (cas normal).
|
||||
Sinon, donne le choix de l'UE de rattachement et utilise la première
|
||||
matière de cette UE (si elle n'existe pas, la crée).
|
||||
"""
|
||||
return module_edit(
|
||||
create=True,
|
||||
matiere_id=matiere_id,
|
||||
module_type=module_type,
|
||||
semestre_id=semestre_id,
|
||||
formation_id=formation_id,
|
||||
)
|
||||
|
||||
|
||||
def can_delete_module(module):
|
||||
"True si le module n'est pas utilisée dans des formsemestre"
|
||||
return len(module.modimpls.all()) == 0
|
||||
|
||||
|
||||
def do_module_delete(oid):
|
||||
"delete module"
|
||||
module = Module.query.get_or_404(oid)
|
||||
mod = module_list({"module_id": oid})[0] # sco7
|
||||
if module_is_locked(module.id):
|
||||
raise ScoLockedFormError()
|
||||
if not can_delete_module(module):
|
||||
raise ScoNonEmptyFormationObject(
|
||||
"Module",
|
||||
msg=module.titre,
|
||||
dest_url=url_for(
|
||||
"notes.ue_table",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formation_id=module.formation_id,
|
||||
semestre_idx=module.ue.semestre_idx,
|
||||
),
|
||||
)
|
||||
|
||||
# S'il y a des moduleimpls, on ne peut pas detruire le module !
|
||||
mods = sco_moduleimpl.moduleimpl_list(module_id=oid)
|
||||
if mods:
|
||||
err_page = f"""
|
||||
<h3>Destruction du module impossible car il est utilisé dans des
|
||||
semestres existants !</h3>
|
||||
<p class="help">Il faut d'abord supprimer le semestre (ou en retirer
|
||||
ce module).
|
||||
Mais il est peut être préférable de laisser ce programme intact et
|
||||
d'en créer une nouvelle version pour la modifier sans affecter
|
||||
les semestres déjà en place.
|
||||
</p>
|
||||
<a href="{url_for('notes.ue_table', scodoc_dept=g.scodoc_dept,
|
||||
formation_id=mod["formation_id"])}">reprendre</a>
|
||||
"""
|
||||
raise ScoGenError(err_page)
|
||||
# delete
|
||||
cnx = ndb.GetDBConnexion()
|
||||
_moduleEditor.delete(cnx, oid)
|
||||
|
||||
# news
|
||||
formation = module.formation
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_FORM,
|
||||
obj=mod["formation_id"],
|
||||
text=f"Modification de la formation {formation.acronyme}",
|
||||
)
|
||||
formation.invalidate_cached_sems()
|
||||
|
||||
|
||||
def module_delete(module_id=None):
|
||||
"""Delete a module"""
|
||||
"""Formulaire suppression d'un module"""
|
||||
module = Module.query.get_or_404(module_id)
|
||||
mod = module_list(args={"module_id": module_id})[0] # sco7
|
||||
|
||||
if not can_delete_module(module):
|
||||
if not module.can_be_deleted():
|
||||
raise ScoNonEmptyFormationObject(
|
||||
"Module",
|
||||
msg=module.titre,
|
||||
@ -221,7 +81,7 @@ def module_delete(module_id=None):
|
||||
request.base_url,
|
||||
scu.get_request_args(),
|
||||
(("module_id", {"input_type": "hidden"}),),
|
||||
initvalues=mod,
|
||||
initvalues=module.to_dict(),
|
||||
submitlabel="Confirmer la suppression",
|
||||
cancelbutton="Annuler",
|
||||
)
|
||||
@ -231,37 +91,38 @@ def module_delete(module_id=None):
|
||||
title="Suppression d'un module",
|
||||
content="\n".join(H) + tf[1],
|
||||
)
|
||||
elif tf[0] == -1:
|
||||
return flask.redirect(dest_url)
|
||||
else:
|
||||
do_module_delete(module_id)
|
||||
if tf[0] == -1: # cancel
|
||||
return flask.redirect(dest_url)
|
||||
|
||||
module.delete()
|
||||
return flask.redirect(dest_url)
|
||||
|
||||
|
||||
def do_module_edit(vals: dict) -> None:
|
||||
"edit a module"
|
||||
# check
|
||||
mod = module_list({"module_id": vals["module_id"]})[0]
|
||||
if module_is_locked(mod["module_id"]):
|
||||
# formation verrouillée: empeche de modifier certains champs:
|
||||
vals = vals.copy()
|
||||
protected_fields = ("coefficient", "ue_id", "matiere_id", "semestre_id")
|
||||
for f in protected_fields:
|
||||
if f in vals:
|
||||
del vals[f]
|
||||
module = Module.get_instance(vals["module_id"])
|
||||
# edit
|
||||
cnx = ndb.GetDBConnexion()
|
||||
_moduleEditor.edit(cnx, vals)
|
||||
db.session.get(Formation, mod["formation_id"]).invalidate_cached_sems()
|
||||
modif = module.from_dict(vals)
|
||||
if modif:
|
||||
module.formation.invalidate_cached_sems()
|
||||
|
||||
|
||||
def check_module_code_unicity(code, field, formation_id, module_id=None):
|
||||
"true si code module unique dans la formation"
|
||||
modules = module_list(args={"code": code, "formation_id": formation_id})
|
||||
if module_id: # edition: supprime le module en cours
|
||||
modules = [m for m in modules if m["module_id"] != module_id]
|
||||
|
||||
return len(modules) == 0
|
||||
def module_create(
|
||||
matiere_id=None, module_type=None, semestre_id=None, formation_id=None
|
||||
):
|
||||
"""Formulaire de création d'un module
|
||||
Si matiere_id est spécifié, le module sera créé dans cette matière (cas normal).
|
||||
Sinon, donne le choix de l'UE de rattachement et utilise la première
|
||||
matière de cette UE (si elle n'existe pas, la crée).
|
||||
"""
|
||||
return module_edit(
|
||||
create=True,
|
||||
matiere_id=matiere_id,
|
||||
module_type=module_type,
|
||||
semestre_id=semestre_id,
|
||||
formation_id=formation_id,
|
||||
)
|
||||
|
||||
|
||||
def module_edit(
|
||||
@ -278,14 +139,12 @@ def module_edit(
|
||||
Sinon, donne le choix de l'UE de rattachement et utilise la première matière
|
||||
de cette UE (si elle n'existe pas, la crée).
|
||||
"""
|
||||
from app.scodoc import sco_tag_module
|
||||
|
||||
# --- Détermination de la formation
|
||||
orig_semestre_idx = semestre_id
|
||||
ue = None
|
||||
if create:
|
||||
if matiere_id:
|
||||
matiere = Matiere.query.get_or_404(matiere_id)
|
||||
matiere = Matiere.get_instance(matiere_id)
|
||||
ue = matiere.ue
|
||||
formation = ue.formation
|
||||
orig_semestre_idx = ue.semestre_idx if semestre_id is None else semestre_id
|
||||
@ -300,7 +159,7 @@ def module_edit(
|
||||
ue = module.ue
|
||||
module_dict = module.to_dict()
|
||||
formation = module.formation
|
||||
unlocked = not module_is_locked(module_id)
|
||||
unlocked = not module.is_locked()
|
||||
|
||||
parcours = codes_cursus.get_cursus_from_code(formation.type_parcours)
|
||||
is_apc = parcours.APC_SAE # BUT
|
||||
@ -326,17 +185,14 @@ def module_edit(
|
||||
if (module and module.matiere and (module.matiere.id == mat.id))
|
||||
or (mat.id == mat.ue.matieres.first().id)
|
||||
]
|
||||
mat_names = [
|
||||
"S%s / %s" % (mat.ue.semestre_idx, mat.ue.acronyme) for mat in matieres
|
||||
]
|
||||
mat_names = [f"S{mat.ue.semestre_idx} / {mat.ue.acronyme}" for mat in matieres]
|
||||
else:
|
||||
mat_names = ["%s / %s" % (mat.ue.acronyme, mat.titre or "") for mat in matieres]
|
||||
mat_names = ["{mat.ue.acronyme} / {mat.titre or ''}" for mat in matieres]
|
||||
|
||||
if module: # edition
|
||||
ue_mat_ids = ["%s!%s" % (mat.ue.id, mat.id) for mat in matieres]
|
||||
module_dict["ue_matiere_id"] = "%s!%s" % (
|
||||
module_dict["ue_id"],
|
||||
module_dict["matiere_id"],
|
||||
ue_mat_ids = [f"{mat.ue.id}!{mat.id}" for mat in matieres]
|
||||
module_dict["ue_matiere_id"] = (
|
||||
f"{module_dict['ue_id']}!{module_dict['matiere_id']}"
|
||||
)
|
||||
|
||||
semestres_indices = list(range(1, parcours.NB_SEM + 1))
|
||||
@ -433,8 +289,8 @@ def module_edit(
|
||||
"explanation": """code du module (issu du programme, exemple M1203,
|
||||
R2.01, ou SAÉ 3.4. Doit être unique dans la formation)""",
|
||||
"allow_null": False,
|
||||
"validator": lambda val, field, formation_id=formation.id: check_module_code_unicity(
|
||||
val, field, formation_id, module_id=module.id if module else None
|
||||
"validator": lambda val, _, formation_id=formation.id: Module.check_module_code_unicity(
|
||||
val, formation_id, module_id=module.id if module else None
|
||||
),
|
||||
},
|
||||
),
|
||||
@ -602,7 +458,8 @@ def module_edit(
|
||||
"title": "UE de rattachement",
|
||||
"explanation": "utilisée notamment pour les malus",
|
||||
"labels": [
|
||||
f"S{u.semestre_idx if u.semestre_idx is not None else '.'} / {u.acronyme} {u.titre}"
|
||||
f"""S{u.semestre_idx if u.semestre_idx is not None else '.'
|
||||
} / {u.acronyme} {u.titre}"""
|
||||
for u in ues
|
||||
],
|
||||
"allowed_values": [u.id for u in ues],
|
||||
@ -631,7 +488,8 @@ def module_edit(
|
||||
"input_type": "menu",
|
||||
"type": "int",
|
||||
"title": parcours.SESSION_NAME.capitalize(),
|
||||
"explanation": f"{parcours.SESSION_NAME} de début du module dans la formation standard",
|
||||
"explanation": f"""{parcours.SESSION_NAME
|
||||
} de début du module dans la formation standard""",
|
||||
"labels": [str(x) for x in semestres_indices],
|
||||
"allowed_values": semestres_indices,
|
||||
"enabled": unlocked,
|
||||
@ -767,6 +625,7 @@ def module_edit(
|
||||
module_dict["semestre_id"] = 1
|
||||
else:
|
||||
module_dict["semestre_id"] = module.ue.semestre_idx
|
||||
tags = module.tags if module else []
|
||||
tf = TrivialFormulator(
|
||||
request.base_url,
|
||||
scu.get_request_args(),
|
||||
@ -774,7 +633,9 @@ def module_edit(
|
||||
html_foot_markup=(
|
||||
f"""<div class="scobox sco_tag_module_edit"><span
|
||||
class="sco_tag_edit"><textarea data-module_id="{module_id}" class="module_tag_editor"
|
||||
>{','.join(sco_tag_module.module_tag_list(module_id))}</textarea></span></div>
|
||||
>{
|
||||
','.join(t.title for t in tags)
|
||||
}</textarea></span></div>
|
||||
"""
|
||||
if not create
|
||||
else ""
|
||||
@ -833,14 +694,17 @@ def module_edit(
|
||||
if matiere:
|
||||
tf[2]["matiere_id"] = matiere.id
|
||||
else:
|
||||
matiere_id = sco_edit_matiere.do_matiere_create(
|
||||
{"ue_id": ue.id, "titre": ue.titre or "", "numero": 1},
|
||||
matiere = Matiere.create_from_dict(
|
||||
{
|
||||
"ue_id": ue.id,
|
||||
"titre": ue.titre or "",
|
||||
"numero": 1,
|
||||
}
|
||||
)
|
||||
tf[2]["matiere_id"] = matiere_id
|
||||
tf[2]["matiere_id"] = matiere.id
|
||||
|
||||
tf[2]["semestre_id"] = ue.semestre_idx
|
||||
module_id = do_module_create(tf[2])
|
||||
module = db.session.get(Module, module_id)
|
||||
module = Module.create_from_dict(tf[2], news=True)
|
||||
else: # EDITION MODULE
|
||||
# l'UE de rattachement peut changer
|
||||
tf[2]["ue_id"], tf[2]["matiere_id"] = tf[2]["ue_matiere_id"].split("!")
|
||||
@ -885,6 +749,7 @@ def module_edit(
|
||||
]
|
||||
db.session.add(module)
|
||||
db.session.commit()
|
||||
module.formation.invalidate_cached_sems()
|
||||
return flask.redirect(
|
||||
url_for(
|
||||
"notes.ue_table",
|
||||
@ -897,30 +762,36 @@ def module_edit(
|
||||
|
||||
def module_table(formation_id):
|
||||
"""Liste des modules de la formation
|
||||
(XXX inutile ou a revoir)
|
||||
(affichage debug)
|
||||
"""
|
||||
from app.scodoc import sco_formations
|
||||
formation = Formation.get_formation(formation_id)
|
||||
editable = current_user.has_permission(Permission.EditFormation)
|
||||
|
||||
if not formation_id:
|
||||
raise ScoValueError("invalid formation !")
|
||||
formation: Formation = Formation.query.get_or_404(formation_id)
|
||||
H = [
|
||||
f"""<h2>Listes des modules dans la formation {formation.titre} ({formation.acronyme}</h2>
|
||||
f"""<h2>Listes des modules dans la formation
|
||||
{formation.titre} ({formation.acronyme} (debug)
|
||||
</h2>
|
||||
<ul class="notes_module_list">
|
||||
""",
|
||||
]
|
||||
editable = current_user.has_permission(Permission.EditFormation)
|
||||
|
||||
for module_dict in module_list(args={"formation_id": formation_id}):
|
||||
H.append('<li class="notes_module_list">%s' % module_dict)
|
||||
for module in formation.modules:
|
||||
m_dict = module.to_dict()
|
||||
m_dict["parcours"] = [p.code for p in module.parcours]
|
||||
str_module = str(m_dict).replace(",", ",\n")
|
||||
H.append(
|
||||
f'<li class="notes_module_list"><pre style="margin-bottom: 1px;">{str_module}</pre>'
|
||||
)
|
||||
if editable:
|
||||
H.append(
|
||||
'<a href="module_edit?module_id=%(module_id)s">modifier</a>'
|
||||
% module_dict
|
||||
)
|
||||
H.append(
|
||||
'<a href="module_delete?module_id=%(module_id)s">supprimer</a>'
|
||||
% module_dict
|
||||
f"""
|
||||
<a class="stdlink" href="{
|
||||
url_for('notes.module_edit', scodoc_dept=g.scodoc_dept, module_id=module.id)
|
||||
}">modifier</a>
|
||||
<a class="stdlink" href="{
|
||||
url_for('notes.module_delete', scodoc_dept=g.scodoc_dept, module_id=module.id)
|
||||
}">supprimer</a>
|
||||
"""
|
||||
)
|
||||
H.append("</li>")
|
||||
H.append("</ul>")
|
||||
@ -931,29 +802,6 @@ def module_table(formation_id):
|
||||
)
|
||||
|
||||
|
||||
def module_is_locked(module_id):
|
||||
"""True if module should not be modified
|
||||
(used in a locked formsemestre)
|
||||
"""
|
||||
r = ndb.SimpleDictFetch(
|
||||
"""SELECT mi.id
|
||||
FROM notes_modules mod, notes_formsemestre sem, notes_moduleimpl mi
|
||||
WHERE mi.module_id = mod.id
|
||||
AND mi.formsemestre_id = sem.id
|
||||
AND mi.module_id = %(module_id)s
|
||||
AND sem.etat = false
|
||||
""",
|
||||
{"module_id": module_id},
|
||||
)
|
||||
return len(r) > 0
|
||||
|
||||
|
||||
def module_count_moduleimpls(module_id):
|
||||
"Number of moduleimpls using this module"
|
||||
mods = sco_moduleimpl.moduleimpl_list(module_id=module_id)
|
||||
return len(mods)
|
||||
|
||||
|
||||
def formation_add_malus_modules(
|
||||
formation_id: int, semestre_id: int = None, titre=None, redirect=True
|
||||
):
|
||||
@ -967,7 +815,7 @@ def formation_add_malus_modules(
|
||||
ues = ues.filter_by(semestre_idx=semestre_id)
|
||||
for ue in ues:
|
||||
if ue.type == codes_cursus.UE_STANDARD:
|
||||
if ue_add_malus_module(ue, titre=titre) != None:
|
||||
if ue_add_malus_module(ue, titre=titre) is not None:
|
||||
nb += 1
|
||||
|
||||
flash(f"Modules de malus ajoutés dans {nb} UEs du S{semestre_id}")
|
||||
@ -1003,7 +851,8 @@ def ue_add_malus_module(ue: UniteEns, titre=None, code=None) -> int:
|
||||
# c'est ennuyeux: dans ce cas, on pourrait demander à indiquer explicitement
|
||||
# le semestre ? ou affecter le malus au semestre 1 ???
|
||||
raise ScoValueError(
|
||||
"Impossible d'ajouter un malus si l'UE n'a pas de numéro de semestre et ne comporte pas d'autres modules"
|
||||
"""Impossible d'ajouter un malus si l'UE n'a pas de numéro de semestre
|
||||
et ne comporte pas d'autres modules"""
|
||||
)
|
||||
else:
|
||||
semestre_id = ue.semestre_idx
|
@ -45,7 +45,6 @@ from app.models import (
|
||||
FormSemestreUECoef,
|
||||
Matiere,
|
||||
Module,
|
||||
ModuleImpl,
|
||||
UniteEns,
|
||||
)
|
||||
from app.models import ApcValidationRCUE, ScolarFormSemestreValidation, ScolarEvent
|
||||
@ -63,11 +62,8 @@ from app.scodoc.sco_exceptions import (
|
||||
|
||||
from app.scodoc import codes_cursus
|
||||
from app.scodoc import sco_edit_apc
|
||||
from app.scodoc import sco_edit_matiere
|
||||
from app.scodoc import sco_edit_module
|
||||
from app.scodoc import sco_groups
|
||||
from app.scodoc import sco_moduleimpl
|
||||
from app.scodoc import sco_tag_module
|
||||
|
||||
_ueEditor = ndb.EditableTable(
|
||||
"notes_ue",
|
||||
@ -376,7 +372,8 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
|
||||
"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.
|
||||
"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,
|
||||
@ -422,7 +419,8 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
|
||||
{
|
||||
"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",
|
||||
"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,
|
||||
},
|
||||
),
|
||||
@ -446,7 +444,8 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
|
||||
"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'UE effectuées à l'extérieur de l'établissement",
|
||||
"explanation": """réservé pour les capitalisations d'UEs
|
||||
effectuées à l'extérieur de l'établissement""",
|
||||
},
|
||||
),
|
||||
(
|
||||
@ -466,7 +465,8 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
|
||||
"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)",
|
||||
"explanation": """créer immédiatement une matière dans cette UE
|
||||
(utile si on n'utilise pas de matières)""",
|
||||
},
|
||||
)
|
||||
)
|
||||
@ -545,15 +545,17 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
|
||||
formation_id, int(tf[2]["semestre_idx"])
|
||||
)
|
||||
ue_id = 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_id = sco_edit_matiere.do_matiere_create(
|
||||
{"ue_id": ue_id, "titre": tf[2]["titre"], "numero": 1},
|
||||
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:
|
||||
_ = sco_edit_module.do_module_create(
|
||||
_ = Module.create_from_dict(
|
||||
{
|
||||
"titre": tf[2]["titre"],
|
||||
"code": tf[2]["acronyme"],
|
||||
@ -565,6 +567,7 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
|
||||
"semestre_id": tf[2]["semestre_idx"],
|
||||
},
|
||||
)
|
||||
db.session.commit()
|
||||
ue = db.session.get(UniteEns, ue_id)
|
||||
flash(f"UE créée (code {ue.ue_code})")
|
||||
else:
|
||||
@ -594,45 +597,21 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
|
||||
)
|
||||
|
||||
|
||||
def _add_ue_semestre_id(ues: list[dict], is_apc):
|
||||
"""ajoute semestre_id dans les ue, en regardant
|
||||
semestre_idx ou à défaut, pour les formations non APC, le premier module
|
||||
de chacune.
|
||||
Les UE sans modules se voient attribuer le numero UE_SEM_DEFAULT (1000000),
|
||||
qui les place à la fin de la liste.
|
||||
"""
|
||||
for ue in ues:
|
||||
if ue["semestre_idx"] is not None:
|
||||
ue["semestre_id"] = ue["semestre_idx"]
|
||||
elif is_apc:
|
||||
ue["semestre_id"] = codes_cursus.UE_SEM_DEFAULT
|
||||
else:
|
||||
# était le comportement ScoDoc7
|
||||
modules = sco_edit_module.module_list(args={"ue_id": ue["ue_id"]})
|
||||
if modules:
|
||||
ue["semestre_id"] = modules[0]["semestre_id"]
|
||||
else:
|
||||
ue["semestre_id"] = codes_cursus.UE_SEM_DEFAULT
|
||||
|
||||
|
||||
def next_ue_numero(formation_id, semestre_id=None):
|
||||
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 = ue_list(args={"formation_id": formation_id})
|
||||
ues = formation.ues.all()
|
||||
if not ues:
|
||||
return 0
|
||||
if semestre_id is None:
|
||||
return ues[-1]["numero"] + 1000
|
||||
else:
|
||||
# Avec semestre: (prend le semestre du 1er module de l'UE)
|
||||
_add_ue_semestre_id(ues, formation.get_cursus().APC_SAE)
|
||||
ue_list_semestre = [ue for ue in ues if ue["semestre_id"] == semestre_id]
|
||||
if ue_list_semestre:
|
||||
return ue_list_semestre[-1]["numero"] + 10
|
||||
else:
|
||||
return ues[-1]["numero"] + 1000
|
||||
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):
|
||||
@ -677,15 +656,14 @@ def ue_delete(ue_id=None, delete_validations=False, dialog_confirmed=False):
|
||||
return do_ue_delete(ue, delete_validations=delete_validations)
|
||||
|
||||
|
||||
def ue_table(formation_id=None, semestre_idx=1, msg=""): # was ue_list
|
||||
"""Liste des matières et modules d'une formation, avec liens pour
|
||||
éditer (si non verrouillée).
|
||||
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 = db.session.get(Formation, formation_id)
|
||||
if not formation:
|
||||
raise ScoValueError("invalid formation_id")
|
||||
formation = Formation.get_formation(formation_id)
|
||||
parcours = formation.get_cursus()
|
||||
is_apc = parcours.APC_SAE
|
||||
if semestre_idx == "all" or semestre_idx == "":
|
||||
@ -695,20 +673,22 @@ def ue_table(formation_id=None, semestre_idx=1, msg=""): # was ue_list
|
||||
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)
|
||||
# transition: on requete ici via l'ORM mais on utilise les fonctions ScoDoc7
|
||||
# basées sur des dicts
|
||||
ues_obj = UniteEns.query.filter_by(
|
||||
formation_id=formation_id, is_external=False
|
||||
).order_by(UniteEns.semestre_idx, UniteEns.numero)
|
||||
|
||||
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_obj}
|
||||
if (None in numeros) or len(numeros) < ues_obj.count():
|
||||
scu.objects_renumber(db, ues_obj)
|
||||
numeros = {ue.numero for ue in ues}
|
||||
if (None in numeros) or len(numeros) < len(ues):
|
||||
scu.objects_renumber(db, ues)
|
||||
|
||||
ues_externes_obj = UniteEns.query.filter_by(
|
||||
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(),
|
||||
@ -718,29 +698,25 @@ def ue_table(formation_id=None, semestre_idx=1, msg=""): # was ue_list
|
||||
|
||||
if is_apc:
|
||||
# Pour faciliter la transition des anciens programmes non APC
|
||||
for ue in ues_obj:
|
||||
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_obj)
|
||||
colorie_anciennes_ues(ues)
|
||||
db.session.commit()
|
||||
ues = [ue.to_dict() for ue in ues_obj]
|
||||
ues_externes = [ue.to_dict() for ue in ues_externes_obj]
|
||||
|
||||
# tri par semestre et numero:
|
||||
_add_ue_semestre_id(ues, is_apc)
|
||||
_add_ue_semestre_id(ues_externes, is_apc)
|
||||
ues.sort(key=lambda u: (u["semestre_id"], u["numero"]))
|
||||
ues_externes.sort(key=lambda u: (u["semestre_id"], u["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"])
|
||||
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]
|
||||
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
|
||||
@ -796,8 +772,8 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
|
||||
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["ue_id"] )
|
||||
+ '">' + ue["acronyme"] + " (code " + ue["ue_code"] + ")</a>"
|
||||
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
|
||||
@ -1112,7 +1088,7 @@ def _html_select_semestre_idx(formation_id, semestre_ids, semestre_idx):
|
||||
|
||||
def _ue_table_ues(
|
||||
parcours,
|
||||
ues: list[dict],
|
||||
ues: list[UniteEns],
|
||||
editable,
|
||||
tag_editable,
|
||||
has_perm_change,
|
||||
@ -1129,33 +1105,25 @@ def _ue_table_ues(
|
||||
cur_ue_semestre_id = None
|
||||
iue = 0
|
||||
for ue in ues:
|
||||
if ue["ects"] is None:
|
||||
ue["ects_str"] = ""
|
||||
else:
|
||||
ue["ects_str"] = ", %g ECTS" % ue["ects"]
|
||||
if editable:
|
||||
klass = "span_apo_edit"
|
||||
else:
|
||||
klass = ""
|
||||
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["ue_id"],
|
||||
ue_id=ue.id,
|
||||
)
|
||||
ue[
|
||||
"code_apogee_str"
|
||||
] = f""", Apo: <span
|
||||
class="{klass}" data-url="{edit_url}" id="{ue['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 ""
|
||||
ue.code_apogee or ""
|
||||
}</span>"""
|
||||
|
||||
if cur_ue_semestre_id != ue["semestre_id"]:
|
||||
cur_ue_semestre_id = ue["semestre_id"]
|
||||
if ue["semestre_id"] == codes_cursus.UE_SEM_DEFAULT:
|
||||
if cur_ue_semestre_id != ue.semestre_id:
|
||||
cur_ue_semestre_id = ue.semestre_id
|
||||
if ue.semestre_id == codes_cursus.UE_SEM_DEFAULT:
|
||||
lab = "Pas d'indication de semestre:"
|
||||
else:
|
||||
lab = f"""Semestre {ue["semestre_id"]}:"""
|
||||
lab = f"""Semestre {ue.semestre_id}:"""
|
||||
H.append(
|
||||
f'<div class="ue_list_div"><div class="ue_list_tit_sem">{lab}</div>'
|
||||
)
|
||||
@ -1163,52 +1131,55 @@ def _ue_table_ues(
|
||||
H.append('<li class="notes_ue_list">')
|
||||
if iue != 0 and editable:
|
||||
H.append(
|
||||
'<a href="ue_move?ue_id=%s&after=0" class="aud">%s</a>'
|
||||
% (ue["ue_id"], arrow_up)
|
||||
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(
|
||||
'<a href="ue_move?ue_id=%s&after=1" class="aud">%s</a>'
|
||||
% (ue["ue_id"], arrow_down)
|
||||
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)
|
||||
ue["acro_titre"] = str(ue["acronyme"])
|
||||
if ue["titre"] != ue["acronyme"]:
|
||||
ue["acro_titre"] += " " + str(ue["titre"])
|
||||
acro_titre = ue.acronyme
|
||||
if ue.titre != ue.acronyme:
|
||||
acro_titre += " " + (ue.titre or "")
|
||||
H.append(
|
||||
"""%(acro_titre)s <span class="ue_code">(code %(ue_code)s%(ects_str)s, coef. %(coefficient)3.2f%(code_apogee_str)s)</span>
|
||||
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>
|
||||
"""
|
||||
% ue
|
||||
)
|
||||
if ue["type"] != codes_cursus.UE_STANDARD:
|
||||
if ue.type != codes_cursus.UE_STANDARD:
|
||||
H.append(
|
||||
'<span class="ue_type">%s</span>'
|
||||
% codes_cursus.UE_TYPE_NAME[ue["type"]]
|
||||
f"""<span class="ue_type">{codes_cursus.UE_TYPE_NAME[ue.type]}</span>"""
|
||||
)
|
||||
if ue["is_external"]:
|
||||
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 len(sco_moduleimpl.moduleimpls_in_external_ue(ue["ue_id"])) > 1:
|
||||
if len(sco_moduleimpl.moduleimpls_in_external_ue(ue.id)) > 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["ue_id"])
|
||||
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["ue_id"])
|
||||
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["ue_id"])
|
||||
url_for("notes.ue_edit", scodoc_dept=g.scodoc_dept, ue_id=ue.id)
|
||||
}">modifier</a>"""
|
||||
)
|
||||
else:
|
||||
@ -1228,11 +1199,14 @@ def _ue_table_ues(
|
||||
delete_disabled_icon,
|
||||
)
|
||||
)
|
||||
if (iue >= len(ues) - 1) or ue["semestre_id"] != ues[iue + 1]["semestre_id"]:
|
||||
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['semestre_id'])
|
||||
}">Ajouter une UE dans le semestre {ue['semestre_id'] or ''}</a></li></ul>
|
||||
f"""</ul><ul><li><a href="{
|
||||
url_for('notes.ue_create', scodoc_dept=g.scodoc_dept,
|
||||
formation_id=ue.formation_id, semestre_idx=ue.semestre_id)
|
||||
}">Ajouter une UE dans le semestre {ue.semestre_id or ''}</a></li></ul>
|
||||
</div>
|
||||
"""
|
||||
)
|
||||
@ -1243,7 +1217,7 @@ def _ue_table_ues(
|
||||
|
||||
def _ue_table_matieres(
|
||||
parcours,
|
||||
ue,
|
||||
ue: UniteEns,
|
||||
editable,
|
||||
tag_editable,
|
||||
arrow_up,
|
||||
@ -1256,23 +1230,23 @@ def _ue_table_matieres(
|
||||
H = []
|
||||
if not parcours.UE_IS_MODULE:
|
||||
H.append('<ul class="notes_matiere_list">')
|
||||
matieres = sco_edit_matiere.matiere_list(args={"ue_id": ue["ue_id"]})
|
||||
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 sco_edit_matiere.matiere_is_locked(mat["matiere_id"]):
|
||||
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["matiere_id"])
|
||||
scodoc_dept=g.scodoc_dept, matiere_id=mat.id)
|
||||
}">
|
||||
"""
|
||||
)
|
||||
H.append("%(titre)s" % mat)
|
||||
if editable and not sco_edit_matiere.matiere_is_locked(mat["matiere_id"]):
|
||||
H.append(f"{mat.titre or 'sans titre'}")
|
||||
if editable and not mat.is_locked():
|
||||
H.append("</a>")
|
||||
|
||||
modules = sco_edit_module.module_list(args={"matiere_id": mat["matiere_id"]})
|
||||
modules = mat.modules.all()
|
||||
H.append(
|
||||
_ue_table_modules(
|
||||
parcours,
|
||||
@ -1294,14 +1268,17 @@ def _ue_table_matieres(
|
||||
H.append("<li>Aucune matière dans cette UE ! ")
|
||||
if editable:
|
||||
H.append(
|
||||
"""<a class="stdlink" href="ue_delete?ue_id=%(ue_id)s">supprimer l'UE</a>"""
|
||||
% ue
|
||||
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(
|
||||
'<li><a class="stdlink" href="matiere_create?ue_id=%(ue_id)s">créer une matière</a> </li>'
|
||||
% ue
|
||||
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>")
|
||||
@ -1310,9 +1287,9 @@ def _ue_table_matieres(
|
||||
|
||||
def _ue_table_modules(
|
||||
parcours,
|
||||
ue,
|
||||
mat,
|
||||
modules,
|
||||
ue: UniteEns,
|
||||
mat: Matiere,
|
||||
modules: list[Module],
|
||||
editable,
|
||||
tag_editable,
|
||||
arrow_up,
|
||||
@ -1320,7 +1297,6 @@ def _ue_table_modules(
|
||||
arrow_none,
|
||||
delete_icon,
|
||||
delete_disabled_icon,
|
||||
unit_name="matière",
|
||||
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",
|
||||
@ -1329,91 +1305,83 @@ def _ue_table_modules(
|
||||
H = ['<ul class="notes_module_list">']
|
||||
im = 0
|
||||
for mod in modules:
|
||||
mod["nb_moduleimpls"] = sco_edit_module.module_count_moduleimpls(
|
||||
mod["module_id"]
|
||||
)
|
||||
nb_moduleimpls = mod.modimpls.count()
|
||||
klass = "notes_module_list"
|
||||
if mod["module_type"] == ModuleType.MALUS:
|
||||
if mod.module_type == ModuleType.MALUS:
|
||||
klass += " module_malus"
|
||||
H.append('<li class="%s">' % klass)
|
||||
H.append(f'<li class="{klass}">')
|
||||
|
||||
H.append('<span class="notes_module_list_buts">')
|
||||
if im != 0 and editable:
|
||||
H.append(
|
||||
'<a href="module_move?module_id=%s&after=0" class="aud">%s</a>'
|
||||
% (mod["module_id"], arrow_up)
|
||||
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(
|
||||
'<a href="module_move?module_id=%s&after=1" class="aud">%s</a>'
|
||||
% (mod["module_id"], arrow_down)
|
||||
f"""<a href="module_move?module_id={mod.id}&after=1" class="aud">{arrow_down}</a>"""
|
||||
)
|
||||
else:
|
||||
H.append(arrow_none)
|
||||
im += 1
|
||||
if mod["nb_moduleimpls"] == 0 and editable:
|
||||
icon = delete_icon
|
||||
else:
|
||||
icon = delete_disabled_icon
|
||||
icon = delete_icon if nb_moduleimpls == 0 and editable else delete_disabled_icon
|
||||
H.append(
|
||||
'<a class="smallbutton" href="module_delete?module_id=%s">%s</a>'
|
||||
% (mod["module_id"], icon)
|
||||
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 # and not sco_edit_module.module_is_locked( Mod['module_id'])
|
||||
)
|
||||
mod_editable = editable
|
||||
if mod_editable:
|
||||
H.append(
|
||||
'<a class="discretelink" title="Modifier le module numéro %(numero)s, utilisé par %(nb_moduleimpls)d sessions" href="module_edit?module_id=%(module_id)s">'
|
||||
% mod
|
||||
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):
|
||||
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(
|
||||
'<span class="formation_module_tit">%s</span>'
|
||||
% scu.join_words(mod["code"], mod["titre"])
|
||||
f"""<span class="formation_module_tit">{scu.join_words(mod.code, mod.titre)}</span>"""
|
||||
)
|
||||
if mod_editable:
|
||||
H.append("</a>")
|
||||
heurescoef = (
|
||||
"%(heures_cours)s/%(heures_td)s/%(heures_tp)s, coef. %(coefficient)s" % mod
|
||||
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["module_id"],
|
||||
module_id=mod.id,
|
||||
)
|
||||
heurescoef += f""", Apo: <span
|
||||
class="{'span_apo_edit' if editable else ''}"
|
||||
data-url="{edit_url}" id="{mod["module_id"]}"
|
||||
data-url="{edit_url}" id="{mod.id}"
|
||||
data-placeholder="{scu.APO_MISSING_CODE_STR}">{
|
||||
mod["code_apogee"] or ""
|
||||
mod.code_apogee or ""
|
||||
}</span>"""
|
||||
if tag_editable:
|
||||
tag_cls = "module_tag_editor"
|
||||
else:
|
||||
tag_cls = "module_tag_editor_ro"
|
||||
tag_mk = """<span class="sco_tag_edit"><form><textarea data-module_id="{}" class="{}">{}</textarea></form></span>"""
|
||||
tag_edit = tag_mk.format(
|
||||
mod["module_id"],
|
||||
tag_cls,
|
||||
",".join(sco_tag_module.module_tag_list(mod["module_id"])),
|
||||
)
|
||||
if ue["semestre_idx"] is not None and mod["semestre_id"] != ue["semestre_idx"]:
|
||||
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(
|
||||
" %s %s%s" % (parcours.SESSION_NAME, mod["semestre_id"], warning_semestre)
|
||||
+ " (%s)" % heurescoef
|
||||
+ tag_edit
|
||||
f""" {parcours.SESSION_NAME} {mod.semestre_id}{warning_semestre}
|
||||
{heurescoef}{tag_edit}"""
|
||||
)
|
||||
H.append("</li>")
|
||||
if not modules:
|
||||
@ -1422,7 +1390,7 @@ def _ue_table_modules(
|
||||
H.append(
|
||||
f"""<a class="stdlink" href="{
|
||||
url_for("notes.matiere_delete",
|
||||
scodoc_dept=g.scodoc_dept, matiere_id=mat["matiere_id"])}"
|
||||
scodoc_dept=g.scodoc_dept, matiere_id=mat.id)}"
|
||||
>la supprimer</a>
|
||||
"""
|
||||
)
|
||||
@ -1431,7 +1399,7 @@ def _ue_table_modules(
|
||||
H.append(
|
||||
f"""<li> <a class="stdlink" href="{
|
||||
url_for("notes.module_create",
|
||||
scodoc_dept=g.scodoc_dept, matiere_id=mat["matiere_id"])}"
|
||||
scodoc_dept=g.scodoc_dept, matiere_id=mat.id)}"
|
||||
>{create_element_msg}</a></li>
|
||||
"""
|
||||
)
|
||||
@ -1505,16 +1473,18 @@ def do_ue_edit(args, bypass_lock=False, dont_invalidate_cache=False):
|
||||
"edit an UE"
|
||||
# check
|
||||
ue_id = args["ue_id"]
|
||||
ue = ue_list({"ue_id": ue_id})[0]
|
||||
ue = UniteEns.get_ue(ue_id)
|
||||
if not bypass_lock:
|
||||
ue_locked, ue_locked_reason = ue_is_locked(ue["ue_id"])
|
||||
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 = ue_list({"formation_id": ue["formation_id"], "acronyme": new_acro})
|
||||
if ues and ues[0]["ue_id"] != ue_id:
|
||||
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.)"""
|
||||
@ -1523,48 +1493,12 @@ def do_ue_edit(args, bypass_lock=False, dont_invalidate_cache=False):
|
||||
if "ue_code" in args and not args["ue_code"]:
|
||||
del args["ue_code"]
|
||||
|
||||
cnx = ndb.GetDBConnexion()
|
||||
_ueEditor.edit(cnx, args)
|
||||
|
||||
formation = db.session.get(Formation, ue["formation_id"])
|
||||
ue.from_dict(args)
|
||||
db.session.commit()
|
||||
if not dont_invalidate_cache:
|
||||
# Invalide les semestres utilisant cette formation
|
||||
# ainsi que les poids et coefs
|
||||
formation.invalidate_module_coefs()
|
||||
|
||||
|
||||
def ue_is_locked(ue_id: int) -> tuple[bool, str]:
|
||||
"""True if UE should not be modified:
|
||||
utilisée dans un formsemestre verrouillé ou validations de jury de cette UE.
|
||||
Renvoie aussi une explication.
|
||||
"""
|
||||
# before 9.7.23: contains modules used in a locked formsemestre
|
||||
# starting from 9.7.23: + existence de validations de jury de cette UE
|
||||
ue = UniteEns.query.get(ue_id)
|
||||
if not ue:
|
||||
return True, "inexistante"
|
||||
if ue.formation.is_apc():
|
||||
# en APC, interdit toute modification d'UE si utilisée dans un semestre verrouillé
|
||||
if False in [formsemestre.etat for formsemestre in ue.formation.formsemestres]:
|
||||
return True, "utilisée dans un semestre verrouillé"
|
||||
else:
|
||||
# en classique: interdit si contient des modules utilisés dans des semestres verrouillés
|
||||
# en effet, dans certaines (très anciennes) formations, une UE peut avoir des modules de
|
||||
# différents semestre
|
||||
if (
|
||||
Module.query.filter(Module.ue_id == ue_id)
|
||||
.join(Module.modimpls)
|
||||
.join(ModuleImpl.formsemestre)
|
||||
.filter_by(etat=False)
|
||||
.count()
|
||||
):
|
||||
return True, "avec modules utilisés dans des semestres verrouillés"
|
||||
|
||||
nb_validations = ScolarFormSemestreValidation.query.filter_by(ue_id=ue_id).count()
|
||||
if nb_validations > 0:
|
||||
return True, f"avec {nb_validations} validations de jury"
|
||||
|
||||
return False, ""
|
||||
ue.formation.invalidate_module_coefs()
|
||||
|
||||
|
||||
UE_PALETTE = [
|
@ -34,10 +34,10 @@ from flask import flash, g, request, url_for
|
||||
from flask_login import current_user
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app import db
|
||||
from app import log
|
||||
from app.models import Formation, FormSemestre, Module, UniteEns
|
||||
from app.formations import edit_ue
|
||||
from app.models import Formation, FormSemestre, Matiere, Module, UniteEns
|
||||
from app.models import ScolarNews
|
||||
from app.models.but_refcomp import (
|
||||
ApcAppCritique,
|
||||
@ -48,49 +48,12 @@ from app.models.but_refcomp import (
|
||||
)
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import codes_cursus
|
||||
from app.scodoc import sco_edit_matiere
|
||||
from app.scodoc import sco_edit_module
|
||||
from app.scodoc import sco_edit_ue
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc import sco_tag_module
|
||||
from app.scodoc import sco_xml
|
||||
import sco_version
|
||||
from app.scodoc.gen_tables import GenTable
|
||||
from app.scodoc.sco_exceptions import ScoValueError, ScoFormatError
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
|
||||
_formationEditor = ndb.EditableTable(
|
||||
"notes_formations",
|
||||
"formation_id",
|
||||
(
|
||||
"formation_id",
|
||||
"acronyme",
|
||||
"titre",
|
||||
"titre_officiel",
|
||||
"version",
|
||||
"formation_code",
|
||||
"type_parcours",
|
||||
"code_specialite",
|
||||
"referentiel_competence_id",
|
||||
"commentaire",
|
||||
),
|
||||
filter_dept=True,
|
||||
sortkey="acronyme",
|
||||
)
|
||||
|
||||
|
||||
def formation_list(formation_id=None, args={}): ### XXX obsolete, à supprimer
|
||||
"""List formation(s) with given id, or matching args
|
||||
(when args is given, formation_id is ignored).
|
||||
"""
|
||||
if not args:
|
||||
if formation_id is None:
|
||||
args = {}
|
||||
else:
|
||||
args = {"formation_id": formation_id}
|
||||
cnx = ndb.GetDBConnexion()
|
||||
r = _formationEditor.list(cnx, args=args)
|
||||
return r
|
||||
import sco_version
|
||||
|
||||
|
||||
def formation_export_dict(
|
||||
@ -149,35 +112,35 @@ def formation_export_dict(
|
||||
ue_dict.pop("code_apogee_rcue", None)
|
||||
if ue_dict.get("ects") is None:
|
||||
ue_dict.pop("ects", None)
|
||||
mats = sco_edit_matiere.matiere_list({"ue_id": ue.id})
|
||||
mats.sort(key=lambda m: m["numero"] or 0)
|
||||
ue_dict["matiere"] = mats
|
||||
for mat in mats:
|
||||
matiere_id = mat["matiere_id"]
|
||||
mats = ue.matieres.all()
|
||||
mats.sort(key=lambda m: m.numero)
|
||||
mats_dict = [mat.to_dict() for mat in mats]
|
||||
ue_dict["matiere"] = mats_dict
|
||||
for mat_d in mats_dict:
|
||||
matiere_id = mat_d["matiere_id"]
|
||||
if not export_ids:
|
||||
del mat["id"]
|
||||
del mat["matiere_id"]
|
||||
del mat["ue_id"]
|
||||
mods = sco_edit_module.module_list({"matiere_id": matiere_id})
|
||||
mods.sort(key=lambda m: (m["numero"] or 0, m["code"]))
|
||||
mat["module"] = mods
|
||||
for mod in mods:
|
||||
module_id = mod["module_id"]
|
||||
del mat_d["id"]
|
||||
del mat_d["matiere_id"]
|
||||
del mat_d["ue_id"]
|
||||
mat = db.session.get(Matiere, matiere_id)
|
||||
mods = mat.modules.all()
|
||||
mods.sort(key=lambda m: (m.numero, m.code))
|
||||
mat_d["module"] = [mod.to_dict(convert_objects=True) for mod in mods]
|
||||
for module, mod_d in zip(mods, mat_d["module"]):
|
||||
if export_tags:
|
||||
tags = sco_tag_module.module_tag_list(module_id=mod["module_id"])
|
||||
tags = [t.title for t in module.tags]
|
||||
if tags:
|
||||
mod["tags"] = [{"name": x} for x in tags]
|
||||
mod_d["tags"] = [{"name": x} for x in tags]
|
||||
#
|
||||
module: Module = db.session.get(Module, module_id)
|
||||
if module.is_apc():
|
||||
# Exporte les coefficients
|
||||
if ue_reference_style == "id":
|
||||
mod["coefficients"] = [
|
||||
mod_d["coefficients"] = [
|
||||
{"ue_reference": str(ue_id), "coef": str(coef)}
|
||||
for (ue_id, coef) in module.get_ue_coef_dict().items()
|
||||
]
|
||||
else:
|
||||
mod["coefficients"] = [
|
||||
mod_d["coefficients"] = [
|
||||
{"ue_reference": ue_acronyme, "coef": str(coef)}
|
||||
for (
|
||||
ue_acronyme,
|
||||
@ -185,29 +148,29 @@ def formation_export_dict(
|
||||
) in module.get_ue_coef_dict_acronyme().items()
|
||||
]
|
||||
# Et les parcours
|
||||
mod["parcours"] = [
|
||||
mod_d["parcours"] = [
|
||||
p.to_dict(with_annees=False) for p in module.parcours
|
||||
]
|
||||
# Et les AC
|
||||
if ac_as_list:
|
||||
# XML préfère une liste
|
||||
mod["app_critiques"] = [
|
||||
mod_d["app_critiques"] = [
|
||||
x.to_dict(with_code=True) for x in module.app_critiques
|
||||
]
|
||||
else:
|
||||
mod["app_critiques"] = {
|
||||
mod_d["app_critiques"] = {
|
||||
x.code: x.to_dict() for x in module.app_critiques
|
||||
}
|
||||
if not export_ids:
|
||||
del mod["id"]
|
||||
del mod["ue_id"]
|
||||
del mod["matiere_id"]
|
||||
del mod["module_id"]
|
||||
del mod["formation_id"]
|
||||
del mod_d["id"]
|
||||
del mod_d["ue_id"]
|
||||
del mod_d["matiere_id"]
|
||||
del mod_d["module_id"]
|
||||
del mod_d["formation_id"]
|
||||
if not export_codes_apo:
|
||||
del mod["code_apogee"]
|
||||
if mod["ects"] is None:
|
||||
del mod["ects"]
|
||||
del mod_d["code_apogee"]
|
||||
if mod_d["ects"] is None:
|
||||
del mod_d["ects"]
|
||||
return f_dict
|
||||
|
||||
|
||||
@ -218,10 +181,12 @@ def formation_export(
|
||||
export_external_ues=False,
|
||||
export_codes_apo=True,
|
||||
fmt=None,
|
||||
) -> flask.Response:
|
||||
) -> flask.Response | dict:
|
||||
"""Get a formation, with UE, matieres, modules
|
||||
in desired format
|
||||
"""
|
||||
if fmt not in (None, "xml", "json"):
|
||||
raise ScoValueError("Format invalide")
|
||||
formation = Formation.get_formation(formation_id)
|
||||
f_dict = formation_export_dict(
|
||||
formation,
|
||||
@ -231,7 +196,11 @@ def formation_export(
|
||||
export_codes_apo=export_codes_apo,
|
||||
ac_as_list=fmt == "xml",
|
||||
)
|
||||
filename = f"scodoc_formation_{formation.departement.acronym}_{formation.acronyme or ''}_v{formation.version}"
|
||||
if fmt is None:
|
||||
return f_dict
|
||||
|
||||
filename = f"""scodoc_formation_{formation.departement.acronym
|
||||
}_{formation.acronyme or ''}_v{formation.version}"""
|
||||
return scu.sendResult(
|
||||
f_dict,
|
||||
name="formation",
|
||||
@ -259,11 +228,10 @@ def _formation_retreive_refcomp(f_dict: dict) -> int:
|
||||
).first()
|
||||
if refcomp:
|
||||
return refcomp.id
|
||||
else:
|
||||
flash(
|
||||
f"""Impossible de trouver le référentiel de compétence pour {
|
||||
refcomp_specialite} : est-il chargé ?"""
|
||||
)
|
||||
flash(
|
||||
f"""Impossible de trouver le référentiel de compétence pour {
|
||||
refcomp_specialite} : est-il chargé ?"""
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
@ -296,7 +264,7 @@ def _formation_retreive_apc_niveau(
|
||||
return niveau.id if niveau is not None else None
|
||||
|
||||
|
||||
def formation_import_xml(doc: str, import_tags=True, use_local_refcomp=False):
|
||||
def formation_import_xml(doc: str | bytes, import_tags=True, use_local_refcomp=False):
|
||||
"""Create a formation from XML representation
|
||||
(format dumped by formation_export( fmt='xml' ))
|
||||
XML may contain object (UE, modules) ids: this function returns two
|
||||
@ -309,24 +277,26 @@ def formation_import_xml(doc: str, import_tags=True, use_local_refcomp=False):
|
||||
Returns:
|
||||
formation_id, modules_old2new, ues_old2new
|
||||
"""
|
||||
from app.scodoc import sco_edit_formation
|
||||
from app.formations import edit_formation
|
||||
|
||||
if isinstance(doc, bytes):
|
||||
doc = doc.decode(scu.SCO_ENCODING)
|
||||
try:
|
||||
dom = xml.dom.minidom.parseString(doc)
|
||||
dom = xml.dom.minidom.parseString(sco_xml.remove_control_characters(doc))
|
||||
except Exception as exc:
|
||||
log("formation_import_xml: invalid XML data")
|
||||
raise ScoValueError("Fichier XML invalide") from exc
|
||||
log(f"formation_import_xml: invalid XML data:\n{exc}")
|
||||
raise ScoValueError(f"Fichier XML invalide {exc}") from exc
|
||||
|
||||
try:
|
||||
f = dom.getElementsByTagName("formation")[0] # or dom.documentElement
|
||||
D = sco_xml.xml_to_dicts(f)
|
||||
xml_dicts = sco_xml.xml_to_dicts(f)
|
||||
except Exception as exc:
|
||||
raise ScoFormatError(
|
||||
"""Ce document xml ne correspond pas à un programme exporté par ScoDoc.
|
||||
(élément 'formation' inexistant par exemple)."""
|
||||
) from exc
|
||||
assert D[0] == "formation"
|
||||
f_dict = D[1]
|
||||
assert xml_dicts[0] == "formation"
|
||||
f_dict = xml_dicts[1]
|
||||
f_dict["dept_id"] = g.scodoc_dept_id
|
||||
# Pour les clonages, on prend le refcomp_id donné:
|
||||
referentiel_competence_id = (
|
||||
@ -352,7 +322,7 @@ def formation_import_xml(doc: str, import_tags=True, use_local_refcomp=False):
|
||||
f_dict["version"] = version + 1
|
||||
|
||||
# create formation
|
||||
formation = sco_edit_formation.do_formation_create(f_dict)
|
||||
formation = edit_formation.do_formation_create(f_dict)
|
||||
log(f"formation {formation.id} created")
|
||||
|
||||
ues_old2new = {} # xml ue_id : new ue_id
|
||||
@ -363,7 +333,7 @@ def formation_import_xml(doc: str, import_tags=True, use_local_refcomp=False):
|
||||
modules_a_coefficienter = [] # Liste des modules avec coefs APC
|
||||
with sco_cache.DeferredSemCacheManager():
|
||||
# -- create UEs
|
||||
for ue_info in D[2]:
|
||||
for ue_info in xml_dicts[2]:
|
||||
assert ue_info[0] == "ue"
|
||||
ue_info[1]["formation_id"] = formation.id
|
||||
if "ue_id" in ue_info[1]:
|
||||
@ -380,7 +350,7 @@ def formation_import_xml(doc: str, import_tags=True, use_local_refcomp=False):
|
||||
)
|
||||
# Note: si le code est indiqué "" dans le xml, il faut le conserver vide
|
||||
# pour la comparaison ultérieure des formations XXX
|
||||
ue_id = sco_edit_ue.do_ue_create(ue_info[1], allow_empty_ue_code=True)
|
||||
ue_id = edit_ue.do_ue_create(ue_info[1], allow_empty_ue_code=True)
|
||||
ue: UniteEns = db.session.get(UniteEns, ue_id)
|
||||
assert ue
|
||||
if xml_ue_id:
|
||||
@ -428,7 +398,8 @@ def formation_import_xml(doc: str, import_tags=True, use_local_refcomp=False):
|
||||
|
||||
assert mat_info[0] == "matiere"
|
||||
mat_info[1]["ue_id"] = ue_id
|
||||
mat_id = sco_edit_matiere.do_matiere_create(mat_info[1])
|
||||
mat = Matiere.create_from_dict(mat_info[1])
|
||||
mat_id = mat.id
|
||||
# -- create modules
|
||||
for mod_info in mat_info[2]:
|
||||
assert mod_info[0] == "module"
|
||||
@ -442,11 +413,12 @@ def formation_import_xml(doc: str, import_tags=True, use_local_refcomp=False):
|
||||
mod_info[1]["ue_id"] = ue_id
|
||||
if not "module_type" in mod_info[1]:
|
||||
mod_info[1]["module_type"] = scu.ModuleType.STANDARD
|
||||
mod_id = sco_edit_module.do_module_create(mod_info[1])
|
||||
module = Module.create_from_dict(
|
||||
mod_info[1], news=True, inval_cache=True
|
||||
)
|
||||
if xml_module_id:
|
||||
modules_old2new[int(xml_module_id)] = mod_id
|
||||
modules_old2new[int(xml_module_id)] = module.id
|
||||
if len(mod_info) > 2:
|
||||
module: Module = db.session.get(Module, mod_id)
|
||||
tag_names = []
|
||||
ue_coef_dict = {}
|
||||
for child in mod_info[2]:
|
||||
@ -489,7 +461,7 @@ def formation_import_xml(doc: str, import_tags=True, use_local_refcomp=False):
|
||||
f"Warning: parcours {code_parcours} inexistant !"
|
||||
)
|
||||
if import_tags and tag_names:
|
||||
sco_tag_module.module_tag_set(mod_id, tag_names)
|
||||
module.set_tags(tag_names)
|
||||
if module.is_apc() and ue_coef_dict:
|
||||
modules_a_coefficienter.append((module, ue_coef_dict))
|
||||
# Fixe les coefs APC (à la fin pour que les UE soient créées)
|
@ -180,7 +180,7 @@ def formation_table_recap(formation: Formation, fmt="html") -> Response:
|
||||
|
||||
|
||||
def export_recap_formations_annee_scolaire(annee_scolaire):
|
||||
"""Exporte un zip des recap (excel) des formatons de tous les semestres
|
||||
"""Exporte un zip des recap (excel) des formations de tous les semestres
|
||||
de l'année scolaire indiquée.
|
||||
"""
|
||||
annee_scolaire = int(annee_scolaire)
|
@ -47,7 +47,7 @@ import app.scodoc.sco_utils as scu
|
||||
|
||||
from app import log
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc import sco_formations
|
||||
from app.formations import formation_io
|
||||
|
||||
|
||||
def formsemestre_associate_new_version(
|
||||
@ -202,7 +202,7 @@ def do_formsemestres_associate_new_version(
|
||||
new_formation_id,
|
||||
modules_old2new,
|
||||
ues_old2new,
|
||||
) = sco_formations.formation_create_new_version(formation_id, redirect=False)
|
||||
) = formation_io.formation_create_new_version(formation_id, redirect=False)
|
||||
# Log new ues:
|
||||
for ue_id in ues_old2new:
|
||||
ue = db.session.get(UniteEns, ue_id)
|
||||
@ -275,13 +275,13 @@ def formations_are_equals(
|
||||
"""True if the two formations are exactly the same, except for their versions.
|
||||
Can specify either formation2 or its dict repr.
|
||||
"""
|
||||
fd1 = sco_formations.formation_export_dict(
|
||||
fd1 = formation_io.formation_export_dict(
|
||||
formation1, export_external_ues=True, ue_reference_style="acronyme"
|
||||
)
|
||||
if formation2_dict is None:
|
||||
if formation2 is None:
|
||||
raise ValueError("must specify formation2 or formation2_dict")
|
||||
formation2_dict = sco_formations.formation_export_dict(
|
||||
formation2_dict = formation_io.formation_export_dict(
|
||||
formation2, export_external_ues=True, ue_reference_style="acronyme"
|
||||
)
|
||||
del fd1["version"]
|
@ -5,7 +5,7 @@ from flask import abort, g
|
||||
from flask_sqlalchemy.query import Query
|
||||
|
||||
import app
|
||||
from app import db
|
||||
from app import db, log
|
||||
from app.comp import df_cache
|
||||
from app.models import ScoDocModel, SHORT_STR_LEN
|
||||
from app.models.but_refcomp import (
|
||||
@ -14,6 +14,7 @@ from app.models.but_refcomp import (
|
||||
ApcParcours,
|
||||
ApcParcoursNiveauCompetence,
|
||||
)
|
||||
from app.models.events import ScolarNews
|
||||
from app.models.modules import Module
|
||||
from app.models.moduleimpls import ModuleImpl
|
||||
from app.models.ues import UniteEns, UEParcours
|
||||
@ -21,6 +22,7 @@ from app.scodoc import sco_cache
|
||||
from app.scodoc import codes_cursus
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.codes_cursus import UE_STANDARD
|
||||
from app.scodoc.sco_exceptions import ScoNonEmptyFormationObject, ScoValueError
|
||||
|
||||
|
||||
class Formation(ScoDocModel):
|
||||
@ -64,7 +66,8 @@ class Formation(ScoDocModel):
|
||||
|
||||
def html(self) -> str:
|
||||
"titre complet pour affichage"
|
||||
return f"""Formation {self.titre} ({self.acronyme}) version {self.version} code <tt>{self.formation_code}</tt>"""
|
||||
return f"""Formation {self.titre} ({self.acronyme}) version {self.version
|
||||
} code <tt>{self.formation_code}</tt>"""
|
||||
|
||||
@classmethod
|
||||
def get_formation(cls, formation_id: int | str, dept_id: int = None) -> "Formation":
|
||||
@ -166,6 +169,7 @@ class Formation(ScoDocModel):
|
||||
sco_cache.invalidate_formsemestre()
|
||||
|
||||
def invalidate_cached_sems(self):
|
||||
"Invalide caches de tous les formssemestres de la formation"
|
||||
for sem in self.formsemestres:
|
||||
sco_cache.invalidate_formsemestre(formsemestre_id=sem.id)
|
||||
|
||||
@ -312,7 +316,9 @@ class Matiere(ScoDocModel):
|
||||
titre = db.Column(db.Text())
|
||||
numero = db.Column(db.Integer, nullable=False, default=0) # ordre de présentation
|
||||
|
||||
modules = db.relationship("Module", lazy="dynamic", backref="matiere")
|
||||
modules = db.relationship(
|
||||
"Module", lazy="dynamic", backref="matiere", cascade="all, delete-orphan"
|
||||
)
|
||||
_sco_dept_relations = ("UniteEns", "Formation") # accès au dept_id
|
||||
|
||||
def __repr__(self):
|
||||
@ -325,5 +331,73 @@ class Matiere(ScoDocModel):
|
||||
e.pop("_sa_instance_state", None)
|
||||
# ScoDoc7 output_formators
|
||||
e["numero"] = e["numero"] if e["numero"] else 0
|
||||
e["ue_id"] = self.id
|
||||
e["matiere_id"] = self.id
|
||||
return e
|
||||
|
||||
def is_locked(self) -> bool:
|
||||
"""True if matiere can't modified
|
||||
because it contains modules used in a locked formsemestre.
|
||||
"""
|
||||
from app.models.formsemestre import FormSemestre
|
||||
|
||||
mat = (
|
||||
db.session.query(Matiere)
|
||||
.filter_by(id=self.id)
|
||||
.join(Module)
|
||||
.join(ModuleImpl)
|
||||
.join(FormSemestre)
|
||||
.filter_by(etat=False)
|
||||
.all()
|
||||
)
|
||||
return bool(mat)
|
||||
|
||||
def can_be_deleted(self) -> bool:
|
||||
"True si la matiere n'est pas utilisée dans des formsemestres"
|
||||
locked = self.is_locked()
|
||||
if locked:
|
||||
return False
|
||||
if any(m.modimpls.all() for m in self.modules):
|
||||
return False
|
||||
return True
|
||||
|
||||
def delete(self):
|
||||
"Delete matière. News, inval cache."
|
||||
formation = self.ue.formation
|
||||
log(f"matiere.delete: matiere_id={self.id}")
|
||||
if not self.can_be_deleted():
|
||||
# il y a au moins un modimpl dans un module de cette matière
|
||||
raise ScoNonEmptyFormationObject("Matière", self.titre)
|
||||
db.session.delete(self)
|
||||
db.session.commit()
|
||||
# news
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_FORM,
|
||||
obj=formation.id,
|
||||
text=f"Modification de la formation {formation.acronyme}",
|
||||
)
|
||||
# cache
|
||||
formation.invalidate_cached_sems()
|
||||
|
||||
@classmethod
|
||||
def create_from_dict(cls, data: dict) -> "Matiere":
|
||||
"""Create matière from dict. Log, news, cache.
|
||||
data must include ue_id, a valid UE id.
|
||||
Commit session.
|
||||
"""
|
||||
# check ue
|
||||
if data.get("ue_id") is None:
|
||||
raise ScoValueError("UE id missing")
|
||||
_ = UniteEns.get_ue(data["ue_id"])
|
||||
|
||||
mat = super().create_from_dict(data)
|
||||
db.session.commit()
|
||||
db.session.refresh(mat)
|
||||
# news
|
||||
formation = mat.ue.formation
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_FORM,
|
||||
obj=formation.id,
|
||||
text=f"Modification de la formation {formation.acronyme}",
|
||||
)
|
||||
formation.invalidate_cached_sems()
|
||||
return mat
|
||||
|
@ -202,22 +202,33 @@ class FormSemestre(models.ScoDocModel):
|
||||
|
||||
@classmethod
|
||||
def get_formsemestre(
|
||||
cls, formsemestre_id: int | str, dept_id: int = None
|
||||
) -> "FormSemestre":
|
||||
"""FormSemestre ou 404, cherche uniquement dans le département spécifié
|
||||
ou le courant (g.scodoc_dept)"""
|
||||
cls, formsemestre_id: int | str, dept_id: int = None, accept_none=False
|
||||
) -> "FormSemestre | None":
|
||||
"""FormSemestre ou 404 (ou None si accept_none), cherche uniquement dans
|
||||
le département spécifié ou le courant (g.scodoc_dept).
|
||||
Si accept_none, return None si l'id est invalide ou ne correspond
|
||||
pas à un formsemestre.
|
||||
"""
|
||||
if not isinstance(formsemestre_id, int):
|
||||
try:
|
||||
formsemestre_id = int(formsemestre_id)
|
||||
except (TypeError, ValueError):
|
||||
if accept_none:
|
||||
return None
|
||||
abort(404, "formsemestre_id invalide")
|
||||
if g.scodoc_dept:
|
||||
dept_id = dept_id if dept_id is not None else g.scodoc_dept_id
|
||||
if dept_id is not None:
|
||||
return cls.query.filter_by(
|
||||
id=formsemestre_id, dept_id=dept_id
|
||||
).first_or_404()
|
||||
return cls.query.filter_by(id=formsemestre_id).first_or_404()
|
||||
|
||||
dept_id = (
|
||||
dept_id
|
||||
if dept_id is not None
|
||||
else (g.scodoc_dept_id if g.scodoc_dept else None)
|
||||
)
|
||||
|
||||
query = (
|
||||
cls.query.filter_by(id=formsemestre_id)
|
||||
if dept_id is None
|
||||
else cls.query.filter_by(id=formsemestre_id, dept_id=dept_id)
|
||||
)
|
||||
return query.first() if accept_none else query.first_or_404()
|
||||
|
||||
@classmethod
|
||||
def create_formsemestre(cls, args: dict, silent=False) -> "FormSemestre":
|
||||
|
@ -7,14 +7,15 @@ from flask_login import current_user
|
||||
from flask_sqlalchemy.query import Query
|
||||
|
||||
import app
|
||||
from app import db
|
||||
from app import db, log
|
||||
from app.auth.models import User
|
||||
from app.comp import df_cache
|
||||
from app.models import APO_CODE_STR_LEN, ScoDocModel
|
||||
from app.models.etudiants import Identite
|
||||
from app.models.evaluations import Evaluation
|
||||
from app.models.modules import Module
|
||||
from app.scodoc.sco_exceptions import AccessDenied, ScoLockedSemError
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc.sco_exceptions import AccessDenied, ScoLockedSemError, ScoValueError
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
@ -65,6 +66,30 @@ class ModuleImpl(ScoDocModel):
|
||||
def __repr__(self):
|
||||
return f"<{self.__class__.__name__} {self.id} module={repr(self.module)}>"
|
||||
|
||||
@classmethod
|
||||
def create_from_dict(cls, data: dict) -> "ModuleImpl":
|
||||
"""Create modimpl from dict. Log, inval. cache.
|
||||
data must include valid formsemestre_id, module_id and responsable_id
|
||||
Commit session.
|
||||
"""
|
||||
from app.models import FormSemestre
|
||||
|
||||
# check required args
|
||||
for required_arg in ("formsemestre_id", "module_id", "responsable_id"):
|
||||
if required_arg not in data:
|
||||
raise ScoValueError(f"missing argument: {required_arg}")
|
||||
_ = FormSemestre.get_formsemestre(data["formsemestre_id"])
|
||||
_ = Module.get_instance(data["module_id"])
|
||||
if not db.session.get(User, data["responsable_id"]):
|
||||
abort(404, "responsable_id invalide")
|
||||
|
||||
modimpl = super().create_from_dict(data)
|
||||
db.session.commit()
|
||||
db.session.refresh(modimpl)
|
||||
log(f"ModuleImpl.create: created {modimpl.id} with {data}")
|
||||
sco_cache.invalidate_formsemestre(formsemestre_id=modimpl.formsemestre_id)
|
||||
return modimpl
|
||||
|
||||
def get_codes_apogee(self) -> set[str]:
|
||||
"""Les codes Apogée (codés en base comme "VRT1,VRT2").
|
||||
(si non renseigné, ceux du module)
|
||||
|
@ -1,9 +1,10 @@
|
||||
"""ScoDoc 9 models : Modules
|
||||
"""
|
||||
|
||||
from flask import current_app, g
|
||||
import http
|
||||
from flask import current_app, g, url_for
|
||||
|
||||
from app import db
|
||||
from app import db, log
|
||||
from app import models
|
||||
from app.models import APO_CODE_STR_LEN
|
||||
from app.models.but_refcomp import (
|
||||
@ -12,9 +13,14 @@ from app.models.but_refcomp import (
|
||||
app_critiques_modules,
|
||||
parcours_modules,
|
||||
)
|
||||
from app.models.events import ScolarNews
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.codes_cursus import UE_SPORT
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc.sco_exceptions import (
|
||||
ScoValueError,
|
||||
ScoLockedFormError,
|
||||
ScoNonEmptyFormationObject,
|
||||
)
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
|
||||
|
||||
@ -75,11 +81,11 @@ class Module(models.ScoDocModel):
|
||||
backref=db.backref("modules", lazy=True),
|
||||
)
|
||||
|
||||
_sco_dept_relations = "Formation" # accès au dept_id
|
||||
_sco_dept_relations = ("Formation",) # accès au dept_id
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.ue_coefs = []
|
||||
super(Module, self).__init__(**kwargs)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def __repr__(self):
|
||||
return f"""<Module{ModuleType(self.module_type or ModuleType.STANDARD).name
|
||||
@ -114,13 +120,39 @@ class Module(models.ScoDocModel):
|
||||
# on ne peut pas affecter directement parcours
|
||||
return super().filter_model_attributes(args, (excluded or set()) | {"parcours"})
|
||||
|
||||
@classmethod
|
||||
def check_module_code_unicity(cls, code, formation_id, module_id=None) -> bool:
|
||||
"true si code module unique dans la formation"
|
||||
from app.models import Formation
|
||||
|
||||
formation = Formation.get_formation(formation_id)
|
||||
query = formation.modules.filter_by(code=code)
|
||||
if module_id is not None: # edition: supprime le module en cours
|
||||
query = query.filter(Module.id != module_id)
|
||||
return query.count() == 0
|
||||
|
||||
def from_dict(self, args: dict, excluded: set[str] | None = None) -> bool:
|
||||
"""Update object's fields given in dict. Add to session but don't commit.
|
||||
True if modification.
|
||||
- can't change ue nor formation
|
||||
- can change matiere_id, iff new matiere in same ue
|
||||
- can change parcours: parcours list of ApcParcour id or instances.
|
||||
Ne modifie pas les coefficients APC ue_coefs
|
||||
"""
|
||||
args = args.copy()
|
||||
if "ue_coefs" in args:
|
||||
del args["ue_coefs"]
|
||||
if self.is_locked():
|
||||
# formation verrouillée: empeche de modifier coefficient, matiere, and semestre_id
|
||||
protected_fields = ("coefficient", "matiere_id", "semestre_id")
|
||||
for f in protected_fields:
|
||||
if f in args:
|
||||
del args[f]
|
||||
# Unicité du code
|
||||
if "code" in args and not Module.check_module_code_unicity(
|
||||
args["code"], self.formation_id, self.id
|
||||
):
|
||||
raise ScoValueError("code module déjà utilisé")
|
||||
# Vérifie les changements de matiere
|
||||
new_matiere_id = args.get("matiere_id", self.matiere_id)
|
||||
if new_matiere_id != self.matiere_id:
|
||||
@ -138,20 +170,103 @@ class Module(models.ScoDocModel):
|
||||
existing_parcours = {p.id for p in self.parcours}
|
||||
new_parcours = args.get("parcours", []) or []
|
||||
if existing_parcours != set(new_parcours):
|
||||
self._set_parcours_from_list(new_parcours)
|
||||
self.set_parcours_from_list(new_parcours)
|
||||
return True
|
||||
return modified
|
||||
|
||||
@classmethod
|
||||
def create_from_dict(cls, data: dict) -> "Module":
|
||||
def create_from_dict(
|
||||
cls,
|
||||
data: dict,
|
||||
inval_cache=False,
|
||||
news=False,
|
||||
) -> "Module":
|
||||
"""Create from given dict, add parcours.
|
||||
Flush session."""
|
||||
Flush session.
|
||||
Si news, commit and log news.
|
||||
"""
|
||||
from app.models.formations import Formation
|
||||
|
||||
# check required arguments
|
||||
for required_arg in ("code", "formation_id", "ue_id"):
|
||||
if required_arg not in data:
|
||||
raise ScoValueError(f"missing argument: {required_arg}")
|
||||
if not data["code"]:
|
||||
raise ScoValueError("module code must be non empty")
|
||||
# Check formation
|
||||
formation = Formation.get_formation(data["formation_id"])
|
||||
ue = UniteEns.get_ue(data["ue_id"])
|
||||
# refuse de créer un module APC avec semestres semestre du module != semestre de l'UE
|
||||
if formation.is_apc():
|
||||
if int(data.get("semestre_id", 1)) != ue.semestre_idx:
|
||||
raise ScoValueError(
|
||||
"Formation incompatible: indices UE et module différents"
|
||||
)
|
||||
module = super().create_from_dict(data)
|
||||
db.session.flush()
|
||||
module._set_parcours_from_list(data.get("parcours", []) or [])
|
||||
module.set_parcours_from_list(data.get("parcours", []) or [])
|
||||
log(f"module_create: created {module.id} with {data}")
|
||||
if news:
|
||||
db.session.commit()
|
||||
db.session.refresh(module)
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_FORM,
|
||||
obj=formation.id,
|
||||
text=f"Modification de la formation {formation.acronyme}",
|
||||
)
|
||||
if inval_cache:
|
||||
formation.invalidate_cached_sems()
|
||||
|
||||
return module
|
||||
|
||||
def _set_parcours_from_list(self, parcours: list[ApcParcours | int]):
|
||||
def is_locked(self) -> bool:
|
||||
"""True if module cannot be modified
|
||||
because it is used in a locked formsemestre.
|
||||
"""
|
||||
from app.models import FormSemestre, ModuleImpl
|
||||
|
||||
mods = (
|
||||
db.session.query(Module)
|
||||
.filter_by(id=self.id)
|
||||
.join(ModuleImpl)
|
||||
.join(FormSemestre)
|
||||
.filter_by(etat=False)
|
||||
.all()
|
||||
)
|
||||
return bool(mods)
|
||||
|
||||
def can_be_deleted(self) -> bool:
|
||||
"""True if module can be deleted"""
|
||||
return self.modimpls.count() == 0
|
||||
|
||||
def delete(self):
|
||||
"Delete module. News, inval cache."
|
||||
if self.is_locked():
|
||||
raise ScoLockedFormError()
|
||||
if not self.can_be_deleted():
|
||||
raise ScoNonEmptyFormationObject(
|
||||
"Module",
|
||||
msg=self.titre or self.code,
|
||||
dest_url=url_for(
|
||||
"notes.ue_table",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formation_id=self.formation_id,
|
||||
semestre_idx=self.ue.semestre_idx,
|
||||
),
|
||||
)
|
||||
formation = self.formation
|
||||
db.session.delete(self)
|
||||
log(f"Module.delete({self.id})")
|
||||
db.session.commit()
|
||||
# news
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_FORM,
|
||||
obj=formation.id,
|
||||
text=f"Modification de la formation {formation.acronyme}",
|
||||
)
|
||||
formation.invalidate_cached_sems()
|
||||
|
||||
def set_parcours_from_list(self, parcours: list[ApcParcours | int]):
|
||||
"""Ajoute ces parcours à la liste des parcours du module.
|
||||
Chaque élément est soit un objet parcours soit un id.
|
||||
S'assure que chaque parcours est dans le référentiel de compétence
|
||||
@ -449,6 +564,43 @@ class Module(models.ScoDocModel):
|
||||
db.session.add(self)
|
||||
db.session.flush()
|
||||
|
||||
def set_tags(self, taglist: str | list[str] | None = None):
|
||||
"""taglist may either be:
|
||||
a string with tag names separated by commas ("un,deux")
|
||||
or a list of strings (["un", "deux"])
|
||||
Remplace les tags existants
|
||||
"""
|
||||
# TODO refactoring ScoTag
|
||||
# TODO code à moderniser (+ revoir classe ScoTag, utiliser modèle)
|
||||
# TODO Voir ItemSuiviTag et api etud_suivi
|
||||
from app.scodoc.sco_tag_module import ScoTag, ModuleTag
|
||||
|
||||
taglist = taglist or []
|
||||
if isinstance(taglist, str):
|
||||
taglist = taglist.split(",")
|
||||
taglist = [t.strip() for t in taglist]
|
||||
taglist = [t for t in taglist if t]
|
||||
log(f"module.set_tags: module_id={self.id} taglist={taglist}")
|
||||
# Check tags syntax
|
||||
for tag in taglist:
|
||||
if not ScoTag.check_tag_title(tag):
|
||||
log(f"module.set_tags({self.id}): invalid tag title")
|
||||
return scu.json_error(404, "invalid tag")
|
||||
|
||||
newtags = set(taglist)
|
||||
oldtags = set(t.title for t in self.tags)
|
||||
to_del = oldtags - newtags
|
||||
to_add = newtags - oldtags
|
||||
|
||||
# should be atomic, but it's not.
|
||||
for tagname in to_add:
|
||||
t = ModuleTag(tagname, object_id=self.id)
|
||||
for tagname in to_del:
|
||||
t = ModuleTag(tagname)
|
||||
t.remove_tag_from_object(self.id)
|
||||
|
||||
return "", http.HTTPStatus.NO_CONTENT
|
||||
|
||||
|
||||
class ModuleUECoef(db.Model):
|
||||
"""Coefficients des modules vers les UE (APC, BUT)
|
||||
@ -499,7 +651,7 @@ class ModuleUECoef(db.Model):
|
||||
return d
|
||||
|
||||
|
||||
class NotesTag(db.Model):
|
||||
class NotesTag(models.ScoDocModel):
|
||||
"""Tag sur un module"""
|
||||
|
||||
__tablename__ = "notes_tags"
|
||||
@ -511,6 +663,9 @@ class NotesTag(db.Model):
|
||||
dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True)
|
||||
title = db.Column(db.Text(), nullable=False)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Tag {self.id} {self.title!r}>"
|
||||
|
||||
@classmethod
|
||||
def get_or_create(cls, title: str, dept_id: int | None = None) -> "NotesTag":
|
||||
"""Get tag, or create it if it doesn't yet exists.
|
||||
|
@ -10,6 +10,7 @@ from app.models import APO_CODE_STR_LEN
|
||||
from app.models import SHORT_STR_LEN
|
||||
from app.models.but_refcomp import ApcNiveau, ApcParcours
|
||||
from app.models.modules import Module
|
||||
from app.scodoc import codes_cursus
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
|
||||
@ -185,10 +186,45 @@ class UniteEns(models.ScoDocModel):
|
||||
return 1 if self.semestre_idx is None else (self.semestre_idx - 1) // 2 + 1
|
||||
|
||||
def is_locked(self) -> tuple[bool, str]:
|
||||
"""True if UE should not be modified"""
|
||||
from app.scodoc import sco_edit_ue
|
||||
"""True if UE should not be modified:
|
||||
utilisée dans un formsemestre verrouillé ou validations de jury de cette UE.
|
||||
Renvoie aussi une explication.
|
||||
"""
|
||||
from app.models import FormSemestre, ModuleImpl, ScolarFormSemestreValidation
|
||||
|
||||
return sco_edit_ue.ue_is_locked(self.id)
|
||||
# before 9.7.23: contains modules used in a locked formsemestre
|
||||
# starting from 9.7.23: + existence de validations de jury de cette UE
|
||||
if self.formation.is_apc():
|
||||
# en APC, interdit toute modification d'UE si il y a un formsemestre verrouillé
|
||||
# de cette formation ayant le semestre de cette UE.
|
||||
# (ne détaille pas les parcours, donc si un semestre Sn d'un parcours est verrouillé
|
||||
# cela va verrouiller toutes les UE d'indice Sn, même si pas de ce parcours)
|
||||
# modifié en 9.7.28
|
||||
locked_sems = self.formation.formsemestres.filter_by(
|
||||
etat=False, semestre_id=self.semestre_idx
|
||||
)
|
||||
if locked_sems.count():
|
||||
return True, "utilisée dans un semestre verrouillé"
|
||||
else:
|
||||
# en classique: interdit si contient des modules utilisés dans des semestres verrouillés
|
||||
# en effet, dans certaines (très anciennes) formations, une UE peut avoir des modules de
|
||||
# différents semestre
|
||||
if (
|
||||
Module.query.filter(Module.ue_id == self.id)
|
||||
.join(Module.modimpls)
|
||||
.join(ModuleImpl.formsemestre)
|
||||
.filter_by(etat=False)
|
||||
.count()
|
||||
):
|
||||
return True, "avec modules utilisés dans des semestres verrouillés"
|
||||
|
||||
nb_validations = ScolarFormSemestreValidation.query.filter_by(
|
||||
ue_id=self.id
|
||||
).count()
|
||||
if nb_validations > 0:
|
||||
return True, f"avec {nb_validations} validations de jury"
|
||||
|
||||
return False, ""
|
||||
|
||||
def can_be_deleted(self) -> bool:
|
||||
"""True si l'UE n'a pas de moduleimpl rattachés
|
||||
@ -214,6 +250,24 @@ class UniteEns(models.ScoDocModel):
|
||||
db.session.commit()
|
||||
return self.semestre_idx
|
||||
|
||||
def get_semestre_id(self) -> int:
|
||||
"""L'indice du semestre de l'UE.
|
||||
Regarde semestre_idx ou, pour les formations non APC,
|
||||
le premier module de chacune.
|
||||
Les UE sans modules se voient attribuer le numero UE_SEM_DEFAULT (1000000),
|
||||
qui les place à la fin de la liste.
|
||||
Contrairement à guess_semestre_idx, ne modifie pas l'UE.
|
||||
"""
|
||||
if self.semestre_idx is not None:
|
||||
return self.semestre_idx
|
||||
if self.formation.is_apc():
|
||||
return codes_cursus.UE_SEM_DEFAULT
|
||||
# était le comportement ScoDoc7
|
||||
module = self.modules.first()
|
||||
if module:
|
||||
return module.semestre_id
|
||||
return codes_cursus.UE_SEM_DEFAULT
|
||||
|
||||
def get_ects(self, parcour: ApcParcours = None, only_parcours=False) -> float:
|
||||
"""Crédits ECTS associés à cette UE.
|
||||
En BUT, cela peut quelquefois dépendre du parcours.
|
||||
|
@ -41,8 +41,8 @@ from app import ScoValueError
|
||||
from app import comp
|
||||
from app.comp.res_but import ResultatsSemestreBUT
|
||||
from app.models import FormSemestre, UniteEns
|
||||
import app.pe.pe_affichage as pe_affichage
|
||||
import app.pe.pe_etudiant as pe_etudiant
|
||||
from app.pe import pe_affichage
|
||||
from app.pe import pe_etudiant
|
||||
from app.pe.moys import pe_tabletags, pe_moytag
|
||||
from app.scodoc import sco_tag_module
|
||||
from app.scodoc import codes_cursus as sco_codes
|
||||
@ -59,13 +59,18 @@ class ResSemBUTTag(ResultatsSemestreBUT, pe_tabletags.TableTag):
|
||||
def __init__(
|
||||
self,
|
||||
formsemestre: FormSemestre,
|
||||
options={"moyennes_tags": True, "moyennes_ue_res_sae": False},
|
||||
options: dict | None = None,
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
formsemestre: le ``FormSemestre`` sur lequel il se base
|
||||
options: Un dictionnaire d'options
|
||||
"""
|
||||
options = (
|
||||
{"moyennes_tags": True, "moyennes_ue_res_sae": False}
|
||||
if options is None
|
||||
else options
|
||||
)
|
||||
ResultatsSemestreBUT.__init__(self, formsemestre)
|
||||
pe_tabletags.TableTag.__init__(self)
|
||||
|
||||
@ -274,7 +279,8 @@ class ResSemBUTTag(ResultatsSemestreBUT, pe_tabletags.TableTag):
|
||||
pole=None,
|
||||
) -> pd.DataFrame:
|
||||
"""Calcule la moyenne par UE des étudiants pour un tag donné,
|
||||
en ayant connaissance des informations sur le tag et des inscriptions des étudiants aux différentes UEs.
|
||||
en ayant connaissance des informations sur le tag et des inscriptions
|
||||
des étudiants aux différentes UEs.
|
||||
|
||||
info_tag détermine les modules pris en compte :
|
||||
* si non `None`, seuls les modules rattachés au tag sont pris en compte
|
||||
@ -342,7 +348,8 @@ class ResSemBUTTag(ResultatsSemestreBUT, pe_tabletags.TableTag):
|
||||
colonnes = [ue.id for ue in self.ues_standards]
|
||||
moyennes_ues_tag = moyennes_ues_tag[colonnes]
|
||||
|
||||
# Applique le masque d'inscription aux UE pour ne conserver que les UE dans lequel l'étudiant est inscrit
|
||||
# Applique le masque d'inscription aux UE pour ne conserver
|
||||
# que les UE dans lequel l'étudiant est inscrit
|
||||
moyennes_ues_tag = moyennes_ues_tag[colonnes] * ues_inscr_parcours_df[colonnes]
|
||||
|
||||
# Transforme les UEs en acronyme
|
||||
@ -405,7 +412,7 @@ class ResSemBUTTag(ResultatsSemestreBUT, pe_tabletags.TableTag):
|
||||
"""Vérifie l'unicité des tags"""
|
||||
noms_tags_perso = sorted(list(set(dict_tags["personnalises"].keys())))
|
||||
noms_tags_auto = sorted(list(set(dict_tags["auto"].keys()))) # + noms_tags_comp
|
||||
noms_tags = noms_tags_perso + noms_tags_auto
|
||||
# noms_tags = noms_tags_perso + noms_tags_auto
|
||||
|
||||
intersection = list(set(noms_tags_perso) & set(noms_tags_auto))
|
||||
|
||||
@ -455,7 +462,7 @@ def get_synthese_tags_personnalises_semestre(formsemestre: FormSemestre):
|
||||
modimpl_id = modimpl.id
|
||||
|
||||
# Liste des tags pour le module concerné
|
||||
tags = sco_tag_module.module_tag_list(modimpl.module.id)
|
||||
tags = [t.title for t in modimpl.module.tags]
|
||||
|
||||
# Traitement des tags recensés, chacun pouvant étant de la forme
|
||||
# "mathématiques", "théorie", "pe:0", "maths:2"
|
||||
|
@ -296,13 +296,18 @@ class TF(object):
|
||||
if not allow_null:
|
||||
if val is None or (isinstance(val, str) and not val.strip()):
|
||||
msg.append(
|
||||
"Le champ '%s' doit être renseigné" % descr.get("title", field)
|
||||
f"Le champ '{descr.get('title', field)}' doit être renseigné"
|
||||
)
|
||||
ok = 0
|
||||
elif val == "" or val == None:
|
||||
elif val in ("", None):
|
||||
continue # allowed empty field, skip
|
||||
# type
|
||||
typ = descr.get("type", "string")
|
||||
typ = descr.get("type", "text")
|
||||
# Option pour striper les chaînes
|
||||
if isinstance(val, str) and typ == "text" and descr.get("strip", False):
|
||||
val = val.strip()
|
||||
self.values[field] = val
|
||||
|
||||
if val != "" and val is not None:
|
||||
# check only non-null values
|
||||
if typ[:3] == "int":
|
||||
@ -311,24 +316,22 @@ class TF(object):
|
||||
self.values[field] = val
|
||||
except ValueError:
|
||||
msg.append(
|
||||
"La valeur du champ '%s' doit être un nombre entier" % field
|
||||
f"La valeur du champ '{field}' doit être un nombre entier"
|
||||
)
|
||||
ok = 0
|
||||
elif typ == "float" or typ == "real":
|
||||
elif typ in ("float", "real"):
|
||||
self.values[field] = self.values[field].replace(",", ".")
|
||||
try:
|
||||
val = float(val.replace(",", ".")) # allow ,
|
||||
self.values[field] = val
|
||||
except ValueError:
|
||||
msg.append(
|
||||
"La valeur du champ '%s' doit être un nombre" % field
|
||||
)
|
||||
msg.append(f"La valeur du champ {field}' doit être un nombre")
|
||||
ok = 0
|
||||
if (
|
||||
ok
|
||||
and (typ[:3] == "int" or typ == "float" or typ == "real")
|
||||
and val != ""
|
||||
and val != None
|
||||
and val is not None
|
||||
):
|
||||
if "min_value" in descr and self.values[field] < descr["min_value"]:
|
||||
msg.append(
|
||||
@ -343,12 +346,12 @@ class TF(object):
|
||||
)
|
||||
ok = 0
|
||||
if typ[:3] == "int":
|
||||
if not (scu.DB_MIN_INT <= self.values[field] <= scu.DB_MAX_INT):
|
||||
if not scu.DB_MIN_INT <= self.values[field] <= scu.DB_MAX_INT:
|
||||
msg.append(
|
||||
f"Le champ '{field}' est a une valeur hors limite"
|
||||
)
|
||||
ok = 0
|
||||
elif typ == "float" or typ == "real":
|
||||
elif typ in ("float", "real"):
|
||||
if not (
|
||||
scu.DB_MIN_FLOAT <= self.values[field] <= scu.DB_MAX_FLOAT
|
||||
):
|
||||
@ -356,7 +359,7 @@ class TF(object):
|
||||
f"Le champ '{field}' est a une valeur hors limite"
|
||||
)
|
||||
ok = 0
|
||||
if ok and (typ[:3] == "str") and "max_length" in descr:
|
||||
if ok and "max_length" in descr and isinstance(self.values[field], str):
|
||||
if len(self.values[field]) > descr["max_length"]:
|
||||
msg.append(
|
||||
"Le champ '%s' est trop long (max %d caractères)"
|
||||
|
@ -36,6 +36,7 @@ from flask import abort
|
||||
from app import db, ScoDocJSONEncoder
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.formations import edit_ue
|
||||
from app.models import but_validations
|
||||
from app.models import BulAppreciations, Evaluation, Matiere, UniteEns
|
||||
from app.models.etudiants import Identite
|
||||
@ -44,7 +45,6 @@ from app.models.formsemestre import FormSemestre
|
||||
import app.scodoc.sco_utils as scu
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app.scodoc import sco_assiduites
|
||||
from app.scodoc import sco_edit_ue
|
||||
from app.scodoc import sco_evaluations
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_groups
|
||||
@ -491,11 +491,11 @@ def dict_decision_jury(
|
||||
]
|
||||
|
||||
d["decision_ue"] = []
|
||||
if decision[
|
||||
"decisions_ue"
|
||||
]: # and sco_preferences.get_preference( 'bul_show_uevalid', formsemestre_id): always publish (car utile pour export Apogee)
|
||||
if decision["decisions_ue"]:
|
||||
# and sco_preferences.get_preference( 'bul_show_uevalid', formsemestre_id):
|
||||
# always publish (car utile pour export Apogee)
|
||||
for ue_id in decision["decisions_ue"].keys():
|
||||
ue = sco_edit_ue.ue_list({"ue_id": ue_id})[0]
|
||||
ue = edit_ue.ue_list({"ue_id": ue_id})[0]
|
||||
d["decision_ue"].append(
|
||||
dict(
|
||||
ue_id=ue["ue_id"],
|
||||
|
@ -46,6 +46,7 @@ from xml.etree.ElementTree import Element
|
||||
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.formations import edit_ue
|
||||
import app.scodoc.sco_utils as scu
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app import log
|
||||
@ -53,7 +54,6 @@ from app.but.bulletin_but_xml_compat import bulletin_but_xml_compat
|
||||
from app.models import BulAppreciations, Evaluation, FormSemestre
|
||||
from app.scodoc import sco_assiduites
|
||||
from app.scodoc import codes_cursus
|
||||
from app.scodoc import sco_edit_ue
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_groups
|
||||
from app.scodoc import sco_photos
|
||||
@ -393,7 +393,7 @@ def make_xml_formsemestre_bulletinetud(
|
||||
"decisions_ue"
|
||||
]: # and sco_preferences.get_preference( 'bul_show_uevalid', formsemestre_id): always publish (car utile pour export Apogee)
|
||||
for ue_id in decision["decisions_ue"].keys():
|
||||
ue = sco_edit_ue.ue_list({"ue_id": ue_id})[0]
|
||||
ue = edit_ue.ue_list({"ue_id": ue_id})[0]
|
||||
doc.append(
|
||||
Element(
|
||||
"decision_ue",
|
||||
|
@ -53,7 +53,7 @@ import subprocess
|
||||
|
||||
import requests
|
||||
|
||||
from flask import g, request
|
||||
from flask import current_app, g, request
|
||||
from flask_login import current_user
|
||||
|
||||
import app.scodoc.notesdb as ndb
|
||||
@ -184,6 +184,7 @@ def _send_db(
|
||||
"serial": _get_scodoc_serial(),
|
||||
"sco_user": str(current_user),
|
||||
"sent_by": f'"{current_user.get_nomcomplet()}" <{current_user.email}>',
|
||||
"admin_mail": current_app.config.get("SCODOC_ADMIN_MAIL"),
|
||||
"sco_version": sco_version.SCOVERSION,
|
||||
"sco_fullversion": scu.get_scodoc_version(),
|
||||
"traceback_str": traceback_str,
|
||||
|
@ -375,65 +375,64 @@ def evaluation_create_form(
|
||||
+ render_template(
|
||||
"scodoc/help/evaluations.j2", is_apc=is_apc, modimpl=modimpl
|
||||
)
|
||||
+ render_template("sco_timepicker.j2")
|
||||
),
|
||||
)
|
||||
elif tf[0] == -1:
|
||||
if tf[0] == -1:
|
||||
return flask.redirect(dest_url)
|
||||
|
||||
# form submission
|
||||
args = tf[2]
|
||||
# modifie le codage des dates
|
||||
# (nb: ce formulaire ne permet de créer que des évaluation sur la même journée)
|
||||
date_debut = scu.convert_fr_date(args["jour"]) if args.get("jour") else None
|
||||
args["date_debut"] = date_debut
|
||||
args["date_fin"] = date_debut # même jour
|
||||
args.pop("jour", None)
|
||||
if date_debut and args.get("heure_debut"):
|
||||
try:
|
||||
heure_debut = heure_to_time(args["heure_debut"])
|
||||
except ValueError as exc:
|
||||
raise ScoValueError("Heure début invalide") from exc
|
||||
args["date_debut"] = datetime.datetime.combine(date_debut, heure_debut)
|
||||
args.pop("heure_debut", None)
|
||||
# note: ce formulaire ne permet de créer que des évaluations
|
||||
# avec debut et fin sur le même jour.
|
||||
if date_debut and args.get("heure_fin"):
|
||||
try:
|
||||
heure_fin = heure_to_time(args["heure_fin"])
|
||||
except ValueError as exc:
|
||||
raise ScoValueError("Heure fin invalide") from exc
|
||||
args["date_fin"] = datetime.datetime.combine(date_debut, heure_fin)
|
||||
args.pop("heure_fin", None)
|
||||
# Blocage:
|
||||
if args.get("blocked"):
|
||||
if args.get("blocked_until"):
|
||||
try:
|
||||
args["blocked_until"] = datetime.datetime.strptime(
|
||||
args["blocked_until"], scu.DATE_FMT
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise ScoValueError("Date déblocage (j/m/a) invalide") from exc
|
||||
else: # bloquage coché sans date
|
||||
args["blocked_until"] = Evaluation.BLOCKED_FOREVER
|
||||
else: # si pas coché, efface date déblocage
|
||||
args["blocked_until"] = None
|
||||
#
|
||||
if edit:
|
||||
check_and_convert_evaluation_args(args, modimpl)
|
||||
evaluation.from_dict(args)
|
||||
else:
|
||||
# form submission
|
||||
args = tf[2]
|
||||
# modifie le codage des dates
|
||||
# (nb: ce formulaire ne permet de créer que des évaluation sur la même journée)
|
||||
date_debut = scu.convert_fr_date(args["jour"]) if args.get("jour") else None
|
||||
args["date_debut"] = date_debut
|
||||
args["date_fin"] = date_debut # même jour
|
||||
args.pop("jour", None)
|
||||
if date_debut and args.get("heure_debut"):
|
||||
try:
|
||||
heure_debut = heure_to_time(args["heure_debut"])
|
||||
except ValueError as exc:
|
||||
raise ScoValueError("Heure début invalide") from exc
|
||||
args["date_debut"] = datetime.datetime.combine(date_debut, heure_debut)
|
||||
args.pop("heure_debut", None)
|
||||
# note: ce formulaire ne permet de créer que des évaluations
|
||||
# avec debut et fin sur le même jour.
|
||||
if date_debut and args.get("heure_fin"):
|
||||
try:
|
||||
heure_fin = heure_to_time(args["heure_fin"])
|
||||
except ValueError as exc:
|
||||
raise ScoValueError("Heure fin invalide") from exc
|
||||
args["date_fin"] = datetime.datetime.combine(date_debut, heure_fin)
|
||||
args.pop("heure_fin", None)
|
||||
# Blocage:
|
||||
if args.get("blocked"):
|
||||
if args.get("blocked_until"):
|
||||
try:
|
||||
args["blocked_until"] = datetime.datetime.strptime(
|
||||
args["blocked_until"], scu.DATE_FMT
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise ScoValueError("Date déblocage (j/m/a) invalide") from exc
|
||||
else: # bloquage coché sans date
|
||||
args["blocked_until"] = Evaluation.BLOCKED_FOREVER
|
||||
else: # si pas coché, efface date déblocage
|
||||
args["blocked_until"] = None
|
||||
#
|
||||
if edit:
|
||||
check_and_convert_evaluation_args(args, modimpl)
|
||||
evaluation.from_dict(args)
|
||||
else:
|
||||
# création d'une evaluation
|
||||
evaluation = Evaluation.create(moduleimpl=modimpl, **args)
|
||||
db.session.add(evaluation)
|
||||
db.session.commit()
|
||||
evaluation_id = evaluation.id
|
||||
if is_apc:
|
||||
# Set poids
|
||||
evaluation = db.session.get(Evaluation, evaluation_id)
|
||||
for ue in sem_ues:
|
||||
evaluation.set_ue_poids(ue, tf[2][f"poids_{ue.id}"])
|
||||
# création d'une evaluation
|
||||
evaluation = Evaluation.create(moduleimpl=modimpl, **args)
|
||||
db.session.add(evaluation)
|
||||
db.session.commit()
|
||||
sco_cache.invalidate_formsemestre(evaluation.moduleimpl.formsemestre.id)
|
||||
return flask.redirect(dest_url)
|
||||
evaluation_id = evaluation.id
|
||||
if is_apc:
|
||||
# Set poids
|
||||
evaluation = db.session.get(Evaluation, evaluation_id)
|
||||
for ue in sem_ues:
|
||||
evaluation.set_ue_poids(ue, tf[2][f"poids_{ue.id}"])
|
||||
db.session.add(evaluation)
|
||||
db.session.commit()
|
||||
sco_cache.invalidate_formsemestre(evaluation.moduleimpl.formsemestre.id)
|
||||
return flask.redirect(dest_url)
|
||||
|
@ -62,7 +62,6 @@ 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_edit_module
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_groups_copy
|
||||
from app.scodoc import sco_moduleimpl
|
||||
@ -939,7 +938,7 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
|
||||
"formsemestre_id": formsemestre_id,
|
||||
"responsable_id": tf[2][f"MI{module_id}"],
|
||||
}
|
||||
_ = sco_moduleimpl.do_moduleimpl_create(modargs)
|
||||
_ = ModuleImpl.create_from_dict(modargs)
|
||||
else:
|
||||
# Modification du semestre:
|
||||
# on doit creer les modules nouvellement selectionnés
|
||||
@ -971,27 +970,23 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
|
||||
"formsemestre_id": formsemestre.id,
|
||||
"responsable_id": tf[2]["MI" + str(module_id)],
|
||||
}
|
||||
moduleimpl_id = sco_moduleimpl.do_moduleimpl_create(modargs)
|
||||
mod = sco_edit_module.module_list({"module_id": module_id})[0]
|
||||
msg += [
|
||||
"création de %s (%s)" % (mod["code"] or "?", mod["titre"] or "?")
|
||||
]
|
||||
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
|
||||
log(
|
||||
'inscription module: %s = "%s"'
|
||||
% ("%s!group_id" % module_id, tf[2]["%s!group_id" % module_id])
|
||||
)
|
||||
group_id = tf[2]["%s!group_id" % module_id]
|
||||
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(
|
||||
"inscription module:module_id=%s,moduleimpl_id=%s: %s"
|
||||
% (module_id, moduleimpl_id, etudids)
|
||||
% (module_id, modimpl.id, etudids)
|
||||
)
|
||||
sco_moduleimpl.do_moduleimpl_inscrit_etuds(
|
||||
moduleimpl_id,
|
||||
modimpl.id,
|
||||
formsemestre.id,
|
||||
etudids,
|
||||
)
|
||||
@ -1002,7 +997,7 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
|
||||
else:
|
||||
log(
|
||||
"inscription module:module_id=%s,moduleimpl_id=%s: aucun etudiant inscrit"
|
||||
% (module_id, moduleimpl_id)
|
||||
% (module_id, modimpl.id)
|
||||
)
|
||||
#
|
||||
ok, diag = formsemestre_delete_moduleimpls(
|
||||
@ -1022,7 +1017,6 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
|
||||
sco_moduleimpl.do_moduleimpl_edit(
|
||||
modargs, formsemestre_id=formsemestre.id
|
||||
)
|
||||
mod = sco_edit_module.module_list({"module_id": module_id})[0]
|
||||
# --- Association des parcours
|
||||
if formsemestre is None:
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
|
@ -64,7 +64,6 @@ from app.scodoc import sco_assiduites as scass
|
||||
from app.scodoc import sco_bulletins
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_evaluations
|
||||
from app.scodoc import sco_formations
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_formsemestre_inscriptions
|
||||
from app.scodoc import sco_groups
|
||||
@ -515,10 +514,9 @@ def formsemestre_page_title(formsemestre_id=None):
|
||||
def fill_formsemestre(sem: dict): # XXX OBSOLETE
|
||||
"""Add some fields in formsemestres dicts"""
|
||||
formsemestre_id = sem["formsemestre_id"]
|
||||
|
||||
F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0]
|
||||
sem["formation"] = F
|
||||
parcours = codes_cursus.get_cursus_from_code(F["type_parcours"])
|
||||
formation = Formation.get_formation(sem["formation_id"])
|
||||
sem["formation"] = formation.to_dict(with_departement=False)
|
||||
parcours = codes_cursus.get_cursus_from_code(formation.type_parcours)
|
||||
if sem["semestre_id"] != -1:
|
||||
sem["num_sem"] = f""", {parcours.SESSION_NAME} {sem["semestre_id"]}"""
|
||||
else:
|
||||
|
@ -790,10 +790,10 @@ def groups_table(
|
||||
Moodle groupe(s) {groups_infos.groups_titles}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="stdlink" href="{{
|
||||
url_for('notes.export_groups_as_moodle_csv',
|
||||
<a class="stdlink" href="{
|
||||
url_for('scolar.export_groups_as_moodle_csv',
|
||||
scodoc_dept=g.scodoc_dept, formsemestre_id=groups_infos.formsemestre_id)
|
||||
}}">
|
||||
}">
|
||||
Fichier CSV pour Moodle (tous les groupes)</a>
|
||||
<em>(voir le paramétrage pour modifier le format des fichiers Moodle exportés)</em>
|
||||
</li>""",
|
||||
|
@ -51,18 +51,6 @@ _moduleimplEditor = ndb.EditableTable(
|
||||
)
|
||||
|
||||
|
||||
def do_moduleimpl_create(args):
|
||||
"create a moduleimpl"
|
||||
# TODO remplacer par une methode de ModuleImpl qui appelle
|
||||
# super().create_from_dict() puis invalide le formsemestre
|
||||
cnx = ndb.GetDBConnexion()
|
||||
r = _moduleimplEditor.create(cnx, args)
|
||||
sco_cache.invalidate_formsemestre(
|
||||
formsemestre_id=args["formsemestre_id"]
|
||||
) # > creation moduleimpl
|
||||
return r
|
||||
|
||||
|
||||
def do_moduleimpl_delete(oid, formsemestre_id=None):
|
||||
"delete moduleimpl (desinscrit tous les etudiants)"
|
||||
cnx = ndb.GetDBConnexion()
|
||||
|
@ -48,11 +48,10 @@ from wtforms import (
|
||||
HiddenField,
|
||||
SelectMultipleField,
|
||||
)
|
||||
from app.models import Evaluation, ModuleImpl
|
||||
from app.models import Evaluation, Module, ModuleImpl
|
||||
import app.scodoc.sco_utils as scu
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc import sco_edit_module
|
||||
from app.scodoc import sco_evaluations
|
||||
from app.scodoc import sco_excel
|
||||
from app.scodoc.sco_excel import ScoExcelBook, COLORS
|
||||
@ -243,9 +242,9 @@ class PlacementRunner:
|
||||
self.moduleimpl_data = sco_moduleimpl.moduleimpl_list(
|
||||
moduleimpl_id=self.moduleimpl_id
|
||||
)[0]
|
||||
self.module_data = sco_edit_module.module_list(
|
||||
args={"module_id": self.moduleimpl_data["module_id"]}
|
||||
)[0]
|
||||
self.module_data = Module.get_module(
|
||||
self.moduleimpl_data["module_id"]
|
||||
).to_dict()
|
||||
self.sem = sco_formsemestre.get_formsemestre(
|
||||
self.moduleimpl_data["formsemestre_id"]
|
||||
)
|
||||
|
@ -1545,7 +1545,18 @@ class BasePreferences:
|
||||
{
|
||||
"initvalue": 1,
|
||||
"title": "Afficher les UE en cours mais capitalisées sur les bulletins",
|
||||
"explanation": "ne concerne pas les bulletins BUT",
|
||||
"explanation": "ne concerne pas les bulletins de BUT",
|
||||
"input_type": "boolcheckbox",
|
||||
"category": "bul",
|
||||
"labels": ["non", "oui"],
|
||||
},
|
||||
),
|
||||
(
|
||||
"bul_show_ue_cap_but",
|
||||
{
|
||||
"initvalue": 1,
|
||||
"title": "Afficher les UEs capitalisées sur les bulletins BUT",
|
||||
"explanation": "ne concerne que les bulletins de BUT",
|
||||
"input_type": "boolcheckbox",
|
||||
"category": "bul",
|
||||
"labels": ["non", "oui"],
|
||||
@ -1586,6 +1597,7 @@ class BasePreferences:
|
||||
{
|
||||
"initvalue": 1,
|
||||
"title": "Faire figurer les UE validées sur les bulletins",
|
||||
"explanation": "dans les décisions de jury sous la table",
|
||||
"input_type": "boolcheckbox",
|
||||
"category": "bul",
|
||||
"labels": ["non", "oui"],
|
||||
|
@ -617,6 +617,7 @@ def do_evaluations_upload_xls(
|
||||
)
|
||||
|
||||
# -- News
|
||||
obj_id = None
|
||||
if len(evaluations) > 1:
|
||||
modules_str = ", ".join(
|
||||
{
|
||||
@ -645,7 +646,8 @@ def do_evaluations_upload_xls(
|
||||
obj_id = (
|
||||
formsemestre.id if formsemestre else (modimpl.id if modimpl else None)
|
||||
)
|
||||
else:
|
||||
elif len(evaluations) > 0:
|
||||
evaluation = evaluation or evaluations[0]
|
||||
modules_str = (
|
||||
evaluation.moduleimpl.module.titre or evaluation.moduleimpl.module.code
|
||||
)
|
||||
@ -655,13 +657,14 @@ def do_evaluations_upload_xls(
|
||||
moduleimpl_id=evaluation.moduleimpl_id,
|
||||
)
|
||||
obj_id = evaluation.moduleimpl_id
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_NOTE,
|
||||
obj=obj_id,
|
||||
text=f"""Notes dans <a href="{status_url}">{modules_str}</a>""",
|
||||
url=status_url,
|
||||
max_frequency=10 * 60, # 10 minutes
|
||||
)
|
||||
if obj_id is not None:
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_NOTE,
|
||||
obj=obj_id,
|
||||
text=f"""Notes dans <a href="{status_url}">{modules_str}</a>""",
|
||||
url=status_url,
|
||||
max_frequency=10 * 60, # 10 minutes
|
||||
)
|
||||
|
||||
msg = "<div>" + "\n".join(messages_by_eval.values()) + "</div>"
|
||||
if etudids_with_decisions:
|
||||
|
@ -30,17 +30,16 @@
|
||||
Implementation expérimentale (Jul. 2016) pour grouper les modules sur
|
||||
les avis de poursuites d'études.
|
||||
|
||||
TODO: réécrire avec SQLAlchemy.
|
||||
|
||||
Pour l'UI, voir https://goodies.pixabay.com/jquery/tag-editor/demo.html
|
||||
"""
|
||||
import http
|
||||
import re
|
||||
|
||||
from flask import g
|
||||
|
||||
from app import db, log
|
||||
from app import db
|
||||
from app.models import Formation, NotesTag
|
||||
from app.scodoc import sco_edit_module
|
||||
import app.scodoc.sco_utils as scu
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
@ -60,7 +59,7 @@ from app.scodoc.sco_exceptions import ScoValueError
|
||||
|
||||
|
||||
# NOTA: ancien code, n'utile pas de modèles SQLAlchemy
|
||||
class ScoTag(object):
|
||||
class ScoTag:
|
||||
"""Generic tags for ScoDoc"""
|
||||
|
||||
# must be overloaded:
|
||||
@ -208,8 +207,6 @@ class ModuleTag(ScoTag):
|
||||
|
||||
|
||||
# API
|
||||
|
||||
|
||||
# TODO placer dans la vraie API et ne plus utiliser sco_publish
|
||||
def module_tag_search(term: str | int):
|
||||
"""List all used tag names (for auto-completion)"""
|
||||
@ -230,60 +227,6 @@ def module_tag_search(term: str | int):
|
||||
return scu.sendJSON(data)
|
||||
|
||||
|
||||
def module_tag_list(module_id="") -> list[str]:
|
||||
"""les noms de tags associés à ce module"""
|
||||
r = ndb.SimpleDictFetch(
|
||||
"""SELECT t.title
|
||||
FROM notes_modules_tags mt, notes_tags t
|
||||
WHERE mt.tag_id = t.id
|
||||
AND mt.module_id = %(module_id)s
|
||||
""",
|
||||
{"module_id": module_id},
|
||||
)
|
||||
return [x["title"] for x in r]
|
||||
|
||||
|
||||
def module_tag_set(module_id="", taglist=None):
|
||||
"""taglist may either be:
|
||||
a string with tag names separated by commas ("un,deux")
|
||||
or a list of strings (["un", "deux"])
|
||||
Remplace les tags existants
|
||||
"""
|
||||
if not taglist:
|
||||
taglist = []
|
||||
elif isinstance(taglist, str):
|
||||
taglist = taglist.split(",")
|
||||
taglist = [t.strip() for t in taglist]
|
||||
log(f"module_tag_set: module_id={module_id} taglist={taglist}")
|
||||
# Check tags syntax
|
||||
for tag in taglist:
|
||||
if not ScoTag.check_tag_title(tag):
|
||||
log(f"module_tag_set({module_id}): invalid tag title")
|
||||
return scu.json_error(404, "invalid tag")
|
||||
|
||||
# TODO code à moderniser (+ revoir classe ScoTag, utiliser modèle)
|
||||
# TODO Voir ItemSuiviTag et api etud_suivi
|
||||
|
||||
# Sanity check:
|
||||
mod_dict = sco_edit_module.module_list(args={"module_id": module_id})
|
||||
if not mod_dict:
|
||||
raise ScoValueError("invalid module !")
|
||||
|
||||
newtags = set(taglist)
|
||||
oldtags = set(module_tag_list(module_id))
|
||||
to_del = oldtags - newtags
|
||||
to_add = newtags - oldtags
|
||||
|
||||
# should be atomic, but it's not.
|
||||
for tagname in to_add:
|
||||
t = ModuleTag(tagname, object_id=module_id)
|
||||
for tagname in to_del:
|
||||
t = ModuleTag(tagname)
|
||||
t.remove_tag_from_object(module_id)
|
||||
|
||||
return "", http.HTTPStatus.NO_CONTENT
|
||||
|
||||
|
||||
def split_tagname_coeff(tag: str, separateur=":") -> tuple[str, float]:
|
||||
"""Découpage d'un tag, tel que saisi par un utilisateur dans le programme,
|
||||
pour en extraire :
|
||||
|
@ -56,15 +56,13 @@ Solution proposée (nov 2014):
|
||||
import flask
|
||||
from flask import flash, g, request, render_template, url_for
|
||||
from flask_login import current_user
|
||||
from app.formations import edit_ue
|
||||
from app.models.formsemestre import FormSemestre
|
||||
|
||||
|
||||
from app import db, log
|
||||
from app.models import Evaluation, Identite, ModuleImpl, UniteEns
|
||||
from app.models import Evaluation, Identite, Matiere, Module, ModuleImpl, UniteEns
|
||||
from app.scodoc import codes_cursus
|
||||
from app.scodoc import sco_edit_matiere
|
||||
from app.scodoc import sco_edit_module
|
||||
from app.scodoc import sco_edit_ue
|
||||
from app.scodoc import sco_moduleimpl
|
||||
from app.scodoc import sco_saisie_notes
|
||||
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
|
||||
@ -97,10 +95,8 @@ def external_ue_create(
|
||||
#
|
||||
formation_id = formsemestre.formation.id
|
||||
|
||||
numero = sco_edit_ue.next_ue_numero(
|
||||
formation_id, semestre_id=formsemestre.semestre_id
|
||||
)
|
||||
ue_id = sco_edit_ue.do_ue_create(
|
||||
numero = edit_ue.next_ue_numero(formation_id, semestre_id=formsemestre.semestre_id)
|
||||
ue_id = edit_ue.do_ue_create(
|
||||
{
|
||||
"formation_id": formation_id,
|
||||
"semestre_idx": formsemestre.semestre_id,
|
||||
@ -114,26 +110,28 @@ def external_ue_create(
|
||||
)
|
||||
ue = db.session.get(UniteEns, ue_id)
|
||||
flash(f"UE créée (code {ue.ue_code})")
|
||||
matiere_id = sco_edit_matiere.do_matiere_create(
|
||||
matiere = Matiere.create_from_dict(
|
||||
{"ue_id": ue_id, "titre": titre or acronyme, "numero": 1}
|
||||
)
|
||||
|
||||
module_id = sco_edit_module.do_module_create(
|
||||
module = Module.create_from_dict(
|
||||
{
|
||||
"titre": "UE extérieure",
|
||||
"code": acronyme,
|
||||
"coefficient": ects, # tous le coef. module est egal à la quantite d'ECTS
|
||||
"ue_id": ue_id,
|
||||
"matiere_id": matiere_id,
|
||||
"matiere_id": matiere.id,
|
||||
"formation_id": formation_id,
|
||||
"semestre_id": formsemestre.semestre_id,
|
||||
"module_type": scu.ModuleType.STANDARD,
|
||||
},
|
||||
news=True,
|
||||
inval_cache=True,
|
||||
)
|
||||
|
||||
moduleimpl_id = sco_moduleimpl.do_moduleimpl_create(
|
||||
modimpl = ModuleImpl.create_from_dict(
|
||||
{
|
||||
"module_id": module_id,
|
||||
"module_id": module.id,
|
||||
"formsemestre_id": formsemestre_id,
|
||||
# affecte le 1er responsable du semestre comme resp. du module
|
||||
"responsable_id": (
|
||||
@ -143,7 +141,6 @@ def external_ue_create(
|
||||
),
|
||||
},
|
||||
)
|
||||
modimpl = db.session.get(ModuleImpl, moduleimpl_id)
|
||||
assert modimpl
|
||||
return modimpl
|
||||
|
||||
|
@ -343,8 +343,12 @@ def user_info(user_name_or_id=None, user: User = None):
|
||||
info = None
|
||||
user_name = "inconnu"
|
||||
else:
|
||||
info = user.to_dict()
|
||||
user_name = user.user_name
|
||||
if user is None: # utilisateur supprimé (rare)
|
||||
user_name = "inconnu !"
|
||||
info = None
|
||||
else:
|
||||
info = user.to_dict()
|
||||
user_name = user.user_name
|
||||
|
||||
if not info:
|
||||
# special case: user is not in our database
|
||||
@ -365,11 +369,10 @@ def user_info(user_name_or_id=None, user: User = None):
|
||||
"status": "",
|
||||
"date_expiration": None,
|
||||
}
|
||||
else:
|
||||
# Ensure we never publish password hash
|
||||
if "password_hash" in info:
|
||||
del info["password_hash"]
|
||||
return info
|
||||
# Ensure we never publish password hash
|
||||
if "password_hash" in info:
|
||||
del info["password_hash"]
|
||||
return info
|
||||
|
||||
|
||||
MSG_OPT = """<br>Attention: (vous pouvez forcer l'opération en cochant "<em>Ignorer les avertissements</em>" en bas de page)"""
|
||||
|
@ -387,6 +387,20 @@ def localize_datetime(date: datetime.datetime) -> datetime.datetime:
|
||||
return new_date
|
||||
|
||||
|
||||
def get_local_timezone_offset() -> str:
|
||||
"""Récupère l'offset de la timezone du serveur, sous la forme
|
||||
"+HH:MM"
|
||||
"""
|
||||
local_time = datetime.datetime.now().astimezone()
|
||||
utc_offset = local_time.utcoffset()
|
||||
total_seconds = int(utc_offset.total_seconds())
|
||||
offset_hours = total_seconds // 3600
|
||||
offset_minutes = (abs(total_seconds) % 3600) // 60
|
||||
offset_sign = "+" if offset_hours >= 0 else "-"
|
||||
offset_str = f"{offset_sign}{abs(offset_hours):02d}:{offset_minutes:02d}"
|
||||
return offset_str
|
||||
|
||||
|
||||
def is_period_overlapping(
|
||||
periode: tuple[datetime.datetime, datetime.datetime],
|
||||
interval: tuple[datetime.datetime, datetime.datetime],
|
||||
@ -1103,8 +1117,9 @@ def sendXML(
|
||||
attached=False,
|
||||
quote=False,
|
||||
filename=None,
|
||||
):
|
||||
if type(data) != list:
|
||||
) -> Response:
|
||||
"Réponse XML: data est une liste d'objets"
|
||||
if not isinstance(data, list):
|
||||
data = [data] # always list-of-dicts
|
||||
if force_outer_xml_tag:
|
||||
data = [{tagname: data}]
|
||||
@ -1304,19 +1319,18 @@ def format_nomprenom(etud, reverse=False):
|
||||
return " ".join([x for x in fs if x])
|
||||
|
||||
|
||||
def format_nom(s, uppercase=True):
|
||||
def format_nom(s: str, uppercase=True) -> str:
|
||||
"Formatte le nom"
|
||||
if not s:
|
||||
return ""
|
||||
if uppercase:
|
||||
return s.upper()
|
||||
else:
|
||||
return format_prenom(s)
|
||||
return (s.upper()).strip()
|
||||
return format_prenom(s)
|
||||
|
||||
|
||||
def format_prenom(s):
|
||||
def format_prenom(s: str) -> str:
|
||||
"""Formatte prenom etudiant pour affichage
|
||||
DEPRECATED: utiliser Identite.prenom_str
|
||||
Pour les étudiants, utiliser Identite.prenom_str
|
||||
"""
|
||||
if not s:
|
||||
return ""
|
||||
@ -1325,7 +1339,7 @@ def format_prenom(s):
|
||||
for frag in frags:
|
||||
fs = frag.split("-")
|
||||
r.append("-".join([x.lower().capitalize() for x in fs]))
|
||||
return " ".join(r)
|
||||
return (" ".join(r)).strip()
|
||||
|
||||
|
||||
def format_telephone(n: str | None) -> str:
|
||||
|
@ -39,6 +39,16 @@ from app.scodoc.sco_vdi import ApoEtapeVDI
|
||||
XML_HEADER = """<?xml version="1.0" encoding="utf-8"?>"""
|
||||
|
||||
|
||||
def remove_control_characters(s: str) -> str:
|
||||
"""supprime tous les caractères de contrôle (code < 32)
|
||||
sauf les tabs (9), les retours chariot (10 et les sauts de ligne (13).
|
||||
"""
|
||||
if not hasattr(remove_control_characters, "control_chars"):
|
||||
control_chars = dict.fromkeys(i for i in range(32) if i not in (9, 10, 13))
|
||||
remove_control_characters.control_chars = control_chars
|
||||
return s.translate(remove_control_characters.control_chars)
|
||||
|
||||
|
||||
def quote_xml_attr(data):
|
||||
"""Escape &, <, >, quotes and double quotes"""
|
||||
return xml.sax.saxutils.escape(str(data), {"'": "'", '"': """})
|
||||
@ -93,28 +103,24 @@ def _dictlist2xml(dictlist, root=None, tagname=None, quote=False):
|
||||
for d in dictlist:
|
||||
elem = ElementTree.Element(tagname)
|
||||
root.append(elem)
|
||||
if isinstance(d, scalar_types) or isinstance(d, ApoEtapeVDI):
|
||||
if isinstance(d, (scalar_types, ApoEtapeVDI)):
|
||||
elem.set("code", _repr_as_xml(d))
|
||||
else:
|
||||
if quote:
|
||||
d_scalar = dict(
|
||||
[
|
||||
(k, quote_xml_attr(_repr_as_xml(v)))
|
||||
for (k, v) in d.items()
|
||||
if isinstance(v, scalar_types)
|
||||
]
|
||||
)
|
||||
d_scalar = {
|
||||
k: quote_xml_attr(_repr_as_xml(v))
|
||||
for (k, v) in d.items()
|
||||
if isinstance(v, scalar_types)
|
||||
}
|
||||
else:
|
||||
d_scalar = dict(
|
||||
[
|
||||
(k, _repr_as_xml(v))
|
||||
for (k, v) in d.items()
|
||||
if isinstance(v, scalar_types)
|
||||
]
|
||||
)
|
||||
for k in d_scalar:
|
||||
elem.set(k, d_scalar[k])
|
||||
d_list = dict([(k, v) for (k, v) in d.items() if isinstance(v, list)])
|
||||
d_scalar = {
|
||||
k: _repr_as_xml(v)
|
||||
for (k, v) in d.items()
|
||||
if isinstance(v, scalar_types)
|
||||
}
|
||||
for k, v in sorted(d_scalar.items()):
|
||||
elem.set(k, v)
|
||||
d_list = {k: v for (k, v) in d.items() if isinstance(v, list)}
|
||||
if d_list:
|
||||
for k, v in d_list.items():
|
||||
_dictlist2xml(v, tagname=k, root=elem, quote=quote)
|
||||
|
@ -223,27 +223,27 @@ function creerLigneEtudiant(etud, index) {
|
||||
</div>
|
||||
</div>
|
||||
<fieldset class="btns_field single" etudid="497" type="creation" assiduite_id="-1">
|
||||
<input
|
||||
<input
|
||||
type="checkbox"
|
||||
value="present"
|
||||
name="btn_assiduites_1"
|
||||
id="rbtn_present"
|
||||
id="rbtn_present"
|
||||
class="rbtn present"
|
||||
title="present"
|
||||
>
|
||||
<input
|
||||
<input
|
||||
type="checkbox"
|
||||
value="retard"
|
||||
name="btn_assiduites_1"
|
||||
id="rbtn_retard"
|
||||
id="rbtn_retard"
|
||||
class="rbtn retard"
|
||||
title="retard"
|
||||
>
|
||||
<input
|
||||
<input
|
||||
type="checkbox"
|
||||
value="absent"
|
||||
name="btn_assiduites_1"
|
||||
id="rbtn_absent"
|
||||
id="rbtn_absent"
|
||||
class="rbtn absent"
|
||||
title="absent"
|
||||
>
|
||||
@ -609,7 +609,7 @@ async function actionAssiduite(etud, etat, type, assiduite = null) {
|
||||
const modimpl_id = $("#moduleimpl_select").val();
|
||||
if (assiduite && assiduite.etat.toLowerCase() === etat) type = "suppression";
|
||||
|
||||
const { deb, fin } = getPeriodAsDate();
|
||||
const { deb, fin } = getPeriodAsDate(true); // en tz server
|
||||
// génération d'un objet assiduité basique qui sera complété
|
||||
let assiduiteObjet = assiduite ?? {
|
||||
date_debut: deb,
|
||||
@ -702,6 +702,18 @@ function erreurModuleImpl(message) {
|
||||
|
||||
openAlertModal("Sélection du module", content);
|
||||
}
|
||||
if (
|
||||
message == "La date de début n'est pas un jour travaillé"
|
||||
) {
|
||||
const HTML = `
|
||||
<p>Attention, la date de début n'est pas un jour travaillé.</p>
|
||||
`;
|
||||
|
||||
const content = document.createElement("div");
|
||||
content.innerHTML = HTML;
|
||||
|
||||
openAlertModal("Date de début", content);
|
||||
}
|
||||
}
|
||||
// Fonction pour ajouter en lot une assiduité à tous les étudiants
|
||||
// Fonctionne uniquement pour créer ou supprimer des assiduités
|
||||
@ -709,7 +721,7 @@ function erreurModuleImpl(message) {
|
||||
function mettreToutLeMonde(etat, el = null) {
|
||||
const lignesEtuds = [...document.querySelectorAll("fieldset.btns_field")];
|
||||
|
||||
const { deb, fin } = getPeriodAsDate();
|
||||
const { deb, fin } = getPeriodAsDate(true); // tz server
|
||||
const assiduiteObjet = {
|
||||
date_debut: deb,
|
||||
date_fin: fin,
|
||||
|
@ -121,5 +121,4 @@ div.submit > input {
|
||||
{% block scripts %}
|
||||
{{ super() }}
|
||||
<script src="{{scu.STATIC_DIR}}/js/etud_info.js"></script>
|
||||
{% include "sco_timepicker.j2" %}
|
||||
{% endblock scripts %}
|
||||
|
@ -191,7 +191,6 @@ div.submit > input {
|
||||
{% block scripts %}
|
||||
{{ super() }}
|
||||
<script src="{{scu.STATIC_DIR}}/js/etud_info.js"></script>
|
||||
{% include "sco_timepicker.j2" %}
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
// On récupère les boutons de suppression de fichiers
|
||||
|
@ -151,7 +151,7 @@
|
||||
{{ form.submit }} {{ form.cancel }}
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</form>
|
||||
<div class="info-row">
|
||||
<a
|
||||
@ -176,5 +176,4 @@
|
||||
{% block scripts %}
|
||||
{{ super() }}
|
||||
<script src="{{scu.STATIC_DIR}}/js/etud_info.js"></script>
|
||||
{% include "sco_timepicker.j2" %}
|
||||
{% endblock scripts %}
|
@ -12,7 +12,6 @@
|
||||
<script src="{{scu.STATIC_DIR}}/js/groups_view.js"></script>
|
||||
<script src="{{scu.STATIC_DIR}}/js/date_utils.js"></script>
|
||||
<script src="{{scu.STATIC_DIR}}/js/assiduites.js"></script>
|
||||
{% include "sco_timepicker.j2" %}
|
||||
|
||||
<script>
|
||||
|
||||
|
@ -75,7 +75,7 @@
|
||||
}
|
||||
|
||||
th {
|
||||
z-index: 1;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.premier th {
|
||||
@ -205,7 +205,6 @@
|
||||
<script src="{{scu.STATIC_DIR}}/js/etud_info.js"></script>
|
||||
<script src="{{scu.STATIC_DIR}}/js/assiduites.js"></script>
|
||||
<script src="{{scu.STATIC_DIR}}/js/date_utils.js"></script>
|
||||
{% include "sco_timepicker.j2" %}
|
||||
|
||||
|
||||
<script>
|
||||
@ -369,7 +368,7 @@
|
||||
}
|
||||
|
||||
html += `<li>
|
||||
<a
|
||||
<a
|
||||
href="edit_assiduite_etud/${a.assiduite_id}"
|
||||
title="Cliquez pour voir/éditer l'assiduité"
|
||||
target="_blank"
|
||||
@ -860,7 +859,7 @@ document.addEventListener("DOMContentLoaded", ()=>{
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
</details>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
@ -59,7 +59,7 @@
|
||||
block.style.width = `${widthPercentage}%`;
|
||||
|
||||
if (assiduité.etat != "CRENEAU") {
|
||||
// Si on clique dessus on veut pouvoir
|
||||
// Si on clique dessus on veut pouvoir
|
||||
// mettre à jour la timeline principale et modifier le moduleimpl_select
|
||||
block.addEventListener("click", () => {
|
||||
let deb = startDate.getHours() + startDate.getMinutes() / 60;
|
||||
@ -71,7 +71,7 @@
|
||||
|
||||
$("#moduleimpl_select").val(getModuleImplId(assiduité))
|
||||
setTimeout(()=>{
|
||||
$("#moduleimpl_select").trigger("change");
|
||||
$("#moduleimpl_select").trigger("change");
|
||||
}, 0)
|
||||
});
|
||||
//ajouter affichage assiduites on over
|
||||
|
@ -12,6 +12,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
const SERVER_TIMEZONE_OFFSET = "{{ scu.get_local_timezone_offset() }}";
|
||||
const timelineContainer = document.querySelector(".timeline-container");
|
||||
const periodTimeLine = document.querySelector(".period");
|
||||
const t_start = {{ t_start }};
|
||||
@ -21,7 +22,7 @@
|
||||
const tick_time = 60 / {{ tick_time }};
|
||||
const tick_delay = 1 / tick_time;
|
||||
|
||||
const period_default = 2;
|
||||
const period_default = 2; // durée créneau par défaut: 2 heures
|
||||
|
||||
let handleMoving = false;
|
||||
|
||||
@ -264,10 +265,10 @@
|
||||
// On les arrondit aux ticks les plus proches
|
||||
const startValue = snapToQuarter(startHour);
|
||||
const endValue = snapToQuarter(endHour);
|
||||
|
||||
|
||||
// on verifie que les valeurs sont bien dans les bornes
|
||||
const computedValues = [Math.max(startValue, t_start), Math.min(t_end, endValue)];
|
||||
|
||||
|
||||
// si les valeurs sont hors des bornes, on les ajuste
|
||||
if (computedValues[0] > t_end || computedValues[1] < t_start) {
|
||||
return [t_start, Math.min(t_end, t_start + period_default)];
|
||||
@ -338,19 +339,22 @@
|
||||
// Renvoie les valeurs de la période sous forme de date
|
||||
// Les heures sont récupérées depuis la timeline
|
||||
// la date est récupérée depuis un champ "#date" (datepicker)
|
||||
function getPeriodAsDate(){
|
||||
function getPeriodAsDate(add_server_tz = false) {
|
||||
let [deb, fin] = getPeriodValues();
|
||||
deb = numberToTime(deb);
|
||||
fin = numberToTime(fin);
|
||||
fin = numberToTime(fin);
|
||||
|
||||
const dateStr = $("#date")
|
||||
.datepicker("getDate")
|
||||
.format("yyyy-mm-dd")
|
||||
.substring(0, 10); // récupération que de la date, pas des heures
|
||||
|
||||
// Les heures deb et fin sont telles qu'affichées, c'est à dire
|
||||
// en heure locale DU SERVEUR (des étudiants donc)
|
||||
let offset = add_server_tz ? SERVER_TIMEZONE_OFFSET : "";
|
||||
return {
|
||||
deb: new Date(`${dateStr}T${deb}`),
|
||||
fin: new Date(`${dateStr}T${fin}`)
|
||||
deb: new Date(`${dateStr}T${deb}${offset}`),
|
||||
fin: new Date(`${dateStr}T${fin}${offset}`)
|
||||
}
|
||||
}
|
||||
// Sauvegarde les valeurs de la période dans le local storage
|
||||
|
@ -27,6 +27,7 @@
|
||||
border-radius: 12px;
|
||||
margin-bottom: 12px;
|
||||
padding: 4px;
|
||||
--color-semaine-courante: yellow;
|
||||
}
|
||||
|
||||
.mois {
|
||||
@ -83,10 +84,14 @@
|
||||
text-align: center;
|
||||
}
|
||||
.sem-courante{
|
||||
--couleur : #ee752c;
|
||||
border-left: solid 3px var(--couleur);
|
||||
border-right: solid 3px var(--couleur);
|
||||
border-left: solid 3px var(--color-semaine-courante);
|
||||
border-right: solid 3px var(--color-semaine-courante);
|
||||
}
|
||||
div.jours:has(div.sem-courante) {
|
||||
background-color: var(--color-semaine-courante);
|
||||
}
|
||||
|
||||
|
||||
.highlight:hover{
|
||||
border: solid 3px yellow;
|
||||
}
|
||||
|
@ -103,6 +103,8 @@
|
||||
|
||||
</script>
|
||||
|
||||
{% include "sco_timepicker.j2" %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% macro render_field_errors(form, field_name) %}
|
||||
|
@ -51,7 +51,6 @@
|
||||
{% endblock app_content %}
|
||||
{% block scripts %}
|
||||
{{super()}}
|
||||
{% include "sco_timepicker.j2" %}
|
||||
<script src="{{scu.STATIC_DIR}}/js/groups_view.js"></script>
|
||||
<script src="{{scu.STATIC_DIR}}/libjs/purl.js"></script>
|
||||
{% endblock scripts %}
|
||||
|
@ -228,9 +228,7 @@ def ajout_assiduite_etud() -> str | Response:
|
||||
Returns:
|
||||
str: l'html généré
|
||||
"""
|
||||
etudid: int = request.args.get("etudid", -1)
|
||||
etud = Identite.get_etud(etudid)
|
||||
|
||||
etud = Identite.get_etud(request.args.get("etudid"))
|
||||
formsemestre_id = request.args.get("formsemestre_id", None)
|
||||
|
||||
# Gestion du semestre
|
||||
@ -605,10 +603,7 @@ def bilan_etud():
|
||||
)
|
||||
|
||||
# Récupération de l'étudiant
|
||||
etudid = request.args.get("etudid", -1)
|
||||
etud: Identite = Identite.query.get_or_404(etudid)
|
||||
if etud.dept_id != g.scodoc_dept_id:
|
||||
abort(404, "étudiant inexistant dans ce département")
|
||||
etud = Identite.get_etud(request.args.get("etudid"))
|
||||
|
||||
# Gestion du filtre de module
|
||||
moduleimpl_id = request.args.get("moduleimpl_id", None)
|
||||
@ -964,12 +959,7 @@ def calendrier_assi_etud():
|
||||
Returns:
|
||||
str: l'html généré
|
||||
"""
|
||||
|
||||
# Récupération de l'étudiant
|
||||
etudid = request.args.get("etudid", -1)
|
||||
etud: Identite = Identite.query.get_or_404(etudid)
|
||||
if etud.dept_id != g.scodoc_dept_id:
|
||||
abort(404, "étudiant inexistant dans ce département")
|
||||
etud = Identite.get_etud(request.args.get("etudid"))
|
||||
|
||||
# Options
|
||||
mode_demi: bool = scu.to_bool(request.args.get("mode_demi", "t"))
|
||||
|
@ -78,8 +78,14 @@ from app.decorators import (
|
||||
permission_required_compat_scodoc7,
|
||||
)
|
||||
|
||||
|
||||
# ---------------
|
||||
from app.formations import (
|
||||
edit_formation,
|
||||
edit_matiere,
|
||||
edit_module,
|
||||
edit_ue,
|
||||
formation_io,
|
||||
formation_versions,
|
||||
)
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
from app.scodoc.sco_exceptions import (
|
||||
@ -97,10 +103,6 @@ from app.scodoc import (
|
||||
sco_cost_formation,
|
||||
sco_debouche,
|
||||
sco_edit_apc,
|
||||
sco_edit_formation,
|
||||
sco_edit_matiere,
|
||||
sco_edit_module,
|
||||
sco_edit_ue,
|
||||
sco_etape_apogee_view,
|
||||
sco_etud,
|
||||
sco_evaluations,
|
||||
@ -109,9 +111,6 @@ from app.scodoc import (
|
||||
sco_evaluation_edit,
|
||||
sco_evaluation_recap,
|
||||
sco_export_results,
|
||||
sco_formations,
|
||||
sco_formation_recap,
|
||||
sco_formation_versions,
|
||||
sco_formsemestre,
|
||||
sco_formsemestre_custommenu,
|
||||
sco_formsemestre_edit,
|
||||
@ -142,6 +141,7 @@ from app.scodoc import (
|
||||
sco_undo_notes,
|
||||
sco_users,
|
||||
)
|
||||
from app.formations import formation_recap
|
||||
from app.scodoc.gen_tables import GenTable
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc.TrivialFormulator import TrivialFormulator
|
||||
@ -192,7 +192,7 @@ sco_publish(
|
||||
)
|
||||
sco_publish(
|
||||
"/formsemestre_associate_new_version",
|
||||
sco_formation_versions.formsemestre_associate_new_version,
|
||||
formation_versions.formsemestre_associate_new_version,
|
||||
Permission.EditFormation,
|
||||
methods=["GET", "POST"],
|
||||
)
|
||||
@ -240,19 +240,19 @@ sco_publish(
|
||||
|
||||
sco_publish(
|
||||
"/formation_create",
|
||||
sco_edit_formation.formation_create,
|
||||
edit_formation.formation_create,
|
||||
Permission.EditFormation,
|
||||
methods=["GET", "POST"],
|
||||
)
|
||||
sco_publish(
|
||||
"/formation_delete",
|
||||
sco_edit_formation.formation_delete,
|
||||
edit_formation.formation_delete,
|
||||
Permission.EditFormation,
|
||||
methods=["GET", "POST"],
|
||||
)
|
||||
sco_publish(
|
||||
"/formation_edit",
|
||||
sco_edit_formation.formation_edit,
|
||||
edit_formation.formation_edit,
|
||||
Permission.EditFormation,
|
||||
methods=["GET", "POST"],
|
||||
)
|
||||
@ -426,13 +426,13 @@ sco_publish(
|
||||
)
|
||||
sco_publish(
|
||||
"/ue_create",
|
||||
sco_edit_ue.ue_create,
|
||||
edit_ue.ue_create,
|
||||
Permission.EditFormation,
|
||||
methods=["GET", "POST"],
|
||||
)
|
||||
sco_publish(
|
||||
"/ue_delete",
|
||||
sco_edit_ue.ue_delete,
|
||||
edit_ue.ue_delete,
|
||||
Permission.EditFormation,
|
||||
methods=["GET", "POST"],
|
||||
)
|
||||
@ -443,7 +443,7 @@ sco_publish(
|
||||
@permission_required(Permission.EditFormation)
|
||||
def ue_edit(ue_id: int):
|
||||
"Edition de l'UE"
|
||||
return sco_edit_ue.ue_edit(ue_id)
|
||||
return edit_ue.ue_edit(ue_id)
|
||||
|
||||
|
||||
@bp.route("/set_ue_niveau_competence", methods=["POST"])
|
||||
@ -485,7 +485,7 @@ def get_ue_niveaux_options_html():
|
||||
@permission_required(Permission.ScoView)
|
||||
@scodoc7func
|
||||
def ue_table(formation_id=None, semestre_idx=1, msg=""):
|
||||
return sco_edit_ue.ue_table(
|
||||
return edit_ue.ue_table(
|
||||
formation_id=formation_id, semestre_idx=semestre_idx, msg=msg
|
||||
)
|
||||
|
||||
@ -528,7 +528,7 @@ def ue_sharing_code():
|
||||
ue_code = request.args.get("ue_code")
|
||||
ue_id = request.args.get("ue_id")
|
||||
hide_ue_id = request.args.get("hide_ue_id")
|
||||
return sco_edit_ue.ue_sharing_code(
|
||||
return edit_ue.ue_sharing_code(
|
||||
ue_code=ue_code,
|
||||
ue_id=None if ((ue_id is None) or ue_id == "") else int(ue_id),
|
||||
hide_ue_id=(
|
||||
@ -552,56 +552,56 @@ def formation_table_recap(formation_id: int):
|
||||
"Tableau récap. de la formation"
|
||||
formation = Formation.get_formation(formation_id)
|
||||
fmt = request.args.get("fmt", "html")
|
||||
return sco_formation_recap.formation_table_recap(formation, fmt=fmt)
|
||||
return formation_recap.formation_table_recap(formation, fmt=fmt)
|
||||
|
||||
|
||||
sco_publish(
|
||||
"/export_recap_formations_annee_scolaire",
|
||||
sco_formation_recap.export_recap_formations_annee_scolaire,
|
||||
formation_recap.export_recap_formations_annee_scolaire,
|
||||
Permission.ScoView,
|
||||
)
|
||||
sco_publish(
|
||||
"/formation_add_malus_modules",
|
||||
sco_edit_module.formation_add_malus_modules,
|
||||
edit_module.formation_add_malus_modules,
|
||||
Permission.EditFormation,
|
||||
)
|
||||
sco_publish(
|
||||
"/matiere_create",
|
||||
sco_edit_matiere.matiere_create,
|
||||
edit_matiere.matiere_create,
|
||||
Permission.EditFormation,
|
||||
methods=["GET", "POST"],
|
||||
)
|
||||
sco_publish(
|
||||
"/matiere_delete",
|
||||
sco_edit_matiere.matiere_delete,
|
||||
edit_matiere.matiere_delete,
|
||||
Permission.EditFormation,
|
||||
methods=["GET", "POST"],
|
||||
)
|
||||
sco_publish(
|
||||
"/matiere_edit",
|
||||
sco_edit_matiere.matiere_edit,
|
||||
edit_matiere.matiere_edit,
|
||||
Permission.EditFormation,
|
||||
methods=["GET", "POST"],
|
||||
)
|
||||
sco_publish(
|
||||
"/module_create",
|
||||
sco_edit_module.module_create,
|
||||
edit_module.module_create,
|
||||
Permission.EditFormation,
|
||||
methods=["GET", "POST"],
|
||||
)
|
||||
sco_publish(
|
||||
"/module_delete",
|
||||
sco_edit_module.module_delete,
|
||||
edit_module.module_delete,
|
||||
Permission.EditFormation,
|
||||
methods=["GET", "POST"],
|
||||
)
|
||||
sco_publish(
|
||||
"/module_edit",
|
||||
sco_edit_module.module_edit,
|
||||
edit_module.module_edit,
|
||||
Permission.EditFormation,
|
||||
methods=["GET", "POST"],
|
||||
)
|
||||
sco_publish("/module_list", sco_edit_module.module_table, Permission.ScoView)
|
||||
sco_publish("/module_list", edit_module.module_table, Permission.ScoView)
|
||||
sco_publish("/module_tag_search", sco_tag_module.module_tag_search, Permission.ScoView)
|
||||
|
||||
|
||||
@ -612,9 +612,7 @@ def formation_tag_modules_by_type(formation_id: int, semestre_idx: int):
|
||||
"""Taggue tous les modules de la formation en fonction de leur type : 'res', 'sae', 'malus'
|
||||
Ne taggue pas les modules standards.
|
||||
"""
|
||||
formation = Formation.query.filter_by(
|
||||
id=formation_id, dept_id=g.scodoc_dept_id
|
||||
).first_or_404()
|
||||
formation = Formation.get_formation(formation_id)
|
||||
sco_tag_module.formation_tag_modules_by_type(formation)
|
||||
flash("Formation tagguée")
|
||||
return flask.redirect(
|
||||
@ -631,11 +629,12 @@ def formation_tag_modules_by_type(formation_id: int, semestre_idx: int):
|
||||
@bp.route("/module_tag_set", methods=["POST"])
|
||||
@scodoc
|
||||
@permission_required(Permission.EditFormationTags)
|
||||
def module_tag_set():
|
||||
def module_tag_set(): # TODO passer dans l'API
|
||||
"""Set tags on module"""
|
||||
module_id = int(request.form.get("module_id"))
|
||||
module_id = request.form.get("module_id")
|
||||
module: Module = Module.get_instance(module_id)
|
||||
taglist = request.form.get("taglist")
|
||||
return sco_tag_module.module_tag_set(module_id, taglist)
|
||||
return module.set_tags(taglist)
|
||||
|
||||
|
||||
@bp.route("/module_clone", methods=["POST"])
|
||||
@ -643,8 +642,8 @@ def module_tag_set():
|
||||
@permission_required(Permission.EditFormation)
|
||||
def module_clone():
|
||||
"""Clone existing module"""
|
||||
module_id = int(request.form.get("module_id"))
|
||||
module = Module.query.get_or_404(module_id)
|
||||
module_id = request.form.get("module_id")
|
||||
module: Module = Module.get_instance(module_id)
|
||||
module2 = module.clone()
|
||||
db.session.add(module2)
|
||||
db.session.commit()
|
||||
@ -670,7 +669,7 @@ def index_html():
|
||||
detail = scu.to_bool(request.args.get("detail", False))
|
||||
|
||||
editable = current_user.has_permission(Permission.EditFormation)
|
||||
table = sco_formations.formation_list_table(detail=detail)
|
||||
table = formation_io.formation_list_table(detail=detail)
|
||||
|
||||
if fmt != "html":
|
||||
return table.make_page(fmt=fmt, filename=f"Formations-{g.scodoc_dept}")
|
||||
@ -747,7 +746,7 @@ def index_html():
|
||||
@scodoc7func
|
||||
def formation_export(formation_id, export_ids=False, fmt=None, export_codes_apo=True):
|
||||
"Export de la formation au format indiqué (xml ou json)"
|
||||
return sco_formations.formation_export(
|
||||
return formation_io.formation_export(
|
||||
formation_id,
|
||||
export_ids=export_ids,
|
||||
fmt=fmt,
|
||||
@ -789,9 +788,7 @@ def formation_import_xml_form():
|
||||
elif tf[0] == -1:
|
||||
return flask.redirect(url_for("notes.index_html", scodoc_dept=g.scodoc_dept))
|
||||
else:
|
||||
formation_id, _, _ = sco_formations.formation_import_xml(
|
||||
tf[2]["xmlfile"].read()
|
||||
)
|
||||
formation_id, _, _ = formation_io.formation_import_xml(tf[2]["xmlfile"].read())
|
||||
|
||||
return render_template(
|
||||
"sco_page_dept.j2",
|
||||
@ -813,8 +810,8 @@ def formation_import_xml_form():
|
||||
)
|
||||
|
||||
|
||||
sco_publish("/module_move", sco_edit_formation.module_move, Permission.EditFormation)
|
||||
sco_publish("/ue_move", sco_edit_formation.ue_move, Permission.EditFormation)
|
||||
sco_publish("/module_move", edit_formation.module_move, Permission.EditFormation)
|
||||
sco_publish("/ue_move", edit_formation.ue_move, Permission.EditFormation)
|
||||
|
||||
|
||||
@bp.route("/ue_clone", methods=["POST"])
|
||||
@ -2578,21 +2575,21 @@ def check_sem_integrity(formsemestre_id, fix=False):
|
||||
bad_sem = []
|
||||
formations_set = set() # les formations mentionnées dans les UE et modules
|
||||
for modimpl in modimpls:
|
||||
mod = sco_edit_module.module_list({"module_id": modimpl["module_id"]})[0]
|
||||
formations_set.add(mod["formation_id"])
|
||||
ue = UniteEns.query.get_or_404(mod["ue_id"])
|
||||
mod = Module.get_instance(modimpl["module_id"])
|
||||
formations_set.add(mod.formation_id)
|
||||
ue = mod.ue
|
||||
ue_dict = ue.to_dict()
|
||||
formations_set.add(ue_dict["formation_id"])
|
||||
if ue_dict["formation_id"] != mod["formation_id"]:
|
||||
modimpl["mod"] = mod
|
||||
if ue_dict["formation_id"] != mod.formation_id:
|
||||
modimpl["mod"] = mod.to_dict()
|
||||
modimpl["ue"] = ue_dict
|
||||
bad_ue.append(modimpl)
|
||||
if sem["formation_id"] != mod["formation_id"]:
|
||||
if sem["formation_id"] != mod.formation_id:
|
||||
bad_sem.append(modimpl)
|
||||
modimpl["mod"] = mod
|
||||
modimpl["mod"] = mod.to_dict()
|
||||
|
||||
H = [
|
||||
"<p>formation_id=%s" % sem["formation_id"],
|
||||
f"""<p>formation_id={sem["formation_id"]}""",
|
||||
]
|
||||
if bad_ue:
|
||||
H += [
|
||||
|
@ -41,6 +41,7 @@ from app.decorators import (
|
||||
scodoc,
|
||||
permission_required,
|
||||
)
|
||||
from app.formations import formation_io, formation_versions
|
||||
from app.forms.formsemestre import (
|
||||
change_formation,
|
||||
edit_modimpls_codes_apo,
|
||||
@ -55,8 +56,6 @@ from app.models import (
|
||||
)
|
||||
from app.scodoc import (
|
||||
sco_edt_cal,
|
||||
sco_formations,
|
||||
sco_formation_versions,
|
||||
sco_groups_view,
|
||||
)
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
@ -78,7 +77,7 @@ def formsemestre_change_formation(formsemestre_id: int):
|
||||
existant.
|
||||
"""
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
formation_dict = sco_formations.formation_export_dict(
|
||||
formation_dict = formation_io.formation_export_dict(
|
||||
formsemestre.formation, export_external_ues=True, ue_reference_style="acronyme"
|
||||
)
|
||||
formations = [
|
||||
@ -87,7 +86,7 @@ def formsemestre_change_formation(formsemestre_id: int):
|
||||
dept_id=formsemestre.dept_id, acronyme=formsemestre.formation.acronyme
|
||||
)
|
||||
if formation.id != formsemestre.formation.id
|
||||
and sco_formation_versions.formations_are_equals(
|
||||
and formation_versions.formations_are_equals(
|
||||
formation, formation2_dict=formation_dict
|
||||
)
|
||||
]
|
||||
@ -108,7 +107,7 @@ def formsemestre_change_formation(formsemestre_id: int):
|
||||
new_formation: Formation = Formation.query.filter_by(
|
||||
dept_id=g.scodoc_dept_id, formation_id=new_formation_id
|
||||
).first_or_404()
|
||||
sco_formation_versions.formsemestre_change_formation(
|
||||
formation_versions.formsemestre_change_formation(
|
||||
formsemestre, new_formation
|
||||
)
|
||||
flash("Formation du semestre modifiée")
|
||||
|
@ -86,7 +86,6 @@ from app.scodoc import (
|
||||
sco_archives_etud,
|
||||
sco_bug_report,
|
||||
sco_cache,
|
||||
sco_debouche,
|
||||
sco_dept,
|
||||
sco_dump_db,
|
||||
sco_etud,
|
||||
|
@ -307,6 +307,7 @@ def create_user_form(user_name=None, edit=0, all_roles=True):
|
||||
"size": 20,
|
||||
"allow_null": False,
|
||||
"readonly": edit_only_roles,
|
||||
"strip": True,
|
||||
},
|
||||
),
|
||||
(
|
||||
@ -316,6 +317,7 @@ def create_user_form(user_name=None, edit=0, all_roles=True):
|
||||
"size": 20,
|
||||
"allow_null": False,
|
||||
"readonly": edit_only_roles,
|
||||
"strip": True,
|
||||
},
|
||||
),
|
||||
]
|
||||
@ -395,6 +397,7 @@ def create_user_form(user_name=None, edit=0, all_roles=True):
|
||||
"size": 36,
|
||||
"allow_null": False,
|
||||
"readonly": edit_only_roles,
|
||||
"strip": True,
|
||||
},
|
||||
),
|
||||
(
|
||||
@ -455,6 +458,7 @@ def create_user_form(user_name=None, edit=0, all_roles=True):
|
||||
else ""
|
||||
),
|
||||
"size": 36,
|
||||
"strip": True,
|
||||
"allow_null": not require_email_institutionnel,
|
||||
"readonly": edit_only_roles,
|
||||
},
|
||||
@ -809,11 +813,13 @@ def create_user_form(user_name=None, edit=0, all_roles=True):
|
||||
db.session.add(the_user)
|
||||
db.session.commit()
|
||||
# envoi éventuel d'un message
|
||||
if mode == Mode.WELCOME_AND_CHANGE_PASSWORD or mode == Mode.WELCOME_ONLY:
|
||||
if mode == Mode.WELCOME_AND_CHANGE_PASSWORD:
|
||||
token = the_user.get_reset_password_token()
|
||||
else:
|
||||
token = None
|
||||
if mode in (Mode.WELCOME_AND_CHANGE_PASSWORD, Mode.WELCOME_ONLY):
|
||||
token = (
|
||||
the_user.get_reset_password_token()
|
||||
if mode == Mode.WELCOME_AND_CHANGE_PASSWORD
|
||||
else None
|
||||
)
|
||||
|
||||
cas_force = ScoDocSiteConfig.get("cas_force")
|
||||
# Le from doit utiliser la préférence du département de l'utilisateur
|
||||
email.send_email(
|
||||
@ -1008,7 +1014,7 @@ def get_user_list_xml(dept=None, start="", limit=25):
|
||||
userlist = [
|
||||
user
|
||||
for user in userlist
|
||||
if scu.suppress_accents((user.nom or "").lower()).startswith(start)
|
||||
if scu.suppress_accents((user.nom or "").strip().lower()).startswith(start)
|
||||
]
|
||||
doc = ElementTree.Element("results")
|
||||
for user in userlist[:limit]:
|
||||
|
@ -1,7 +1,9 @@
|
||||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
SCOVERSION = "9.7.25"
|
||||
"Infos sur version ScoDoc"
|
||||
|
||||
SCOVERSION = "9.7.28"
|
||||
|
||||
SCONAME = "ScoDoc"
|
||||
|
||||
|
@ -117,7 +117,7 @@ def GET(
|
||||
print("reply", reply.text)
|
||||
raise APIError(
|
||||
errmsg or f"""erreur get {url} !""",
|
||||
reply.json(),
|
||||
reply if reply.status_code == 404 else reply.json(),
|
||||
status_code=reply.status_code,
|
||||
)
|
||||
if raw:
|
||||
@ -220,7 +220,8 @@ def check_failure_get(path: str, headers: dict, err: str = None):
|
||||
# ^ Renvoi un 404
|
||||
except APIError as api_err:
|
||||
if err is not None:
|
||||
assert api_err.payload["message"] == err
|
||||
if "message" in api_err.payload:
|
||||
assert api_err.payload["message"] == err
|
||||
else:
|
||||
raise APIError("Le GET n'aurait pas du fonctionner")
|
||||
|
||||
|
@ -177,7 +177,7 @@ def test_formation_export(api_headers):
|
||||
assert isinstance(module["heures_td"], float)
|
||||
assert isinstance(module["heures_tp"], float)
|
||||
assert isinstance(module["coefficient"], float)
|
||||
assert isinstance(module["ects"], str)
|
||||
assert isinstance(module["ects"], str) if "ects" in module else True
|
||||
assert isinstance(module["semestre_id"], int)
|
||||
assert isinstance(module["numero"], int)
|
||||
assert isinstance(module["code_apogee"], str)
|
||||
|
@ -1,5 +1,6 @@
|
||||
"""Utilitaires pour les tests de l'API
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
|
||||
@ -37,11 +38,11 @@ def verify_occurences_ids_etuds(json_response) -> bool:
|
||||
|
||||
|
||||
DEPARTEMENT_FIELDS = [
|
||||
"id",
|
||||
"acronym",
|
||||
"description",
|
||||
"visible",
|
||||
"date_creation",
|
||||
"description",
|
||||
"id",
|
||||
"visible",
|
||||
]
|
||||
|
||||
# Champs "données personnelles"
|
||||
@ -67,17 +68,17 @@ ETUD_FIELDS = {
|
||||
|
||||
|
||||
FORMATION_FIELDS = {
|
||||
"dept_id",
|
||||
"acronyme",
|
||||
"titre_officiel",
|
||||
"formation_code",
|
||||
"code_specialite",
|
||||
"id",
|
||||
"titre",
|
||||
"version",
|
||||
"type_parcours",
|
||||
"referentiel_competence_id",
|
||||
"dept_id",
|
||||
"formation_code",
|
||||
"formation_id",
|
||||
"id",
|
||||
"referentiel_competence_id",
|
||||
"titre_officiel",
|
||||
"titre",
|
||||
"type_parcours",
|
||||
"version",
|
||||
}
|
||||
|
||||
FORMATION_EXPORT_FIELDS = {
|
||||
@ -95,39 +96,42 @@ FORMATION_EXPORT_FIELDS = {
|
||||
|
||||
FORMATION_EXPORT_UE_FIELDS = {
|
||||
"acronyme",
|
||||
"code_apogee",
|
||||
"coefficient",
|
||||
"color",
|
||||
"ects",
|
||||
"is_external",
|
||||
"matiere",
|
||||
"numero",
|
||||
"reference",
|
||||
"semestre_idx",
|
||||
"titre",
|
||||
"type",
|
||||
"ue_code",
|
||||
"ects",
|
||||
"is_external",
|
||||
"code_apogee",
|
||||
"coefficient",
|
||||
"semestre_idx",
|
||||
"color",
|
||||
"reference",
|
||||
"matiere",
|
||||
}
|
||||
|
||||
FORMATION_EXPORT_UE_MATIERE_FIELDS = {
|
||||
"titre",
|
||||
"numero",
|
||||
"module",
|
||||
"numero",
|
||||
"titre",
|
||||
}
|
||||
|
||||
FORMATION_EXPORT_UE_MATIERE_MODULE_FIELDS = {
|
||||
"titre",
|
||||
"abbrev",
|
||||
"app_critiques",
|
||||
"code_apogee",
|
||||
"code",
|
||||
"coefficient",
|
||||
"coefficients",
|
||||
"edt_id",
|
||||
"heures_cours",
|
||||
"heures_td",
|
||||
"coefficient",
|
||||
"ects",
|
||||
"semestre_id",
|
||||
"numero",
|
||||
"code_apogee",
|
||||
"heures_tp",
|
||||
"module_type",
|
||||
"coefficients",
|
||||
"numero",
|
||||
"parcours",
|
||||
"semestre_id",
|
||||
"titre",
|
||||
}
|
||||
|
||||
FORMATION_EXPORT_UE_MATIERE_MODULE_COEF_FIELDS = {
|
||||
@ -136,42 +140,42 @@ FORMATION_EXPORT_UE_MATIERE_MODULE_COEF_FIELDS = {
|
||||
}
|
||||
|
||||
FORMSEMESTRE_FIELDS = [
|
||||
"titre",
|
||||
"gestion_semestrielle",
|
||||
"scodoc7_id",
|
||||
"date_debut",
|
||||
"block_moyenne_generale",
|
||||
"block_moyennes",
|
||||
"bul_bgcolor",
|
||||
"bul_hide_xml",
|
||||
"date_debut_iso",
|
||||
"date_debut",
|
||||
"date_fin_iso",
|
||||
"date_fin",
|
||||
"resp_can_edit",
|
||||
"departement",
|
||||
"dept_id",
|
||||
"elt_annee_apo",
|
||||
"elt_sem_apo",
|
||||
"ens_can_edit_eval",
|
||||
"etape_apo",
|
||||
"etat",
|
||||
"resp_can_change_ens",
|
||||
"formation_id",
|
||||
"formation",
|
||||
"formsemestre_id",
|
||||
"gestion_compensation",
|
||||
"gestion_semestrielle",
|
||||
"id",
|
||||
"modalite",
|
||||
"ens_can_edit_eval",
|
||||
"formation_id",
|
||||
"gestion_compensation",
|
||||
"elt_sem_apo",
|
||||
"semestre_id",
|
||||
"bul_hide_xml",
|
||||
"elt_annee_apo",
|
||||
"block_moyenne_generale",
|
||||
"formsemestre_id",
|
||||
"titre_num",
|
||||
"titre_formation",
|
||||
"date_debut_iso",
|
||||
"date_fin_iso",
|
||||
"responsables",
|
||||
"parcours",
|
||||
"departement",
|
||||
"formation",
|
||||
"etape_apo",
|
||||
"block_moyennes",
|
||||
"resp_can_change_ens",
|
||||
"resp_can_edit",
|
||||
"responsables",
|
||||
"scodoc7_id",
|
||||
"semestre_id",
|
||||
"titre_formation",
|
||||
"titre_num",
|
||||
"titre",
|
||||
]
|
||||
|
||||
FSEM_FIELDS = {
|
||||
"block_moyennes",
|
||||
"block_moyenne_generale",
|
||||
"block_moyennes",
|
||||
"bul_bgcolor",
|
||||
"bul_hide_xml",
|
||||
"date_debut_iso",
|
||||
@ -198,123 +202,123 @@ FSEM_FIELDS = {
|
||||
}
|
||||
|
||||
MODIMPL_FIELDS = {
|
||||
"id",
|
||||
"formsemestre_id",
|
||||
"computation_expr",
|
||||
"module_id",
|
||||
"responsable_id",
|
||||
"moduleimpl_id",
|
||||
"ens",
|
||||
"formsemestre_id",
|
||||
"id",
|
||||
"module_id",
|
||||
"module",
|
||||
"moduleimpl_id",
|
||||
"responsable_id",
|
||||
}
|
||||
|
||||
MODULE_FIELDS = {
|
||||
"heures_tp",
|
||||
"code_apogee",
|
||||
"titre",
|
||||
"coefficient",
|
||||
"module_type",
|
||||
"id",
|
||||
"ects",
|
||||
"abbrev",
|
||||
"ue_id",
|
||||
"code_apogee",
|
||||
"code",
|
||||
"coefficient",
|
||||
"ects",
|
||||
"formation_id",
|
||||
"heures_cours",
|
||||
"matiere_id",
|
||||
"heures_td",
|
||||
"semestre_id",
|
||||
"numero",
|
||||
"heures_tp",
|
||||
"id",
|
||||
"matiere_id",
|
||||
"module_id",
|
||||
"module_type",
|
||||
"numero",
|
||||
"semestre_id",
|
||||
"titre",
|
||||
"ue_id",
|
||||
}
|
||||
|
||||
UE_FIELDS = {
|
||||
"semestre_idx",
|
||||
"type",
|
||||
"formation_id",
|
||||
"ue_code",
|
||||
"id",
|
||||
"ects",
|
||||
"acronyme",
|
||||
"is_external",
|
||||
"numero",
|
||||
"code_apogee",
|
||||
"titre",
|
||||
"coefficient",
|
||||
"color",
|
||||
"ects",
|
||||
"formation_id",
|
||||
"id",
|
||||
"is_external",
|
||||
"numero",
|
||||
"semestre_idx",
|
||||
"titre",
|
||||
"type",
|
||||
"ue_code",
|
||||
"ue_id",
|
||||
}
|
||||
|
||||
BULLETIN_FIELDS = {
|
||||
"version",
|
||||
"type",
|
||||
"date",
|
||||
"publie",
|
||||
"etat_inscription",
|
||||
"etudiant",
|
||||
"formation",
|
||||
"formsemestre_id",
|
||||
"etat_inscription",
|
||||
"options",
|
||||
"publie",
|
||||
"ressources",
|
||||
"saes",
|
||||
"ues",
|
||||
"semestre",
|
||||
"type",
|
||||
"ues",
|
||||
"version",
|
||||
}
|
||||
|
||||
|
||||
BULLETIN_ETUDIANT_FIELDS = {
|
||||
"boursier",
|
||||
"civilite",
|
||||
"code_ine",
|
||||
"code_nip",
|
||||
"codepostaldomicile",
|
||||
"date_naissance",
|
||||
"dept_id",
|
||||
"dept_acronym",
|
||||
"dept_id",
|
||||
"dept_naissance",
|
||||
"description",
|
||||
"domicile",
|
||||
"email",
|
||||
"emailperso",
|
||||
"etudid",
|
||||
"nom",
|
||||
"prenom",
|
||||
"nomprenom",
|
||||
"lieu_naissance",
|
||||
"dept_naissance",
|
||||
"nationalite",
|
||||
"boursier",
|
||||
"fiche_url",
|
||||
"photo_url",
|
||||
"id",
|
||||
"domicile",
|
||||
"villedomicile",
|
||||
"telephone",
|
||||
"fax",
|
||||
"description",
|
||||
"codepostaldomicile",
|
||||
"fiche_url",
|
||||
"id",
|
||||
"lieu_naissance",
|
||||
"nationalite",
|
||||
"nom",
|
||||
"nomprenom",
|
||||
"paysdomicile",
|
||||
"photo_url",
|
||||
"prenom",
|
||||
"telephone",
|
||||
"telephonemobile",
|
||||
"typeadresse",
|
||||
"villedomicile",
|
||||
}
|
||||
|
||||
BULLETIN_FORMATION_FIELDS = {"id", "acronyme", "titre_officiel", "titre"}
|
||||
|
||||
BULLETIN_OPTIONS_FIELDS = {
|
||||
"show_abs",
|
||||
"show_abs_modules",
|
||||
"show_ects",
|
||||
"show_abs",
|
||||
"show_codemodules",
|
||||
"show_coef",
|
||||
"show_date_inscr",
|
||||
"show_ects",
|
||||
"show_matieres",
|
||||
"show_rangs",
|
||||
"show_ue_rangs",
|
||||
"show_minmax_eval",
|
||||
"show_minmax_mod",
|
||||
"show_minmax",
|
||||
"show_mod_rangs",
|
||||
"show_moypromo",
|
||||
"show_minmax",
|
||||
"show_minmax_mod",
|
||||
"show_minmax_eval",
|
||||
"show_coef",
|
||||
"show_ue_cap_details",
|
||||
"show_ue_cap_current",
|
||||
"show_rangs",
|
||||
"show_temporary",
|
||||
"temporary_txt",
|
||||
"show_ue_cap_current",
|
||||
"show_ue_cap_details",
|
||||
"show_ue_rangs",
|
||||
"show_uevalid",
|
||||
"show_date_inscr",
|
||||
"temporary_txt",
|
||||
}
|
||||
|
||||
BULLETIN_RESSOURCES_FIELDS = {
|
||||
@ -346,23 +350,23 @@ BULLETIN_SAES_FIELDS = {
|
||||
|
||||
########### RESSOURCES ET SAES ###########
|
||||
BULLETIN_RESSOURCES_ET_SAES_RESSOURCE_ET_SAE_FIELDS = {
|
||||
"id",
|
||||
"titre",
|
||||
"code_apogee",
|
||||
"url",
|
||||
"moyenne",
|
||||
"evaluations",
|
||||
"id",
|
||||
"moyenne",
|
||||
"titre",
|
||||
"url",
|
||||
}
|
||||
|
||||
BULLETIN_RESSOURCES_ET_SAES_RESSOURCE_ET_SAE_EVALUATION_FIELDS = {
|
||||
"id",
|
||||
"description",
|
||||
"coef",
|
||||
"date",
|
||||
"description",
|
||||
"heure_debut",
|
||||
"heure_fin",
|
||||
"coef",
|
||||
"poids",
|
||||
"id",
|
||||
"note",
|
||||
"poids",
|
||||
"url",
|
||||
}
|
||||
|
||||
@ -373,10 +377,10 @@ BULLETIN_RESSOURCES_ET_SAES_RESSOURCE_ET_SAE_EVALUATION_POIDS_FIELDS = {
|
||||
}
|
||||
|
||||
BULLETIN_RESSOURCES_ET_SAES_RESSOURCE_ET_SAE_EVALUATION_NOTE_FIELDS = {
|
||||
"value",
|
||||
"min",
|
||||
"max",
|
||||
"min",
|
||||
"moy",
|
||||
"value",
|
||||
}
|
||||
|
||||
|
||||
@ -384,19 +388,19 @@ BULLETIN_RESSOURCES_ET_SAES_RESSOURCE_ET_SAE_EVALUATION_NOTE_FIELDS = {
|
||||
BULLETIN_UES_FIELDS = {"RT1.1", "RT2.1", "RT3.1"}
|
||||
|
||||
BULLETIN_UES_UE_FIELDS = {
|
||||
"id",
|
||||
"titre",
|
||||
"numero",
|
||||
"type",
|
||||
"bonus",
|
||||
"capitalise",
|
||||
"color",
|
||||
"competence",
|
||||
"moyenne",
|
||||
"bonus",
|
||||
"ECTS",
|
||||
"id",
|
||||
"malus",
|
||||
"capitalise",
|
||||
"moyenne",
|
||||
"numero",
|
||||
"ressources",
|
||||
"saes",
|
||||
"ECTS",
|
||||
"titre",
|
||||
"type",
|
||||
}
|
||||
|
||||
BULLETIN_UES_UE_MOYENNE_FIELDS = {"value", "min", "max", "moy", "rang", "total"}
|
||||
@ -461,16 +465,16 @@ BULLETIN_UES_UE_ECTS_FIELDS = {"acquis", "total"}
|
||||
|
||||
########### SEMESTRE ###########
|
||||
BULLETIN_SEMESTRE_FIELDS = {
|
||||
"etapes",
|
||||
"absences",
|
||||
"annee_universitaire",
|
||||
"date_debut",
|
||||
"date_fin",
|
||||
"annee_universitaire",
|
||||
"numero",
|
||||
"inscription",
|
||||
"groupes",
|
||||
"absences",
|
||||
"ECTS",
|
||||
"etapes",
|
||||
"groupes",
|
||||
"inscription",
|
||||
"notes",
|
||||
"numero",
|
||||
"rang",
|
||||
}
|
||||
|
||||
@ -484,78 +488,78 @@ BULLETIN_SEMESTRE_RANG_FIELDS = {"value", "total"}
|
||||
|
||||
|
||||
EVAL_FIELDS = {
|
||||
"id",
|
||||
"description",
|
||||
"coefficient",
|
||||
"date_debut",
|
||||
"date_fin",
|
||||
"coefficient",
|
||||
"description",
|
||||
"etat",
|
||||
"evaluation_type",
|
||||
"id",
|
||||
"moduleimpl_id",
|
||||
"nb_inscrits",
|
||||
"nb_notes_abs",
|
||||
"nb_notes_att",
|
||||
"nb_notes_exc",
|
||||
"nb_notes_manquantes",
|
||||
"note_max",
|
||||
"numero",
|
||||
"poids",
|
||||
"publish_incomplete",
|
||||
"visibulletin",
|
||||
"etat",
|
||||
"nb_inscrits",
|
||||
"nb_notes_manquantes",
|
||||
"nb_notes_abs",
|
||||
"nb_notes_att",
|
||||
"nb_notes_exc",
|
||||
"saisie_notes",
|
||||
"visibulletin",
|
||||
}
|
||||
|
||||
SAISIE_NOTES_FIELDS = {"datetime_debut", "datetime_fin", "datetime_mediane"}
|
||||
|
||||
REF_COMP_FIELDS = {
|
||||
"dept_id",
|
||||
"annexe",
|
||||
"specialite",
|
||||
"specialite_long",
|
||||
"type_structure",
|
||||
"type_departement",
|
||||
"type_titre",
|
||||
"version_orebut",
|
||||
"competences",
|
||||
"dept_id",
|
||||
"parcours",
|
||||
"scodoc_date_loaded",
|
||||
"scodoc_orig_filename",
|
||||
"competences",
|
||||
"parcours",
|
||||
"specialite_long",
|
||||
"specialite",
|
||||
"type_departement",
|
||||
"type_structure",
|
||||
"type_titre",
|
||||
"version_orebut",
|
||||
}
|
||||
|
||||
ABSENCES_FIELDS = {
|
||||
"jour",
|
||||
"matin",
|
||||
"begin",
|
||||
"description",
|
||||
"end",
|
||||
"estabs",
|
||||
"estjust",
|
||||
"description",
|
||||
"begin",
|
||||
"end",
|
||||
"jour",
|
||||
"matin",
|
||||
}
|
||||
|
||||
ABSENCES_GROUP_ETAT_FIELDS = {"etudid", "list_abs"}
|
||||
|
||||
|
||||
FORMSEMESTRE_ETUD_FIELDS = {
|
||||
"id",
|
||||
"code_nip",
|
||||
"code_ine",
|
||||
"nom",
|
||||
"nom_usuel",
|
||||
"prenom",
|
||||
"civilite",
|
||||
"code_ine",
|
||||
"code_nip",
|
||||
"groups",
|
||||
"id",
|
||||
"nom_usuel",
|
||||
"nom",
|
||||
"prenom",
|
||||
}
|
||||
|
||||
FORMSEMESTRE_ETUS_GROUPS_FIELDS = {
|
||||
"partition_id",
|
||||
"id",
|
||||
"formsemestre_id",
|
||||
"partition_name",
|
||||
"numero",
|
||||
"bul_show_rank",
|
||||
"show_in_lists",
|
||||
"formsemestre_id",
|
||||
"group_id",
|
||||
"group_name",
|
||||
"id",
|
||||
"numero",
|
||||
"partition_id",
|
||||
"partition_name",
|
||||
"show_in_lists",
|
||||
}
|
||||
|
||||
EVALUATIONS_FIELDS = {
|
||||
@ -573,107 +577,107 @@ EVALUATIONS_FIELDS = {
|
||||
}
|
||||
|
||||
NOTES_FIELDS = {
|
||||
"etudid",
|
||||
"evaluation_id",
|
||||
"value",
|
||||
"comment",
|
||||
"date",
|
||||
"etudid",
|
||||
"evaluation_id",
|
||||
"uid",
|
||||
"value",
|
||||
}
|
||||
|
||||
|
||||
PARTITIONS_FIELDS = {
|
||||
"id",
|
||||
"formsemestre_id",
|
||||
"partition_name",
|
||||
"numero",
|
||||
"bul_show_rank",
|
||||
"formsemestre_id",
|
||||
"id",
|
||||
"numero",
|
||||
"partition_name",
|
||||
"show_in_lists",
|
||||
}
|
||||
|
||||
PARTITION_GROUPS_ETUD_FIELDS = {
|
||||
"id",
|
||||
"civilite",
|
||||
"code_ine",
|
||||
"code_nip",
|
||||
"dept_id",
|
||||
"id",
|
||||
"nom_usuel",
|
||||
"nom",
|
||||
"prenom",
|
||||
"nom_usuel",
|
||||
"civilite",
|
||||
"code_nip",
|
||||
"code_ine",
|
||||
}
|
||||
|
||||
FORMSEMESTRE_BULLETINS_FIELDS = {
|
||||
"version",
|
||||
"type",
|
||||
"date",
|
||||
"publie",
|
||||
"etat_inscription",
|
||||
"etudiant",
|
||||
"formation",
|
||||
"formsemestre_id",
|
||||
"etat_inscription",
|
||||
"options",
|
||||
"publie",
|
||||
"ressources",
|
||||
"saes",
|
||||
"ues",
|
||||
"semestre",
|
||||
"type",
|
||||
"ues",
|
||||
"version",
|
||||
}
|
||||
|
||||
FORMSEMESTRE_BULLETINS_ETU_FIELDS = {
|
||||
"boursier",
|
||||
"civilite",
|
||||
"code_ine",
|
||||
"code_nip",
|
||||
"codepostaldomicile",
|
||||
"date_naissance",
|
||||
"dept_id",
|
||||
"dept_acronym",
|
||||
"dept_id",
|
||||
"dept_naissance",
|
||||
"description",
|
||||
"domicile",
|
||||
"email",
|
||||
"emailperso",
|
||||
"etudid",
|
||||
"nom",
|
||||
"prenom",
|
||||
"nomprenom",
|
||||
"lieu_naissance",
|
||||
"dept_naissance",
|
||||
"nationalite",
|
||||
"boursier",
|
||||
"fax",
|
||||
"fiche_url",
|
||||
"photo_url",
|
||||
"id",
|
||||
"codepostaldomicile",
|
||||
"lieu_naissance",
|
||||
"nationalite",
|
||||
"nom",
|
||||
"nomprenom",
|
||||
"paysdomicile",
|
||||
"photo_url",
|
||||
"prenom",
|
||||
"telephone",
|
||||
"telephonemobile",
|
||||
"typeadresse",
|
||||
"domicile",
|
||||
"villedomicile",
|
||||
"telephone",
|
||||
"fax",
|
||||
"description",
|
||||
}
|
||||
|
||||
FORMSEMESTRE_BULLETINS_FORMATION_FIELDS = {
|
||||
"id",
|
||||
"acronyme",
|
||||
"id",
|
||||
"titre_officiel",
|
||||
"titre",
|
||||
}
|
||||
|
||||
FORMSEMESTRE_BULLETINS_OPT_FIELDS = {
|
||||
"show_abs",
|
||||
"show_abs_modules",
|
||||
"show_ects",
|
||||
"show_abs",
|
||||
"show_codemodules",
|
||||
"show_coef",
|
||||
"show_date_inscr",
|
||||
"show_ects",
|
||||
"show_matieres",
|
||||
"show_rangs",
|
||||
"show_ue_rangs",
|
||||
"show_minmax_eval",
|
||||
"show_minmax_mod",
|
||||
"show_minmax",
|
||||
"show_mod_rangs",
|
||||
"show_moypromo",
|
||||
"show_minmax",
|
||||
"show_minmax_mod",
|
||||
"show_minmax_eval",
|
||||
"show_coef",
|
||||
"show_ue_cap_details",
|
||||
"show_ue_cap_current",
|
||||
"show_rangs",
|
||||
"show_temporary",
|
||||
"temporary_txt",
|
||||
"show_ue_cap_current",
|
||||
"show_ue_cap_details",
|
||||
"show_ue_rangs",
|
||||
"show_uevalid",
|
||||
"show_date_inscr",
|
||||
"temporary_txt",
|
||||
}
|
||||
|
@ -12,8 +12,8 @@ Usage: pytest tests/scenarios/test_scenario1_formation.py
|
||||
# code écrit par Fares Amer, mai 2021 et porté sur ScoDoc 8 en août 2021
|
||||
|
||||
from tests.unit import sco_fake_gen
|
||||
from app.scodoc import sco_edit_module
|
||||
from app.scodoc import sco_formations
|
||||
from app.formations import formation_io
|
||||
from app.models import Formation
|
||||
|
||||
|
||||
@pytest.mark.skip # test obsolete
|
||||
@ -32,7 +32,7 @@ def run_scenario1():
|
||||
doc = f.read()
|
||||
|
||||
# --- Création de la formation
|
||||
f = sco_formations.formation_import_xml(doc=doc)
|
||||
f = formation_io.formation_import_xml(doc=doc)
|
||||
|
||||
# --- Création des semestres
|
||||
formation_id = f[0]
|
||||
@ -53,11 +53,11 @@ def run_scenario1():
|
||||
]
|
||||
|
||||
# --- Implémentation des modules
|
||||
modules = sco_edit_module.module_list({"formation_id": formation_id})
|
||||
formation = Formation.get_formation(formation_id)
|
||||
mods_imp = []
|
||||
for mod in modules:
|
||||
for mod in formation.modules:
|
||||
mi = G.create_moduleimpl(
|
||||
module_id=mod["module_id"],
|
||||
formsemestre_id=sems[mod["semestre_id"] - 1]["formsemestre_id"],
|
||||
module_id=mod.id,
|
||||
formsemestre_id=sems[mod.semestre_id - 1]["formsemestre_id"],
|
||||
)
|
||||
mods_imp.append(mi)
|
||||
|
@ -16,6 +16,7 @@ import typing
|
||||
|
||||
from app import db, log
|
||||
from app.auth.models import User
|
||||
from app.formations import edit_ue
|
||||
from app.models import (
|
||||
Departement,
|
||||
Evaluation,
|
||||
@ -23,16 +24,13 @@ from app.models import (
|
||||
FormationModalite,
|
||||
Identite,
|
||||
Matiere,
|
||||
Module,
|
||||
ModuleImpl,
|
||||
)
|
||||
from app.scodoc import codes_cursus
|
||||
from app.scodoc import sco_edit_matiere
|
||||
from app.scodoc import sco_edit_module
|
||||
from app.scodoc import sco_edit_ue
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_formsemestre_inscriptions
|
||||
from app.scodoc import sco_formsemestre_validation
|
||||
from app.scodoc import sco_moduleimpl
|
||||
from app.scodoc import sco_saisie_notes
|
||||
from app.scodoc import sco_synchro_etuds
|
||||
from app.scodoc import sco_utils as scu
|
||||
@ -198,20 +196,17 @@ class ScoFake(object):
|
||||
return: ue_id
|
||||
"""
|
||||
if numero is None:
|
||||
numero = sco_edit_ue.next_ue_numero(formation_id, 0)
|
||||
oid = sco_edit_ue.do_ue_create(locals())
|
||||
oids = sco_edit_ue.ue_list(args={"ue_id": oid})
|
||||
numero = edit_ue.next_ue_numero(formation_id, 0)
|
||||
oid = edit_ue.do_ue_create(locals())
|
||||
oids = edit_ue.ue_list(args={"ue_id": oid})
|
||||
if not oids:
|
||||
raise ScoValueError("ue not created !")
|
||||
return oid
|
||||
|
||||
@logging_meth
|
||||
def create_matiere(self, ue_id=None, titre=None, numero=0) -> int:
|
||||
oid = sco_edit_matiere.do_matiere_create(locals())
|
||||
oids = sco_edit_matiere.matiere_list(args={"matiere_id": oid})
|
||||
if not oids:
|
||||
raise ScoValueError("matiere not created !")
|
||||
return oid
|
||||
mat = Matiere.create_from_dict(locals())
|
||||
return mat.id
|
||||
|
||||
@logging_meth
|
||||
def create_module(
|
||||
@ -233,11 +228,8 @@ class ScoFake(object):
|
||||
matiere = db.session.get(Matiere, matiere_id)
|
||||
ue_id = matiere.ue.id
|
||||
formation_id = matiere.ue.formation.id
|
||||
oid = sco_edit_module.do_module_create(locals())
|
||||
oids = sco_edit_module.module_list(args={"module_id": oid})
|
||||
if not oids:
|
||||
raise ScoValueError(f"module not created ! (oid={oid})")
|
||||
return oid
|
||||
module = Module.create_from_dict(locals(), news=True, inval_cache=True)
|
||||
return module.id
|
||||
|
||||
@logging_meth
|
||||
def create_formsemestre(
|
||||
@ -281,11 +273,8 @@ class ScoFake(object):
|
||||
) -> int:
|
||||
if not responsable_id:
|
||||
responsable_id = self.default_user.id
|
||||
oid = sco_moduleimpl.do_moduleimpl_create(locals())
|
||||
oids = sco_moduleimpl.moduleimpl_list(moduleimpl_id=oid) # API inconsistency
|
||||
if not oids:
|
||||
raise ScoValueError("moduleimpl not created !")
|
||||
return oid
|
||||
modimpl = ModuleImpl.create_from_dict(locals())
|
||||
return modimpl.id
|
||||
|
||||
@logging_meth
|
||||
def inscrit_etudiant(self, formsemestre_id: int, etud: dict):
|
||||
|
@ -34,10 +34,7 @@
|
||||
# - moduleimpl_list
|
||||
# - do_module_impl_with_module_list
|
||||
# - do_formsemestre_delete
|
||||
# - module_list
|
||||
# - do_module_delete
|
||||
# - matiere_list
|
||||
# - do_matiere_delete
|
||||
# - Module.delete
|
||||
# - ue_list
|
||||
# - do_ue_delete
|
||||
# - do_formation_delete
|
||||
@ -48,13 +45,15 @@ import os
|
||||
import pytest
|
||||
|
||||
from app import db
|
||||
from app.models import Formation, ModuleImpl
|
||||
from app.scodoc import sco_edit_formation, sco_formsemestre
|
||||
from app.scodoc import sco_edit_matiere
|
||||
from app.scodoc import sco_edit_module
|
||||
from app.scodoc import sco_edit_ue
|
||||
from app.formations import (
|
||||
edit_formation,
|
||||
edit_module,
|
||||
edit_ue,
|
||||
formation_io,
|
||||
)
|
||||
from app.models import Formation, Matiere, Module, ModuleImpl
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_exceptions
|
||||
from app.scodoc import sco_formations
|
||||
from app.scodoc import sco_formsemestre_edit
|
||||
from app.scodoc import sco_moduleimpl
|
||||
from app.views import notes
|
||||
@ -183,7 +182,7 @@ def test_formations(test_client):
|
||||
)
|
||||
|
||||
# --- Export de formation vers JSON
|
||||
exp = sco_formations.formation_export(
|
||||
exp = formation_io.formation_export(
|
||||
formation_id=formation_id, fmt="json", export_ids=True
|
||||
).get_data(as_text=True)
|
||||
assert isinstance(exp, str)
|
||||
@ -259,22 +258,23 @@ def test_formations(test_client):
|
||||
formsemestre_id=sem2["formsemestre_id"]
|
||||
)
|
||||
|
||||
li_module = sco_edit_module.module_list()
|
||||
li_module = Module.query.all()
|
||||
assert len(li_module) == 4
|
||||
# Suppression impossible car utilisé dans le semestre formsemestre_idt:
|
||||
module3 = db.session.get(ModuleImpl, mi3).module
|
||||
with pytest.raises(sco_exceptions.ScoNonEmptyFormationObject):
|
||||
sco_edit_module.module_delete(module_id=module3.id)
|
||||
edit_module.module_delete(module_id=module3.id)
|
||||
|
||||
sco_formsemestre_edit.do_formsemestre_delete(formsemestre_idt)
|
||||
|
||||
li_module2_before = sco_edit_module.module_list()
|
||||
li_module2_before = Module.query.all()
|
||||
|
||||
sco_edit_module.do_module_delete(module3.id)
|
||||
sco_edit_module.do_module_delete(module_id_t)
|
||||
module3.delete()
|
||||
module_t = db.session.get(Module, module_id_t)
|
||||
module_t.delete()
|
||||
|
||||
# deuxieme methode de supression d'un module
|
||||
li_module2_after = sco_edit_module.module_list()
|
||||
li_module2_after = Module.query.all()
|
||||
|
||||
assert (
|
||||
len(li_module2_after) == len(li_module2_before) - 2
|
||||
@ -284,21 +284,23 @@ def test_formations(test_client):
|
||||
|
||||
assert len(lim_sem2) == 0 # deuxieme vérification si le module s'est bien sup
|
||||
|
||||
li_mat = sco_edit_matiere.matiere_list()
|
||||
li_mat = Matiere.query.all()
|
||||
assert len(li_mat) == 4
|
||||
sco_edit_matiere.do_matiere_delete(oid=matiere_id3) # on supprime la matiere
|
||||
li_mat2 = sco_edit_matiere.matiere_list()
|
||||
assert matiere_id3 in [m.matiere_id for m in li_mat]
|
||||
matiere = db.session.get(Matiere, matiere_id3)
|
||||
matiere.delete() # on supprime la matiere
|
||||
li_mat2 = Matiere.query.all()
|
||||
assert len(li_mat2) == 3 # verification de la suppression de la matiere
|
||||
|
||||
li_ue = sco_edit_ue.ue_list()
|
||||
li_ue = edit_ue.ue_list()
|
||||
assert len(li_ue) == 4
|
||||
sco_edit_ue.ue_delete(ue_id=uet_id, dialog_confirmed=True)
|
||||
li_ue2 = sco_edit_ue.ue_list()
|
||||
edit_ue.ue_delete(ue_id=uet_id, dialog_confirmed=True)
|
||||
li_ue2 = edit_ue.ue_list()
|
||||
assert len(li_ue2) == 3 # verification de la suppression de l'UE
|
||||
|
||||
# --- Suppression d'une formation
|
||||
|
||||
sco_edit_formation.do_formation_delete(formation_id=formation_id2)
|
||||
edit_formation.do_formation_delete(formation_id=formation_id2)
|
||||
formation = db.session.get(Formation, formation_id2)
|
||||
assert formation is None
|
||||
|
||||
@ -315,11 +317,11 @@ def test_import_formation(test_client, filename="formation-exemple-1.xml"):
|
||||
doc = f.read()
|
||||
|
||||
# --- Création de la formation
|
||||
f = sco_formations.formation_import_xml(doc)
|
||||
f = formation_io.formation_import_xml(doc)
|
||||
assert len(f) == 3 # 3-uple
|
||||
formation_id = f[0]
|
||||
# --- Vérification des UE
|
||||
ues = sco_edit_ue.ue_list({"formation_id": formation_id})
|
||||
ues = edit_ue.ue_list({"formation_id": formation_id})
|
||||
assert len(ues) == 10
|
||||
assert all(not ue["is_external"] for ue in ues) # aucune UE externe dans le XML
|
||||
# --- Mise en place de 4 semestres
|
||||
@ -338,17 +340,15 @@ def test_import_formation(test_client, filename="formation-exemple-1.xml"):
|
||||
)
|
||||
]
|
||||
# et les modules
|
||||
modules = sco_edit_module.module_list({"formation_id": formation_id})
|
||||
for mod in modules:
|
||||
formation = Formation.get_formation(formation_id)
|
||||
for mod in formation.modules:
|
||||
moduleimpl_id = G.create_moduleimpl(
|
||||
module_id=mod["module_id"],
|
||||
formsemestre_id=formsemestre_ids[mod["semestre_id"] - 1],
|
||||
module_id=mod.id,
|
||||
formsemestre_id=formsemestre_ids[mod.semestre_id - 1],
|
||||
)
|
||||
mi = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
|
||||
assert mi["module_id"] == mod["module_id"]
|
||||
assert mi["module_id"] == mod.id
|
||||
|
||||
# --- Export formation en XML
|
||||
doc1 = sco_formations.formation_export(formation_id, fmt="xml").get_data(
|
||||
as_text=True
|
||||
)
|
||||
doc1 = formation_io.formation_export(formation_id, fmt="xml").get_data(as_text=True)
|
||||
assert isinstance(doc1, str)
|
||||
|
@ -8,16 +8,15 @@ import pytest
|
||||
|
||||
import app
|
||||
from app import db
|
||||
from app.formations import edit_ue, formation_versions
|
||||
from app.models import Formation, FormSemestre, FormSemestreDescription
|
||||
from app.scodoc import (
|
||||
sco_archives_formsemestre,
|
||||
sco_cost_formation,
|
||||
sco_debouche,
|
||||
sco_edit_ue,
|
||||
sco_evaluations,
|
||||
sco_evaluation_check_abs,
|
||||
sco_evaluation_recap,
|
||||
sco_formation_versions,
|
||||
sco_formsemestre_edit,
|
||||
sco_formsemestre_inscriptions,
|
||||
sco_formsemestre_status,
|
||||
@ -61,7 +60,7 @@ def test_formsemestres_associate_new_version(test_client):
|
||||
assert {s.semestre_id for s in formsemestres} == {1}
|
||||
# Les rattache à une nouvelle version de la formation:
|
||||
formsemestre_ids = [s.id for s in formsemestres]
|
||||
sco_formation_versions.do_formsemestres_associate_new_version(
|
||||
formation_versions.do_formsemestres_associate_new_version(
|
||||
formation.id, formsemestre_ids
|
||||
)
|
||||
new_formation: Formation = Formation.query.filter_by(
|
||||
@ -91,7 +90,7 @@ def test_formsemestre_misc_views(test_client):
|
||||
|
||||
# ----- MENU SEMESTRE
|
||||
_ = sco_formsemestre_status.formsemestre_status(formsemestre_id=formsemestre.id)
|
||||
_ = sco_edit_ue.ue_table(formsemestre.formation_id)
|
||||
_ = edit_ue.ue_table(formsemestre.formation_id)
|
||||
_ = sco_formsemestre_edit.formsemestre_editwithmodules(formsemestre.id)
|
||||
_ = sco_preferences.SemPreferences(formsemestre_id=formsemestre.id).edit()
|
||||
_ = sco_formsemestre_edit.formsemestre_edit_options(formsemestre.id)
|
||||
@ -123,7 +122,7 @@ def test_formsemestre_misc_views(test_client):
|
||||
assert isinstance(ans, (str, Response)) # ici str
|
||||
# Juste la page dialogue avant opération::
|
||||
ans = sco_formsemestre_edit.formsemestre_clone(formsemestre.id)
|
||||
ans = sco_formation_versions.formsemestre_associate_new_version(
|
||||
ans = formation_versions.formsemestre_associate_new_version(
|
||||
formsemestre.formation_id, formsemestre.id
|
||||
)
|
||||
ans = sco_formsemestre_edit.formsemestre_delete(formsemestre.id)
|
||||
|
@ -50,7 +50,7 @@ from flask import g
|
||||
from app import db
|
||||
|
||||
from app.auth.models import User
|
||||
|
||||
from app.formations import formation_io
|
||||
from app.models import (
|
||||
ApcParcours,
|
||||
DispenseUE,
|
||||
@ -63,7 +63,6 @@ from app.models import (
|
||||
UniteEns,
|
||||
)
|
||||
|
||||
from app.scodoc import sco_formations
|
||||
from app.scodoc import sco_formsemestre_inscriptions
|
||||
from app.scodoc import sco_groups
|
||||
from app.scodoc import sco_saisie_notes
|
||||
@ -86,7 +85,7 @@ def setup_formation(formation_infos: dict) -> Formation:
|
||||
doc = f.read()
|
||||
|
||||
# --- Création de la formation
|
||||
formation_id, _, _ = sco_formations.formation_import_xml(doc)
|
||||
formation_id, _, _ = formation_io.formation_import_xml(doc)
|
||||
formation: Formation = db.session.get(Formation, formation_id)
|
||||
assert formation
|
||||
return formation
|
||||
|
@ -17,6 +17,7 @@ from app import db
|
||||
from app.auth.models import Role, User
|
||||
from app.but.import_refcomp import orebut_import_refcomp
|
||||
from app import models
|
||||
from app.formations import formation_io
|
||||
from app.models import departements
|
||||
from app.models import (
|
||||
Absence,
|
||||
@ -33,8 +34,6 @@ from app.models import (
|
||||
)
|
||||
from app.scodoc import (
|
||||
sco_cache,
|
||||
sco_evaluation_db,
|
||||
sco_formations,
|
||||
sco_formsemestre_inscriptions,
|
||||
sco_groups,
|
||||
)
|
||||
@ -68,7 +67,7 @@ def import_formation(dept_id: int) -> Formation:
|
||||
with open(FORMATION_XML_FILENAME, encoding="utf-8") as f:
|
||||
doc = f.read()
|
||||
# --- Création de la formation (import programme)
|
||||
f = sco_formations.formation_import_xml(doc)
|
||||
f = formation_io.formation_import_xml(doc)
|
||||
formation = db.session.get(Formation, f[0])
|
||||
# --- Association ref. comp.
|
||||
with open(REFCOMP_FILENAME, encoding="utf-8") as f:
|
||||
|
Loading…
Reference in New Issue
Block a user