From 05953bd96da8f78894841425026f10b902ad4e10 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 22 Jan 2023 17:09:26 -0300 Subject: [PATCH 01/15] Fix #572: Affichage date dans table Description du semestre --- app/scodoc/sco_formsemestre_status.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py index 11ba8c80..c3ab9690 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"] From a466486766d47718dbe5213580e29e0680049fab Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 22 Jan 2023 17:27:37 -0300 Subject: [PATCH 02/15] Fix #568: affichage cursus --- app/but/cursus_but.py | 2 +- app/scodoc/sco_page_etud.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/but/cursus_but.py b/app/but/cursus_but.py index 7f5c00b8..2e18d955 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] = {} diff --git a/app/scodoc/sco_page_etud.py b/app/scodoc/sco_page_etud.py index de4ed6f3..9b843d4d 100644 --- a/app/scodoc/sco_page_etud.py +++ b/app/scodoc/sco_page_etud.py @@ -459,7 +459,7 @@ def ficheEtud(etudid=None): # XXX dev info["but_cursus_mkup"] = "" if info["sems"]: - last_sem = FormSemestre.query.get_or_404(info["sems"][-1]["formsemestre_id"]) + last_sem = FormSemestre.query.get_or_404(info["sems"][0]["formsemestre_id"]) if last_sem.formation.is_apc(): but_cursus = cursus_but.EtudCursusBUT(etud, last_sem.formation) info["but_cursus_mkup"] = render_template( From ccc9e1317dbe71599821ea359851dbeb45aadcdd Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 22 Jan 2023 17:44:57 -0300 Subject: [PATCH 03/15] Fix #569 front: ADJR --- app/but/jury_but.py | 2 +- app/static/js/jury_but.js | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/but/jury_but.py b/app/but/jury_but.py index ae0bfb4d..6ef89a37 100644 --- a/app/but/jury_but.py +++ b/app/but/jury_but.py @@ -1057,7 +1057,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 diff --git a/app/static/js/jury_but.js b/app/static/js/jury_but.js index 7c881c44..909a76d9 100644 --- a/app/static/js/jury_but.js +++ b/app/static/js/jury_but.js @@ -26,8 +26,10 @@ function change_menu_code(elt) { let ue_selects = elt.parentElement.parentElement.parentElement.querySelectorAll( "select.ue_rcue_" + elt.dataset.niveau_id); ue_selects.forEach(select => { - select.value = "ADJR"; - change_menu_code(select); // pour changer les styles + if (select.value != "ADM") { + select.value = "ADJR"; + change_menu_code(select); // pour changer les styles + } }); } } From 5fdc7db32a4f21668ea5a1e109a9b5e36621c7c3 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 22 Jan 2023 18:15:56 -0300 Subject: [PATCH 04/15] Fix #573 (API set group) --- app/api/partitions.py | 20 +++++--------------- app/models/groups.py | 1 + 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/app/api/partitions.py b/app/api/partitions.py index 699f18e7..0b297871 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 @@ -172,22 +173,11 @@ def set_etud_group(etudid: int, group_id: int): group = query.first_or_404() 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}) diff --git a/app/models/groups.py b/app/models/groups.py index 4fd25a94..6fafa234 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) From 165dac04963e1ca5ba1d70ba34cdc5f4f2d5b3cf Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 23 Jan 2023 07:05:06 -0300 Subject: [PATCH 05/15] Fix: missing import --- app/scodoc/sco_moduleimpl_inscriptions.py | 1 + sco_version.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/scodoc/sco_moduleimpl_inscriptions.py b/app/scodoc/sco_moduleimpl_inscriptions.py index ecbcc90b..bf330adb 100644 --- a/app/scodoc/sco_moduleimpl_inscriptions.py +++ b/app/scodoc/sco_moduleimpl_inscriptions.py @@ -41,6 +41,7 @@ from app import log from app.scodoc.scolog import logdb from app.scodoc import html_sco_header from app.scodoc import htmlutils +from app.scodoc import sco_cache from app.scodoc import sco_codes_parcours from app.scodoc import sco_edit_module from app.scodoc import sco_edit_ue diff --git a/sco_version.py b/sco_version.py index d01a292e..2a68efad 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.4.29" +SCOVERSION = "9.4.30" SCONAME = "ScoDoc" From d3248a37ad385bb4f4d9df41df696e6426a434a0 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 23 Jan 2023 07:38:47 -0300 Subject: [PATCH 06/15] =?UTF-8?q?Saisie=20automatique=20des=20d=C3=A9cisio?= =?UTF-8?q?ns=20de=20jury=20BUT=20pour=20semestres=20pairs=20ou=20impairs.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/but/jury_but.py | 70 +++++++++++++------ app/but/jury_but_validation_auto.py | 30 ++++---- app/pe/pe_jurype.py | 5 +- app/scodoc/sco_codes_parcours.py | 21 +++--- app/scodoc/sco_cursus_dut.py | 2 +- app/scodoc/sco_formsemestre_validation.py | 4 +- .../but/formsemestre_validation_auto_but.html | 18 +++-- app/views/notes.py | 18 +++-- 8 files changed, 107 insertions(+), 61 deletions(-) diff --git a/app/but/jury_but.py b/app/but/jury_but.py index 6ef89a37..244989ed 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) @@ -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_validation_auto.py b/app/but/jury_but_validation_auto.py index d5e308ab..674389b5 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/pe/pe_jurype.py b/app/pe/pe_jurype.py index 7b0c7997..4aed6fc6 100644 --- a/app/pe/pe_jurype.py +++ b/app/pe/pe_jurype.py @@ -563,9 +563,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 bbaa61b8..df5484fb 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 3474081f..0549f667 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_validation.py b/app/scodoc/sco_formsemestre_validation.py index 2af4b69f..25b47df3 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( 'Code semestre: