diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py
index 738a47442..eb10f57b7 100644
--- a/app/models/formsemestre.py
+++ b/app/models/formsemestre.py
@@ -36,6 +36,7 @@ from app.models.config import ScoDocSiteConfig
from app.models.departements import Departement
from app.models.etudiants import Identite
from app.models.evaluations import Evaluation
+from app.models.events import ScolarNews
from app.models.formations import Formation
from app.models.groups import GroupDescr, Partition
from app.models.moduleimpls import (
@@ -207,6 +208,70 @@ class FormSemestre(models.ScoDocModel):
).first_or_404()
return cls.query.filter_by(id=formsemestre_id).first_or_404()
+ @classmethod
+ def create_formsemestre(cls, args: dict, silent=False) -> "FormSemestre":
+ """Création d'un formsemestre, avec toutes les valeurs par défaut
+ et notification (sauf si silent).
+ Crée la partition par défaut.
+ """
+ # was sco_formsemestre.do_formsemestre_create
+ if "dept_id" not in args:
+ args["dept_id"] = g.scodoc_dept_id
+ formsemestre: "FormSemestre" = cls.create_from_dict(args)
+ db.session.flush()
+ for etape in args["etapes"]:
+ formsemestre.add_etape(etape)
+ db.session.commit()
+ for u in args["responsables"]:
+ formsemestre.responsables.append(u)
+ # create default partition
+ partition = Partition(
+ formsemestre=formsemestre, partition_name=None, numero=1000000
+ )
+ db.session.add(partition)
+ partition.create_group(default=True)
+ db.session.commit()
+
+ if not silent:
+ url = url_for(
+ "notes.formsemestre_status",
+ scodoc_dept=formsemestre.departement.acronym,
+ formsemestre_id=formsemestre.id,
+ )
+ ScolarNews.add(
+ typ=ScolarNews.NEWS_SEM,
+ text=f"""Création du semestre {formsemestre.titre}""",
+ url=url,
+ max_frequency=0,
+ )
+
+ return formsemestre
+
+ @classmethod
+ def convert_dict_fields(cls, args: dict) -> dict:
+ """Convert fields in the given dict.
+ args: dict with args in application.
+ returns: dict to store in model's db.
+ """
+ if "date_debut" in args:
+ args["date_debut"] = scu.convert_fr_date(args["date_debut"])
+ if "date_fin" in args:
+ args["date_fin"] = scu.convert_fr_date(args["date_debut"])
+ if "etat" in args:
+ args["etat"] = bool(args["etat"])
+ if "bul_bgcolor" in args:
+ args["bul_bgcolor"] = args.get("bul_bgcolor") or "white"
+ if "titre" in args:
+ args["titre"] = args.get("titre") or "sans titre"
+ return args
+
+ @classmethod
+ def filter_model_attributes(cls, data: dict, excluded: set[str] = None) -> dict:
+ """Returns a copy of dict with only the keys belonging to the Model and not in excluded.
+ Add 'etapes' to excluded."""
+ # on ne peut pas affecter directement etapes
+ return super().filter_model_attributes(data, (excluded or set()) | {"etapes"})
+
def sort_key(self) -> tuple:
"""clé pour tris par ordre de date_debut, le plus ancien en tête
(pour avoir le plus récent d'abord, sort avec reverse=True)"""
@@ -729,7 +794,7 @@ class FormSemestre(models.ScoDocModel):
FormSemestre.titre,
)
- def etapes_apo_vdi(self) -> list[ApoEtapeVDI]:
+ def etapes_apo_vdi(self) -> list["ApoEtapeVDI"]:
"Liste des vdis"
# was read_formsemestre_etapes
return [e.as_apovdi() for e in self.etapes if e.etape_apo]
@@ -742,9 +807,9 @@ class FormSemestre(models.ScoDocModel):
return ""
return ", ".join(sorted([etape.etape_apo for etape in self.etapes if etape]))
- def add_etape(self, etape_apo: str):
+ def add_etape(self, etape_apo: str | ApoEtapeVDI):
"Ajoute une étape"
- etape = FormSemestreEtape(formsemestre_id=self.id, etape_apo=etape_apo)
+ etape = FormSemestreEtape(formsemestre_id=self.id, etape_apo=str(etape_apo))
db.session.add(etape)
def regroupements_coherents_etud(self) -> list[tuple[UniteEns, UniteEns]]:
@@ -1271,7 +1336,7 @@ class FormSemestreEtape(db.Model):
def __str__(self):
return self.etape_apo or ""
- def as_apovdi(self) -> ApoEtapeVDI:
+ def as_apovdi(self) -> "ApoEtapeVDI":
return ApoEtapeVDI(self.etape_apo)
diff --git a/app/models/groups.py b/app/models/groups.py
index 7250f1e67..68c7156b8 100644
--- a/app/models/groups.py
+++ b/app/models/groups.py
@@ -93,6 +93,10 @@ class Partition(ScoDocModel):
):
group.remove_etud(etud)
+ def is_default(self) -> bool:
+ "vrai si partition par défault (tous les étudiants)"
+ return not self.partition_name
+
def is_parcours(self) -> bool:
"Vrai s'il s'agit de la partition de parcours"
return self.partition_name == scu.PARTITION_PARCOURS
diff --git a/app/scodoc/sco_edit_ue.py b/app/scodoc/sco_edit_ue.py
index 152103aef..9d8df2d44 100644
--- a/app/scodoc/sco_edit_ue.py
+++ b/app/scodoc/sco_edit_ue.py
@@ -1056,10 +1056,10 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
if current_user.has_permission(Permission.EditFormSemestre):
H.append(
f"""
"""
)
diff --git a/app/scodoc/sco_formsemestre.py b/app/scodoc/sco_formsemestre.py
index 0a9264ea8..16daef1a7 100644
--- a/app/scodoc/sco_formsemestre.py
+++ b/app/scodoc/sco_formsemestre.py
@@ -229,11 +229,14 @@ def etapes_apo_str(etapes):
return ", ".join([str(x) for x in etapes])
-def do_formsemestre_create(args, silent=False):
+def do_formsemestre_create( # DEPRECATED, use FormSemestre.create_formsemestre()
+ args, silent=False
+):
"create a formsemestre"
from app.models import ScolarNews
from app.scodoc import sco_groups
+ log("Warning: do_formsemestre_create is deprecated")
cnx = ndb.GetDBConnexion()
formsemestre_id = _formsemestreEditor.create(cnx, args)
if args["etapes"]:
diff --git a/app/scodoc/sco_formsemestre_edit.py b/app/scodoc/sco_formsemestre_edit.py
index 07717f591..70237d20b 100644
--- a/app/scodoc/sco_formsemestre_edit.py
+++ b/app/scodoc/sco_formsemestre_edit.py
@@ -37,16 +37,17 @@ from app import db
from app.auth.models import User
from app.models import APO_CODE_STR_LEN, SHORT_STR_LEN
from app.models import (
- Module,
- ModuleImpl,
- Evaluation,
- UniteEns,
- ScoDocSiteConfig,
- ScolarFormSemestreValidation,
- ScolarAutorisationInscription,
ApcValidationAnnee,
ApcValidationRCUE,
+ Evaluation,
+ FormSemestreUECoef,
+ Module,
+ ModuleImpl,
+ ScoDocSiteConfig,
+ ScolarAutorisationInscription,
+ ScolarFormSemestreValidation,
ScolarNews,
+ UniteEns,
)
from app.models.formations import Formation
from app.models.formsemestre import FormSemestre
@@ -439,12 +440,13 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
{
"size": 32,
"title": "Element(s) Apogée sem.:",
- "explanation": "associé(s) au résultat du semestre (ex: VRTW1). Inutile en BUT. Séparés par des virgules.",
- "allow_null": not sco_preferences.get_preference(
- "always_require_apo_sem_codes"
- )
- or (formsemestre and formsemestre.modalite == "EXT")
- or (formsemestre.formation.is_apc()),
+ "explanation": """associé(s) au résultat du semestre (ex: VRTW1).
+ Inutile en BUT. Séparés par des virgules.""",
+ "allow_null": (
+ not sco_preferences.get_preference("always_require_apo_sem_codes")
+ or (formsemestre and formsemestre.modalite == "EXT")
+ or (formsemestre and formsemestre.formation.is_apc())
+ ),
},
)
)
@@ -1250,7 +1252,7 @@ def formsemestre_clone(formsemestre_id):
raise ScoValueError("id responsable invalide")
new_formsemestre_id = do_formsemestre_clone(
formsemestre_id,
- resp.id,
+ resp,
tf[2]["date_debut"],
tf[2]["date_fin"],
clone_evaluations=tf[2]["clone_evaluations"],
@@ -1268,7 +1270,7 @@ def formsemestre_clone(formsemestre_id):
def do_formsemestre_clone(
orig_formsemestre_id,
- responsable_id, # new resp.
+ responsable: User, # new resp.
date_debut,
date_fin, # 'dd/mm/yyyy'
clone_evaluations=False,
@@ -1281,49 +1283,63 @@ def do_formsemestre_clone(
formsemestre_orig: FormSemestre = FormSemestre.query.get_or_404(
orig_formsemestre_id
)
- orig_sem = sco_formsemestre.get_formsemestre(orig_formsemestre_id)
- cnx = ndb.GetDBConnexion()
# 1- create sem
- args = orig_sem.copy()
+ args = formsemestre_orig.to_dict()
del args["formsemestre_id"]
- args["responsables"] = [responsable_id]
+ del args["id"]
+ del args["parcours"] # copiés ensuite
+ args["responsables"] = [responsable]
args["date_debut"] = date_debut
args["date_fin"] = date_fin
args["etat"] = 1 # non verrouillé
- formsemestre_id = sco_formsemestre.do_formsemestre_create(args)
- log(f"created formsemestre {formsemestre_id}")
- formsemestre: FormSemestre = db.session.get(FormSemestre, formsemestre_id)
+
+ formsemestre = FormSemestre.create_formsemestre(args)
+ log(f"created formsemestre {formsemestre}")
# 2- create moduleimpls
modimpl_orig: ModuleImpl
for modimpl_orig in formsemestre_orig.modimpls:
+ assert isinstance(modimpl_orig, ModuleImpl)
+ assert isinstance(modimpl_orig.id, int)
+ log(f"cloning {modimpl_orig}")
args = modimpl_orig.to_dict(with_module=False)
- args["formsemestre_id"] = formsemestre_id
+ args["formsemestre_id"] = formsemestre.id
modimpl_new = ModuleImpl.create_from_dict(args)
+ log(f"created ModuleImpl from {args}")
db.session.flush()
# copy enseignants
for ens in modimpl_orig.enseignants:
modimpl_new.enseignants.append(ens)
db.session.add(modimpl_new)
+ db.session.flush()
+ log(f"new moduleimpl.id = {modimpl_new.id}")
# optionally, copy evaluations
if clone_evaluations:
+ e: Evaluation
for e in Evaluation.query.filter_by(moduleimpl_id=modimpl_orig.id):
+ log(f"cloning evaluation {e.id}")
# copie en enlevant la date
- new_eval = e.clone(
- not_copying=("date_debut", "date_fin", "moduleimpl_id")
- )
- new_eval.moduleimpl_id = modimpl_new.id
+ args = dict(e.__dict__)
+ args.pop("_sa_instance_state")
+ args.pop("id")
+ args["moduleimpl_id"] = modimpl_new.id
+ new_eval = Evaluation(**args)
+ db.session.add(new_eval)
+ db.session.commit()
# Copie les poids APC de l'évaluation
new_eval.set_ue_poids_dict(e.get_ue_poids_dict())
db.session.commit()
# 3- copy uecoefs
- objs = sco_formsemestre.formsemestre_uecoef_list(
- cnx, args={"formsemestre_id": orig_formsemestre_id}
- )
- for obj in objs:
- args = obj.copy()
- args["formsemestre_id"] = formsemestre_id
- _ = sco_formsemestre.formsemestre_uecoef_create(cnx, args)
+ for ue_coef in FormSemestreUECoef.query.filter_by(
+ formsemestre_id=formsemestre_orig.id
+ ):
+ new_ue_coef = FormSemestreUECoef(
+ formsemestre_id=formsemestre.id,
+ ue_id=ue_coef.ue_id,
+ coefficient=ue_coef.coefficient,
+ )
+ db.session.add(new_ue_coef)
+ db.session.flush()
# NB: don't copy notes_formsemestre_custommenu (usually specific)
@@ -1335,11 +1351,11 @@ def do_formsemestre_clone(
if not prefs.is_global(pname):
pvalue = prefs[pname]
try:
- prefs.base_prefs.set(formsemestre_id, pname, pvalue)
+ prefs.base_prefs.set(formsemestre.id, pname, pvalue)
except ValueError:
log(
- "do_formsemestre_clone: ignoring old preference %s=%s for %s"
- % (pname, pvalue, formsemestre_id)
+ f"""do_formsemestre_clone: ignoring old preference {
+ pname}={pvalue} for {formsemestre}"""
)
# 5- Copie les parcours
@@ -1350,10 +1366,10 @@ def do_formsemestre_clone(
# 6- Copy partitions and groups
if clone_partitions:
sco_groups_copy.clone_partitions_and_groups(
- orig_formsemestre_id, formsemestre_id
+ orig_formsemestre_id, formsemestre.id
)
- return formsemestre_id
+ return formsemestre.id
def formsemestre_delete(formsemestre_id: int) -> str | flask.Response:
diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py
index fc7df7311..8c3ac414a 100755
--- a/app/scodoc/sco_formsemestre_status.py
+++ b/app/scodoc/sco_formsemestre_status.py
@@ -794,7 +794,7 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str:
{
'Groupes de ' + partition.partition_name
if partition.partition_name else
- 'Tous les étudiants'}
+ ('aucun étudiant inscrit' if partition_is_empty else 'Tous les étudiants')}
{
"Assiduité" if not partition_is_empty else ""
@@ -885,7 +885,7 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str:
)
H.append("
") # /sem-groups-assi
- if partition_is_empty:
+ if partition_is_empty and not partition.is_default():
H.append(
'Aucun groupe peuplé dans cette partition'
)
diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py
index 07aaacae8..bc5943e23 100644
--- a/app/scodoc/sco_utils.py
+++ b/app/scodoc/sco_utils.py
@@ -109,13 +109,17 @@ ETATS_INSCRIPTION = {
}
-def convert_fr_date(date_str: str, allow_iso=True) -> datetime.datetime:
+def convert_fr_date(
+ date_str: str | datetime.datetime, allow_iso=True
+) -> datetime.datetime:
"""Converti une date saisie par un humain français avant 2070
en un objet datetime.
12/2/1972 => 1972-02-12, 12/2/72 => 1972-02-12, mais 12/2/24 => 2024-02-12
Le pivot est 70.
ScoValueError si date invalide.
"""
+ if isinstance(date_str, datetime.datetime):
+ return date_str
try:
return datetime.datetime.strptime(date_str, DATE_FMT)
except ValueError:
diff --git a/app/scodoc/sco_vdi.py b/app/scodoc/sco_vdi.py
index 09d1a90a2..0ceca257a 100644
--- a/app/scodoc/sco_vdi.py
+++ b/app/scodoc/sco_vdi.py
@@ -30,7 +30,7 @@
from app.scodoc.sco_exceptions import ScoValueError
-class ApoEtapeVDI(object):
+class ApoEtapeVDI:
"""Classe stockant le VDI avec le code étape (noms de fichiers maquettes et code semestres)"""
_ETAPE_VDI_SEP = "!"
@@ -118,7 +118,7 @@ class ApoEtapeVDI(object):
else:
return etape_vdi, ""
- def concat_etape_vdi(self, etape, vdi=""):
+ def concat_etape_vdi(self, etape: str, vdi: str = "") -> str:
if vdi:
return self._ETAPE_VDI_SEP.join([etape, vdi])
else: