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.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 <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:
"""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)

View File

@ -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

View File

@ -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"""<ul>
<li><a class="stdlink" href="{
<li><b><a class="stdlink" href="{
url_for('notes.formsemestre_createwithmodules', scodoc_dept=g.scodoc_dept,
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>
</ul>"""
)

View File

@ -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"]:

View File

@ -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:

View File

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

View File

@ -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: