From dfbfd41b9faa93dfd82e6c24b8e99cfc63ee21cc Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Fri, 22 Sep 2023 21:33:58 +0200 Subject: [PATCH 1/8] =?UTF-8?q?ajoute=20cr=C3=A9ation=20automatique=20du?= =?UTF-8?q?=20groupe=20par=20d=C3=A9faut?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/scodoc/sco_groups_view.py | 4 +++- sco_version.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/scodoc/sco_groups_view.py b/app/scodoc/sco_groups_view.py index 835207735..179160baa 100644 --- a/app/scodoc/sco_groups_view.py +++ b/app/scodoc/sco_groups_view.py @@ -324,7 +324,9 @@ class DisplayedGroupsInfos: if not formsemestre_id: raise Exception("missing parameter formsemestre_id or group_ids") if select_all_when_unspecified: - group_ids = [sco_groups.get_default_group(formsemestre_id)] + group_ids = [ + sco_groups.get_default_group(formsemestre_id, fix_if_missing=True) + ] else: # selectionne le premier groupe trouvé, s'il y en a un partition = sco_groups.get_partitions_list( diff --git a/sco_version.py b/sco_version.py index 49d42d09a..48b3bc566 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.6.33" +SCOVERSION = "9.6.34" SCONAME = "ScoDoc" From d869c3d938e8f27545e87e74e3f33f4f12bd4b50 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Fri, 22 Sep 2023 22:53:35 +0200 Subject: [PATCH 2/8] cosmetic --- app/scodoc/sco_formsemestre_status.py | 8 ++++++-- app/templates/assiduites/widgets/differee.j2 | 3 ++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py index 1b46a0484..304817238 100755 --- a/app/scodoc/sco_formsemestre_status.py +++ b/app/scodoc/sco_formsemestre_status.py @@ -1044,7 +1044,7 @@ def formsemestre_status_head(formsemestre_id: int = None, page_title: str = None Le classement des étudiants n'a qu'une valeur indicative.""" ) if sem.bul_hide_xml: - warnings.append("""Bulletins non publiés sur le portail. """) + warnings.append("""Bulletins non publiés sur la passerelle.""") if sem.block_moyennes: warnings.append("Calcul des moyennes bloqué !") if sem.semestre_id >= 0 and not sem.est_sur_une_annee(): @@ -1243,7 +1243,11 @@ def formsemestre_tableau_modules( mod_descr = "Module " + (mod.titre or "") if mod.is_apc(): coef_descr = ", ".join( - [f"{ue.acronyme}: {co}" for ue, co in mod.ue_coefs_list()] + [ + f"{ue.acronyme}: {co}" + for ue, co in mod.ue_coefs_list() + if isinstance(co, float) and co > 0 + ] ) if coef_descr: mod_descr += " Coefs: " + coef_descr diff --git a/app/templates/assiduites/widgets/differee.j2 b/app/templates/assiduites/widgets/differee.j2 index 33e5356a3..3496e9c9e 100644 --- a/app/templates/assiduites/widgets/differee.j2 +++ b/app/templates/assiduites/widgets/differee.j2 @@ -101,7 +101,8 @@ } .td[assiduite_id='insc']::after { - content: "Etudiant non inscrit"; + content: "non inscrit au module"; + font-style: italic; } .sticky { From f2549a24e7d04c3e260dac0cffe31fc18f2a736b Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sat, 23 Sep 2023 09:48:05 +0200 Subject: [PATCH 3/8] =?UTF-8?q?CAS:=20calcul=20du=20cas=5Fid=20=C3=A0=20pa?= =?UTF-8?q?rtir=20du=20mail=20via=20regexp?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/auth/models.py | 7 +++++++ app/forms/main/config_cas.py | 26 +++++++++++++++++++++++-- app/models/config.py | 37 ++++++++++++++++++++++++++++++++++++ app/templates/config_cas.j2 | 1 + app/views/scodoc.py | 19 ++++++++++++------ app/views/users.py | 13 ++++++++++--- sco_version.py | 2 +- 7 files changed, 93 insertions(+), 12 deletions(-) diff --git a/app/auth/models.py b/app/auth/models.py index a293685ee..073f687e9 100644 --- a/app/auth/models.py +++ b/app/auth/models.py @@ -306,6 +306,13 @@ class User(UserMixin, db.Model): role, dept = UserRole.role_dept_from_string(r_d) self.add_role(role, dept) + # Set cas_id using regexp if configured: + exp = ScoDocSiteConfig.get("cas_uid_from_mail_regexp") + if exp and self.email_institutionnel: + cas_id = ScoDocSiteConfig.extract_cas_id(self.email_institutionnel) + if cas_id is not None: + self.cas_id = cas_id + def get_token(self, expires_in=3600): "Un jeton pour cet user. Stocké en base, non commité." now = datetime.utcnow() diff --git a/app/forms/main/config_cas.py b/app/forms/main/config_cas.py index f68aa3ccb..7e2b73c56 100644 --- a/app/forms/main/config_cas.py +++ b/app/forms/main/config_cas.py @@ -30,8 +30,17 @@ Formulaire configuration CAS """ from flask_wtf import FlaskForm -from wtforms import BooleanField, SubmitField +from wtforms import BooleanField, SubmitField, ValidationError from wtforms.fields.simple import FileField, StringField +from wtforms.validators import Optional + +from app.models import ScoDocSiteConfig + + +def check_cas_uid_from_mail_regexp(form, field): + "Vérifie la regexp fournie pur l'extraction du CAS id" + if not ScoDocSiteConfig.cas_uid_from_mail_regexp_is_valid(field.data): + raise ValidationError("expression régulière invalide") class ConfigCASForm(FlaskForm): @@ -50,7 +59,8 @@ class ConfigCASForm(FlaskForm): ) cas_login_route = StringField( label="Route du login CAS", - description="""ajouté à l'URL du serveur: exemple /cas (si commence par /, part de la racine)""", + description="""ajouté à l'URL du serveur: exemple /cas + (si commence par /, part de la racine)""", default="/cas", ) cas_logout_route = StringField( @@ -70,6 +80,18 @@ class ConfigCASForm(FlaskForm): comptes utilisateurs.""", ) + cas_uid_from_mail_regexp = StringField( + label="Expression pour extraire l'identifiant utilisateur", + description="""regexp python appliquée au mail institutionnel de l'utilisateur, + dont le premier groupe doit donner l'identifiant CAS. + Si non fournie, le super-admin devra saisir cet identifiant pour chaque compte. + Par exemple, (.*)@ indique que le mail sans le domaine (donc toute + la partie avant le @) est l'identifiant. + Pour prendre le mail complet, utiliser (.*). + """, + validators=[Optional(), check_cas_uid_from_mail_regexp], + ) + cas_ssl_verify = BooleanField("Vérification du certificat SSL") cas_ssl_certificate_file = FileField( label="Certificat (PEM)", diff --git a/app/models/config.py b/app/models/config.py index 15da815f0..60ce884b4 100644 --- a/app/models/config.py +++ b/app/models/config.py @@ -5,6 +5,7 @@ import json import urllib.parse +import re from flask import flash from app import current_app, db, log @@ -103,6 +104,7 @@ class ScoDocSiteConfig(db.Model): "cas_logout_route": str, "cas_validate_route": str, "cas_attribute_id": str, + "cas_uid_from_mail_regexp": str, # Assiduité "morning_time": str, "lunch_time": str, @@ -395,6 +397,41 @@ class ScoDocSiteConfig(db.Model): data_links = json.dumps(links_dict) cls.set("personalized_links", data_links) + @classmethod + def extract_cas_id(cls, email_addr: str) -> str | None: + "Extract cas_id from maill, using regexp in config. None if not possible." + exp = cls.get("cas_uid_from_mail_regexp") + if not exp or not email_addr: + return None + try: + match = re.search(exp, email_addr) + except re.error: + log("error extracting CAS id from '{email_addr}' using regexp '{exp}'") + return None + if not match: + log("no match extracting CAS id from '{email_addr}' using regexp '{exp}'") + return None + try: + cas_id = match.group(1) + except IndexError: + log( + "no group found extracting CAS id from '{email_addr}' using regexp '{exp}'" + ) + return None + return cas_id + + @classmethod + def cas_uid_from_mail_regexp_is_valid(cls, exp: str) -> bool: + "True si l'expression régulière semble valide" + # check that it compiles + try: + pattern = re.compile(exp) + except re.error: + return False + # and returns at least one group on a simple cannonical address + match = pattern.search("emmanuel@exemple.fr") + return len(match.groups()) > 0 + @classmethod def assi_get_rounded_time(cls, label: str, default: str) -> float: "Donne l'heure stockée dans la config globale sous label, en float arrondi au quart d'heure" diff --git a/app/templates/config_cas.j2 b/app/templates/config_cas.j2 index ce45cfa67..ca500b1c9 100644 --- a/app/templates/config_cas.j2 +++ b/app/templates/config_cas.j2 @@ -23,6 +23,7 @@ {{ wtf.form_field(form.cas_logout_route) }} {{ wtf.form_field(form.cas_validate_route) }} {{ wtf.form_field(form.cas_attribute_id) }} + {{ wtf.form_field(form.cas_uid_from_mail_regexp) }}
{{ wtf.form_field(form.cas_ssl_verify) }} {{ wtf.form_field(form.cas_ssl_certificate_file) }} diff --git a/app/views/scodoc.py b/app/views/scodoc.py index 7891e52b6..c112e04f6 100644 --- a/app/views/scodoc.py +++ b/app/views/scodoc.py @@ -173,6 +173,10 @@ def config_cas(): flash("Route de validation CAS enregistrée") if ScoDocSiteConfig.set("cas_attribute_id", form.data["cas_attribute_id"]): flash("Attribut CAS ID enregistré") + if ScoDocSiteConfig.set( + "cas_uid_from_mail_regexp", form.data["cas_uid_from_mail_regexp"] + ): + flash("Expression extraction identifiant CAS enregistrée") if ScoDocSiteConfig.set("cas_ssl_verify", form.data["cas_ssl_verify"]): flash("Vérification SSL modifiée") if form.cas_ssl_certificate_file.data: @@ -197,13 +201,16 @@ def config_cas(): form.cas_logout_route.data = ScoDocSiteConfig.get("cas_logout_route") form.cas_validate_route.data = ScoDocSiteConfig.get("cas_validate_route") form.cas_attribute_id.data = ScoDocSiteConfig.get("cas_attribute_id") - form.cas_ssl_verify.data = ScoDocSiteConfig.get("cas_ssl_verify") - return render_template( - "config_cas.j2", - form=form, - title="Configuration du Service d'Authentification Central (CAS)", - cas_ssl_certificate_loaded=ScoDocSiteConfig.get("cas_ssl_certificate"), + form.cas_uid_from_mail_regexp.data = ScoDocSiteConfig.get( + "cas_uid_from_mail_regexp" ) + form.cas_ssl_verify.data = ScoDocSiteConfig.get("cas_ssl_verify") + return render_template( + "config_cas.j2", + form=form, + title="Configuration du Service d'Authentification Central (CAS)", + cas_ssl_certificate_loaded=ScoDocSiteConfig.get("cas_ssl_certificate"), + ) @bp.route("/ScoDoc/config_assiduites", methods=["GET", "POST"]) diff --git a/app/views/users.py b/app/views/users.py index 6f60b090f..905307604 100644 --- a/app/views/users.py +++ b/app/views/users.py @@ -400,9 +400,16 @@ def create_user_form(user_name=None, edit=0, all_roles=True): "title": "Identifiant CAS", "input_type": "text", "explanation": "id du compte utilisateur sur le CAS de l'établissement " - + "(service CAS activé)" - if cas_enabled - else "(service CAS non activé)", + + ( + "(sera déduit de son e-mail institutionnel) " + if ScoDocSiteConfig.get("cas_uid_from_mail_regexp") + else "" + ) + + ( + "(service CAS activé)" + if cas_enabled + else "(service CAS non activé)" + ), "size": 36, "allow_null": True, "readonly": not cas_enabled diff --git a/sco_version.py b/sco_version.py index 48b3bc566..61dc9fa95 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.6.34" +SCOVERSION = "9.6.35" SCONAME = "ScoDoc" From 71b41d3dc6c9ccbaaa7a6fab08e0fbf050c39a88 Mon Sep 17 00:00:00 2001 From: Iziram Date: Sun, 24 Sep 2023 08:53:52 +0200 Subject: [PATCH 4/8] Assiduites : fix err js #749 --- app/views/assiduites.py | 1 - 1 file changed, 1 deletion(-) diff --git a/app/views/assiduites.py b/app/views/assiduites.py index 2af34bdb1..d8dda7168 100644 --- a/app/views/assiduites.py +++ b/app/views/assiduites.py @@ -835,7 +835,6 @@ def visu_assiduites_group(): + [ # Voir fonctionnement JS "js/etud_info.js", - "js/abs_ajax.js", "js/groups_view.js", "js/assiduites.js", "libjs/moment.new.min.js", From 14286b31ac311752d138ca98c656df4675bb1651 Mon Sep 17 00:00:00 2001 From: Iziram Date: Sun, 24 Sep 2023 09:18:13 +0200 Subject: [PATCH 5/8] Assiduites : fix justificatifs #748 --- app/api/justificatifs.py | 2 +- app/models/assiduites.py | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/app/api/justificatifs.py b/app/api/justificatifs.py index f753fc0e0..a85685f7e 100644 --- a/app/api/justificatifs.py +++ b/app/api/justificatifs.py @@ -457,7 +457,7 @@ def justif_edit(justif_id: int): "après": compute_assiduites_justified( justificatif_unique.etudid, [justificatif_unique], - False, + True, ), } } diff --git a/app/models/assiduites.py b/app/models/assiduites.py index 3fc6d8787..82b6d3dc3 100644 --- a/app/models/assiduites.py +++ b/app/models/assiduites.py @@ -362,9 +362,17 @@ def compute_assiduites_justified( for assi in assiduites: if assi.etat == EtatAssiduite.PRESENT: continue + + assi_justificatifs = Justificatif.query.filter( + Justificatif.etudid == assi.etudid, + Justificatif.date_debut <= assi.date_debut, + Justificatif.date_fin >= assi.date_fin, + Justificatif.etat == EtatJustificatif.VALIDE, + ).all() + if any( assi.date_debut >= j.date_debut and assi.date_fin <= j.date_fin - for j in justificatifs + for j in justificatifs + assi_justificatifs ): assi.est_just = True assiduites_justifiees.append(assi.assiduite_id) From 5551003ffff58df858a358411b8356786b64901b Mon Sep 17 00:00:00 2001 From: Iziram Date: Sun, 24 Sep 2023 09:33:24 +0200 Subject: [PATCH 6/8] Assiduites : Filtrage Justif #745 --- app/templates/assiduites/pages/bilan_dept.j2 | 2 +- app/templates/assiduites/pages/bilan_etud.j2 | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/templates/assiduites/pages/bilan_dept.j2 b/app/templates/assiduites/pages/bilan_dept.j2 index 2db0dce9e..5da0ae640 100644 --- a/app/templates/assiduites/pages/bilan_dept.j2 +++ b/app/templates/assiduites/pages/bilan_dept.j2 @@ -39,7 +39,7 @@ function getDeptJustificatifsFromPeriod(action) { const formsemestre = formsemestre_id ? `&formsemestre_id=${formsemestre_id}` : "" const group = group_id ? `&group_id=${group_id}` : "" - const path = getUrl() + `/api/justificatifs/dept/${dept_id}/query?date_debut=${bornes.deb}&date_fin=${bornes.fin}&etat=attente,modifie${formsemestre}${group}` + const path = getUrl() + `/api/justificatifs/dept/${dept_id}/query?date_debut=${bornes.deb}&date_fin=${bornes.fin}${formsemestre}${group}` async_get( path, (data, status) => { diff --git a/app/templates/assiduites/pages/bilan_etud.j2 b/app/templates/assiduites/pages/bilan_etud.j2 index 3a9ee64b2..8bc9a90f9 100644 --- a/app/templates/assiduites/pages/bilan_etud.j2 +++ b/app/templates/assiduites/pages/bilan_etud.j2 @@ -27,9 +27,17 @@

Absences et retards non justifiés

+ + + + {% include "assiduites/widgets/tableau_assi.j2" %}

Justificatifs en attente (ou modifiés)

+ + + + {% include "assiduites/widgets/tableau_justi.j2" %}
From a7e0c05d051b8e957593925b9c67ebfff39028f3 Mon Sep 17 00:00:00 2001 From: Iziram Date: Sun, 24 Sep 2023 10:15:06 +0200 Subject: [PATCH 7/8] Assiduites : fix etud_info #744 --- app/scodoc/sco_formsemestre_status.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py index 304817238..1a0163b4b 100755 --- a/app/scodoc/sco_formsemestre_status.py +++ b/app/scodoc/sco_formsemestre_status.py @@ -502,13 +502,10 @@ def retreive_formsemestre_from_request() -> int: group = sco_groups.get_group(args["group_id"]) formsemestre_id = group["formsemestre_id"] elif group_ids: - if group_ids: - if isinstance(group_ids, str): - group_id = group_ids - else: - # prend le semestre du 1er groupe de la liste: - group_id = group_ids[0] - group = sco_groups.get_group(group_id) + if isinstance(group_ids, str): + group_ids = group_ids.split(",") + group_id = group_ids[0] + group = sco_groups.get_group(group_id) formsemestre_id = group["formsemestre_id"] elif "partition_id" in args: partition = sco_groups.get_partition(args["partition_id"]) From 43f7f354014f32f9e3e4f1f4a06aedfba424db35 Mon Sep 17 00:00:00 2001 From: Iziram Date: Sun, 24 Sep 2023 10:17:05 +0200 Subject: [PATCH 8/8] Assiduites : fix offset justi #749 --- app/templates/assiduites/pages/ajout_justificatif.j2 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/templates/assiduites/pages/ajout_justificatif.j2 b/app/templates/assiduites/pages/ajout_justificatif.j2 index bef71862f..28c4b6159 100644 --- a/app/templates/assiduites/pages/ajout_justificatif.j2 +++ b/app/templates/assiduites/pages/ajout_justificatif.j2 @@ -138,8 +138,8 @@ const raison = field.querySelector('#justi_raison').value; return { - date_debut: deb, - date_fin: fin, + date_debut: moment.tz(deb, TIMEZONE).format(), + date_fin: moment.tz(fin, TIMEZONE).format(), etat: etat, raison: raison, }