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: