diff --git a/app/__init__.py b/app/__init__.py index 6fb6cf052..51e122cd5 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -550,3 +550,22 @@ def scodoc_flash_status_messages(): f"Mode test: mails redirigés vers {email_test_mode_address}", category="warning", ) + + +def critical_error(msg): + """Handle a critical error: flush all caches, display message to the user""" + import app.scodoc.sco_utils as scu + + log(f"\n*** CRITICAL ERROR: {msg}") + send_scodoc_alarm(f"CRITICAL ERROR: {msg}", msg) + clear_scodoc_cache() + raise ScoValueError( + f""" + Une erreur est survenue. + + Si le problème persiste, merci de contacter le support ScoDoc via + {scu.SCO_DISCORD_ASSISTANCE} + + {msg} + """ + ) diff --git a/app/api/partitions.py b/app/api/partitions.py index 699f18e71..42307656b 100644 --- a/app/api/partitions.py +++ b/app/api/partitions.py @@ -19,6 +19,7 @@ from app.models import FormSemestre, FormSemestreInscription, Identite from app.models import GroupDescr, Partition from app.models.groups import group_membership from app.scodoc import sco_cache +from app.scodoc import sco_groups from app.scodoc.sco_permissions import Permission from app.scodoc import sco_utils as scu @@ -170,24 +171,15 @@ def set_etud_group(etudid: int, group_id: int): query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id) ) group = query.first_or_404() + if not group.partition.formsemestre.etat: + return json_error(403, "formsemestre verrouillé") if etud.id not in {e.id for e in group.partition.formsemestre.etuds}: return json_error(404, "etud non inscrit au formsemestre du groupe") - groups = ( - GroupDescr.query.filter_by(partition_id=group.partition.id) - .join(group_membership) - .filter_by(etudid=etudid) + + sco_groups.change_etud_group_in_partition( + etudid, group_id, group.partition.to_dict() ) - ok = False - for other_group in groups: - if other_group.id == group_id: - ok = True - else: - other_group.etuds.remove(etud) - if not ok: - group.etuds.append(etud) - log(f"set_etud_group({etud}, {group})") - db.session.commit() - sco_cache.invalidate_formsemestre(group.partition.formsemestre_id) + return jsonify({"group_id": group_id, "etudid": etudid}) @@ -207,6 +199,8 @@ def group_remove_etud(group_id: int, etudid: int): query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id) ) group = query.first_or_404() + if not group.partition.formsemestre.etat: + return json_error(403, "formsemestre verrouillé") if etud in group.etuds: group.etuds.remove(etud) db.session.commit() @@ -232,6 +226,8 @@ def partition_remove_etud(partition_id: int, etudid: int): if g.scodoc_dept: query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id) partition = query.first_or_404() + if not partition.formsemestre.etat: + return json_error(403, "formsemestre verrouillé") groups = ( GroupDescr.query.filter_by(partition_id=partition_id) .join(group_membership) @@ -262,8 +258,10 @@ def group_create(partition_id: int): if g.scodoc_dept: query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id) partition: Partition = query.first_or_404() + if not partition.formsemestre.etat: + return json_error(403, "formsemestre verrouillé") if not partition.groups_editable: - return json_error(404, "partition non editable") + return json_error(403, "partition non editable") data = request.get_json(force=True) # may raise 400 Bad Request group_name = data.get("group_name") if group_name is None: @@ -294,8 +292,10 @@ def group_delete(group_id: int): query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id) ) group: GroupDescr = query.first_or_404() + if not group.partition.formsemestre.etat: + return json_error(403, "formsemestre verrouillé") if not group.partition.groups_editable: - return json_error(404, "partition non editable") + return json_error(403, "partition non editable") formsemestre_id = group.partition.formsemestre_id log(f"deleting {group}") db.session.delete(group) @@ -318,8 +318,10 @@ def group_edit(group_id: int): query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id) ) group: GroupDescr = query.first_or_404() + if not group.partition.formsemestre.etat: + return json_error(403, "formsemestre verrouillé") if not group.partition.groups_editable: - return json_error(404, "partition non editable") + return json_error(403, "partition non editable") data = request.get_json(force=True) # may raise 400 Bad Request group_name = data.get("group_name") if group_name is not None: @@ -358,6 +360,8 @@ def partition_create(formsemestre_id: int): if g.scodoc_dept: query = query.filter_by(dept_id=g.scodoc_dept_id) formsemestre: FormSemestre = query.first_or_404(formsemestre_id) + if not formsemestre.etat: + return json_error(403, "formsemestre verrouillé") data = request.get_json(force=True) # may raise 400 Bad Request partition_name = data.get("partition_name") if partition_name is None: @@ -406,6 +410,8 @@ def formsemestre_order_partitions(formsemestre_id: int): if g.scodoc_dept: query = query.filter_by(dept_id=g.scodoc_dept_id) formsemestre: FormSemestre = query.first_or_404(formsemestre_id) + if not formsemestre.etat: + return json_error(403, "formsemestre verrouillé") partition_ids = request.get_json(force=True) # may raise 400 Bad Request if not isinstance(partition_ids, int) and not all( isinstance(x, int) for x in partition_ids @@ -443,6 +449,8 @@ def partition_order_groups(partition_id: int): if g.scodoc_dept: query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id) partition: Partition = query.first_or_404() + if not partition.formsemestre.etat: + return json_error(403, "formsemestre verrouillé") group_ids = request.get_json(force=True) # may raise 400 Bad Request if not isinstance(group_ids, int) and not all( isinstance(x, int) for x in group_ids @@ -484,6 +492,8 @@ def partition_edit(partition_id: int): if g.scodoc_dept: query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id) partition: Partition = query.first_or_404() + if not partition.formsemestre.etat: + return json_error(403, "formsemestre verrouillé") data = request.get_json(force=True) # may raise 400 Bad Request modified = False partition_name = data.get("partition_name") @@ -542,6 +552,8 @@ def partition_delete(partition_id: int): if g.scodoc_dept: query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id) partition: Partition = query.first_or_404() + if not partition.formsemestre.etat: + return json_error(403, "formsemestre verrouillé") if not partition.partition_name: return json_error(404, "ne peut pas supprimer la partition par défaut") is_parcours = partition.is_parcours() diff --git a/app/but/cursus_but.py b/app/but/cursus_but.py index 682a03777..bfa97a703 100644 --- a/app/but/cursus_but.py +++ b/app/but/cursus_but.py @@ -102,7 +102,7 @@ class EtudCursusBUT: ) "Liste des inscriptions aux sem. de la formation, triées par indice et chronologie" self.parcour: ApcParcours = self.inscriptions[-1].parcour - "Le parcour à valider: celui du DERNIER semestre suivi (peut être None)" + "Le parcours à valider: celui du DERNIER semestre suivi (peut être None)" self.niveaux_by_annee = {} "{ annee : liste des niveaux à valider }" self.niveaux: dict[int, ApcNiveau] = {} @@ -142,7 +142,7 @@ class EtudCursusBUT: self.validation_par_competence_et_annee[niveau.competence.id] = {} previous_validation = self.validation_par_competence_et_annee.get( niveau.competence.id - ) + ).get(validation_rcue.annee()) # prend la "meilleure" validation if (not previous_validation) or ( sco_codes.BUT_CODES_ORDERED[validation_rcue.code] diff --git a/app/but/jury_but.py b/app/but/jury_but.py index ae0bfb4dd..244989edd 100644 --- a/app/but/jury_but.py +++ b/app/but/jury_but.py @@ -693,20 +693,20 @@ class DecisionsProposeesAnnee(DecisionsProposees): db.session.commit() - def record(self, code: str, no_overwrite=False): + def record(self, code: str, no_overwrite=False) -> bool: """Enregistre le code de l'année, et au besoin l'autorisation d'inscription. Si no_overwrite, ne fait rien si un code est déjà enregistré. Si l'étudiant est DEM ou DEF, ne fait rien. """ if self.inscription_etat != scu.INSCRIT: - return + return False if code and not code in self.codes: raise ScoValueError( f"code annee {html.escape(code)} invalide pour formsemestre {html.escape(self.formsemestre)}" ) if code == self.code_valide or (self.code_valide is not None and no_overwrite): self.recorded = True - return # no change + return False # no change if self.validation: db.session.delete(self.validation) db.session.commit() @@ -746,9 +746,10 @@ class DecisionsProposeesAnnee(DecisionsProposees): next_semestre_id, ) - self.recorded = True db.session.commit() + self.recorded = True self.invalidate_formsemestre_cache() + return True def invalidate_formsemestre_cache(self): "invalide le résultats des deux formsemestres" @@ -759,13 +760,20 @@ class DecisionsProposeesAnnee(DecisionsProposees): if self.formsemestre_pair is not None: sco_cache.invalidate_formsemestre(formsemestre_id=self.formsemestre_pair.id) - def record_all(self, no_overwrite: bool = True): + def record_all( + self, no_overwrite: bool = True, only_validantes: bool = False + ) -> bool: """Enregistre les codes qui n'ont pas été spécifiés par le formulaire, et sont donc en mode "automatique". - Si "à cheval", ne modifie pas les codes UE de l'année scolaire précédente. - Pour les RCUE: n'enregistre que si la nouvelle décision est plus favorable que l'ancienne. + + Si only_validantes, n'enregistre que des décisions "validantes" de droit: ADM ou CMP. + + Return: True si au moins un code modifié et enregistré. """ - # Toujours valider dans l'ordre UE, RCUE, Année: + modif = False + # Toujours valider dans l'ordre UE, RCUE, Année annee_scolaire = self.formsemestre.annee_scolaire() # UEs for dec_ue in self.decisions_ues.values(): @@ -774,25 +782,40 @@ class DecisionsProposeesAnnee(DecisionsProposees): ) and dec_ue.formsemestre.annee_scolaire() == annee_scolaire: # rappel: le code par défaut est en tête code = dec_ue.codes[0] if dec_ue.codes else None - # enregistre le code jury seulement s'il n'y a pas déjà de code - # (no_overwrite=True) sauf en mode test yaml - dec_ue.record(code, no_overwrite=no_overwrite) - # RCUE : enregistre seulement si pas déjà validé "mieux" + if (not only_validantes) or code in sco_codes.CODES_UE_VALIDES_DE_DROIT: + # enregistre le code jury seulement s'il n'y a pas déjà de code + # (no_overwrite=True) sauf en mode test yaml + modif |= dec_ue.record(code, no_overwrite=no_overwrite) + # RCUE : for dec_rcue in self.decisions_rcue_by_niveau.values(): code = dec_rcue.codes[0] if dec_rcue.codes else None - if (not dec_rcue.recorded) and ( - (not dec_rcue.validation) - or BUT_CODES_ORDERED.get(dec_rcue.validation.code, 0) - < BUT_CODES_ORDERED.get(code, 0) + if ( + (not dec_rcue.recorded) + and ( # enregistre seulement si pas déjà validé "mieux" + (not dec_rcue.validation) + or BUT_CODES_ORDERED.get(dec_rcue.validation.code, 0) + < BUT_CODES_ORDERED.get(code, 0) + ) + and ( # décision validante de droit ? + ( + (not only_validantes) + or code in sco_codes.CODES_RCUE_VALIDES_DE_DROIT + ) + ) ): - dec_rcue.record(code, no_overwrite=no_overwrite) + modif |= dec_rcue.record(code, no_overwrite=no_overwrite) # Année: if not self.recorded: # rappel: le code par défaut est en tête code = self.codes[0] if self.codes else None # enregistre le code jury seulement s'il n'y a pas déjà de code # (no_overwrite=True) sauf en mode test yaml - self.record(code, no_overwrite=no_overwrite) + if ( + not only_validantes + ) or code in sco_codes.CODES_ANNEE_BUT_VALIDES_DE_DROIT: + modif |= self.record(code, no_overwrite=no_overwrite) + + return modif def erase(self, only_one_sem=False): """Efface les décisions de jury de cet étudiant @@ -1005,23 +1028,23 @@ class DecisionsProposeesRCUE(DecisionsProposees): return f"""<{self.__class__.__name__} rcue={self.rcue} valid={self.code_valide } codes={self.codes} explanation={self.explanation}""" - def record(self, code: str, no_overwrite=False): + def record(self, code: str, no_overwrite=False) -> bool: """Enregistre le code RCUE. Note: - si le RCUE est ADJ, les UE non validées sont passées à ADJ XXX on pourra imposer ici d'autres règles de cohérence """ if self.rcue is None: - return # pas de RCUE a enregistrer + return False # pas de RCUE a enregistrer if self.inscription_etat != scu.INSCRIT: - return + return False if code and not code in self.codes: raise ScoValueError( f"code UE invalide pour ue_id={self.ue.id}: {html.escape(code)}" ) if code == self.code_valide or (self.code_valide is not None and no_overwrite): self.recorded = True - return # no change + return False # no change parcours_id = self.parcour.id if self.parcour is not None else None if self.validation: db.session.delete(self.validation) @@ -1057,7 +1080,7 @@ class DecisionsProposeesRCUE(DecisionsProposees): flash( f"""UEs du RCUE "{dec_ue.ue.niveau_competence.competence.titre}" passées en ADJR""" ) - dec_ue.record("ADJR") + dec_ue.record(sco_codes.ADJR) # Valide les niveaux inférieurs de la compétence (code ADSUP) # TODO @@ -1072,6 +1095,7 @@ class DecisionsProposeesRCUE(DecisionsProposees): ) self.code_valide = code # mise à jour état self.recorded = True + return True def erase(self): """Efface la décision de jury de cet étudiant pour cet RCUE""" @@ -1203,9 +1227,10 @@ class DecisionsProposeesUE(DecisionsProposees): self.codes = [sco_codes.AJ, sco_codes.ADJ] + self.codes self.explanation = "notes insuffisantes" - def record(self, code: str, no_overwrite=False): + def record(self, code: str, no_overwrite=False) -> bool: """Enregistre le code jury pour cette UE. Si no_overwrite, n'enregistre pas s'il y a déjà un code. + Return: True si code enregistré (modifié) """ if code and not code in self.codes: raise ScoValueError( @@ -1213,7 +1238,7 @@ class DecisionsProposeesUE(DecisionsProposees): ) if code == self.code_valide or (self.code_valide is not None and no_overwrite): self.recorded = True - return # no change + return False # no change self.erase() if code is None: self.validation = None @@ -1244,6 +1269,7 @@ class DecisionsProposeesUE(DecisionsProposees): sco_cache.invalidate_formsemestre(formsemestre_id=self.formsemestre.id) self.code_valide = code # mise à jour self.recorded = True + return True def erase(self): """Efface la décision de jury de cet étudiant pour cette UE""" diff --git a/app/but/jury_but_recap.py b/app/but/jury_but_recap.py index 386579575..809294408 100644 --- a/app/but/jury_but_recap.py +++ b/app/but/jury_but_recap.py @@ -284,6 +284,10 @@ class RowCollector: self["_nom_disp_order"] = etud.sort_key self.add_cell("prenom", "Prénom", etud.prenom, "identite_detail") self.add_cell("nom_short", "Nom", etud.nom_short, "identite_court") + self["_nom_short_data"] = { + "etudid": etud.id, + "nomprenom": etud.nomprenom, + } if with_links: self["_nom_short_order"] = etud.sort_key self["_nom_short_target"] = url_for( @@ -368,10 +372,6 @@ class RowCollector: + ((" " + scu.EMO_WARNING) if deca.nb_rcues_under_8 > 0 else ""), "col_rcue col_rcues_validables" + klass, ) - self["_rcues_validables_data"] = { - "etudid": deca.etud.id, - "nomprenom": deca.etud.nomprenom, - } if len(deca.rcues_annee) > 0: # permet un tri par nb de niveaux validables + moyenne gen indicative S_pair if deca.res_pair and deca.etud.id in deca.res_pair.etud_moy_gen: diff --git a/app/but/jury_but_validation_auto.py b/app/but/jury_but_validation_auto.py index d5e308ab9..674389b54 100644 --- a/app/but/jury_but_validation_auto.py +++ b/app/but/jury_but_validation_auto.py @@ -18,29 +18,29 @@ from app.scodoc.sco_exceptions import ScoValueError def formsemestre_validation_auto_but( formsemestre: FormSemestre, only_adm: bool = True, no_overwrite: bool = True ) -> int: - """Calcul automatique des décisions de jury sur une année BUT. - Ne modifie jamais de décisions de l'année scolaire précédente, même + """Calcul automatique des décisions de jury sur une "année" BUT. + + - N'enregistre jamais de décisions de l'année scolaire précédente, même si on a des RCUE "à cheval". - Normalement, only_adm est True et on n'enregistre que les décisions ADM (de droit). - Si only_adm est faux, on enregistre la première décision proposée par ScoDoc - (mode à n'utiliser que pour les tests) + - Ne modifie jamais de décisions déjà enregistrées (sauf si no_overwrite est faux, + ce qui est utilisé pour certains tests unitaires). + - Normalement, only_adm est True et on n'enregistre que les décisions validantes + de droit: ADM ou CMP. + En revanche, si only_adm est faux, on enregistre la première décision proposée par ScoDoc + (mode à n'utiliser que pour les tests unitaires vérifiant la saisie des jurys) - Si no_overwrite est vrai (défaut), ne ré-écrit jamais les codes déjà enregistrés - (utiliser faux pour certains tests) - - Returns: nombre d'étudiants "admis" + Returns: nombre d'étudiants pour lesquels on a enregistré au moins un code. """ if not formsemestre.formation.is_apc(): raise ScoValueError("fonction réservée aux formations BUT") - nb_admis = 0 + nb_etud_modif = 0 with sco_cache.DeferredSemCacheManager(): for etudid in formsemestre.etuds_inscriptions: etud: Identite = Identite.query.get(etudid) deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre) - if deca.admis: # année réussie - nb_admis += 1 - if deca.admis or not only_adm: - deca.record_all(no_overwrite=no_overwrite) + nb_etud_modif += deca.record_all( + no_overwrite=no_overwrite, only_validantes=only_adm + ) db.session.commit() - return nb_admis + return nb_etud_modif diff --git a/app/comp/moy_mod.py b/app/comp/moy_mod.py index dfced34cc..1fccbda15 100644 --- a/app/comp/moy_mod.py +++ b/app/comp/moy_mod.py @@ -39,6 +39,7 @@ from dataclasses import dataclass import numpy as np import pandas as pd +import app from app import db from app.models import Evaluation, EvaluationUEPoids, ModuleImpl from app.scodoc import sco_cache @@ -484,7 +485,8 @@ class ModuleImplResultsClassic(ModuleImplResults): if nb_etuds == 0: return pd.Series() evals_coefs = self.get_evaluations_coefs(modimpl).reshape(-1) - assert evals_coefs.shape == (nb_evals,) + if evals_coefs.shape != (nb_evals,): + app.critical_error("compute_module_moy: vals_coefs.shape != nb_evals") evals_notes_20 = self.get_eval_notes_sur_20(modimpl) # Les coefs des évals pour chaque étudiant: là où il a des notes # non neutralisées diff --git a/app/comp/res_common.py b/app/comp/res_common.py index e43b1a8c6..820675fa8 100644 --- a/app/comp/res_common.py +++ b/app/comp/res_common.py @@ -553,7 +553,11 @@ class ResultatsSemestre(ResultatsCache): "Nom", etud.nom_short, "identite_court", - data={"order": etud.sort_key}, + data={ + "order": etud.sort_key, + "etudid": etud.id, + "nomprenom": etud.nomprenom, + }, target=url_bulletin, target_attrs=f'class="etudinfo" id="{etudid}"', ) diff --git a/app/models/but_validations.py b/app/models/but_validations.py index 0f3e239e7..ccef89cd4 100644 --- a/app/models/but_validations.py +++ b/app/models/but_validations.py @@ -71,6 +71,11 @@ class ApcValidationRCUE(db.Model): enregistrée le {self.date.strftime("%d/%m/%Y")} à {self.date.strftime("%Hh%M")}""" + def annee(self) -> str: + """l'année BUT concernée: "BUT1", "BUT2" ou "BUT3" """ + niveau = self.niveau() + return niveau.annee if niveau else None + def niveau(self) -> ApcNiveau: """Le niveau de compétence associé à cet RCUE.""" # Par convention, il est donné par la seconde UE diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index 3c457d980..bafad116e 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -63,51 +63,51 @@ class FormSemestre(db.Model): "False si verrouillé" modalite = db.Column( db.String(SHORT_STR_LEN), db.ForeignKey("notes_form_modalites.modalite") - ) # "FI", "FAP", "FC", ... - # gestion compensation sem DUT: + ) + "Modalité de formation: 'FI', 'FAP', 'FC', ..." gestion_compensation = db.Column( db.Boolean(), nullable=False, default=False, server_default="false" ) - # ne publie pas le bulletin XML ou JSON: + "gestion compensation sem DUT (inutilisé en APC)" bul_hide_xml = db.Column( db.Boolean(), nullable=False, default=False, server_default="false" ) - # Bloque le calcul des moyennes (générale et d'UE) + "ne publie pas le bulletin XML ou JSON" block_moyennes = db.Column( db.Boolean(), nullable=False, default=False, server_default="false" ) - # Bloque le calcul de la moyenne générale (utile pour BUT) + "Bloque le calcul des moyennes (générale et d'UE)" block_moyenne_generale = db.Column( db.Boolean(), nullable=False, default=False, server_default="false" ) "Si vrai, la moyenne générale indicative BUT n'est pas calculée" - # semestres decales (pour gestion jurys): gestion_semestrielle = db.Column( db.Boolean(), nullable=False, default=False, server_default="false" ) - # couleur fond bulletins HTML: + "Semestres décalés (pour gestion jurys DUT, pas implémenté ou utile en BUT)" bul_bgcolor = db.Column( db.String(SHORT_STR_LEN), default="white", server_default="white", nullable=False, ) - # autorise resp. a modifier semestre: + "couleur fond bulletins HTML" resp_can_edit = db.Column( db.Boolean(), nullable=False, default=False, server_default="false" ) - # autorise resp. a modifier slt les enseignants: + "autorise resp. à modifier le formsemestre" resp_can_change_ens = db.Column( db.Boolean(), nullable=False, default=True, server_default="true" ) - # autorise les ens a creer des evals: + "autorise resp. a modifier slt les enseignants" ens_can_edit_eval = db.Column( db.Boolean(), nullable=False, default=False, server_default="False" ) - # code element semestre Apogee, eg 'VRTW1' ou 'V2INCS4,V2INLS4,...' + "autorise les enseignants à créer des évals dans leurs modimpls" elt_sem_apo = db.Column(db.Text()) # peut être fort long ! - # code element annee Apogee, eg 'VRT1A' ou 'V2INLA,V2INCA,...' + "code element semestre Apogee, eg 'VRTW1' ou 'V2INCS4,V2INLS4,...'" elt_annee_apo = db.Column(db.Text()) + "code element annee Apogee, eg 'VRT1A' ou 'V2INLA,V2INCA,...'" # Relations: etapes = db.relationship( diff --git a/app/models/groups.py b/app/models/groups.py index 4fd25a940..6fafa234b 100644 --- a/app/models/groups.py +++ b/app/models/groups.py @@ -87,6 +87,7 @@ class Partition(db.Model): def to_dict(self, with_groups=False) -> dict: """as a dict, with or without groups""" d = dict(self.__dict__) + d["partition_id"] = self.id d.pop("_sa_instance_state", None) d.pop("formsemestre", None) diff --git a/app/models/ues.py b/app/models/ues.py index 387fc8d28..94954b93d 100644 --- a/app/models/ues.py +++ b/app/models/ues.py @@ -219,6 +219,8 @@ class UniteEns(db.Model): db.session.add(self) db.session.commit() + # Invalidation du cache + self.formation.invalidate_cached_sems() log(f"ue.set_niveau_competence( {self}, {niveau} )") def set_parcour(self, parcour: ApcParcours): @@ -246,6 +248,8 @@ class UniteEns(db.Model): self.niveau_competence = None db.session.add(self) db.session.commit() + # Invalidation du cache + self.formation.invalidate_cached_sems() log(f"ue.set_parcour( {self}, {parcour} )") diff --git a/app/pe/pe_jurype.py b/app/pe/pe_jurype.py index 7b0c79970..502027529 100644 --- a/app/pe/pe_jurype.py +++ b/app/pe/pe_jurype.py @@ -459,8 +459,7 @@ class JuryPE(object): etud = self.get_cache_etudInfo_d_un_etudiant(etudid) (_, parcours) = sco_report.get_codeparcoursetud(etud) if ( - len(set(sco_codes_parcours.CODES_SEM_REO.keys()) & set(parcours.values())) - > 0 + len(sco_codes_parcours.CODES_SEM_REO & set(parcours.values())) > 0 ): # Eliminé car NAR apparait dans le parcours reponse = True if pe_tools.PE_DEBUG and pe_tools.PE_DEBUG >= 2: @@ -563,9 +562,8 @@ class JuryPE(object): dec = nt.get_etud_decision_sem( etudid ) # quelle est la décision du jury ? - if dec and dec["code"] in list( - sco_codes_parcours.CODES_SEM_VALIDES.keys() - ): # isinstance( sesMoyennes[i+1], float) and + if dec and (dec["code"] in sco_codes_parcours.CODES_SEM_VALIDES): + # isinstance( sesMoyennes[i+1], float) and # mT = sesMoyennes[i+1] # substitue la moyenne si le semestre suivant est "valide" leFid = sem["formsemestre_id"] else: diff --git a/app/scodoc/sco_codes_parcours.py b/app/scodoc/sco_codes_parcours.py index bbaa61b8f..df5484fb0 100644 --- a/app/scodoc/sco_codes_parcours.py +++ b/app/scodoc/sco_codes_parcours.py @@ -187,20 +187,23 @@ CODES_EXPL = { # Les codes de semestres: CODES_JURY_SEM = {ADC, ADJ, ADM, AJ, ATB, ATJ, ATT, DEF, NAR, RAT} -CODES_SEM_VALIDES = {ADM: True, ADC: True, ADJ: True} # semestre validé -CODES_SEM_ATTENTES = {ATT: True, ATB: True, ATJ: True} # semestre en attente +CODES_SEM_VALIDES_DE_DROIT = {ADM, ADC} +CODES_SEM_VALIDES = CODES_SEM_VALIDES_DE_DROIT | {ADJ} # semestre validé +CODES_SEM_ATTENTES = {ATT, ATB, ATJ} # semestre en attente -CODES_SEM_REO = {NAR: 1} # reorientation +CODES_SEM_REO = {NAR} # reorientation -CODES_UE_VALIDES = {ADM: True, CMP: True, ADJ: True, ADJR: True} +CODES_UE_VALIDES_DE_DROIT = {ADM, CMP} # validation "de droit" +CODES_UE_VALIDES = CODES_UE_VALIDES_DE_DROIT | {ADJ, ADJR} "UE validée" -CODES_RCUE_VALIDES = {ADM, CMP, ADJ} +CODES_RCUE_VALIDES_DE_DROIT = {ADM, CMP} +CODES_RCUE_VALIDES = CODES_RCUE_VALIDES_DE_DROIT | {ADJ} "Niveau RCUE validé" # Pour le BUT: +CODES_ANNEE_BUT_VALIDES_DE_DROIT = {ADM, PASD} CODES_ANNEE_ARRET = {DEF, DEM, ABAN, ABL} -CODES_RCUE = {ADM, AJ, CMP} BUT_BARRE_UE8 = 8.0 - NOTES_TOLERANCE BUT_BARRE_UE = BUT_BARRE_RCUE = 10.0 - NOTES_TOLERANCE BUT_RCUE_SUFFISANT = 8.0 - NOTES_TOLERANCE @@ -230,17 +233,17 @@ BUT_CODES_ORDERED = { def code_semestre_validant(code: str) -> bool: "Vrai si ce CODE entraine la validation du semestre" - return CODES_SEM_VALIDES.get(code, False) + return code in CODES_SEM_VALIDES def code_semestre_attente(code: str) -> bool: "Vrai si ce CODE est un code d'attente (semestre validable plus tard par jury ou compensation)" - return CODES_SEM_ATTENTES.get(code, False) + return code in CODES_SEM_ATTENTES def code_ue_validant(code: str) -> bool: "Vrai si ce code d'UE est validant (ie attribue les ECTS)" - return CODES_UE_VALIDES.get(code, False) + return code in CODES_UE_VALIDES DEVENIR_EXPL = { diff --git a/app/scodoc/sco_cursus_dut.py b/app/scodoc/sco_cursus_dut.py index 3474081ff..0549f6677 100644 --- a/app/scodoc/sco_cursus_dut.py +++ b/app/scodoc/sco_cursus_dut.py @@ -890,7 +890,7 @@ def formsemestre_validate_ues(formsemestre_id, etudid, code_etat_sem, assiduite) car ils ne dépendent que de la note d'UE et de la validation ou non du semestre. Les UE des semestres NON ASSIDUS ne sont jamais validées (code AJ). """ - valid_semestre = CODES_SEM_VALIDES.get(code_etat_sem, False) + valid_semestre = code_etat_sem in CODES_SEM_VALIDES cnx = ndb.GetDBConnexion() formsemestre = FormSemestre.query.get_or_404(formsemestre_id) nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) diff --git a/app/scodoc/sco_formsemestre_edit.py b/app/scodoc/sco_formsemestre_edit.py index 399cf05db..a584aa08e 100644 --- a/app/scodoc/sco_formsemestre_edit.py +++ b/app/scodoc/sco_formsemestre_edit.py @@ -1185,7 +1185,10 @@ def do_formsemestre_clone( """Clone a semestre: make copy, same modules, same options, same resps, same partitions. New dates, responsable_id """ - log("cloning %s" % orig_formsemestre_id) + log(f"cloning orig_formsemestre_id") + 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 @@ -1196,7 +1199,8 @@ def do_formsemestre_clone( args["date_fin"] = date_fin args["etat"] = 1 # non verrouillé formsemestre_id = sco_formsemestre.do_formsemestre_create(args) - log("created formsemestre %s" % formsemestre_id) + log(f"created formsemestre {formsemestre_id}") + formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id) # 2- create moduleimpls mods_orig = sco_moduleimpl.moduleimpl_list(formsemestre_id=orig_formsemestre_id) for mod_orig in mods_orig: @@ -1258,7 +1262,12 @@ def do_formsemestre_clone( args["formsemestre_id"] = formsemestre_id _ = sco_compute_moy.formsemestre_ue_computation_expr_create(cnx, args) - # 5- Copy partitions and groups + # 6- Copie les parcours + formsemestre.parcours = formsemestre_orig.parcours + db.session.add(formsemestre) + db.session.commit() + + # 7- Copy partitions and groups if clone_partitions: sco_groups_copy.clone_partitions_and_groups( orig_formsemestre_id, formsemestre_id diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py index 11ba8c80a..c3ab96908 100644 --- a/app/scodoc/sco_formsemestre_status.py +++ b/app/scodoc/sco_formsemestre_status.py @@ -643,12 +643,12 @@ def formsemestre_description_table( titles = {title: title for title in columns_ids} titles.update({f"ue_{ue.id}": ue.acronyme for ue in ues}) titles["ects"] = "ECTS" - titles["jour"] = "Evaluation" + titles["jour"] = "Évaluation" titles["description"] = "" titles["coefficient"] = "Coef. éval." titles["evalcomplete_str"] = "Complète" titles["parcours"] = "Parcours" - titles["publish_incomplete_str"] = "Toujours Utilisée" + titles["publish_incomplete_str"] = "Toujours utilisée" title = f"{parcours.SESSION_NAME.capitalize()} {formsemestre.titre_mois()}" R = [] @@ -727,6 +727,8 @@ def formsemestre_description_table( evals.reverse() # ordre chronologique # Ajoute etat: for e in evals: + e["_jour_order"] = e["jour"].isoformat() + e["jour"] = e["jour"].strftime("%d/%m/%Y") if e["jour"] else "" e["UE"] = l["UE"] e["_UE_td_attrs"] = l["_UE_td_attrs"] e["Code"] = l["Code"] diff --git a/app/scodoc/sco_formsemestre_validation.py b/app/scodoc/sco_formsemestre_validation.py index 2af4b69f3..25b47df3f 100644 --- a/app/scodoc/sco_formsemestre_validation.py +++ b/app/scodoc/sco_formsemestre_validation.py @@ -781,8 +781,8 @@ def form_decision_manuelle(Se, formsemestre_id, etudid, desturl="", sortcol=None ) # Choix code semestre: - codes = list(sco_codes_parcours.CODES_JURY_SEM) - codes.sort() # fortuitement, cet ordre convient bien ! + codes = sorted(sco_codes_parcours.CODES_JURY_SEM) + # fortuitement, cet ordre convient bien ! H.append( '
+ En conséquence, saisir ensuite manuellement les décisions manquantes, + notamment sur les UEs en dessous de 10. +
Il est nécessaire de relire soigneusement les décisions à l'issue de cette procédure !
diff --git a/app/views/notes.py b/app/views/notes.py index 4ce3eb57d..2e20e2830 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -2350,7 +2350,7 @@ def formsemestre_validation_but( etud: Identite = Identite.query.filter_by( id=etudid, dept_id=g.scodoc_dept_id ).first_or_404() - + nb_etuds = formsemestre.etuds.count() # la route ne donne pas le type d'etudid pour pouvoir construire des URLs # provisoires avec NEXT et PREV try: @@ -2360,16 +2360,24 @@ def formsemestre_validation_but( read_only = not sco_permissions_check.can_validate_sem(formsemestre_id) # --- Navigation - prev_lnk = f"""{scu.EMO_PREV_ARROW} précédent """ - next_lnk = f"""suivant {scu.EMO_NEXT_ARROW} """ + if nb_etuds > 1 + else "" + ) navigation_div = f"""