Clonage semestre: améliore code + qq modifs cosmétiques

This commit is contained in:
Emmanuel Viennet 2024-06-18 01:21:33 +02:00
parent 07c2f00277
commit 8a49d99292
8 changed files with 143 additions and 51 deletions

View File

@ -36,6 +36,7 @@ from app.models.config import ScoDocSiteConfig
from app.models.departements import Departement from app.models.departements import Departement
from app.models.etudiants import Identite from app.models.etudiants import Identite
from app.models.evaluations import Evaluation from app.models.evaluations import Evaluation
from app.models.events import ScolarNews
from app.models.formations import Formation from app.models.formations import Formation
from app.models.groups import GroupDescr, Partition from app.models.groups import GroupDescr, Partition
from app.models.moduleimpls import ( from app.models.moduleimpls import (
@ -207,6 +208,70 @@ class FormSemestre(models.ScoDocModel):
).first_or_404() ).first_or_404()
return cls.query.filter_by(id=formsemestre_id).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 <a href="{url}">{formsemestre.titre}</a>""",
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: def sort_key(self) -> tuple:
"""clé pour tris par ordre de date_debut, le plus ancien en tête """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)""" (pour avoir le plus récent d'abord, sort avec reverse=True)"""
@ -729,7 +794,7 @@ class FormSemestre(models.ScoDocModel):
FormSemestre.titre, FormSemestre.titre,
) )
def etapes_apo_vdi(self) -> list[ApoEtapeVDI]: def etapes_apo_vdi(self) -> list["ApoEtapeVDI"]:
"Liste des vdis" "Liste des vdis"
# was read_formsemestre_etapes # was read_formsemestre_etapes
return [e.as_apovdi() for e in self.etapes if e.etape_apo] return [e.as_apovdi() for e in self.etapes if e.etape_apo]
@ -742,9 +807,9 @@ class FormSemestre(models.ScoDocModel):
return "" return ""
return ", ".join(sorted([etape.etape_apo for etape in self.etapes if etape])) 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" "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) db.session.add(etape)
def regroupements_coherents_etud(self) -> list[tuple[UniteEns, UniteEns]]: def regroupements_coherents_etud(self) -> list[tuple[UniteEns, UniteEns]]:
@ -1271,7 +1336,7 @@ class FormSemestreEtape(db.Model):
def __str__(self): def __str__(self):
return self.etape_apo or "" return self.etape_apo or ""
def as_apovdi(self) -> ApoEtapeVDI: def as_apovdi(self) -> "ApoEtapeVDI":
return ApoEtapeVDI(self.etape_apo) return ApoEtapeVDI(self.etape_apo)

View File

@ -93,6 +93,10 @@ class Partition(ScoDocModel):
): ):
group.remove_etud(etud) 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: def is_parcours(self) -> bool:
"Vrai s'il s'agit de la partition de parcours" "Vrai s'il s'agit de la partition de parcours"
return self.partition_name == scu.PARTITION_PARCOURS return self.partition_name == scu.PARTITION_PARCOURS

View File

@ -1056,10 +1056,10 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
if current_user.has_permission(Permission.EditFormSemestre): if current_user.has_permission(Permission.EditFormSemestre):
H.append( H.append(
f"""<ul> f"""<ul>
<li><a class="stdlink" href="{ <li><b><a class="stdlink" href="{
url_for('notes.formsemestre_createwithmodules', scodoc_dept=g.scodoc_dept, url_for('notes.formsemestre_createwithmodules', scodoc_dept=g.scodoc_dept,
formation_id=formation_id, semestre_id=1) formation_id=formation_id, semestre_id=1)
}">Mettre en place un nouveau semestre de formation {formation.acronyme}</a> }">Mettre en place un nouveau semestre de formation {formation.acronyme}</a></b>
</li> </li>
</ul>""" </ul>"""
) )

View File

@ -229,11 +229,14 @@ def etapes_apo_str(etapes):
return ", ".join([str(x) for x in 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" "create a formsemestre"
from app.models import ScolarNews from app.models import ScolarNews
from app.scodoc import sco_groups from app.scodoc import sco_groups
log("Warning: do_formsemestre_create is deprecated")
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
formsemestre_id = _formsemestreEditor.create(cnx, args) formsemestre_id = _formsemestreEditor.create(cnx, args)
if args["etapes"]: if args["etapes"]:

View File

@ -37,16 +37,17 @@ from app import db
from app.auth.models import User from app.auth.models import User
from app.models import APO_CODE_STR_LEN, SHORT_STR_LEN from app.models import APO_CODE_STR_LEN, SHORT_STR_LEN
from app.models import ( from app.models import (
Module,
ModuleImpl,
Evaluation,
UniteEns,
ScoDocSiteConfig,
ScolarFormSemestreValidation,
ScolarAutorisationInscription,
ApcValidationAnnee, ApcValidationAnnee,
ApcValidationRCUE, ApcValidationRCUE,
Evaluation,
FormSemestreUECoef,
Module,
ModuleImpl,
ScoDocSiteConfig,
ScolarAutorisationInscription,
ScolarFormSemestreValidation,
ScolarNews, ScolarNews,
UniteEns,
) )
from app.models.formations import Formation from app.models.formations import Formation
from app.models.formsemestre import FormSemestre from app.models.formsemestre import FormSemestre
@ -439,12 +440,13 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
{ {
"size": 32, "size": 32,
"title": "Element(s) Apogée sem.:", "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.", "explanation": """associé(s) au résultat du semestre (ex: VRTW1).
"allow_null": not sco_preferences.get_preference( Inutile en BUT. Séparés par des virgules.""",
"always_require_apo_sem_codes" "allow_null": (
) not sco_preferences.get_preference("always_require_apo_sem_codes")
or (formsemestre and formsemestre.modalite == "EXT") or (formsemestre and formsemestre.modalite == "EXT")
or (formsemestre.formation.is_apc()), or (formsemestre and formsemestre.formation.is_apc())
),
}, },
) )
) )
@ -1250,7 +1252,7 @@ def formsemestre_clone(formsemestre_id):
raise ScoValueError("id responsable invalide") raise ScoValueError("id responsable invalide")
new_formsemestre_id = do_formsemestre_clone( new_formsemestre_id = do_formsemestre_clone(
formsemestre_id, formsemestre_id,
resp.id, resp,
tf[2]["date_debut"], tf[2]["date_debut"],
tf[2]["date_fin"], tf[2]["date_fin"],
clone_evaluations=tf[2]["clone_evaluations"], clone_evaluations=tf[2]["clone_evaluations"],
@ -1268,7 +1270,7 @@ def formsemestre_clone(formsemestre_id):
def do_formsemestre_clone( def do_formsemestre_clone(
orig_formsemestre_id, orig_formsemestre_id,
responsable_id, # new resp. responsable: User, # new resp.
date_debut, date_debut,
date_fin, # 'dd/mm/yyyy' date_fin, # 'dd/mm/yyyy'
clone_evaluations=False, clone_evaluations=False,
@ -1281,49 +1283,63 @@ def do_formsemestre_clone(
formsemestre_orig: FormSemestre = FormSemestre.query.get_or_404( formsemestre_orig: FormSemestre = FormSemestre.query.get_or_404(
orig_formsemestre_id orig_formsemestre_id
) )
orig_sem = sco_formsemestre.get_formsemestre(orig_formsemestre_id)
cnx = ndb.GetDBConnexion()
# 1- create sem # 1- create sem
args = orig_sem.copy() args = formsemestre_orig.to_dict()
del args["formsemestre_id"] 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_debut"] = date_debut
args["date_fin"] = date_fin args["date_fin"] = date_fin
args["etat"] = 1 # non verrouillé args["etat"] = 1 # non verrouillé
formsemestre_id = sco_formsemestre.do_formsemestre_create(args)
log(f"created formsemestre {formsemestre_id}") formsemestre = FormSemestre.create_formsemestre(args)
formsemestre: FormSemestre = db.session.get(FormSemestre, formsemestre_id) log(f"created formsemestre {formsemestre}")
# 2- create moduleimpls # 2- create moduleimpls
modimpl_orig: ModuleImpl modimpl_orig: ModuleImpl
for modimpl_orig in formsemestre_orig.modimpls: 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 = modimpl_orig.to_dict(with_module=False)
args["formsemestre_id"] = formsemestre_id args["formsemestre_id"] = formsemestre.id
modimpl_new = ModuleImpl.create_from_dict(args) modimpl_new = ModuleImpl.create_from_dict(args)
log(f"created ModuleImpl from {args}")
db.session.flush() db.session.flush()
# copy enseignants # copy enseignants
for ens in modimpl_orig.enseignants: for ens in modimpl_orig.enseignants:
modimpl_new.enseignants.append(ens) modimpl_new.enseignants.append(ens)
db.session.add(modimpl_new) db.session.add(modimpl_new)
db.session.flush()
log(f"new moduleimpl.id = {modimpl_new.id}")
# optionally, copy evaluations # optionally, copy evaluations
if clone_evaluations: if clone_evaluations:
e: Evaluation
for e in Evaluation.query.filter_by(moduleimpl_id=modimpl_orig.id): for e in Evaluation.query.filter_by(moduleimpl_id=modimpl_orig.id):
log(f"cloning evaluation {e.id}")
# copie en enlevant la date # copie en enlevant la date
new_eval = e.clone( args = dict(e.__dict__)
not_copying=("date_debut", "date_fin", "moduleimpl_id") args.pop("_sa_instance_state")
) args.pop("id")
new_eval.moduleimpl_id = modimpl_new.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 # Copie les poids APC de l'évaluation
new_eval.set_ue_poids_dict(e.get_ue_poids_dict()) new_eval.set_ue_poids_dict(e.get_ue_poids_dict())
db.session.commit() db.session.commit()
# 3- copy uecoefs # 3- copy uecoefs
objs = sco_formsemestre.formsemestre_uecoef_list( for ue_coef in FormSemestreUECoef.query.filter_by(
cnx, args={"formsemestre_id": orig_formsemestre_id} formsemestre_id=formsemestre_orig.id
) ):
for obj in objs: new_ue_coef = FormSemestreUECoef(
args = obj.copy() formsemestre_id=formsemestre.id,
args["formsemestre_id"] = formsemestre_id ue_id=ue_coef.ue_id,
_ = sco_formsemestre.formsemestre_uecoef_create(cnx, args) coefficient=ue_coef.coefficient,
)
db.session.add(new_ue_coef)
db.session.flush()
# NB: don't copy notes_formsemestre_custommenu (usually specific) # NB: don't copy notes_formsemestre_custommenu (usually specific)
@ -1335,11 +1351,11 @@ def do_formsemestre_clone(
if not prefs.is_global(pname): if not prefs.is_global(pname):
pvalue = prefs[pname] pvalue = prefs[pname]
try: try:
prefs.base_prefs.set(formsemestre_id, pname, pvalue) prefs.base_prefs.set(formsemestre.id, pname, pvalue)
except ValueError: except ValueError:
log( log(
"do_formsemestre_clone: ignoring old preference %s=%s for %s" f"""do_formsemestre_clone: ignoring old preference {
% (pname, pvalue, formsemestre_id) pname}={pvalue} for {formsemestre}"""
) )
# 5- Copie les parcours # 5- Copie les parcours
@ -1350,10 +1366,10 @@ def do_formsemestre_clone(
# 6- Copy partitions and groups # 6- Copy partitions and groups
if clone_partitions: if clone_partitions:
sco_groups_copy.clone_partitions_and_groups( 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: def formsemestre_delete(formsemestre_id: int) -> str | flask.Response:

View File

@ -794,7 +794,7 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str:
<div class="sem-groups-partition-titre">{ <div class="sem-groups-partition-titre">{
'Groupes de ' + partition.partition_name 'Groupes de ' + partition.partition_name
if partition.partition_name else if partition.partition_name else
'Tous les étudiants'} ('aucun étudiant inscrit' if partition_is_empty else 'Tous les étudiants')}
</div> </div>
<div class="sem-groups-partition-titre">{ <div class="sem-groups-partition-titre">{
"Assiduité" if not partition_is_empty else "" "Assiduité" if not partition_is_empty else ""
@ -885,7 +885,7 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str:
) )
H.append("</div>") # /sem-groups-assi H.append("</div>") # /sem-groups-assi
if partition_is_empty: if partition_is_empty and not partition.is_default():
H.append( H.append(
'<div class="help sem-groups-none">Aucun groupe peuplé dans cette partition' '<div class="help sem-groups-none">Aucun groupe peuplé dans cette partition'
) )

View File

@ -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 """Converti une date saisie par un humain français avant 2070
en un objet datetime. en un objet datetime.
12/2/1972 => 1972-02-12, 12/2/72 => 1972-02-12, mais 12/2/24 => 2024-02-12 12/2/1972 => 1972-02-12, 12/2/72 => 1972-02-12, mais 12/2/24 => 2024-02-12
Le pivot est 70. Le pivot est 70.
ScoValueError si date invalide. ScoValueError si date invalide.
""" """
if isinstance(date_str, datetime.datetime):
return date_str
try: try:
return datetime.datetime.strptime(date_str, DATE_FMT) return datetime.datetime.strptime(date_str, DATE_FMT)
except ValueError: except ValueError:

View File

@ -30,7 +30,7 @@
from app.scodoc.sco_exceptions import ScoValueError 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)""" """Classe stockant le VDI avec le code étape (noms de fichiers maquettes et code semestres)"""
_ETAPE_VDI_SEP = "!" _ETAPE_VDI_SEP = "!"
@ -118,7 +118,7 @@ class ApoEtapeVDI(object):
else: else:
return etape_vdi, "" return etape_vdi, ""
def concat_etape_vdi(self, etape, vdi=""): def concat_etape_vdi(self, etape: str, vdi: str = "") -> str:
if vdi: if vdi:
return self._ETAPE_VDI_SEP.join([etape, vdi]) return self._ETAPE_VDI_SEP.join([etape, vdi])
else: else: