From 3575e89dc038328403618b35aa669f4ae93c855d Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sat, 1 Jun 2024 14:27:02 +0200 Subject: [PATCH 1/4] check invalid etudid --- app/models/etudiants.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/models/etudiants.py b/app/models/etudiants.py index a81ac73d..30e42d25 100644 --- a/app/models/etudiants.py +++ b/app/models/etudiants.py @@ -199,6 +199,11 @@ class Identite(models.ScoDocModel): @classmethod def get_etud(cls, etudid: int) -> "Identite": """Etudiant ou 404, cherche uniquement dans le département courant""" + if not isinstance(etudid, int): + try: + etudid = int(etudid) + except (TypeError, ValueError): + abort(404, "etudid invalide") if g.scodoc_dept: return cls.query.filter_by( id=etudid, dept_id=g.scodoc_dept_id From 912a213dcdee3c3af0e0961d3b5e36380d63f7b7 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sat, 1 Jun 2024 14:28:42 +0200 Subject: [PATCH 2/4] Rafraichissement image lors changement photo etud. Pres. trombi. Photos pour demos. --- app/scodoc/sco_photos.py | 21 +++++++++++++++++---- app/static/css/scodoc.css | 33 ++++++++++++++++++++++++++------- sco_version.py | 2 +- tools/fakeportal/fakeportal.py | 33 +++++++++++++++++++++++++++------ 4 files changed, 71 insertions(+), 18 deletions(-) diff --git a/app/scodoc/sco_photos.py b/app/scodoc/sco_photos.py index 5c4bfbdc..35b2ea12 100755 --- a/app/scodoc/sco_photos.py +++ b/app/scodoc/sco_photos.py @@ -96,13 +96,16 @@ def photo_portal_url(code_nip: str): return None -def get_etud_photo_url(etudid, size="small"): +def get_etud_photo_url(etudid, size="small", seed=None): + "L'URL scodoc vers la photo de l'étudiant" + kwargs = {"seed": seed} if seed else {} return ( url_for( "scolar.get_photo_image", scodoc_dept=g.scodoc_dept, etudid=etudid, size=size, + **kwargs, ) if has_request_context() else "" @@ -114,9 +117,11 @@ def etud_photo_url(etud: dict, size="small", fast=False) -> str: If ScoDoc doesn't have an image and a portal is configured, link to it. """ - photo_url = get_etud_photo_url(etud["etudid"], size=size) if fast: - return photo_url + return get_etud_photo_url(etud["etudid"], size=size) + photo_url = get_etud_photo_url( + etud["etudid"], size=size, seed=hash(etud.get("photo_filename")) + ) path = photo_pathname(etud["photo_filename"], size=size) if not path: # Portail ? @@ -374,7 +379,15 @@ def copy_portal_photo_to_fs(etudid: int): portal_timeout = sco_preferences.get_preference("portal_timeout") error_message = None try: - r = requests.get(url, timeout=portal_timeout) + r = requests.get( + url, + timeout=portal_timeout, + params={ + "nom": etud.nom or "", + "prenom": etud.prenom or "", + "civilite": etud.civilite, + }, + ) except requests.ConnectionError: error_message = "ConnectionError" except requests.Timeout: diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css index 0f152235..c4d55976 100644 --- a/app/static/css/scodoc.css +++ b/app/static/css/scodoc.css @@ -1085,18 +1085,35 @@ span.spanlink:hover { } .trombi_box { - display: inline-block; - width: 110px; - vertical-align: top; margin-left: 5px; margin-top: 5px; + width: 140px; + /* Constant width for the box */ + display: inline-flex; + flex-direction: column; + /* Ensures trombi-photo is above trombi_legend */ + align-items: center; + /* Centers content horizontally */ } -span.trombi_legend { - display: inline-block; +.trombi-photo { + display: flex; + justify-content: center; + /* Centers image horizontally within the photo container */ + margin-bottom: 10px; + /* Adds some space between the photo and the legend */ } -span.trombi-photo { +.trombi-photo img { + width: auto; + /* Maintains aspect ratio */ + height: 120px; + /* Sets the height to 90px */ + max-width: 100%; + /* Ensures the image doesn't exceed the container's width */ +} + +/* span.trombi_legend { display: inline-block; } @@ -1106,7 +1123,9 @@ span.trombi_box a { span.trombi_box a img { display: inline-block; -} + height: 128px; + width: auto; +} */ .trombi_nom { display: block; diff --git a/sco_version.py b/sco_version.py index ffcc3e8a..71a0b594 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.6.969" +SCOVERSION = "9.6.970" SCONAME = "ScoDoc" diff --git a/tools/fakeportal/fakeportal.py b/tools/fakeportal/fakeportal.py index 785f5856..7e9abe7e 100755 --- a/tools/fakeportal/fakeportal.py +++ b/tools/fakeportal/fakeportal.py @@ -28,7 +28,20 @@ script_dir = Path(os.path.abspath(__file__)).parent os.chdir(script_dir) # Les "photos" des étudiants -FAKE_FACES_PATHS = list((Path("faces").glob("*.jpg"))) +if os.path.exists("/opt/ExtraFaces"): + FAKE_FACES_PATHS = list((Path("extra_faces").glob("*/*.jpg"))) + FAKE_FACES_PATHS_BY_CIVILITE = { + "M": list((Path("extra_faces").glob("M/*.jpg"))), + "F": list((Path("extra_faces").glob("F/*.jpg"))), + "X": list((Path("extra_faces").glob("X/*.jpg"))), + } +else: + FAKE_FACES_PATHS = list((Path("faces").glob("*.jpg"))) + FAKE_FACES_PATHS_BY_CIVILITE = { + "M": FAKE_FACES_PATHS, + "F": FAKE_FACES_PATHS, + "X": FAKE_FACES_PATHS, + } # Etudiant avec tous les champs (USPN) ETUD_TEMPLATE_FULL = open(script_dir / "etud_template.xml").read() @@ -84,16 +97,22 @@ def make_random_etape_etuds(etape, annee): return "\n".join(L) -def get_photo_filename(nip: str) -> str: +def get_photo_filename(nip: str, civilite: str | None = None) -> str: """get an existing filename for a fake photo, found in faces/ Returns a path relative to the current working dir + If civilite is not None, use it to select a subdir """ - # - nb_faces = len(FAKE_FACES_PATHS) + print("get_photo_filename") + if civilite: + faces = FAKE_FACES_PATHS_BY_CIVILITE[civilite] + else: + faces = FAKE_FACES_PATHS + nb_faces = len(faces) if nb_faces == 0: print("WARNING: aucun fichier image disponible !") return "" - return FAKE_FACES_PATHS[hash(nip) % nb_faces] + print(faces[hash(nip) % nb_faces]) + return faces[hash(nip) % nb_faces] class MyHttpRequestHandler(http.server.SimpleHTTPRequestHandler): @@ -139,7 +158,9 @@ class MyHttpRequestHandler(http.server.SimpleHTTPRequestHandler): return elif ("getPhoto" in self.path) or ("scodocPhoto" in self.path): nip = query_components["nip"][0] - self.path = str(get_photo_filename(nip)) + civilite = query_components.get("civilite") + civilite = civilite[0] if civilite else None + self.path = str(get_photo_filename(nip, civilite=civilite)) print(f"photo for nip={nip}: {self.path}") else: print(f"Error 404: path={self.path}") From dcdf6a801263eee1306400a8b4fee9b544374434 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 2 Jun 2024 10:05:15 +0200 Subject: [PATCH 3/4] =?UTF-8?q?Assiduite:=20supprime=20lien=20saisie=20dif?= =?UTF-8?q?f=C3=A9r=C3=A9e=20+=20lien=20choix=20semaine?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/scodoc/sco_formsemestre_status.py | 15 ------------- .../pages/signal_assiduites_hebdo.j2 | 19 ++++++++-------- app/views/assiduites.py | 22 ++++++++++++++----- 3 files changed, 27 insertions(+), 29 deletions(-) diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py index 2bd32f2b..fc7df731 100755 --- a/app/scodoc/sco_formsemestre_status.py +++ b/app/scodoc/sco_formsemestre_status.py @@ -884,21 +884,6 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str: """ ) - if can_edit_abs: - H.append( - f""" -
- - (Saisie différée) -
- """ - ) - H.append("") # /sem-groups-assi if partition_is_empty: H.append( diff --git a/app/templates/assiduites/pages/signal_assiduites_hebdo.j2 b/app/templates/assiduites/pages/signal_assiduites_hebdo.j2 index 8422b3b2..a312fb6c 100644 --- a/app/templates/assiduites/pages/signal_assiduites_hebdo.j2 +++ b/app/templates/assiduites/pages/signal_assiduites_hebdo.j2 @@ -214,7 +214,7 @@ ] // [0]=Lundi ... [6]=Dimanche -> à 00h00 //Une fonction d'action quand un bouton est cliqué - // 3 possibilités : + // 3 possibilités : // - assiduite_id = null -> créer nv assi avec état du bouton // - assiduite_id non null et bouton coché == etat assi -> suppression de l'assiduité // - assiduite_id non null et bouton coché != etat assi -> modification de l'assiduité @@ -418,14 +418,14 @@ // Peuplement des boutons en fonction des assiduités let boutons = ` - - ` if (!non_present) { - boutons = ``+boutons; } @@ -437,8 +437,8 @@ const deb = new Date(assi.date_debut); const fin = new Date(assi.date_fin); - // si dates == periode -> cocher bouton correspondant - // Sinon supprimer boutons et mettre case "rouge" + tooltip + // si dates == periode -> cocher bouton correspondant + // Sinon supprimer boutons et mettre case "rouge" + tooltip if (deb.isSame(morningPeriod.deb, "minutes") && fin.isSame(morningPeriod.fin, "minutes")) { let etat = assi.etat.toLowerCase(); @@ -468,7 +468,7 @@ const deb = new Date(assi.date_debut); const fin = new Date(assi.date_fin); - // si dates == periode -> cocher bouton correspondant + // si dates == periode -> cocher bouton correspondant // Sinon supprimer boutons et mettre case "rouge" + tooltip if (deb.isSame(afternoonPeriod.deb, "minutes") && fin.isSame(afternoonPeriod.fin, "minutes")) { @@ -504,7 +504,7 @@ let target = e.target; let parent = target.parentElement; - + let isCancelled = await actionButton(target, !target.checked); if (isCancelled) { e.preventDefault(); @@ -690,7 +690,7 @@ } } - + document.getElementById("text-matin").addEventListener("click", (e)=>{ e.preventDefault(); @@ -801,6 +801,7 @@ document.addEventListener("DOMContentLoaded", ()=>{ {{moduleimpl_select | safe}} + autre semaine

diff --git a/app/views/assiduites.py b/app/views/assiduites.py index d3115f9c..6286024d 100644 --- a/app/views/assiduites.py +++ b/app/views/assiduites.py @@ -1980,7 +1980,7 @@ def signal_assiduites_hebdo(): # Vérification semaine dans format iso 8601 et formsemestre regex_iso8601 = r"^\d{4}-W\d{2}$" - if not re.match(regex_iso8601, week): + if week and not re.match(regex_iso8601, week): raise ScoValueError("Semaine invalide", dest_url=request.referrer) fs_deb_iso8601 = formsemestre.date_debut.strftime("%Y-W%W") @@ -1989,10 +1989,12 @@ def signal_assiduites_hebdo(): # Utilisation de la propriété de la norme iso 8601 # les chaines sont triables par ordre alphanumérique croissant # et produiront le même ordre que les dates par ordre chronologique croissant - if week < fs_deb_iso8601 or week > fs_fin_iso8601: - flash( - "La semaine n'est pas dans le semestre, choisissez la semaine sur laquelle saisir l'assiduité" - ) + if (not week) or week < fs_deb_iso8601 or week > fs_fin_iso8601: + if week: + flash( + """La semaine n'est pas dans le semestre, + choisissez la semaine sur laquelle saisir l'assiduité""" + ) return sco_gen_cal.calendrier_choix_date( date_debut=formsemestre.date_debut, date_fin=formsemestre.date_fin, @@ -2076,6 +2078,15 @@ def signal_assiduites_hebdo(): for key, val in jours.items(): hebdo_jours.append((key in non_travail, val)) + url_choix_semaine = url_for( + "assiduites.signal_assiduites_hebdo", + group_ids=",".join(map(str, groups_infos.group_ids)), + week="", + scodoc_dept=g.scodoc_dept, + formsemestre_id=groups_infos.formsemestre_id, + moduleimpl_id=moduleimpl_id, + ) + return render_template( "assiduites/pages/signal_assiduites_hebdo.j2", title="Assiduité: saisie hebdomadaire", @@ -2091,6 +2102,7 @@ def signal_assiduites_hebdo(): formsemestre_id=formsemestre_id, dept_id=g.scodoc_dept_id, ), + url_choix_semaine=url_choix_semaine, ) From 02a5b00ecf73bbf1311972a1cc48bd7922f89a38 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 2 Jun 2024 12:05:01 +0200 Subject: [PATCH 4/4] =?UTF-8?q?Changement=20r=C3=A9f.=20comp.=20=C3=A9quiv?= =?UTF-8?q?alent:=20SD=20<>=20STID.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/models/but_refcomp.py | 8 ++++++++ app/views/refcomp.py | 4 +++- ressources/referentiels/equivalences.yaml | 8 +++++++- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/app/models/but_refcomp.py b/app/models/but_refcomp.py index 22d40785..6831f092 100644 --- a/app/models/but_refcomp.py +++ b/app/models/but_refcomp.py @@ -274,6 +274,11 @@ class ApcReferentielCompetences(db.Model, XMLModel): return "type_departement mismatch" # Table d'équivalences entre refs: equiv = self._load_config_equivalences() + # Même specialité (ou alias) ? + if self.specialite != other.specialite and other.specialite not in equiv.get( + "alias", [] + ): + return "specialite mismatch" # mêmes parcours ? eq_parcours = equiv.get("parcours", {}) parcours_by_code_1 = {eq_parcours.get(p.code, p.code): p for p in self.parcours} @@ -317,6 +322,9 @@ class ApcReferentielCompetences(db.Model, XMLModel): def _load_config_equivalences(self) -> dict: """Load config file ressources/referentiels/equivalences.yaml used to define equivalences between distinct referentiels + return a dict, with optional keys: + alias: list of equivalent names for speciality (eg SD == STID) + parcours: dict with equivalent parcours acronyms """ try: with open(REFCOMP_EQUIVALENCE_FILENAME, encoding="utf-8") as f: diff --git a/app/views/refcomp.py b/app/views/refcomp.py index 42193d7c..bf4bd9f4 100644 --- a/app/views/refcomp.py +++ b/app/views/refcomp.py @@ -56,7 +56,9 @@ def refcomp(refcomp_id): @permission_required(Permission.ScoView) def refcomp_show(refcomp_id): """Affichage du référentiel de compétences.""" - referentiel_competence = ApcReferentielCompetences.query.get_or_404(refcomp_id) + referentiel_competence: ApcReferentielCompetences = ( + ApcReferentielCompetences.query.get_or_404(refcomp_id) + ) # Autres référentiels "équivalents" pour proposer de changer les formations: referentiels_equivalents = referentiel_competence.equivalents() return render_template( diff --git a/ressources/referentiels/equivalences.yaml b/ressources/referentiels/equivalences.yaml index d7ac623e..097a2f8e 100644 --- a/ressources/referentiels/equivalences.yaml +++ b/ressources/referentiels/equivalences.yaml @@ -15,4 +15,10 @@ QLIO: # la clé est 'specialite' ATN: MTD # competences: # titres de compétences ('nom_court' dans le XML) -SD: STID +STID: # passage de STID à SD + alias: + - SD + +SD: # pour revenir en arrière au besoin + alias: + - STID