From 03faa901770d4c48567866a2e1303c31302d40b1 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 16 Jan 2023 15:53:31 -0300 Subject: [PATCH 01/28] =?UTF-8?q?Fix:=20list=5Fbut=5Fue=5Finscriptions=20s?= =?UTF-8?q?i=20aucun=20=C3=A9tudiant.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/scodoc/sco_moduleimpl_inscriptions.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/scodoc/sco_moduleimpl_inscriptions.py b/app/scodoc/sco_moduleimpl_inscriptions.py index 101de24d9..ecbcc90b3 100644 --- a/app/scodoc/sco_moduleimpl_inscriptions.py +++ b/app/scodoc/sco_moduleimpl_inscriptions.py @@ -505,7 +505,11 @@ def _list_but_ue_inscriptions(res: NotesTableCompat, read_only: bool = True) -> """ ] table_inscr = _table_but_ue_inscriptions(res) - ue_ids = set.union(*(set(x.keys()) for x in table_inscr.values())) + ue_ids = ( + set.union(*(set(x.keys()) for x in table_inscr.values())) + if table_inscr + else set() + ) ues = sorted( (UniteEns.query.get(ue_id) for ue_id in ue_ids), key=lambda u: (u.numero or 0, u.acronyme), From cada9c24d0f3b5ca7f225898b2f256ecd5f796c6 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 16 Jan 2023 21:05:48 -0300 Subject: [PATCH 02/28] WIP: Affichage validation cursus BUT sur page etudiant. --- app/but/cursus_but.py | 117 ++++++++++++++++++++++++++++- app/models/but_refcomp.py | 57 ++++++++++++-- app/models/but_validations.py | 6 ++ app/scodoc/sco_page_etud.py | 42 ++++++++--- app/static/css/cursus_but.css | 42 +++++++++++ app/static/css/ref-competences.css | 75 ++++++++++++------ app/templates/but/cursus_etud.j2 | 26 +++++++ scodoc.py | 19 ++++- 8 files changed, 336 insertions(+), 48 deletions(-) create mode 100644 app/static/css/cursus_but.css create mode 100644 app/templates/but/cursus_etud.j2 diff --git a/app/but/cursus_but.py b/app/but/cursus_but.py index 15ac1fa37..9ae1671ab 100644 --- a/app/but/cursus_but.py +++ b/app/but/cursus_but.py @@ -13,7 +13,7 @@ Classe raccordant avec ScoDoc 7: avec la même interface. """ - +import collections from typing import Union from flask import g, url_for @@ -47,12 +47,14 @@ from app.models.validations import ScolarFormSemestreValidation from app.scodoc import sco_codes_parcours as sco_codes from app.scodoc.sco_codes_parcours import RED, UE_STANDARD from app.scodoc import sco_utils as scu -from app.scodoc.sco_exceptions import ScoException, ScoValueError +from app.scodoc.sco_exceptions import ScoNoReferentielCompetences, ScoValueError from app.scodoc import sco_cursus_dut class SituationEtudCursusBUT(sco_cursus_dut.SituationEtudCursusClassic): + """Pour compat ScoDoc 7: à revoir pour le BUT""" + def __init__(self, etud: dict, formsemestre_id: int, res: ResultatsSemestreBUT): super().__init__(etud, formsemestre_id, res) # Ajustements pour le BUT @@ -65,3 +67,114 @@ class SituationEtudCursusBUT(sco_cursus_dut.SituationEtudCursusClassic): def parcours_validated(self): "True si le parcours est validé" return False # XXX TODO + + +class EtudCursusBUT: + """L'état de l'étudiant dans son cursus BUT + Liste des niveaux validés/à valider + """ + + def __init__(self, etud: Identite, formation: Formation): + """formation indique la spécialité préparée""" + # Vérifie que l'étudiant est bien inscrit à un sem. de cette formation + if formation.id not in ( + ins.formsemestre.formation.id for ins in etud.formsemestre_inscriptions + ): + raise ScoValueError( + f"{etud.nomprenom} non inscrit dans {formation.titre} v{formation.version}" + ) + if not formation.referentiel_competence: + raise ScoNoReferentielCompetences(formation=formation) + # + self.etud = etud + self.formation = formation + self.inscriptions = sorted( + [ + ins + for ins in etud.formsemestre_inscriptions + if ins.formsemestre.formation.referentiel_competence.id + == formation.referentiel_competence.id + ], + key=lambda s: (s.formsemestre.semestre_id, s.formsemestre.date_debut), + ) + "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)" + self.niveaux_by_annee = {} + "{ annee : liste des niveaux à valider }" + self.niveaux: dict[int, ApcNiveau] = {} + "cache les niveaux" + for annee in (1, 2, 3): + niveaux_d = formation.referentiel_competence.get_niveaux_by_parcours( + annee, self.parcour + )[1] + # groupe les niveaux de tronc commun et ceux spécifiques au parcour + self.niveaux_by_annee[annee] = niveaux_d["TC"] + ( + niveaux_d[self.parcour.id] if self.parcour else [] + ) + self.niveaux.update( + {niveau.id: niveau for niveau in self.niveaux_by_annee[annee]} + ) + # Probablement inutile: + # # Cherche les validations de jury enregistrées pour chaque niveau + # self.validations_by_niveau = collections.defaultdict(lambda: []) + # " { niveau_id : [ ApcValidationRCUE ] }" + # for validation_rcue in ApcValidationRCUE.query.filter_by(etud=etud): + # self.validations_by_niveau[validation_rcue.niveau().id].append( + # validation_rcue + # ) + # self.validation_by_niveau = { + # niveau_id: sorted( + # validations, key=lambda v: sco_codes.BUT_CODES_ORDERED[v.code] + # )[0] + # for niveau_id, validations in self.validations_by_niveau.items() + # } + # "{ niveau_id : meilleure validation pour ce niveau }" + + self.validation_par_competence_et_annee = {} + "{ competence_id : { 'BUT1' : validation_rcue, ... } }" + for validation_rcue in ApcValidationRCUE.query.filter_by(etud=etud): + niveau = validation_rcue.niveau() + if not niveau.competence.id in self.validation_par_competence_et_annee: + self.validation_par_competence_et_annee[niveau.competence.id] = {} + previous_validation = self.validation_par_competence_et_annee.get( + niveau.competence.id + ) + # prend la "meilleure" validation + if (not previous_validation) or ( + sco_codes.BUT_CODES_ORDERED[validation_rcue.code] + > sco_codes.BUT_CODES_ORDERED[previous_validation.code] + ): + self.validation_par_competence_et_annee[niveau.competence.id][ + niveau.annee + ] = validation_rcue + + self.competences = { + competence.id: competence + for competence in ( + self.parcour.query_competences() + if self.parcour + else self.formation.referentiel_competence.get_competences_tronc_commun() + ) + } + "cache { competence_id : competence }" + + def to_dict(self): + """ + { + competence_id : { + annee : meilleure_validation + } + } + """ + return { + competence.id: { + annee: { + self.validation_par_competence_et_annee.get(competence.id, {}).get( + annee + ) + } + for annee in ("BUT1", "BUT2", "BUT3") + } + for competence in self.competences.values() + } diff --git a/app/models/but_refcomp.py b/app/models/but_refcomp.py index 647e6ee89..425ff1923 100644 --- a/app/models/but_refcomp.py +++ b/app/models/but_refcomp.py @@ -94,9 +94,10 @@ class ApcReferentielCompetences(db.Model, XMLModel): return "" return self.version_orebut.split()[0] - def to_dict(self): + def to_dict(self, parcours: list["ApcParcours"] = None, with_app_critiques=True): """Représentation complète du ref. de comp. comme un dict. + Si parcours est une liste de parcours, restreint l'export aux parcours listés. """ return { "dept_id": self.dept_id, @@ -111,8 +112,14 @@ class ApcReferentielCompetences(db.Model, XMLModel): if self.scodoc_date_loaded else "", "scodoc_orig_filename": self.scodoc_orig_filename, - "competences": {x.titre: x.to_dict() for x in self.competences}, - "parcours": {x.code: x.to_dict() for x in self.parcours}, + "competences": { + x.titre: x.to_dict(with_app_critiques=with_app_critiques) + for x in self.competences + }, + "parcours": { + x.code: x.to_dict() + for x in (self.parcours if parcours is None else parcours) + }, } def get_niveaux_by_parcours( @@ -174,6 +181,27 @@ class ApcReferentielCompetences(db.Model, XMLModel): niveaux_by_parcours_no_tc["TC"] = niveaux_tc return parcours, niveaux_by_parcours_no_tc + def get_competences_tronc_commun(self) -> list["ApcCompetence"]: + """Liste des compétences communes à tous les parcours du référentiel.""" + parcours = self.parcours.all() + if not parcours: + return [] + + ids = set.intersection( + *[ + {competence.id for competence in parcour.query_competences()} + for parcour in parcours + ] + ) + return sorted( + [ + competence + for competence in parcours[0].query_competences() + if competence.id in ids + ], + key=lambda c: c.numero or 0, + ) + class ApcCompetence(db.Model, XMLModel): "Compétence" @@ -215,7 +243,7 @@ class ApcCompetence(db.Model, XMLModel): def __repr__(self): return f"" - def to_dict(self): + def to_dict(self, with_app_critiques=True): "repr dict recursive sur situations, composantes, niveaux" return { "id_orebut": self.id_orebut, @@ -227,7 +255,10 @@ class ApcCompetence(db.Model, XMLModel): "composantes_essentielles": [ x.to_dict() for x in self.composantes_essentielles ], - "niveaux": {x.annee: x.to_dict() for x in self.niveaux}, + "niveaux": { + x.annee: x.to_dict(with_app_critiques=with_app_critiques) + for x in self.niveaux + }, } def to_dict_bul(self) -> dict: @@ -293,13 +324,15 @@ class ApcNiveau(db.Model, XMLModel): return f"""<{self.__class__.__name__} {self.id} ordre={self.ordre!r} annee={ self.annee!r} {self.competence!r}>""" - def to_dict(self): - "as a dict, recursif sur les AC" + def to_dict(self, with_app_critiques=True): + "as a dict, recursif (ou non) sur les AC" return { "libelle": self.libelle, "annee": self.annee, "ordre": self.ordre, - "app_critiques": {x.code: x.to_dict() for x in self.app_critiques}, + "app_critiques": {x.code: x.to_dict() for x in self.app_critiques} + if with_app_critiques + else {}, } def to_dict_bul(self): @@ -471,6 +504,14 @@ class ApcParcours(db.Model, XMLModel): d["annees"] = {x.ordre: x.to_dict() for x in self.annees} return d + def query_competences(self) -> flask_sqlalchemy.BaseQuery: + "Les compétences associées à ce parcours" + return ( + ApcCompetence.query.join(ApcParcoursNiveauCompetence, ApcAnneeParcours) + .filter_by(parcours_id=self.id) + .order_by(ApcCompetence.numero) + ) + class ApcAnneeParcours(db.Model, XMLModel): id = db.Column(db.Integer, primary_key=True) diff --git a/app/models/but_validations.py b/app/models/but_validations.py index 52826003e..0f3e239e7 100644 --- a/app/models/but_validations.py +++ b/app/models/but_validations.py @@ -76,6 +76,12 @@ class ApcValidationRCUE(db.Model): # Par convention, il est donné par la seconde UE return self.ue2.niveau_competence + def to_dict(self): + "as a dict" + d = dict(self.__dict__) + d.pop("_sa_instance_state", None) + return d + def to_dict_bul(self) -> dict: "Export dict pour bulletins: le code et le niveau de compétence" niveau = self.niveau() diff --git a/app/scodoc/sco_page_etud.py b/app/scodoc/sco_page_etud.py index 55bfa0e92..7115d21b0 100644 --- a/app/scodoc/sco_page_etud.py +++ b/app/scodoc/sco_page_etud.py @@ -30,14 +30,15 @@ Fiche description d'un étudiant et de son parcours """ -from flask import abort, url_for, g, request +from flask import abort, url_for, g, render_template, request from flask_login import current_user import app.scodoc.sco_utils as scu import app.scodoc.notesdb as ndb from app import log -from app.but import jury_but_view -from app.models.etudiants import make_etud_args +from app.but import cursus_but, jury_but_view +from app.models.etudiants import Identite, make_etud_args +from app.models.formsemestre import FormSemestre from app.scodoc import html_sco_header from app.scodoc import htmlutils from app.scodoc import sco_archives_etud @@ -169,11 +170,12 @@ def ficheEtud(etudid=None): if not etuds: log(f"ficheEtud: etudid={etudid!r} request.args={request.args!r}") raise ScoValueError("Étudiant inexistant !") - etud = etuds[0] - etudid = etud["etudid"] - sco_etud.fill_etuds_info([etud]) + etud_ = etuds[0] # transition: etud_ à éliminer et remplacer par etud + etudid = etud_["etudid"] + etud = Identite.query.get(etudid) + sco_etud.fill_etuds_info([etud_]) # - info = etud + info = etud_ info["ScoURL"] = scu.ScoURL() info["authuser"] = authuser info["info_naissance"] = info["date_naissance"] @@ -181,7 +183,7 @@ def ficheEtud(etudid=None): info["info_naissance"] += " à " + info["lieu_naissance"] if info["dept_naissance"]: info["info_naissance"] += f" ({info['dept_naissance']})" - info["etudfoto"] = sco_photos.etud_photo_html(etud) + info["etudfoto"] = sco_photos.etud_photo_html(etud_) if ( (not info["domicile"]) and (not info["codepostaldomicile"]) @@ -206,7 +208,7 @@ def ficheEtud(etudid=None): info["emaillink"] = ", ".join( [ '%s' % (m, m) - for m in [etud["email"], etud["emailperso"]] + for m in [etud_["email"], etud_["emailperso"]] if m ] ) @@ -277,7 +279,7 @@ def ficheEtud(etudid=None): sem_info[sem["formsemestre_id"]] = grlink if info["sems"]: - Se = sco_cursus.get_situation_etud_cursus(etud, info["last_formsemestre_id"]) + Se = sco_cursus.get_situation_etud_cursus(etud_, info["last_formsemestre_id"]) info["liste_inscriptions"] = formsemestre_recap_parcours_table( Se, etudid, @@ -454,6 +456,18 @@ def ficheEtud(etudid=None): # raccordement provisoire pour juillet 2022, avant refonte complète de cette fiche... info["but_infos_mkup"] = jury_but_view.infos_fiche_etud_html(etudid) + # XXX dev + info["but_cursus_mkup"] = "" + if info["sems"]: + last_sem = FormSemestre.query.get_or_404(info["sems"][-1]["formsemestre_id"]) + if last_sem.formation.is_apc(): + but_cursus = cursus_but.EtudCursusBUT(etud, last_sem.formation) + info["but_cursus_mkup"] = render_template( + "but/cursus_etud.j2", + cursus=but_cursus, + scu=scu, + ) + tmpl = """
{"".join([gen_cell(key, row, elt, with_col_class=with_col_classes) for key in keys if not key.startswith('_')])}""" + return f"""{ + "".join([gen_cell(key, row, elt, with_col_class=with_col_classes) + for key in keys if not key.startswith('_')]) + }""" # Pour accès depuis les templates jinja From 4fd9012133d705e2378fd7589371abb400b71b9c Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 19 Jan 2023 11:51:57 -0300 Subject: [PATCH 11/28] Envoi mail bulletins: bcc multiple addr. --- app/scodoc/sco_bulletins.py | 4 ++-- app/scodoc/sco_preferences.py | 2 +- sco_version.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/scodoc/sco_bulletins.py b/app/scodoc/sco_bulletins.py index 8e2f29145..e47177d64 100644 --- a/app/scodoc/sco_bulletins.py +++ b/app/scodoc/sco_bulletins.py @@ -1084,7 +1084,7 @@ def mail_bulletin(formsemestre_id, infos, pdfdata, filename, recipient_addr): recipients = [recipient_addr] sender = sco_preferences.get_preference("email_from_addr", formsemestre_id) if copy_addr: - bcc = copy_addr.strip() + bcc = copy_addr.strip().split(",") else: bcc = "" @@ -1094,7 +1094,7 @@ def mail_bulletin(formsemestre_id, infos, pdfdata, filename, recipient_addr): subject, sender, recipients, - bcc=[bcc], + bcc=bcc, text_body=hea, attachments=[ {"filename": filename, "mimetype": scu.PDF_MIMETYPE, "data": pdfdata} diff --git a/app/scodoc/sco_preferences.py b/app/scodoc/sco_preferences.py index 1a5963332..e0c8c0a35 100644 --- a/app/scodoc/sco_preferences.py +++ b/app/scodoc/sco_preferences.py @@ -1565,7 +1565,7 @@ class BasePreferences(object): "initvalue": "", "title": "e-mail copie bulletins", "size": 40, - "explanation": "adresse recevant une copie des bulletins envoyés aux étudiants", + "explanation": "adresse(s) recevant une copie des bulletins envoyés aux étudiants (si plusieurs, les séparer par des virgules)", "category": "bul_mail", }, ), diff --git a/sco_version.py b/sco_version.py index f62df4c93..d01a292e1 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.4.28" +SCOVERSION = "9.4.29" SCONAME = "ScoDoc" From 05953bd96da8f78894841425026f10b902ad4e10 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 22 Jan 2023 17:09:26 -0300 Subject: [PATCH 12/28] 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 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"] From a466486766d47718dbe5213580e29e0680049fab Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 22 Jan 2023 17:27:37 -0300 Subject: [PATCH 13/28] 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 7f5c00b86..2e18d9558 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 de4ed6f3a..9b843d4de 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 14/28] 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 ae0bfb4dd..6ef89a37b 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 7c881c445..909a76d95 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 15/28] 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 699f18e71..0b297871c 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 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) From 165dac04963e1ca5ba1d70ba34cdc5f4f2d5b3cf Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 23 Jan 2023 07:05:06 -0300 Subject: [PATCH 16/28] 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 ecbcc90b3..bf330adb3 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 d01a292e1..2a68efadd 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 17/28] =?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 6ef89a37b..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) @@ -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 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/pe/pe_jurype.py b/app/pe/pe_jurype.py index 7b0c79970..4aed6fc6d 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 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_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( '
@@ -488,6 +502,8 @@ def ficheEtud(etudid=None): %(but_infos_mkup)s +%(but_cursus_mkup)s +
%(adm_data)s @@ -524,7 +540,11 @@ def ficheEtud(etudid=None): """ header = html_sco_header.sco_header( page_title="Fiche étudiant %(prenom)s %(nom)s" % info, - cssstyles=["libjs/jQuery-tagEditor/jquery.tag-editor.css", "css/jury_but.css"], + cssstyles=[ + "libjs/jQuery-tagEditor/jquery.tag-editor.css", + "css/jury_but.css", + "css/cursus_but.css", + ], javascripts=[ "libjs/jinplace-1.2.1.min.js", "js/ue_list.js", diff --git a/app/static/css/cursus_but.css b/app/static/css/cursus_but.css new file mode 100644 index 000000000..79cde97cb --- /dev/null +++ b/app/static/css/cursus_but.css @@ -0,0 +1,42 @@ +/* Affichage cursus BUT étudiant (sur sa fiche) */ + + +.cursus_but { + margin-left: 32px; + display: inline-grid; + grid-template-columns: repeat(4, auto); + gap: 8px; +} + +.cursus_but>* { + display: flex; + align-items: center; + padding-top: 0px; + padding-bottom: 0px; + padding-left: 16px; + padding-right: 0px; + + background: #FFF; + border: 1px solid #aaa; + border-radius: 8px; +} + +.cursus_but>div.cb_head { + background: rgb(242, 242, 238); + border: none; + border-radius: 0px; + border-bottom: 1px solid gray; + font-weight: bold; +} + +div.cb_titre_competence { + background: #09c !important; + color: #FFF; + padding: 8px !important; +} + +div.code_rcue { + padding-top: 8px; + padding-bottom: 8px; + position: relative; +} \ No newline at end of file diff --git a/app/static/css/ref-competences.css b/app/static/css/ref-competences.css index 96e857d07..61586bc4d 100644 --- a/app/static/css/ref-competences.css +++ b/app/static/css/ref-competences.css @@ -1,24 +1,27 @@ -:host{ +:host { font-family: Verdana; - background: #222; + background: rgb(14, 5, 73); display: block; padding: 12px 32px; color: #FFF; max-width: 1000px; margin: auto; } -h1{ + +h1 { font-weight: 100; } + /**********************/ /* Zone parcours */ /**********************/ -.parcours{ +.parcours { display: flex; gap: 4px; padding-right: 4px; } -.parcours>div{ + +.parcours>div { background: #09c; font-size: 18px; text-align: center; @@ -29,65 +32,89 @@ h1{ transition: 0.1s; opacity: 0.7; } + .parcours>div:hover, -.competence>div:hover{ +.competence>div:hover { color: #ccc; } -.parcours>.focus{ + +.parcours>.focus { opacity: 1; } /**********************/ /* Zone compétences */ /**********************/ -.competences{ - display: grid; +.competences { + display: grid; margin-top: 8px; row-gap: 4px; } -.competences>div{ + +.competences>div { padding: 4px 8px; - border-radius: 4px; + border-radius: 4px; cursor: pointer; width: var(--competence-size); margin-right: 4px; } -.comp1{background:#a44} -.comp2{background:#84a} -.comp3{background:#a84} -.comp4{background:#8a4} -.comp5{background:#4a8} -.comp6{background:#48a} +.comp1 { + background: #a44 +} -.competences>.focus{ +.comp2 { + background: #84a +} + +.comp3 { + background: #a84 +} + +.comp4 { + background: #8a4 +} + +.comp5 { + background: #4a8 +} + +.comp6 { + background: #48a +} + +.competences>.focus { outline: 2px solid; } /**********************/ /* Zone AC */ /**********************/ -h2{ +h2 { display: table; padding: 8px 16px; font-size: 20px; border-radius: 16px 0; } -.ACs{ + +.ACs { padding-right: 4px; } -.AC li{ + +.AC li { display: grid; grid-template-columns: auto 1fr; align-items: start; gap: 4px; margin-bottom: 4px; - border-bottom: 1px solid; + border-bottom: 1px solid; } -.AC li>div:nth-child(1){ + +.AC li>div:nth-child(1) { padding: 2px 4px; border-radius: 4px; } -.AC li>div:nth-child(2){ + +.AC li>div:nth-child(2) { padding-bottom: 2px; } \ No newline at end of file diff --git a/app/templates/but/cursus_etud.j2 b/app/templates/but/cursus_etud.j2 new file mode 100644 index 000000000..04f450d2e --- /dev/null +++ b/app/templates/but/cursus_etud.j2 @@ -0,0 +1,26 @@ +{# Affichage cursus BUT fiche étudiant #} + +
+
+
BUT 1
+
BUT 2
+
BUT 3
+ {% for competence_id in cursus.to_dict() %} +
{{ cursus.competences[competence_id].titre }}
+ {% for annee in ('BUT1', 'BUT2', 'BUT3') %} + {% set validation = cursus.validation_par_competence_et_annee.get(competence_id, {}).get(annee) %} +
+ {% if validation %} +
+
{{validation.code}}
+
Validé le {{ + validation.date.strftime("%d/%m/%Y à %H:%M") + }}
+
+ {% else %} + - + {%endif%} +
+ {% endfor %} + {% endfor %} +
\ No newline at end of file diff --git a/scodoc.py b/scodoc.py index 1484883f0..7f46030e7 100755 --- a/scodoc.py +++ b/scodoc.py @@ -33,6 +33,13 @@ from app.models import Identite from app.models import ModuleImpl, ModuleImplInscription from app.models import Partition from app.models import ScolarFormSemestreValidation +from app.models.but_refcomp import ( + ApcCompetence, + ApcNiveau, + ApcParcours, + ApcReferentielCompetences, +) +from app.models.but_validations import ApcValidationAnnee, ApcValidationRCUE from app.models.evaluations import Evaluation from app.scodoc.sco_logos import make_logo_local from app.scodoc.sco_permissions import Permission @@ -57,6 +64,12 @@ def make_shell_context(): from app.scodoc import sco_utils as scu return { + "ApcCompetence": ApcCompetence, + "ApcNiveau": ApcNiveau, + "ApcParcours": ApcParcours, + "ApcReferentielCompetences": ApcReferentielCompetences, + "ApcValidationRCUE": ApcValidationRCUE, + "ApcValidationAnnee": ApcValidationAnnee, "ctx": app.test_request_context(), "current_app": flask.current_app, "current_user": current_user, @@ -71,21 +84,21 @@ def make_shell_context(): "login_user": login_user, "logout_user": logout_user, "mapp": mapp, - "models": models, "Matiere": Matiere, + "models": models, "Module": Module, "ModuleImpl": ModuleImpl, "ModuleImplInscription": ModuleImplInscription, - "Partition": Partition, "ndb": ndb, "notes": notes, "np": np, + "Partition": Partition, "pd": pd, "Permission": Permission, "pp": pp, - "Role": Role, "res_sem": res_sem, "ResultatsSemestreBUT": ResultatsSemestreBUT, + "Role": Role, "scolar": scolar, "ScolarFormSemestreValidation": ScolarFormSemestreValidation, "ScolarNews": models.ScolarNews, From 1d5064c955c2e0b073c24389a08dea80c7ab07c4 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 17 Jan 2023 10:57:49 -0300 Subject: [PATCH 03/28] =?UTF-8?q?V=C3=A9rifie=20type=20id=20pour=20vues=20?= =?UTF-8?q?ScoDoc7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/decorators.py | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/app/decorators.py b/app/decorators.py index 83441275e..5338828f4 100644 --- a/app/decorators.py +++ b/app/decorators.py @@ -16,6 +16,7 @@ import flask_login import app from app.auth.models import User import app.scodoc.sco_utils as scu +from app.scodoc.sco_exceptions import ScoValueError class ZUser(object): @@ -180,19 +181,24 @@ def scodoc7func(func): else: arg_names = argspec.args for arg_name in arg_names: # pour chaque arg de la fonction vue - if arg_name == "REQUEST": # ne devrait plus arriver ! - # debug check, TODO remove after tests - raise ValueError("invalid REQUEST parameter !") - else: - # peut produire une KeyError s'il manque un argument attendu: - v = req_args[arg_name] - # try to convert all arguments to INTEGERS - # necessary for db ids and boolean values - try: - v = int(v) - except (ValueError, TypeError): - pass - pos_arg_values.append(v) + # peut produire une KeyError s'il manque un argument attendu: + v = req_args[arg_name] + # try to convert all arguments to INTEGERS + # necessary for db ids and boolean values + try: + v = int(v) if v else v + except (ValueError, TypeError) as exc: + if arg_name in { + "etudid", + "formation_id", + "formsemestre_id", + "module_id", + "moduleimpl_id", + "partition_id", + "ue_id", + }: + raise ScoValueError("page introuvable (id invalide)") from exc + pos_arg_values.append(v) # current_app.logger.info("pos_arg_values=%s" % pos_arg_values) # current_app.logger.info("req_args=%s" % req_args) # Add keyword arguments From 35ccdbf39ebbf91025a7a6d6b9b628386252a163 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 17 Jan 2023 12:34:36 -0300 Subject: [PATCH 04/28] =?UTF-8?q?Jury=20BUT:=20RCUE=20redoubl=C3=A9s:=20l'?= =?UTF-8?q?UE=20impaire=20doit=20=C3=AAtre=20actuellement=20meilleure=20qu?= =?UTF-8?q?e=20celle=20=C3=A9ventuellement=20capitalis=C3=A9e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/but/jury_but.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/but/jury_but.py b/app/but/jury_but.py index c3e5b856a..85b7a1198 100644 --- a/app/but/jury_but.py +++ b/app/but/jury_but.py @@ -554,7 +554,6 @@ class DecisionsProposeesAnnee(DecisionsProposees): """Liste des regroupements d'UE à considérer cette année. On peut avoir un RCUE à cheval sur plusieurs années (redoublants avec UE capitalisées). Si on n'a pas les deux semestres, aucun RCUE. - Raises ScoValueError s'il y a des UE sans RCUE. <= ??? XXX """ if self.formsemestre_pair is None or self.formsemestre_impair is None: return [] @@ -570,6 +569,10 @@ class DecisionsProposeesAnnee(DecisionsProposees): not in CODES_UE_VALIDES ): continue # ignore cette UE antérieure non capitalisée + # et l'UE impaire doit être actuellement meilleure que + # celle éventuellement capitalisée + if self.decisions_ues[ue_impair.id].ue_status["is_capitalized"]: + continue # ignore cette UE car capitalisée et actuelle moins bonne if ue_pair.niveau_competence_id == ue_impair.niveau_competence_id: rcue = RegroupementCoherentUE( self.etud, From fe58222ce072c3223d2c8c04862acb453636af9a Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 17 Jan 2023 12:35:19 -0300 Subject: [PATCH 05/28] =?UTF-8?q?Am=C3=A9lioration=20tests.=20Cas=20geii84?= =?UTF-8?q?=20OK?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/unit/cursus_but_geii_lyon.yaml | 93 ++++++++++++++++++++++++++++ tests/unit/yaml_setup.py | 9 ++- 2 files changed, 100 insertions(+), 2 deletions(-) diff --git a/tests/unit/cursus_but_geii_lyon.yaml b/tests/unit/cursus_but_geii_lyon.yaml index 1230273c7..e625010cd 100644 --- a/tests/unit/cursus_but_geii_lyon.yaml +++ b/tests/unit/cursus_but_geii_lyon.yaml @@ -276,3 +276,96 @@ Etudiants: code_valide: AJ moy_ue: 7.00 decision_annee: AJ + geii84: + prenom: etugeii84 + civilite: M + formsemestres: + S1: + notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" + "S1.1": 11.95 + "S1.2": 12.76 + attendu: # les codes jury que l'on doit vérifier + deca: + passage_de_droit: False + nb_competences: 2 + nb_rcue_annee: 0 + decisions_ues: + "UE11": + codes: [ "ADM", "..." ] + code_valide: ADM + decision_jury: ADM + moy_ue: 11.95 + "UE12": + codes: [ "ADM", "..." ] + code_valide: ADM + decision_jury: ADM + moy_ue: 12.76 + S2: + notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" + "S2.1": 7.83 + "S2.2": 8.15 + attendu: # les codes jury que l'on doit vérifier + deca: + passage_de_droit: False + nb_competences: 2 + nb_rcue_annee: 2 + valide_moitie_rcue: False + codes: [ "RED", "..." ] + decisions_ues: + "UE21": + codes: [ "AJ", "..." ] + code_valide: AJ + decision_jury: AJ + moy_ue: 7.83 + "UE22": + codes: [ "CMP", "..." ] + code_valide: CMP + moy_ue: 8.15 + decisions_rcues: # on repère ici les RCUE par l'acronyme de leur 1ere UE (donc du S1) + "UE11": + code_valide: AJ + decision_jury: AJ + rcue: + moy_rcue: 9.89 + est_compensable: False + "UE12": + code_valide: CMP + decision_jury: CMP + rcue: + moy_rcue: 10.455 # ! attention à la précision + est_compensable: True + decision_annee: RED + S1-red: + notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" + "S1.1": 13.71 + "S1.2": 9.50 + attendu: # les codes jury que l'on doit vérifier + deca: + passage_de_droit: False + nb_competences: 2 + nb_rcue_annee: 0 + decisions_ues: + "UE11": + codes: [ "ADM", "..." ] + code_valide: ADM + decision_jury: ADM + moy_ue: 13.71 + "UE12": + codes: [ "AJ", "ADJ", "RAT", "DEF", "ABAN", "ADJR", "ATJ", "DEM", "UEBSL" ] + code_valide: AJ # c'est l'UE12 du S1 de l'année prec. qui est ADM + moy_ue: 9.5 # moyenne non capitalisée ici + moy_ue_with_cap: 12.76 +# Pas de décisions RCUE +# "UE11": -- non applicable +# code_valide: ADM -- non applicable +# decision_jury: ADM -- non applicable +# rcue: -- non applicable +# moy_rcue: 10.94 -- non applicable +# est_compensable: False -- non applicable +# "UE12": -- non applicable +# code_valide: ADM -- non applicable +# decision_jury: ADM -- non applicable +# rcue: -- non applicable +# moy_rcue: 10.94 -- non applicable +# est_compensable: False -- non applicable + decision_annee: AJ \ No newline at end of file diff --git a/tests/unit/yaml_setup.py b/tests/unit/yaml_setup.py index 12e9d0ce4..997298461 100644 --- a/tests/unit/yaml_setup.py +++ b/tests/unit/yaml_setup.py @@ -374,7 +374,7 @@ def setup_from_yaml(filename: str) -> dict: def _check_codes_jury(codes: list[str], codes_att: list[str]): """Vérifie (assert) la liste des codes l'ordre n'a pas d'importance ici. - Si codes_att contient un "...", on se contente de vérifie que + Si codes_att contient un "...", on se contente de vérifier que les codes de codes_att sont tous présents dans codes. """ codes_set = set(codes) @@ -404,13 +404,18 @@ def _check_decisions_ues( if "codes" in dec_ue_att: _check_codes_jury(dec_ue.codes, dec_ue_att["codes"]) - for attr in ("moy_ue", "moy_ue_with_cap", "explanation", "code_valide"): + for attr in ("explanation", "code_valide"): if attr in dec_ue_att: if getattr(dec_ue, attr) != dec_ue_att[attr]: raise ValueError( f"""Erreur: décision d'UE: {dec_ue.ue.acronyme } : champs {attr}={getattr(dec_ue, attr)} != attendu {dec_ue_att[attr]}""" ) + for attr in ("moy_ue", "moy_ue_with_cap"): + if attr in dec_ue_att: + assert ( + abs(getattr(dec_ue, attr) - dec_ue_att[attr]) < scu.NOTES_PRECISION + ) # Force décision de jury: code_manuel = dec_ue_att.get("decision_jury") if code_manuel is not None: From 2c489873ff35aaa7f9c1437d558b156bd3e16a2a Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 17 Jan 2023 14:06:25 -0300 Subject: [PATCH 06/28] Form config APo: ADJR --- app/forms/main/config_apo.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/forms/main/config_apo.py b/app/forms/main/config_apo.py index f607822c1..34e04d8ea 100644 --- a/app/forms/main/config_apo.py +++ b/app/forms/main/config_apo.py @@ -63,6 +63,7 @@ class CodesDecisionsForm(FlaskForm): ABL = _build_code_field("ABL") ADC = _build_code_field("ADC") ADJ = _build_code_field("ADJ") + ADJR = _build_code_field("ADJR") ADM = _build_code_field("ADM") AJ = _build_code_field("AJ") ATB = _build_code_field("ATB") From 10f0bf894c571efa3e1a4f4ddff7f8542d94dc5f Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 17 Jan 2023 19:13:31 -0300 Subject: [PATCH 07/28] Tests cas S1/S2/S1-red --- tests/unit/cursus_but_geii_lyon.yaml | 655 ++++++++++++++++++++++++++- 1 file changed, 654 insertions(+), 1 deletion(-) diff --git a/tests/unit/cursus_but_geii_lyon.yaml b/tests/unit/cursus_but_geii_lyon.yaml index e625010cd..7a38f3bb3 100644 --- a/tests/unit/cursus_but_geii_lyon.yaml +++ b/tests/unit/cursus_but_geii_lyon.yaml @@ -368,4 +368,657 @@ Etudiants: # rcue: -- non applicable # moy_rcue: 10.94 -- non applicable # est_compensable: False -- non applicable - decision_annee: AJ \ No newline at end of file + decision_annee: AJ +# Nouveaux cas RED (mardi 17/01/2023) + geii8bis: + prenom: "etugeii8 bis" + civilite: M + formsemestres: + S1: + notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" + "S1.1": 7.0000 + "S1.2": 9.0000 + attendu: # les codes jury que l'on doit vérifier + deca: + passage_de_droit: False + nb_competences: 2 + nb_rcue_annee: 0 + decisions_ues: + "UE11": + code_valide: AJ + moy_ue: 7.0000 + "UE12": + code_valide: AJ # ne sera compensée qu'en fin de S2 + moy_ue: 9.0000 + S2: + notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" + "S2.1": 12.0000 + "S2.2": 12.0000 + attendu: # les codes jury que l'on doit vérifier + deca: + passage_de_droit: False + nb_competences: 2 + nb_rcue_annee: 2 + valide_moitie_rcue: False + codes: [ "RED", "..." ] + decisions_ues: + "UE21": + codes: [ "ADM", "..." ] + code_valide: ADM + moy_ue: 12.0000 + "UE22": + codes: [ "ADM", "..." ] + code_valide: ADM + moy_ue: 12.0000 + decisions_rcues: # on repère ici les RCUE par l'acronyme de leur 1ere UE (donc du S1) + "UE11": + code_valide: AJ + rcue: + moy_rcue: 9.5000 + est_compensable: False + "UE12": + code_valide: CMP + decision_jury: CMP + rcue: + moy_rcue: 10.5000 + est_compensable: True + decision_annee: RED + S1-red: + notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" + "S1.1": 9.5000 + "S1.2": 7.0000 + attendu: # les codes jury que l'on doit vérifier + deca: + passage_de_droit: False + nb_competences: 2 + nb_rcue_annee: 2 + decisions_ues: + "UE11": + codes: [ "CMP", "..." ] + code_valide: CMP + decision_jury: CMP + moy_ue: 9.5000 + "UE12": + codes: [ "AJ", "..." ] + code_valide: AJ + decision_jury: AJ + moy_ue: 7.0000 + decisions_rcues: # on repère ici les RCUE par l'acronyme de leur 1ere UE (donc du S1) + "UE11": + code_valide: CMP + decision_jury: CMP + rcue: + moy_rcue: 10.75 + est_compensable: True + decision_annee: ADM + geii10: + prenom: etugeii10 + civilite: M + formsemestres: + S1: + notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" + "S1.1": 9.0000 + "S1.2": 7.0000 + attendu: # les codes jury que l'on doit vérifier + deca: + passage_de_droit: False + nb_competences: 2 + nb_rcue_annee: 0 + decisions_ues: + "UE11": + code_valide: AJ # en fin de S1, sera compensée en fin de S2 + moy_ue: 9.0000 + "UE12": + code_valide: AJ + moy_ue: 7.0000 + S2: + notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" + "S2.1": 12.0000 + "S2.2": 12.0000 + attendu: # les codes jury que l'on doit vérifier + deca: + passage_de_droit: False + nb_competences: 2 + nb_rcue_annee: 2 + valide_moitie_rcue: False + codes: [ "RED", "..." ] + decisions_ues: + "UE21": + code_valide: ADM + decision_jury: ADM + moy_ue: 12.0000 + "UE22": + code_valide: ADM + moy_ue: 12.0000 + decisions_rcues: # on repère ici les RCUE par l'acronyme de leur 1ere UE (donc du S1) + "UE11": + code_valide: CMP + rcue: + moy_rcue: 10.5000 + est_compensable: True + "UE12": + code_valide: AJ + decision_jury: AJ + rcue: + moy_rcue: 9.5000 + est_compensable: False + decision_annee: RED + S1-red: + notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" + "S1.1": 12.0000 + "S1.2": 7.5000 + attendu: # les codes jury que l'on doit vérifier + deca: + passage_de_droit: False + nb_competences: 2 + nb_rcue_annee: 0 + decisions_ues: + "UE11": + codes: [ "ADM", "..." ] + code_valide: ADM + decision_jury: ADM + moy_ue: 12.0000 + "UE12": + codes: [ "AJ", "..." ] + code_valide: AJ + decision_jury: AJ + moy_ue: 7.5000 + decisions_rcues: # on repère ici les RCUE par l'acronyme de leur 1ere UE (donc du S1) + "UE11": + code_valide: ADM + decision_jury: ADM + rcue: + moy_rcue: 12.00 + est_compensable: False + "UE12": + code_valide: AJ + decision_jury: AJ + rcue: + moy_rcue: 9.75 + est_compensable: False + decision_annee: AJ + geii11: + prenom: etugeii11 + civilite: M + formsemestres: + S1: + notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" + "S1.1": 7.0000 + "S1.2": 7.0000 + attendu: # les codes jury que l'on doit vérifier + deca: + passage_de_droit: False + nb_competences: 2 + nb_rcue_annee: 0 + decisions_ues: + "UE11": + codes: [ "AJ", "..." ] + code_valide: AJ + decision_jury: AJ + moy_ue: 7.0000 + "UE12": + codes: [ "AJ", "..." ] + code_valide: AJ + decision_jury: AJ + moy_ue: 7.0000 + S2: + notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" + "S2.1": 12.0000 + "S2.2": 12.0000 + attendu: # les codes jury que l'on doit vérifier + deca: + passage_de_droit: False + nb_competences: 2 + nb_rcue_annee: 2 + valide_moitie_rcue: False + codes: [ "RED", "..." ] + decisions_ues: + "UE21": + codes: [ "ADM", "..." ] + code_valide: ADM + decision_jury: ADM + moy_ue: 12.0000 + "UE22": + codes: [ "ADM", "..." ] + code_valide: ADM + decision_jury: ADM + moy_ue: 12.0000 + decisions_rcues: # on repère ici les RCUE par l'acronyme de leur 1ere UE (donc du S1) + "UE11": + code_valide: AJ + decision_jury: AJ + rcue: + moy_rcue: 9.5000 + est_compensable: False + "UE12": + code_valide: AJ + decision_jury: AJ + rcue: + moy_rcue: 9.5000 + est_compensable: False + decision_annee: RED + S1-red: + notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" + "S1.1": 9.0000 + "S1.2": 9.0000 + attendu: # les codes jury que l'on doit vérifier + deca: + passage_de_droit: True + nb_competences: 2 + nb_rcue_annee: 2 + decisions_ues: + "UE11": + codes: [ "CMP", "..." ] + code_valide: CMP + decision_jury: CMP + moy_ue: 9.0000 + "UE12": + codes: [ "CMP", "..." ] + code_valide: CMP + decision_jury: CMP + moy_ue: 9.0000 + decisions_rcues: # on repère ici les RCUE par l'acronyme de leur 1ere UE (donc du S1) + "UE11": + code_valide: CMP + decision_jury: CMP + rcue: + moy_rcue: 10.50 + est_compensable: True + "UE12": + code_valide: CMP + decision_jury: CMP + rcue: + moy_rcue: 10.50 + est_compensable: True + decision_annee: ADM + geii13: + prenom: etugeii13 + civilite: M + formsemestres: + S1: + notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" + "S1.1": 9.0000 + "S1.2": 12.0000 + attendu: # les codes jury que l'on doit vérifier + deca: + passage_de_droit: False + nb_competences: 2 + nb_rcue_annee: 0 + decisions_ues: + "UE11": + codes: [ "AJ", "..." ] + code_valide: AJ + decision_jury: AJ + moy_ue: 9.0000 + "UE12": + codes: [ "ADM", "..." ] + code_valide: ADM + decision_jury: ADM + moy_ue: 12.0000 + S2: + notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" + "S2.1": 9.0000 + "S2.2": 12.0000 + attendu: # les codes jury que l'on doit vérifier + deca: + passage_de_droit: False + nb_competences: 2 + nb_rcue_annee: 2 + valide_moitie_rcue: False + codes: [ "RED", "..." ] + decisions_ues: + "UE21": + code_valide: AJ + moy_ue: 9.0000 + "UE22": + code_valide: ADM + moy_ue: 12.0000 + decisions_rcues: # on repère ici les RCUE par l'acronyme de leur 1ere UE (donc du S1) + "UE11": + code_valide: AJ + decision_jury: AJ + rcue: + moy_rcue: 9.0000 + est_compensable: False + "UE12": + code_valide: ADM + decision_jury: ADM + rcue: + moy_rcue: 12.0000 + est_compensable: False + decision_annee: RED + S1-red: + notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" + "S1.1": 12.0000 + "S1.2": ATT + attendu: # les codes jury que l'on doit vérifier + deca: + passage_de_droit: False + nb_competences: 2 + nb_rcue_annee: 0 + decisions_ues: + "UE11": + code_valide: ADM + moy_ue: 12.0000 + "UE12": + code_valide: AJ + # PAS DE RCUE car UE12 capitalisée mailleure qu'actuelle + decision_annee: AJ + geii20: + prenom: etugeii20 + civilite: M + formsemestres: + S1: + notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" + "S1.1": 7.0000 + "S1.2": 7.0000 + attendu: # les codes jury que l'on doit vérifier + deca: + passage_de_droit: False + nb_competences: 2 + nb_rcue_annee: 0 + decisions_ues: + "UE11": + codes: [ "AJ", "..." ] + code_valide: AJ + decision_jury: ADJR + moy_ue: 7.0000 + "UE12": + codes: [ "AJ", "..." ] + code_valide: AJ + decision_jury: AJ + moy_ue: 7.0000 + S2: + notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" + "S2.1": 9.0000 + "S2.2": 12.0000 + attendu: # les codes jury que l'on doit vérifier + deca: + passage_de_droit: False + nb_competences: 2 + nb_rcue_annee: 2 + valide_moitie_rcue: False + codes: [ "RED", "..." ] + decisions_ues: + "UE21": + codes: [ "AJ", "..." ] + code_valide: AJ + decision_jury: ADJR + moy_ue: 9.0000 + "UE22": + codes: [ "ADM", "..." ] + code_valide: ADM + decision_jury: ADM + moy_ue: 12.0000 + decisions_rcues: # on repère ici les RCUE par l'acronyme de leur 1ere UE (donc du S1) + "UE11": + code_valide: AJ + decision_jury: AJ + rcue: + moy_rcue: 8.0000 + est_compensable: False + "UE12": + code_valide: AJ + decision_jury: ADJ + rcue: + moy_rcue: 9.5000 + est_compensable: False + decision_annee: RED + S1-red: + notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" + "S1.1": 12.0000 + "S1.2": 4.0000 + attendu: # les codes jury que l'on doit vérifier + deca: + passage_de_droit: false + nb_competences: 2 + nb_rcue_annee: 0 + decisions_ues: + "UE11": + code_valide: ADM + moy_ue: 12.0000 + "UE12": + code_valide: AJ + moy_ue: 4.0000 + decisions_rcues: # on repère ici les RCUE par l'acronyme de leur 1ere UE (donc du S1) + "UE12": + code_valide: ADJ + rcue: + moy_rcue: 8.00 + est_compensable: 0 + decision_annee: AJ + geii33: + prenom: etugeii33 + civilite: M + formsemestres: + S1: + notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" + "S1.1": 12.0000 + "S1.2": 9.0000 + attendu: # les codes jury que l'on doit vérifier + deca: + passage_de_droit: False + nb_competences: 2 + nb_rcue_annee: 0 + decisions_ues: + "UE11": + codes: [ "ADM", "..." ] + code_valide: ADM + decision_jury: ADM + moy_ue: 12.0000 + "UE12": + codes: [ "AJ", "..." ] + code_valide: AJ + decision_jury: AJ + moy_ue: 9.0000 + S2: + notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" + "S2.1": 12.0000 + "S2.2": 9.0000 + attendu: # les codes jury que l'on doit vérifier + deca: + passage_de_droit: False + nb_competences: 2 + nb_rcue_annee: 2 + valide_moitie_rcue: False + codes: [ "RED", "..." ] + decisions_ues: + "UE21": + codes: [ "ADM", "..." ] + code_valide: ADM + decision_jury: ADM + moy_ue: 12.0000 + "UE22": + codes: [ "AJ", "..." ] + code_valide: AJ + decision_jury: AJ + moy_ue: 9.0000 + decisions_rcues: # on repère ici les RCUE par l'acronyme de leur 1ere UE (donc du S1) + "UE11": + code_valide: ADM + decision_jury: ADM + rcue: + moy_rcue: 12.0000 + est_compensable: False + "UE12": + code_valide: AJ + decision_jury: AJ + rcue: + moy_rcue: 9.0000 + est_compensable: False + decision_annee: RED + S1-red: + notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" + "S1.1": 5.0000 + "S1.2": 12.0000 + attendu: # les codes jury que l'on doit vérifier + deca: + passage_de_droit: False + nb_competences: 2 + nb_rcue_annee: 0 + decisions_ues: + "UE11": + code_valide: AJ + moy_ue: 5. # LA MOYENNE COURANTE + moy_ue_with_cap: 12.0000 + "UE12": + code_valide: ADM + decision_jury: ADM + moy_ue: 12.0000 + # PAS DE RCUE ICI + decision_annee: AJ + geii43: + prenom: etugeii43 + civilite: M + formsemestres: + S1: + notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" + "S1.1": 9.0000 + "S1.2": 9.0000 + attendu: # les codes jury que l'on doit vérifier + deca: + passage_de_droit: False + nb_competences: 2 + nb_rcue_annee: 0 + decisions_ues: + "UE11": + code_valide: AJ + decision_jury: ADJR + moy_ue: 9.0000 + "UE12": + code_valide: AJ + decision_jury: AJ + moy_ue: 9.0000 + S2: + notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" + "S2.1": 9.0000 + "S2.2": 9.0000 + attendu: # les codes jury que l'on doit vérifier + deca: + passage_de_droit: False + nb_competences: 2 + nb_rcue_annee: 2 + valide_moitie_rcue: False + codes: [ "RED", "..." ] + decisions_ues: + "UE21": + code_valide: AJ + decision_jury: ADJR + moy_ue: 9.0000 + "UE22": + code_valide: AJ + decision_jury: AJ + moy_ue: 9.0000 + decisions_rcues: # on repère ici les RCUE par l'acronyme de leur 1ere UE (donc du S1) + "UE11": + code_valide: AJ + decision_jury: AJ + rcue: + moy_rcue: 9.0000 + est_compensable: False + "UE12": + code_valide: AJ + decision_jury: ADJ + rcue: + moy_rcue: 9.0000 + est_compensable: False + decision_annee: RED + S1-red: + notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" + "S1.1": 11.0000 + "S1.2": 7.0000 + attendu: # les codes jury que l'on doit vérifier + deca: + passage_de_droit: False + nb_competences: 2 + nb_rcue_annee: 0 + decisions_ues: + "UE11": + codes: [ "ADM", "..." ] + code_valide: ADM + decision_jury: ADM + moy_ue: 11.0000 + "UE12": + codes: [ "AJ", "..." ] + code_valide: AJ + decision_jury: AJ + moy_ue: 7.0000 + decision_annee: AJ + geii84bis: + prenom: "etugeii84 bis" + civilite: M + formsemestres: + S1: + notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" + "S1.1": 11.9500 + "S1.2": 12.7600 + attendu: # les codes jury que l'on doit vérifier + deca: + passage_de_droit: False + nb_competences: 2 + nb_rcue_annee: 0 + decisions_ues: + "UE11": + codes: [ "ADM", "..." ] + code_valide: ADM + decision_jury: ADM + moy_ue: 11.9500 + "UE12": + codes: [ "ADM", "..." ] + code_valide: ADM + decision_jury: ADM + moy_ue: 12.7600 + S2: + notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" + "S2.1": 7.8300 + "S2.2": 8.1500 + attendu: # les codes jury que l'on doit vérifier + deca: + passage_de_droit: False + nb_competences: 2 + nb_rcue_annee: 2 + valide_moitie_rcue: False + codes: [ "RED", "..." ] + decisions_ues: + "UE21": + codes: [ "AJ", "..." ] + code_valide: AJ + decision_jury: AJ + moy_ue: 7.8300 + "UE22": + codes: [ "CMP", "..." ] + code_valide: CMP + decision_jury: CMP + moy_ue: 8.1500 + decisions_rcues: # on repère ici les RCUE par l'acronyme de leur 1ere UE (donc du S1) + "UE11": + code_valide: AJ + decision_jury: AJ + rcue: + moy_rcue: 9.8900 + est_compensable: False + "UE12": + code_valide: CMP + decision_jury: CMP + rcue: + moy_rcue: 10.4550 + est_compensable: True + decision_annee: RED + S1-red: + notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" + "S1.1": 13.7100 + "S1.2": 9.5000 + attendu: # les codes jury que l'on doit vérifier + deca: + passage_de_droit: False + nb_competences: 2 + nb_rcue_annee: 0 + decisions_ues: + "UE11": + code_valide: ADM + moy_ue: 13.7100 + "UE12": + code_valide: AJ + moy_ue: 9.5000 + moy_ue_with_cap: 12.7600 + decision_annee: AJ From a6598dcd92e0e53372b304fef37d03e314cb14f6 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 17 Jan 2023 19:14:58 -0300 Subject: [PATCH 08/28] =?UTF-8?q?Jury=20et=20cursus=20BUT:=20ajout=20d'inf?= =?UTF-8?q?ormations=20+=20modif=20fiche=20=C3=A9tudiant?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/but/jury_but.py | 8 +++++++- app/but/jury_but_view.py | 5 +++-- app/scodoc/sco_codes_parcours.py | 8 ++++++-- app/scodoc/sco_page_etud.py | 4 +--- app/templates/but/cursus_etud.j2 | 12 ++++++++---- app/views/notes.py | 5 +++-- 6 files changed, 28 insertions(+), 14 deletions(-) diff --git a/app/but/jury_but.py b/app/but/jury_but.py index 85b7a1198..ae0bfb4dd 100644 --- a/app/but/jury_but.py +++ b/app/but/jury_but.py @@ -64,7 +64,7 @@ import re from typing import Union import numpy as np -from flask import g, url_for +from flask import flash, g, url_for from app import db from app import log @@ -1054,8 +1054,14 @@ class DecisionsProposeesRCUE(DecisionsProposees): dec_ue = deca.decisions_ues.get(ue_id) if dec_ue and dec_ue.code_valide not in CODES_UE_VALIDES: log(f"rcue.record: force ADJR sur {dec_ue}") + flash( + f"""UEs du RCUE "{dec_ue.ue.niveau_competence.competence.titre}" passées en ADJR""" + ) dec_ue.record("ADJR") + # Valide les niveaux inférieurs de la compétence (code ADSUP) + # TODO + if self.rcue.formsemestre_1 is not None: sco_cache.invalidate_formsemestre( formsemestre_id=self.rcue.formsemestre_1.id diff --git a/app/but/jury_but_view.py b/app/but/jury_but_view.py index 3140bd02e..acb6b695b 100644 --- a/app/but/jury_but_view.py +++ b/app/but/jury_but_view.py @@ -196,7 +196,7 @@ def _gen_but_niveau_ue(
UE en cours { "sans notes" if np.isnan(dec_ue.moy_ue) else - ("avec moyenne" + scu.fmt_note(dec_ue.moy_ue)) + ("avec moyenne " + scu.fmt_note(dec_ue.moy_ue) + "") }
@@ -205,9 +205,10 @@ def _gen_but_niveau_ue( moy_ue_str = f"""{scu.fmt_note(dec_ue.moy_ue)}""" if dec_ue.code_valide: scoplement = f"""
- Code {dec_ue.code_valide} enregistré le {dec_ue.validation.event_date.strftime("%d/%m/%Y")} +
Code {dec_ue.code_valide} enregistré le {dec_ue.validation.event_date.strftime("%d/%m/%Y")} à {dec_ue.validation.event_date.strftime("%Hh%M")}
+
""" else: scoplement = "" diff --git a/app/scodoc/sco_codes_parcours.py b/app/scodoc/sco_codes_parcours.py index 21b2b0c87..bbaa61b8f 100644 --- a/app/scodoc/sco_codes_parcours.py +++ b/app/scodoc/sco_codes_parcours.py @@ -192,8 +192,12 @@ CODES_SEM_ATTENTES = {ATT: True, ATB: True, ATJ: True} # semestre en attente CODES_SEM_REO = {NAR: 1} # reorientation -CODES_UE_VALIDES = {ADM: True, CMP: True, ADJ: True, ADJR: True} # UE validée -CODES_RCUE_VALIDES = CODES_UE_VALIDES # Niveau RCUE validé +CODES_UE_VALIDES = {ADM: True, CMP: True, ADJ: True, ADJR: True} +"UE validée" + +CODES_RCUE_VALIDES = {ADM, CMP, ADJ} +"Niveau RCUE validé" + # Pour le BUT: CODES_ANNEE_ARRET = {DEF, DEM, ABAN, ABL} CODES_RCUE = {ADM, AJ, CMP} diff --git a/app/scodoc/sco_page_etud.py b/app/scodoc/sco_page_etud.py index 7115d21b0..de4ed6f3a 100644 --- a/app/scodoc/sco_page_etud.py +++ b/app/scodoc/sco_page_etud.py @@ -454,7 +454,7 @@ def ficheEtud(etudid=None): info["bourse_span"] = "" # raccordement provisoire pour juillet 2022, avant refonte complète de cette fiche... - info["but_infos_mkup"] = jury_but_view.infos_fiche_etud_html(etudid) + # info["but_infos_mkup"] = jury_but_view.infos_fiche_etud_html(etudid) # XXX dev info["but_cursus_mkup"] = "" @@ -500,8 +500,6 @@ def ficheEtud(etudid=None): %(inscriptions_mkup)s -%(but_infos_mkup)s - %(but_cursus_mkup)s
diff --git a/app/templates/but/cursus_etud.j2 b/app/templates/but/cursus_etud.j2 index 04f450d2e..571909683 100644 --- a/app/templates/but/cursus_etud.j2 +++ b/app/templates/but/cursus_etud.j2 @@ -12,10 +12,14 @@
{% if validation %}
-
{{validation.code}}
-
Validé le {{ - validation.date.strftime("%d/%m/%Y à %H:%M") - }}
+
{{validation.code}}
+
+
{{validation.ue1.acronyme}} - {{validation.ue2.acronyme}}
+
Jury de {{validation.formsemestre.titre_annee()}}
+
enregistré le {{ + validation.date.strftime("%d/%m/%Y à %H:%M") + }}
+
{% else %} - diff --git a/app/views/notes.py b/app/views/notes.py index 415b6cc80..4ce3eb57d 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -2821,8 +2821,9 @@ def formsemestre_jury_but_erase( explanation=f"""Les validations d'UE et autorisations de passage du semestre S{formsemestre.semestre_id} seront effacées.""" if only_one_sem - else """Les validations de toutes les UE, RCUE (compétences) et année seront effacées. - Les décisions de l'année scolaire précédente ne seront pas modifiées. + else """Ses validations de toutes les UE, RCUE (compétences) et année + issues de cette année scolaire seront effacées. + Les décisions des années scolaires précédentes ne seront pas modifiées. """, cancel_url=dest_url, ) From 4b06c4cab554d12e689307cb4ca57076340d4b94 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 18 Jan 2023 06:31:49 -0300 Subject: [PATCH 09/28] Fix: affichage cursus BUT si un sem n'a pas de ref. comp. --- app/but/cursus_but.py | 7 +++++-- sco_version.py | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/but/cursus_but.py b/app/but/cursus_but.py index 9ae1671ab..7f5c00b86 100644 --- a/app/but/cursus_but.py +++ b/app/but/cursus_but.py @@ -92,8 +92,11 @@ class EtudCursusBUT: [ ins for ins in etud.formsemestre_inscriptions - if ins.formsemestre.formation.referentiel_competence.id - == formation.referentiel_competence.id + if ins.formsemestre.formation.referentiel_competence + and ( + ins.formsemestre.formation.referentiel_competence.id + == formation.referentiel_competence.id + ) ], key=lambda s: (s.formsemestre.semestre_id, s.formsemestre.date_debut), ) diff --git a/sco_version.py b/sco_version.py index 89e2cd427..f62df4c93 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.4.27" +SCOVERSION = "9.4.28" SCONAME = "ScoDoc" From 40857f317b92f56adce3e2c2285c23aaa67806f9 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 18 Jan 2023 14:25:40 -0300 Subject: [PATCH 10/28] Fix: ordre col. tableau recap (groupe admission) --- app/comp/res_common.py | 2 +- app/scodoc/sco_recapcomplet.py | 3 ++- app/scodoc/sco_utils.py | 5 ++++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/comp/res_common.py b/app/comp/res_common.py index 902d24050..8326b6403 100644 --- a/app/comp/res_common.py +++ b/app/comp/res_common.py @@ -905,7 +905,7 @@ class ResultatsSemestre(ResultatsCache): } first = True for i, cid in enumerate(fields): - titles[f"_{cid}_col_order"] = 10000 + i # tout à droite + titles[f"_{cid}_col_order"] = 100000 + i # tout à droite if first: titles[f"_{cid}_class"] = "admission admission_first" first = False diff --git a/app/scodoc/sco_recapcomplet.py b/app/scodoc/sco_recapcomplet.py index 64ffe13a4..de6299316 100644 --- a/app/scodoc/sco_recapcomplet.py +++ b/app/scodoc/sco_recapcomplet.py @@ -37,6 +37,7 @@ from flask import abort, url_for from app import log from app.but import bulletin_but from app.comp import res_sem +from app.comp.res_common import ResultatsSemestre from app.comp.res_compat import NotesTableCompat from app.models import FormSemestre from app.models.etudiants import Identite @@ -407,7 +408,7 @@ def gen_formsemestre_recapcomplet_html( def _gen_formsemestre_recapcomplet_html( formsemestre: FormSemestre, - res: NotesTableCompat, + res: ResultatsSemestre, include_evaluations=False, mode_jury=False, filename: str = "", diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index e57a50703..00365b4b2 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -1182,7 +1182,10 @@ def gen_row( tr_id = ( f"""id="row_selected" """ if (row.get("etudid", "") == selected_etudid) else "" ) - return f"""
Code semestre:
""") - H.append("""""") - - # Le formulaire de saisie des notes: - destination = url_for( - "notes.moduleimpl_status", - scodoc_dept=g.scodoc_dept, - moduleimpl_id=E["moduleimpl_id"], + + """ ) - form = _form_saisie_notes(E, M, groups_infos, destination=destination) + # Le formulaire de saisie des notes: + form = _form_saisie_notes(E, M, groups_infos, destination=moduleimpl_status_url) if form is None: - log(f"redirecting to {destination}") - return flask.redirect(destination) + return flask.redirect(moduleimpl_status_url) H.append(form) # H.append("") # /saisie_notes @@ -1104,6 +1109,9 @@ def _get_sorted_etuds(eval_dict: dict, etudids: list, formsemestre_id: int): for etudid in etudids: # infos identite etudiant e = sco_etud.etudident_list(cnx, {"etudid": etudid})[0] + etud: Identite = Identite.query.get(etudid) + # TODO: refactor et eliminer etudident_list. + e["etud"] = etud # utilisé seulement pour le tri -- a refactorer sco_etud.format_etud_ident(e) etuds.append(e) # infos inscription dans ce semestre @@ -1155,7 +1163,7 @@ def _get_sorted_etuds(eval_dict: dict, etudids: list, formsemestre_id: int): e["val"] = "DEM" e["explanation"] = "Démission" - etuds.sort(key=lambda x: (x["nom"], x["prenom"])) + etuds.sort(key=lambda x: x["etud"].sort_key) return etuds diff --git a/tests/unit/cursus_but_gb.yaml b/tests/unit/cursus_but_gb.yaml index 30b1d6a27..0b3588a66 100644 --- a/tests/unit/cursus_but_gb.yaml +++ b/tests/unit/cursus_but_gb.yaml @@ -129,7 +129,7 @@ FormSemestres: Etudiants: - Aaaaa: + Aïaaa: # avec un i trema prenom: Étudiant_SEE civilite: M formsemestres: @@ -196,7 +196,7 @@ Etudiants: S3: parcours: SEE - Bbbbb: + Azbbbb: # Az devrait être trié après Aï. prenom: Étudiante_BMB civilite: F formsemestres: From 3ba553c090c05011f3cb0a5d8f86128687c2c342 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 24 Jan 2023 11:45:46 -0300 Subject: [PATCH 27/28] typo --- app/scodoc/sco_saisie_notes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/scodoc/sco_saisie_notes.py b/app/scodoc/sco_saisie_notes.py index c7eb2cc28..bf8d8c296 100644 --- a/app/scodoc/sco_saisie_notes.py +++ b/app/scodoc/sco_saisie_notes.py @@ -1309,7 +1309,7 @@ def _form_saisie_notes(E, M, groups_infos, destination=""): H = [] if nb_decisions > 0: H.append( - """
+ f"""
  • Attention: il y a déjà des décisions de jury enregistrées pour {nb_decisions} étudiants. Après changement des notes, vérifiez la situation !
  • From fca090649cb24f581899307271144a64b8100658 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 25 Jan 2023 11:17:52 -0300 Subject: [PATCH 28/28] =?UTF-8?q?Am=C3=A9liore=20import/export=20formation?= =?UTF-8?q?s=20APC.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/models/formations.py | 3 +- app/models/ues.py | 1 + app/scodoc/sco_edit_apc.py | 10 +-- app/scodoc/sco_edit_ue.py | 12 ++-- app/scodoc/sco_formations.py | 69 ++++++++++++++----- app/scodoc/sco_photos.py | 4 +- .../pn/{form_descr.html => form_descr.j2} | 0 .../pn/{form_mods.html => form_mods.j2} | 3 +- ...ue_coefs.html => form_modules_ue_coefs.j2} | 0 .../pn/{form_ues.html => form_ues.j2} | 0 .../pn/{ue_infos.html => ue_infos.j2} | 0 app/views/notes.py | 9 ++- app/views/pn_modules.py | 2 +- sco_version.py | 2 +- 14 files changed, 81 insertions(+), 34 deletions(-) rename app/templates/pn/{form_descr.html => form_descr.j2} (100%) rename app/templates/pn/{form_mods.html => form_mods.j2} (97%) rename app/templates/pn/{form_modules_ue_coefs.html => form_modules_ue_coefs.j2} (100%) rename app/templates/pn/{form_ues.html => form_ues.j2} (100%) rename app/templates/pn/{ue_infos.html => ue_infos.j2} (100%) diff --git a/app/models/formations.py b/app/models/formations.py index 36e356472..986ef7e74 100644 --- a/app/models/formations.py +++ b/app/models/formations.py @@ -55,7 +55,8 @@ class Formation(db.Model): modules = db.relationship("Module", lazy="dynamic", backref="formation") def __repr__(self): - return f"<{self.__class__.__name__}(id={self.id}, dept_id={self.dept_id}, acronyme='{self.acronyme!r}')>" + return f"""<{self.__class__.__name__}(id={self.id}, dept_id={ + self.dept_id}, acronyme={self.acronyme!r}, version={self.version})>""" def to_html(self) -> str: "titre complet pour affichage" diff --git a/app/models/ues.py b/app/models/ues.py index 94954b93d..596e0bef6 100644 --- a/app/models/ues.py +++ b/app/models/ues.py @@ -111,6 +111,7 @@ class UniteEns(db.Model): e["ects"] = e["ects"] e["coefficient"] = e["coefficient"] if e["coefficient"] else 0.0 e["code_apogee"] = e["code_apogee"] or "" # pas de None + e["parcour"] = self.parcour.to_dict() if self.parcour else None if with_module_ue_coefs: if convert_objects: e["module_ue_coefs"] = [ diff --git a/app/scodoc/sco_edit_apc.py b/app/scodoc/sco_edit_apc.py index 1bea3b71f..b904bd5b5 100644 --- a/app/scodoc/sco_edit_apc.py +++ b/app/scodoc/sco_edit_apc.py @@ -99,7 +99,7 @@ def html_edit_formation_apc( H = [ render_template( - "pn/form_ues.html", + "pn/form_ues.j2", formation=formation, semestre_ids=semestre_ids, editable=editable, @@ -122,7 +122,7 @@ def html_edit_formation_apc( ).first() H += [ render_template( - "pn/form_mods.html", + "pn/form_mods.j2", formation=formation, titre=f"Ressources du S{semestre_idx}", create_element_msg="créer une nouvelle ressource", @@ -138,7 +138,7 @@ def html_edit_formation_apc( if ues_by_sem[semestre_idx].count() > 0 else "", render_template( - "pn/form_mods.html", + "pn/form_mods.j2", formation=formation, titre=f"Situations d'Apprentissage et d'Évaluation (SAÉs) S{semestre_idx}", create_element_msg="créer une nouvelle SAÉ", @@ -154,7 +154,7 @@ def html_edit_formation_apc( if ues_by_sem[semestre_idx].count() > 0 else "", render_template( - "pn/form_mods.html", + "pn/form_mods.j2", formation=formation, titre=f"Autres modules (non BUT) du S{semestre_idx}", create_element_msg="créer un nouveau module", @@ -196,7 +196,7 @@ def html_ue_infos(ue): and ue.matieres.count() == 0 ) return render_template( - "pn/ue_infos.html", + "pn/ue_infos.j2", titre=f"UE {ue.acronyme} {ue.titre}", ue=ue, formsemestres=formsemestres, diff --git a/app/scodoc/sco_edit_ue.py b/app/scodoc/sco_edit_ue.py index 5d269875e..906493a2a 100644 --- a/app/scodoc/sco_edit_ue.py +++ b/app/scodoc/sco_edit_ue.py @@ -723,7 +723,7 @@ def ue_table(formation_id=None, semestre_idx=1, msg=""): # was ue_list "libjs/jQuery-tagEditor/jquery.caret.min.js", "js/module_tag_editor.js", ], - page_title=f"Programme {formation.acronyme}", + page_title=f"Programme {formation.acronyme} v{formation.version}", ), f"""

    {formation.to_html()} {lockicon}

    @@ -765,7 +765,7 @@ du programme" (menu "Semestre") si vous avez un semestre en cours); # Description de la formation H.append( render_template( - "pn/form_descr.html", + "pn/form_descr.j2", formation=formation, parcours=parcours, editable=editable, @@ -913,8 +913,12 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
  • Export XML de la formation - (permet de la sauvegarder pour l'échanger avec un autre site) + }">Export XML de la formation ou + sans codes Apogée + (permet de l'enregistrer pour l'échanger avec un autre site)
  • return abort(404, "etudiant inconnu") etud = etuds[0] else: - raise ValueError("etud_photo_html: either etud or etudid must be specified") + abort(404, "etud_photo_html: either etud or etudid must be specified") photo_url = etud_photo_url(etud, size=size) nom = etud.get("nomprenom", etud["nom_disp"]) if title is None: @@ -244,7 +244,7 @@ def photo_pathname(photo_filename: str, size="orig"): elif size == "orig": version = "" else: - raise ValueError("invalid size parameter for photo") + abort(404, "invalid size parameter for photo") if not photo_filename: return False path = os.path.join(PHOTO_DIR, photo_filename) + version + IMAGE_EXT diff --git a/app/templates/pn/form_descr.html b/app/templates/pn/form_descr.j2 similarity index 100% rename from app/templates/pn/form_descr.html rename to app/templates/pn/form_descr.j2 diff --git a/app/templates/pn/form_mods.html b/app/templates/pn/form_mods.j2 similarity index 97% rename from app/templates/pn/form_mods.html rename to app/templates/pn/form_mods.j2 index 805d47081..21677f404 100644 --- a/app/templates/pn/form_mods.html +++ b/app/templates/pn/form_mods.j2 @@ -49,7 +49,8 @@ ({{mod.ue.acronyme}}), {% endif %} - parcours {{ mod.get_parcours()|map(attribute="code")|join(", ")|default('tronc commun', true)|safe + - parcours {{ mod.get_parcours()|map(attribute="code")|join(", ")|default('tronc commun', + true)|safe }} {% if mod.heures_cours or mod.heures_td or mod.heures_tp %} ({{mod.heures_cours|default(" ",true)|safe}}/{{mod.heures_td|default(" ",true)|safe}}/{{mod.heures_tp|default(" ",true)|safe}}, diff --git a/app/templates/pn/form_modules_ue_coefs.html b/app/templates/pn/form_modules_ue_coefs.j2 similarity index 100% rename from app/templates/pn/form_modules_ue_coefs.html rename to app/templates/pn/form_modules_ue_coefs.j2 diff --git a/app/templates/pn/form_ues.html b/app/templates/pn/form_ues.j2 similarity index 100% rename from app/templates/pn/form_ues.html rename to app/templates/pn/form_ues.j2 diff --git a/app/templates/pn/ue_infos.html b/app/templates/pn/ue_infos.j2 similarity index 100% rename from app/templates/pn/ue_infos.html rename to app/templates/pn/ue_infos.j2 diff --git a/app/views/notes.py b/app/views/notes.py index 2e20e2830..d32a7fee2 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -704,10 +704,15 @@ def formation_list(format=None, formation_id=None, args={}): @scodoc @permission_required(Permission.ScoView) @scodoc7func -def formation_export(formation_id, export_ids=False, format=None): +def formation_export( + formation_id, export_ids=False, format=None, export_codes_apo=True +): "Export de la formation au format indiqué (xml ou json)" return sco_formations.formation_export( - formation_id, export_ids=export_ids, format=format + formation_id, + export_ids=export_ids, + format=format, + export_codes_apo=export_codes_apo, ) diff --git a/app/views/pn_modules.py b/app/views/pn_modules.py index c67e79a3e..6868b8bd5 100644 --- a/app/views/pn_modules.py +++ b/app/views/pn_modules.py @@ -216,7 +216,7 @@ def edit_modules_ue_coefs(): """, render_template( - "pn/form_modules_ue_coefs.html", + "pn/form_modules_ue_coefs.j2", formation=formation, data_source=url_for( "notes.table_modules_ue_coefs", diff --git a/sco_version.py b/sco_version.py index d3e3cd25e..56d215357 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.4.31" +SCOVERSION = "9.4.32" SCONAME = "ScoDoc"