From 2a41cf972c04d8e2e425eb37900fa5bc2f024681 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 20 Dec 2022 10:11:45 -0300 Subject: [PATCH 01/98] =?UTF-8?q?Test=20yaml=20GMP:=20inscrit=20=C3=A0=20u?= =?UTF-8?q?n=20parcours?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/unit/cursus_but_gmp_iutlm.yaml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/unit/cursus_but_gmp_iutlm.yaml b/tests/unit/cursus_but_gmp_iutlm.yaml index e78997338..ec2ef38b8 100644 --- a/tests/unit/cursus_but_gmp_iutlm.yaml +++ b/tests/unit/cursus_but_gmp_iutlm.yaml @@ -195,13 +195,14 @@ Etudiants: S3: + parcours: SNRV # Inscrit dans le parcours SNRV notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" "S3.01": 9 "S3.SNRV.02": 12.5 attendu: # les codes jury que l'on doit vérifier deca: passage_de_droit: False - nb_competences: 4 # et non 5 car pas inscrit à un parcours + nb_competences: 5 # 4 de Tronc Commun + 1 de parcours nb_rcue_annee: 0 decisions_ues: "UE3.1-C1": @@ -220,10 +221,10 @@ Etudiants: codes: [ "AJ", "..." ] code_valide: AJ moy_ue: 9 - # "UE3.5.SNRV": - # codes: [ "ADM", "..." ] - # code_valide: ADM - # moy_ue: 12.5 + "UE3.5.SNRV": + codes: [ "ADM", "..." ] + code_valide: ADM + moy_ue: 12.5 gmp02: prenom: etugmp02 From a28f58a4434e4a4d8eae8dc4460422ce6cf9ac56 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 20 Dec 2022 11:52:20 -0300 Subject: [PATCH 02/98] =?UTF-8?q?Test=20yaml=20GMP:=20ajoute=20S1=20redoub?= =?UTF-8?q?l=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/unit/cursus_but_gmp_iutlm.yaml | 30 ++++++++++++++++++++++++++-- tests/unit/test_but_jury.py | 3 --- tests/unit/yaml_setup.py | 5 ++++- 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/tests/unit/cursus_but_gmp_iutlm.yaml b/tests/unit/cursus_but_gmp_iutlm.yaml index ec2ef38b8..a8c854759 100644 --- a/tests/unit/cursus_but_gmp_iutlm.yaml +++ b/tests/unit/cursus_but_gmp_iutlm.yaml @@ -108,7 +108,11 @@ FormSemestres: date_debut: 2023-09-01 date_fin: 2024-01-13 codes_parcours: ['II', 'SNRV'] - + # Un S1 pour les redoublants + S1-red: + idx: 1 + date_debut: 2023-09-02 + date_fin: 2024-01-12 Etudiants: gmp01: @@ -419,4 +423,26 @@ Etudiants: code_valide: AJ rcue: moy_rcue: 9.1 - est_compensable: False \ No newline at end of file + est_compensable: False + S1-red: + # On a capitalisé les UE/RCUE UE1.1-C1 et UE1.3-C3 + # L'étudiant décide de refaire qd même l'UE UE1.1-C1 + notes_modules: # on ne note ici que les UE à refaire + "SAE1.1": 14. # il améliore son UE 1 + "SAE1.2": 12. # et cette fois reussi les autres + "SAE1.4": 13. + attendu: + nb_competences: 4 + nb_rcue_annee: 0 + decisions_ues: + "UE1.1-C1": + code_valide: ADM + moy_ue: 14 # nouvelle moyenne + "UE1.2-C2": + code_valide: ADM + moy_ue: 12 + "UE1.3-C3": + moy_ue: 10.1 # capitalisée du S1 précédent XXX à vérifier + "UE1.4-C4": + code_valide: ADM + moy_ue: 13 diff --git a/tests/unit/test_but_jury.py b/tests/unit/test_but_jury.py index b52a432a8..cec1c58ed 100644 --- a/tests/unit/test_but_jury.py +++ b/tests/unit/test_but_jury.py @@ -88,9 +88,6 @@ def _check_deca(formsemestre: FormSemestre, etud: Identite = None): else: assert deca.formsemestre_pair == formsemestre assert formsemestre.query_ues_parcours_etud(etud.id).all() == deca.ues_pair - if formsemestre.semestre_id == 1: - assert deca.formsemestre_pair is None # jury de S1, pas de S2 - assert deca.rcues_annee == [] # S1, pas de RCUEs assert deca.inscription_etat == scu.INSCRIT assert deca.inscription_etat_impair == scu.INSCRIT assert (deca.parcour is None) or ( diff --git a/tests/unit/yaml_setup.py b/tests/unit/yaml_setup.py index 18c922aa8..c8f19577d 100644 --- a/tests/unit/yaml_setup.py +++ b/tests/unit/yaml_setup.py @@ -355,7 +355,10 @@ def _check_decisions_ues( for attr in ("moy_ue", "moy_ue_with_cap", "explanation", "code_valide"): if attr in dec_ue_att: - assert getattr(dec_ue, attr) == dec_ue_att[attr] + 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]}" + ) def _check_decisions_rcues( From a4840f494b3942636001ae60cb1ab2184369203c Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 20 Dec 2022 14:56:52 -0300 Subject: [PATCH 03/98] Fix: acces photo sans photos ni portail --- app/scodoc/sco_photos.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/app/scodoc/sco_photos.py b/app/scodoc/sco_photos.py index c0f3cbd8a..37908d512 100644 --- a/app/scodoc/sco_photos.py +++ b/app/scodoc/sco_photos.py @@ -73,7 +73,7 @@ from config import Config PHOTO_DIR = os.path.join(Config.SCODOC_VAR_DIR, "photos") ICONS_DIR = os.path.join(Config.SCODOC_DIR, "app", "static", "icons") UNKNOWN_IMAGE_PATH = os.path.join(ICONS_DIR, "unknown.jpg") -UNKNOWN_IMAGE_URL = "get_photo_image?etudid=" # with empty etudid => unknown face image + IMAGE_EXT = ".jpg" JPG_QUALITY = 0.92 REDUCED_HEIGHT = 90 # pixels @@ -81,6 +81,11 @@ MAX_FILE_SIZE = 4 * 1024 * 1024 # max allowed size for uploaded image, in bytes H90 = ".h90" # suffix for reduced size images +def unknown_image_url() -> str: + "URL for 'unkwown' face image" + return url_for("scolar.get_photo_image", scodoc_dept=g.scodoc_dept, etudid="") + + def photo_portal_url(etud): """Returns external URL to retreive photo on portal, or None if no portal configured""" @@ -118,7 +123,7 @@ def etud_photo_url(etud: dict, size="small", fast=False) -> str: ext_url = photo_portal_url(etud) if not ext_url: # fallback: Photo "unknown" - photo_url = scu.ScoURL() + "/" + UNKNOWN_IMAGE_URL + photo_url = unknown_image_url() else: # essaie de copier la photo du portail new_path, _ = copy_portal_photo_to_fs(etud) @@ -128,7 +133,7 @@ def etud_photo_url(etud: dict, size="small", fast=False) -> str: if scu.CONFIG.PUBLISH_PORTAL_PHOTO_URL: photo_url = ext_url else: - photo_url = scu.ScoURL() + "/" + UNKNOWN_IMAGE_URL + photo_url = unknown_image_url() return photo_url @@ -143,7 +148,8 @@ def get_photo_image(etudid=None, size="small"): filename = photo_pathname(etud.photo_filename, size=size) if not filename: filename = UNKNOWN_IMAGE_PATH - return _http_jpeg_file(filename) + r = _http_jpeg_file(filename) + return r def _http_jpeg_file(filename): @@ -166,7 +172,7 @@ def _http_jpeg_file(filename): except ValueError: mod_since = None if (mod_since is not None) and last_modified <= mod_since: - return "", 304 # not modified + return make_response(b"", 304) # not modified # last_modified_str = time.strftime( "%a, %d %b %Y %H:%M:%S GMT", time.gmtime(last_modified) @@ -183,7 +189,7 @@ def etud_photo_is_local(etud: dict, size="small"): return photo_pathname(etud["photo_filename"], size=size) -def etud_photo_html(etud: dict = None, etudid=None, title=None, size="small"): +def etud_photo_html(etud: dict = None, etudid=None, title=None, size="small") -> str: """HTML img tag for the photo, either in small size (h90) or original size (size=="orig") """ @@ -201,7 +207,7 @@ def etud_photo_html(etud: dict = None, etudid=None, title=None, size="small"): title = nom if not etud_photo_is_local(etud): fallback = ( - """onerror='this.onerror = null; this.src="%s"'""" % UNKNOWN_IMAGE_URL + f"""onerror='this.onerror = null; this.src="{unknown_image_url()}"'""" ) else: fallback = "" @@ -218,7 +224,7 @@ def etud_photo_html(etud: dict = None, etudid=None, title=None, size="small"): ) -def etud_photo_orig_html(etud=None, etudid=None, title=None): +def etud_photo_orig_html(etud=None, etudid=None, title=None) -> str: """HTML img tag for the photo, in full size. Full-size images are always stored locally in the filesystem. They are the original uploaded images, converted in jpeg. From 8e6dc37a876ca52a82c7abfd7b50118876dcfed9 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 20 Dec 2022 15:20:00 -0300 Subject: [PATCH 04/98] =?UTF-8?q?BUT:=20jury=20inter-ann=C3=A9e=20pour=20l?= =?UTF-8?q?es=20redoublants?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/but/jury_but.py | 46 +++++++++++---- app/but/jury_but_view.py | 109 ++++++++++++++++++++++-------------- app/models/validations.py | 2 +- app/static/css/jury_but.css | 14 ++++- tests/unit/test_but_jury.py | 5 +- 5 files changed, 121 insertions(+), 55 deletions(-) diff --git a/app/but/jury_but.py b/app/but/jury_but.py index 69fefd511..42b65372a 100644 --- a/app/but/jury_but.py +++ b/app/but/jury_but.py @@ -91,7 +91,7 @@ from app.models.ues import UniteEns from app.models.validations import ScolarFormSemestreValidation from app.scodoc import sco_cache from app.scodoc import sco_codes_parcours as sco_codes -from app.scodoc.sco_codes_parcours import RED, UE_STANDARD +from app.scodoc.sco_codes_parcours import CODES_UE_VALIDES, RED, UE_STANDARD from app.scodoc import sco_utils as scu from app.scodoc.sco_exceptions import ScoException, ScoValueError @@ -205,6 +205,8 @@ class DecisionsProposeesAnnee(DecisionsProposees): formsemestre: FormSemestre, ): super().__init__(etud=etud) + self.formsemestre = formsemestre + "le formsemestre utilisé pour construire ce deca" self.formsemestre_id = formsemestre.id "l'id du formsemestre utilisé pour construire ce deca" formsemestre_impair, formsemestre_pair = self.comp_formsemestres(formsemestre) @@ -219,23 +221,34 @@ class DecisionsProposeesAnnee(DecisionsProposees): ) ) ) + # Si les années scolaires sont distinctes, on est "à cheval" + self.a_cheval = ( + formsemestre_impair + and formsemestre_pair + and formsemestre_impair.annee_scolaire() + != formsemestre_pair.annee_scolaire() + ) + "vrai si on groupe deux semestres d'années scolaires différentes" # Si on part d'un semestre IMPAIR, il n'y aura pas de décision année proposée # (mais on pourra évidemment valider des UE et même des RCUE) self.jury_annuel: bool = formsemestre.semestre_id in (2, 4, 6) "vrai si jury de fin d'année scolaire (propose code annuel)" self.formsemestre_impair = formsemestre_impair - "le 1er semestre de l'année scolaire considérée (S1, S3, S5)" + "le 1er semestre du groupement (S1, S3, S5)" self.formsemestre_pair = formsemestre_pair - "le second formsemestre de la même année scolaire (S2, S4, S6)" + "le second formsemestre (S2, S4, S6), de la même année scolaire ou d'une précédente" formsemestre_last = formsemestre_pair or formsemestre_impair - "le formsemestre le plus avancé dans cette année" + "le formsemestre le plus avancé (en indice de semestre) dans le groupement" self.annee_but = (formsemestre_last.semestre_id + 1) // 2 "le rang de l'année dans le BUT: 1, 2, 3" assert self.annee_but in (1, 2, 3) self.rcues_annee = [] - "RCUEs de l'année" + """RCUEs de l'année + (peuvent concerner l'année scolaire antérieur pour les redoublants + avec UE capitalisées) + """ self.inscription_etat = etud.inscription_etat(formsemestre_last.id) "état de l'inscription dans le semestre le plus avancé (pair si année complète)" self.inscription_etat_pair = ( @@ -526,9 +539,9 @@ class DecisionsProposeesAnnee(DecisionsProposees): def compute_rcues_annee(self) -> list[RegroupementCoherentUE]: """Liste des regroupements d'UE à considérer cette année. - Pour le moment on ne considère pas de RCUE à cheval sur plusieurs années (redoublants). + 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. + Raises ScoValueError s'il y a des UE sans RCUE. <= ??? XXX """ if self.formsemestre_pair is None or self.formsemestre_impair is None: return [] @@ -537,6 +550,13 @@ class DecisionsProposeesAnnee(DecisionsProposees): for ue_pair in self.ues_pair: rcue = None for ue_impair in self.ues_impair: + if self.a_cheval: + # l'UE paire DOIT être capitalisée pour être utilisée + if ( + self.decisions_ues[ue_pair.id].code_valide + not in CODES_UE_VALIDES + ): + continue # ignore cette UE antérieure non capitalisée if ue_pair.niveau_competence_id == ue_impair.niveau_competence_id: rcue = RegroupementCoherentUE( self.etud, @@ -548,10 +568,12 @@ class DecisionsProposeesAnnee(DecisionsProposees): ) ues_impair_sans_rcue.discard(ue_impair.id) break - if rcue is None: + if rcue is None and not self.a_cheval: raise NoRCUEError(deca=self, ue=ue_pair) - rcues_annee.append(rcue) - if len(ues_impair_sans_rcue) > 0: + if rcue is not None: + rcues_annee.append(rcue) + # Si jury annuel (pas à cheval), on doit avoir tous les RCUEs: + if len(ues_impair_sans_rcue) > 0 and not self.a_cheval: ue = UniteEns.query.get(ues_impair_sans_rcue.pop()) raise NoRCUEError(deca=self, ue=ue) return rcues_annee @@ -1026,6 +1048,10 @@ class DecisionsProposeesUE(DecisionsProposees): self.moy_ue_with_cap = ue_status["moy"] self.ue_status = ue_status + def __repr__(self) -> str: + return f"""<{self.__class__.__name__} ue={self.ue.acronyme} valid={self.code_valide + } codes={self.codes} explanation={self.explanation}""" + def set_rcue(self, rcue: RegroupementCoherentUE): """Rattache cette UE à un RCUE. Cela peut modifier les codes proposés (si compensation)""" diff --git a/app/but/jury_but_view.py b/app/but/jury_but_view.py index c8efb357d..4cd6044f6 100644 --- a/app/but/jury_but_view.py +++ b/app/but/jury_but_view.py @@ -72,16 +72,24 @@ def show_etud(deca: DecisionsProposeesAnnee, read_only: bool = True) -> str: else: avertissement_redoublement = "" + formsemestre_1 = deca.formsemestre_impair + formsemestre_2 = deca.formsemestre_pair + # Ordonne selon les dates des 2 semestres considérés (pour les redoublants à cheval): + if deca.formsemestre_pair.date_debut < deca.formsemestre_impair.date_debut: + formsemestre_1, formsemestre_2 = formsemestre_2, formsemestre_1 H.append( f"""
Niveaux de compétences et unités d'enseignement du BUT{deca.annee_but}
-
S{deca.formsemestre_impair.semestre_id - if deca.formsemestre_impair else "-"}
-
S{deca.formsemestre_pair.semestre_id - if deca.formsemestre_pair else "-"} - {avertissement_redoublement}
+
S{formsemestre_1.semestre_id + if formsemestre_1 else "-"} + {formsemestre_1.annee_scolaire_str() if formsemestre_1 else ""} +
+
S{formsemestre_2.semestre_id + if formsemestre_2 else "-"} + {formsemestre_2.annee_scolaire_str() if formsemestre_2 else ""} +
RCUE
""" ) @@ -91,43 +99,54 @@ def show_etud(deca: DecisionsProposeesAnnee, read_only: bool = True) -> str:
{niveau.competence.titre}
""" ) - dec_rcue = deca.decisions_rcue_by_niveau.get(niveau.id) - if dec_rcue is None: + dec_rcue = deca.decisions_rcue_by_niveau.get(niveau.id) # peut être None + ues = [ue for ue in deca.ues_impair if ue.niveau_competence.id == niveau.id] + ue_impair = ues[0] if ues else None + ues = [ue for ue in deca.ues_pair if ue.niveau_competence.id == niveau.id] + ue_pair = ues[0] if ues else None + # Les UEs à afficher, toujours en readonly sur le formsemestre de l'année précédente du redoublant + ues_ro = [ + ( + ue_impair, + (deca.a_cheval and deca.formsemestre_id != deca.formsemestre_impair.id), + ), + ( + ue_pair, + deca.a_cheval and deca.formsemestre_id != deca.formsemestre_pair.id, + ), + ] + # Ordonne selon les dates des 2 semestres considérés: + if deca.formsemestre_pair.date_debut < deca.formsemestre_impair.date_debut: + ues_ro[0], ues_ro[1] = ues_ro[1], ues_ro[0] + # Colonnes d'UE: + for ue, ue_read_only in ues_ro: H.append( - """
""" - ) - continue - # Semestre impair - H.append( - _gen_but_niveau_ue( - dec_rcue.rcue.ue_1, - deca.decisions_ues[dec_rcue.rcue.ue_1.id], - disabled=read_only, - ) - ) - # Semestre pair - H.append( - _gen_but_niveau_ue( - dec_rcue.rcue.ue_2, - deca.decisions_ues[dec_rcue.rcue.ue_2.id], - disabled=read_only, - ) - ) - # RCUE - H.append( - f"""
-
{scu.fmt_note(dec_rcue.rcue.moy_rcue)}
-
{ - _gen_but_select("code_rcue_"+str(niveau.id), - dec_rcue.codes, - dec_rcue.code_valide, - disabled=True, klass="manual" + _gen_but_niveau_ue( + ue, + deca.decisions_ues[ue.id], + disabled=read_only or ue_read_only, + annee_prec=ue_read_only, ) - }
-
""" - ) + ) + + # RCUE + if dec_rcue is None: + H.append("""
""") + else: + H.append( + f"""
+
{scu.fmt_note(dec_rcue.rcue.moy_rcue)}
+
{ + _gen_but_select("code_rcue_"+str(niveau.id), + dec_rcue.codes, + dec_rcue.code_valide, + disabled=True, klass="manual" + ) + }
+
""" + ) H.append("") # but_annee return "\n".join(H) @@ -140,6 +159,8 @@ def _gen_but_select( klass: str = "", ) -> str: "Le menu html select avec les codes" + # if disabled: # mauvaise idée car le disabled est traité en JS + # return f"""
{code_valide}
""" h = "\n".join( [ f"""