From 4e2fab29d5faf9bee558030dc411c7b9371aa734 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 5 Sep 2021 18:14:16 +0200 Subject: [PATCH 01/25] fix typo role RespPe --- tools/import_scodoc7_user_db.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/import_scodoc7_user_db.py b/tools/import_scodoc7_user_db.py index 1cd7774f8..199f23e2c 100644 --- a/tools/import_scodoc7_user_db.py +++ b/tools/import_scodoc7_user_db.py @@ -59,7 +59,7 @@ def import_scodoc7_user_db(scodoc7_db="dbname=SCOUSERS"): roles7 = [] for role_dept in roles7: # Migre les rôles RespPeX, EnsX, AdminX, SecrX et ignore les autres - m = re.match(r"^(-?Ens|-?Secr|-?ResPe|-?Admin)(.*)$", role_dept) + m = re.match(r"^(-?Ens|-?Secr|-?RespPe|-?Admin)(.*)$", role_dept) if not m: msg = f"User {user_name}: role inconnu '{role_dept}' (ignoré)" current_app.logger.warning(msg) @@ -75,7 +75,7 @@ def import_scodoc7_user_db(scodoc7_db="dbname=SCOUSERS"): dept = m.group(2) role = Role.query.filter_by(name=role_name).first() if not role: - msg = f"User {user_name}: ignoring role '{role_dept}'" + msg = f"Role '{role_name}' introuvable. User {user_name}: ignoring role '{role_dept}'" current_app.logger.warning(msg) messages.append(msg) else: From 0bc8138c72d0e7ca1ac3b0e33c7ecd1865214e96 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 6 Sep 2021 08:25:06 +0200 Subject: [PATCH 02/25] version bump --- sco_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sco_version.py b/sco_version.py index 5fa98d284..d2735ed14 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.0.8" +SCOVERSION = "9.0.9" SCONAME = "ScoDoc" From ee0961d247c2d8a061ca2f92e74ac0dd0a272503 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 6 Sep 2021 21:32:56 +0200 Subject: [PATCH 03/25] Debian desinstall (purge) script --- tools/debian/postrm | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100755 tools/debian/postrm diff --git a/tools/debian/postrm b/tools/debian/postrm new file mode 100755 index 000000000..a01b1a38c --- /dev/null +++ b/tools/debian/postrm @@ -0,0 +1,19 @@ +#!/bin/bash + +# Déinstallation de scodoc +# Ne touche pas aux données (/opt/scodoc-data) +# N'enlève complètement /opt/scodoc qui si --purge + +systemctl stop scodoc9 +systemctl disable scodoc9 + +if [ "$#" == 1 ] && [ "$1" == "purge" ] +then + /bin/rm -rf /opt/scodoc + /bin/rm -f scodoc9.service + /bin/rm -f /etc/systemd/system/scodoc-updater.service + /bin/rm -f /etc/systemd/system/scodoc-updater.timer + /bin/rm -f /etc/nginx/sites-enabled/scodoc9.nginx +fi + +systemctl reload nginx From 04d5dd2ad72202e468719e10387707eaa9808c72 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 6 Sep 2021 21:35:17 +0200 Subject: [PATCH 04/25] better help messages --- tools/configure-scodoc9.sh | 9 +++++---- tools/debian/postinst | 2 +- tools/debian/preinst | 0 3 files changed, 6 insertions(+), 5 deletions(-) mode change 100644 => 100755 tools/debian/postinst mode change 100644 => 100755 tools/debian/preinst diff --git a/tools/configure-scodoc9.sh b/tools/configure-scodoc9.sh index 1c914830a..b1c18ec5e 100755 --- a/tools/configure-scodoc9.sh +++ b/tools/configure-scodoc9.sh @@ -108,8 +108,7 @@ change_scodoc_file_ownership # ------------ CREATION BASE DE DONNEES echo echo "Voulez-vous créer la base SQL SCODOC ?" -echo "répondre oui sauf si vous avez déjà une base existante" -echo "que vous souhaitez conserver (mais pour les migrations, répondre oui)." +echo "(répondre oui sauf si vous savez vraiment ce que vous faites)" echo -n 'Créer la base de données SCODOC ? (y/n) [y] ' read -r ans if [ "$(norm_ans "$ans")" != 'N' ] @@ -121,9 +120,10 @@ then echo echo "Création des tables et du compte admin" echo - su -c "(cd /opt/scodoc; source venv/bin/activate; flask db upgrade; flask sco-db-init; flask user-password admin)" "$SCODOC_USER" || die "Erreur: sco-db-init" + msg="Saisir le mot de passe de l\'administrateur \(admin\):" + su -c "(cd /opt/scodoc; source venv/bin/activate; flask db upgrade; flask sco-db-init; echo; echo $msg; flask user-password admin)" "$SCODOC_USER" || die "Erreur: sco-db-init" echo - echo "base initialisée et admin créé." + echo "Base initialisée et admin créé." echo fi @@ -134,6 +134,7 @@ systemctl start scodoc9 echo echo "Service configuré et démarré." +echo "Vous pouvez vous connecter en web et vous identifier comme \"admin\"." echo diff --git a/tools/debian/postinst b/tools/debian/postinst old mode 100644 new mode 100755 index 73167c38a..412c298a2 --- a/tools/debian/postinst +++ b/tools/debian/postinst @@ -29,7 +29,7 @@ do /usr/sbin/locale-gen --keep-existing fi done -echo "debian postinst: scodoc9 is $(systemctl is-active scodoc9)" +echo "debian postinst: scodoc9 systemd service is $(systemctl is-active scodoc9)" # On a besoin d'un postgresql lancé pour la mise à jour systemctl restart postgresql diff --git a/tools/debian/preinst b/tools/debian/preinst old mode 100644 new mode 100755 From af77a2a389e3d459489ada56cbbc597cc39c9b7d Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 6 Sep 2021 21:38:40 +0200 Subject: [PATCH 05/25] removed obsolete test-interactive command --- scodoc.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/scodoc.py b/scodoc.py index fcefe749b..3a0852277 100755 --- a/scodoc.py +++ b/scodoc.py @@ -188,24 +188,6 @@ def create_dept(dept): # create-dept return 0 -@app.cli.command() -@click.argument("filename") -@with_appcontext -def test_interactive(filename=None): - "Run interactive test" - import flask_login - from app import decorators - - click.echo("Executing {}".format(filename)) - with app.test_request_context(""): - u = User.query.first() - flask_login.login_user(u) - REQUEST = decorators.ZRequest() - exec(open(filename).read()) - - click.echo("Done.") - - @app.cli.command() @with_appcontext def import_scodoc7_users(): # import-scodoc7-users From 998f28d4a4771f26749255d8766a2cded2058277 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 7 Sep 2021 22:03:33 +0200 Subject: [PATCH 06/25] =?UTF-8?q?Modifie=20l'impl=C3=A9mentation=20des=20p?= =?UTF-8?q?r=C3=A9f=C3=A9rences=20pour=20ScoDoc=209?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/scodoc/sco_preferences.py | 444 +++++++++++++++++--------------- tests/unit/sco_fake_gen.py | 10 +- tests/unit/test_departements.py | 3 + 3 files changed, 243 insertions(+), 214 deletions(-) diff --git a/app/scodoc/sco_preferences.py b/app/scodoc/sco_preferences.py index b8c4eafae..f46153133 100644 --- a/app/scodoc/sco_preferences.py +++ b/app/scodoc/sco_preferences.py @@ -147,6 +147,52 @@ def get_preference(name, formsemestre_id=None): return get_base_preferences().get(formsemestre_id, name) +def _convert_pref_type(p, pref_spec): + """p est une ligne de la bd + {'id': , 'dept_id': , 'name': '', 'value': '', 'formsemestre_id': } + converti la valeur chane en le type désiré spécifié par pref_spec + """ + if "type" in pref_spec: + typ = pref_spec["type"] + if typ == "float": + # special case for float values (where NULL means 0) + if p["value"]: + p["value"] = float(p["value"]) + else: + p["value"] = 0.0 + else: + func = eval(typ) + p["value"] = func(p["value"]) + if pref_spec.get("input_type", None) == "boolcheckbox": + # boolcheckbox: la valeur stockée en base est une chaine "0" ou "1" + # que l'on ressort en True|False + if p["value"]: + try: + p["value"] = bool(int(p["value"])) + except ValueError: + log( + f"""Warning: invalid value for boolean pref in db: '{p["value"]}'""" + ) + p["value"] = False + else: + p["value"] = False # NULL (backward compat) + + +def _get_pref_default_value_from_config(name, pref_spec): + """get default value store in application level config. + If not found, use defalut value hardcoded in pref_spec. + """ + # XXX va changer avec la nouvelle base + # search in scu.CONFIG + if hasattr(scu.CONFIG, name): + value = getattr(scu.CONFIG, name) + log("sco_preferences: found default value in config for %s=%s" % (name, value)) + else: + # uses hardcoded default + value = pref_spec["initvalue"] + return value + + PREF_CATEGORIES = ( # sur page "Paramètres" ("general", {}), @@ -469,21 +515,27 @@ class BasePreferences(object): "abs_notification_mail_tmpl", { "initvalue": """ - --- Ceci est un message de notification automatique issu de ScoDoc --- + --- Ceci est un message de notification automatique issu de ScoDoc --- + L'étudiant %(nomprenom)s L'étudiant %(nomprenom)s + L'étudiant %(nomprenom)s + inscrit en %(inscription)s) inscrit en %(inscription)s) + inscrit en %(inscription)s) + a cumulé %(nbabsjust)s absences justifiées a cumulé %(nbabsjust)s absences justifiées - et %(nbabsnonjust)s absences NON justifiées. + a cumulé %(nbabsjust)s absences justifiées + et %(nbabsnonjust)s absences NON justifiées. - Le compte a pu changer depuis cet envoi, voir la fiche sur %(url_ficheetud)s. + Le compte a pu changer depuis cet envoi, voir la fiche sur %(url_ficheetud)s. - Votre dévoué serveur ScoDoc. + Votre dévoué serveur ScoDoc. - PS: Au dela de %(abs_notify_abs_threshold)s, un email automatique est adressé toutes les %(abs_notify_abs_increment)s absences. Ces valeurs sont modifiables dans les préférences de ScoDoc. - """, + PS: Au dela de %(abs_notify_abs_threshold)s, un email automatique est adressé toutes les %(abs_notify_abs_increment)s absences. Ces valeurs sont modifiables dans les préférences de ScoDoc. + """, "title": """Message notification e-mail""", "explanation": """Balises remplacées, voir la documentation""", "input_type": "textarea", @@ -826,14 +878,18 @@ class BasePreferences(object): "PV_INTRO", { "initvalue": """- - Vu l'arrêté du 3 août 2005 relatif au diplôme universitaire de technologie et notamment son article 4 et 6; - + Vu l'arrêté du 3 août 2005 relatif au diplôme universitaire de technologie et notamment son article 4 et 6; + + - - - vu l'arrêté n° %(Decnum)s du Président de l'%(UnivName)s; - + - + vu l'arrêté n° %(Decnum)s du Président de l'%(UnivName)s; + + - - - vu la délibération de la commission %(Type)s en date du %(Date)s présidée par le Chef du département; - """, + - + vu la délibération de la commission %(Type)s en date du %(Date)s présidée par le Chef du département; + """, "title": """Paragraphe d'introduction sur le PV""", "explanation": """Balises remplacées: %(Univname)s = nom de l'université, %(DecNum)s = numéro de l'arrêté, %(Date)s = date de la commission, %(Type)s = type de commission (passage ou délivrance), %(VDICode)s = code diplôme""", "input_type": "textarea", @@ -940,8 +996,8 @@ class BasePreferences(object): "PV_LETTER_PASSAGE_SIGNATURE", { "initvalue": """Pour le Directeur de l'IUT
- et par délégation
- Le Chef du département""", + et par délégation
+ Le Chef du département""", "title": """Signature des lettres individuelles de passage d'un semestre à l'autre""", "explanation": """%(DirectorName)s et %(DirectorTitle)s remplacés""", "input_type": "textarea", @@ -965,43 +1021,45 @@ class BasePreferences(object): "PV_LETTER_TEMPLATE", { "initvalue": """  - %(INSTITUTION_CITY)s, le %(date_jury)s - + %(INSTITUTION_CITY)s, le %(date_jury)s + - - à %(nomprenom)s - - %(domicile)s - %(codepostaldomicile)s %(villedomicile)s + + à %(nomprenom)s + + %(domicile)s + %(codepostaldomicile)s %(villedomicile)s - - Jury de %(type_jury)s
%(titre_formation)s
-
+ + Jury de %(type_jury)s
%(titre_formation)s
+
- - Le jury de %(type_jury_abbrv)s du département %(DeptName)s + + Le jury de %(type_jury_abbrv)s du département %(DeptName)s + s'est réuni le %(date_jury)s. s'est réuni le %(date_jury)s. - - Les décisions vous concernant sont : - + s'est réuni le %(date_jury)s. + + Les décisions vous concernant sont : + - %(prev_decision_sem_txt)s - - Décision %(decision_orig)s : %(decision_sem_descr)s - + %(prev_decision_sem_txt)s + + Décision %(decision_orig)s : %(decision_sem_descr)s + - - %(decision_ue_txt)s - + + %(decision_ue_txt)s + - - %(observation_txt)s - + + %(observation_txt)s + - %(autorisations_txt)s + %(autorisations_txt)s - %(diplome_txt)s - """, + %(diplome_txt)s + """, "title": """Lettre individuelle""", "explanation": """Balises remplacées et balisage XML, voir la documentation""", "input_type": "textarea", @@ -1362,24 +1420,24 @@ class BasePreferences(object): "bul_pdf_title", { "initvalue": """ - %(UnivName)s - - - %(InstituteName)s - - - RELEVÉ DE NOTES - + %(UnivName)s +
+ + %(InstituteName)s + + + RELEVÉ DE NOTES + - - %(nomprenom)s %(demission)s - + + %(nomprenom)s %(demission)s + - - Formation: %(titre_num)s - - Année scolaire: %(anneescolaire)s - """, + + Formation: %(titre_num)s + + Année scolaire: %(anneescolaire)s + """, "title": "Bulletins PDF: paragraphe de titre", "explanation": "(balises interprétées, voir documentation)", "input_type": "textarea", @@ -1404,10 +1462,10 @@ class BasePreferences(object): "bul_pdf_sig_left", { "initvalue": """La direction des études -
- %(responsable)s -
- """, +
+ %(responsable)s +
+ """, "title": "Bulletins PDF: signature gauche", "explanation": "(balises interprétées, voir documentation)", "input_type": "textarea", @@ -1420,10 +1478,10 @@ class BasePreferences(object): "bul_pdf_sig_right", { "initvalue": """Le chef de département -
- %(ChiefDeptName)s -
- """, +
+ %(ChiefDeptName)s +
+ """, "title": "Bulletins PDF: signature droite", "explanation": "(balises interprétées, voir documentation)", "input_type": "textarea", @@ -1799,88 +1857,57 @@ class BasePreferences(object): def load(self): """Load all preferences from db""" log(f"loading preferences for dept_id={self.dept_id}") - try: - scu.GSL.acquire() - cnx = ndb.GetDBConnexion() - preflist = self._editor.list(cnx, {"dept_id": self.dept_id}) - self.prefs = {None: {}} # { formsemestre_id (or None) : { name : value } } - self.default = {} # { name : default_value } - for p in preflist: - if not p["formsemestre_id"] in self.prefs: - self.prefs[p["formsemestre_id"]] = {} - # Ignore les noms de préférences non utilisés dans le code: - if p["name"] not in self.prefs_dict: - continue - # Convert types: - if ( - p["name"] in self.prefs_dict - and "type" in self.prefs_dict[p["name"]] - ): - typ = self.prefs_dict[p["name"]]["type"] - if typ == "float": - # special case for float values (where NULL means 0) - if p["value"]: - p["value"] = float(p["value"]) - else: - p["value"] = 0.0 - else: - func = eval(typ) - p["value"] = func(p["value"]) - if ( - p["name"] in self.prefs_dict - and self.prefs_dict[p["name"]].get("input_type", None) - == "boolcheckbox" - ): - # boolcheckbox: la valeur stockée en base est une chaine "0" ou "1" - # que l'on ressort en True|False - if p["value"]: - try: - p["value"] = bool(int(p["value"])) - except ValueError: - log( - f"""Warning: invalid value for boolean pref in db: '{p["value"]}'""" - ) - p["value"] = False - else: - p["value"] = False # NULL (backward compat) - self.prefs[p["formsemestre_id"]][p["name"]] = p["value"] - # add defaults for missing prefs - for pref in self.prefs_definition: - name = pref[0] - # search preferences in configuration file - if name and name[0] != "_" and name not in self.prefs[None]: - # search in scu.CONFIG - if hasattr(scu.CONFIG, name): - value = getattr(scu.CONFIG, name) - log( - "sco_preferences: found default value in config for %s=%s" - % (name, value) - ) - else: - # uses hardcoded default - value = pref[1]["initvalue"] + cnx = ndb.GetDBConnexion() + preflist = self._editor.list(cnx, {"dept_id": self.dept_id}) + self.prefs = {None: {}} # { formsemestre_id (or None) : { name : value } } + self.default = {} # { name : default_value } + for p in preflist: + if not p["formsemestre_id"] in self.prefs: + self.prefs[p["formsemestre_id"]] = {} + # Ignore les noms de préférences non utilisés dans le code: + if p["name"] not in self.prefs_dict: + continue - self.default[name] = value - self.prefs[None][name] = value - log("creating missing preference for %s=%s" % (name, value)) - # add to db table - self._editor.create( - cnx, {"dept_id": self.dept_id, "name": name, "value": value} - ) - finally: - scu.GSL.release() + # Convert types: + if p["name"] in self.prefs_dict: + _convert_pref_type(p, self.prefs_dict[p["name"]]) + + self.prefs[p["formsemestre_id"]][p["name"]] = p["value"] + + # add defaults for missing prefs + for pref in self.prefs_definition: + name = pref[0] + # search preferences in configuration file + if name and name[0] != "_" and name not in self.prefs[None]: + value = _get_pref_default_value_from_config(name, pref[1]) + self.default[name] = value + self.prefs[None][name] = value + log("creating missing preference for %s=%s" % (name, value)) + # add to db table + self._editor.create( + cnx, {"dept_id": self.dept_id, "name": name, "value": value} + ) def get(self, formsemestre_id, name): """Returns preference value. - If no value defined for this semestre, returns global value. + If global_lookup, when no value defined for this semestre, returns global value. """ - if formsemestre_id in self.prefs and name in self.prefs[formsemestre_id]: - return self.prefs[formsemestre_id][name] - elif name in self.prefs[None]: - return self.prefs[None][name] - else: - return self.default[name] + params = { + "dept_id": self.dept_id, + "name": name, + "formsemestre_id": formsemestre_id, + } + cnx = ndb.GetDBConnexion() + plist = self._editor.list(cnx, params) + if not plist: + del params["formsemestre_id"] + plist = self._editor.list(cnx, params) + if not plist: + return self.default[name] + p = plist[0] + _convert_pref_type(p, self.prefs_dict[name]) + return p["value"] def __contains__(self, item): return item in self.prefs[None] @@ -1890,74 +1917,75 @@ class BasePreferences(object): def is_global(self, formsemestre_id, name): "True if name if not defined for semestre" - if ( - not (formsemestre_id in self.prefs) - or not name in self.prefs[formsemestre_id] - ): - return True - else: - return False + params = { + "dept_id": self.dept_id, + "name": name, + "formsemestre_id": formsemestre_id, + } + cnx = ndb.GetDBConnexion() + plist = self._editor.list(cnx, params) + return len(plist) == 0 def save(self, formsemestre_id=None, name=None): """Write one or all (if name is None) values to db""" - try: - scu.GSL.acquire() - modif = False - cnx = ndb.GetDBConnexion() - if name is None: - names = list(self.prefs[formsemestre_id].keys()) - else: - names = [name] - for name in names: - value = self.get(formsemestre_id, name) - if self.prefs_dict[name].get("input_type", None) == "boolcheckbox": - # repasse les booleens en chaines "0":"1" - value = "1" if value else "0" - # existe deja ? - pdb = self._editor.list( + modif = False + cnx = ndb.GetDBConnexion() + if name is None: + names = list(self.prefs[formsemestre_id].keys()) + else: + names = [name] + for name in names: + value = self.prefs[formsemestre_id][name] + if self.prefs_dict[name].get("input_type", None) == "boolcheckbox": + # repasse les booleens en chaines "0":"1" + value = "1" if value else "0" + # existe deja ? + pdb = self._editor.list( + cnx, + args={ + "dept_id": self.dept_id, + "formsemestre_id": formsemestre_id, + "name": name, + }, + ) + if not pdb: + # crée préférence + log("create pref sem=%s %s=%s" % (formsemestre_id, name, value)) + self._editor.create( cnx, - args={ + { "dept_id": self.dept_id, - "formsemestre_id": formsemestre_id, "name": name, + "value": value, + "formsemestre_id": formsemestre_id, }, ) - if not pdb: - # crée préférence - log("create pref sem=%s %s=%s" % (formsemestre_id, name, value)) - self._editor.create( + modif = True + log("create pref sem=%s %s=%s" % (formsemestre_id, name, value)) + else: + # edit existing value + + existing_value = pdb[0]["value"] # old stored value + if ( + (existing_value != value) + and (existing_value != str(value)) + and (existing_value or str(value)) + ): + self._editor.edit( cnx, { - "dept_id": self.dept_id, + "pref_id": pdb[0]["pref_id"], + "formsemestre_id": formsemestre_id, "name": name, "value": value, - "formsemestre_id": formsemestre_id, }, ) modif = True - log("create pref sem=%s %s=%s" % (formsemestre_id, name, value)) - else: - # edit existing value - if pdb[0]["value"] != str(value) and ( - pdb[0]["value"] or str(value) - ): - self._editor.edit( - cnx, - { - "pref_id": pdb[0]["pref_id"], - "formsemestre_id": formsemestre_id, - "name": name, - "value": value, - }, - ) - modif = True - log("save pref sem=%s %s=%s" % (formsemestre_id, name, value)) + log("save pref sem=%s %s=%s" % (formsemestre_id, name, value)) - # les preferences peuvent affecter les PDF cachés et les notes calculées: - if modif: - sco_cache.invalidate_formsemestre() - finally: - scu.GSL.release() + # les preferences peuvent affecter les PDF cachés et les notes calculées: + if modif: + sco_cache.invalidate_formsemestre() def set(self, formsemestre_id, name, value): if not name or name[0] == "_" or name not in self.prefs_name: @@ -1972,26 +2000,24 @@ class BasePreferences(object): def delete(self, formsemestre_id, name): if not formsemestre_id: raise ScoException() - try: - scu.GSL.acquire() - if formsemestre_id in self.prefs and name in self.prefs[formsemestre_id]: - del self.prefs[formsemestre_id][name] - cnx = ndb.GetDBConnexion() - pdb = self._editor.list( - cnx, args={"formsemestre_id": formsemestre_id, "name": name} - ) - if pdb: - log("deleting pref sem=%s %s" % (formsemestre_id, name)) - assert pdb[0]["dept_id"] == self.dept_id - self._editor.delete(cnx, pdb[0]["pref_id"]) - sco_cache.invalidate_formsemestre() # > modif preferences - finally: - scu.GSL.release() + + if formsemestre_id in self.prefs and name in self.prefs[formsemestre_id]: + del self.prefs[formsemestre_id][name] + cnx = ndb.GetDBConnexion() + pdb = self._editor.list( + cnx, args={"formsemestre_id": formsemestre_id, "name": name} + ) + if pdb: + log("deleting pref sem=%s %s" % (formsemestre_id, name)) + assert pdb[0]["dept_id"] == self.dept_id + self._editor.delete(cnx, pdb[0]["pref_id"]) + sco_cache.invalidate_formsemestre() # > modif preferences def edit(self, REQUEST): """HTML dialog: edit global preferences""" from app.scodoc import html_sco_header + self.load() H = [ html_sco_header.sco_header(page_title="Préférences"), "

Préférences globales pour %s

" % scu.ScoURL(), diff --git a/tests/unit/sco_fake_gen.py b/tests/unit/sco_fake_gen.py index cd5bddde3..c2df6738f 100644 --- a/tests/unit/sco_fake_gen.py +++ b/tests/unit/sco_fake_gen.py @@ -40,11 +40,11 @@ from app.scodoc.sco_exceptions import ScoValueError random.seed(12345) # tests reproductibles -DEMO_DIR = Config.SCODOC_DIR + "/tools/demo/" -NOMS = [x.strip() for x in open(DEMO_DIR + "/noms.txt").readlines()] -PRENOMS_H = [x.strip() for x in open(DEMO_DIR + "/prenoms-h.txt").readlines()] -PRENOMS_F = [x.strip() for x in open(DEMO_DIR + "/prenoms-f.txt").readlines()] -PRENOMS_X = [x.strip() for x in open(DEMO_DIR + "/prenoms-x.txt").readlines()] +NOMS_DIR = Config.SCODOC_DIR + "/tools/fakeportal/nomsprenoms" +NOMS = [x.strip() for x in open(NOMS_DIR + "/noms.txt").readlines()] +PRENOMS_H = [x.strip() for x in open(NOMS_DIR + "/prenoms-h.txt").readlines()] +PRENOMS_F = [x.strip() for x in open(NOMS_DIR + "/prenoms-f.txt").readlines()] +PRENOMS_X = [x.strip() for x in open(NOMS_DIR + "/prenoms-x.txt").readlines()] def id_generator(size=6, chars=string.ascii_uppercase + string.digits): diff --git a/tests/unit/test_departements.py b/tests/unit/test_departements.py index 83a6650b5..e8ca47f60 100644 --- a/tests/unit/test_departements.py +++ b/tests/unit/test_departements.py @@ -73,6 +73,8 @@ def test_preferences(test_client): assert len(prefs2) == len(prefs) prefs2.set(None, "abs_notification_mail_tmpl", "toto") assert prefs2.get(None, "abs_notification_mail_tmpl") == "toto" + # Vérifie que les prefs sont bien sur un seul département: + app.set_sco_dept(current_dept.acronym) assert prefs.get(None, "abs_notification_mail_tmpl") != "toto" orm_val = ( ScoPreference.query.filter_by(dept_id=d.id, name="abs_notification_mail_tmpl") @@ -82,6 +84,7 @@ def test_preferences(test_client): assert orm_val == "toto" # --- Preferences d'un semestre # rejoue ce test pour avoir un semestre créé + app.set_sco_dept("D2") test_sco_basic.run_sco_basic() sem = sco_formsemestre.do_formsemestre_list()[0] formsemestre_id = sem["formsemestre_id"] From 97fe4cc73f5d912db92fea3bb7eee33ea9718f03 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 7 Sep 2021 23:54:33 +0200 Subject: [PATCH 07/25] =?UTF-8?q?Ne=20quote=20plus=20par=20d=C3=A9faut=20l?= =?UTF-8?q?e=20HTML=20des=20chaines=20entrants=20en=20base?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/scodoc/notesdb.py | 8 +++++--- app/scodoc/sco_moduleimpl.py | 1 - app/scodoc/sco_pdf.py | 7 ++++++- tools/fakeportal/fakeportal.py | 2 +- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/app/scodoc/notesdb.py b/app/scodoc/notesdb.py index 135675cc7..99c4ca9cb 100644 --- a/app/scodoc/notesdb.py +++ b/app/scodoc/notesdb.py @@ -287,7 +287,7 @@ class EditableTable(object): input_formators={}, aux_tables=[], convert_null_outputs_to_empty=True, - html_quote=True, + html_quote=False, # changed in 9.0.10 fields_creators={}, # { field : [ sql_command_to_create_it ] } filter_nulls=True, # dont allow to set fields to null filter_dept=False, # ajoute selection sur g.scodoc_dept_id @@ -321,8 +321,10 @@ class EditableTable(object): del vals["id"] if self.filter_dept: vals["dept_id"] = g.scodoc_dept_id - if self.html_quote: - quote_dict(vals) # quote all HTML markup + if ( + self.html_quote + ): # quote all HTML markup (une bien mauvaise idée venue des ages obscurs) + quote_dict(vals) # format value for title in vals: if title in self.input_formators: diff --git a/app/scodoc/sco_moduleimpl.py b/app/scodoc/sco_moduleimpl.py index fbb4b350f..a5db24a23 100644 --- a/app/scodoc/sco_moduleimpl.py +++ b/app/scodoc/sco_moduleimpl.py @@ -221,7 +221,6 @@ _moduleimpl_inscriptionEditor = ndb.EditableTable( def do_moduleimpl_inscription_create(args, formsemestre_id=None): "create a moduleimpl_inscription" cnx = ndb.GetDBConnexion() - log("do_moduleimpl_inscription_create: " + str(args)) r = _moduleimpl_inscriptionEditor.create(cnx, args) sco_cache.invalidate_formsemestre( formsemestre_id=formsemestre_id diff --git a/app/scodoc/sco_pdf.py b/app/scodoc/sco_pdf.py index 1f4964632..2a97e716b 100755 --- a/app/scodoc/sco_pdf.py +++ b/app/scodoc/sco_pdf.py @@ -33,6 +33,7 @@ En ScoDoc 9, ce n'est pas nécessaire car on est multiptocessus / monothread. """ +import html import io import os import queue @@ -85,7 +86,11 @@ def SU(s): # car les "combining accents" ne sont pas traités par ReportLab mais peuvent # nous être envoyés par certains navigateurs ou imports # (on en a dans les bases de données) - return unicodedata.normalize("NFC", s) + s = unicodedata.normalize("NFC", s) + # Remplace les entité XML/HTML + # reportlab ne les supporte pas non plus. + s = html.unescape(s) + return s def _splitPara(txt): diff --git a/tools/fakeportal/fakeportal.py b/tools/fakeportal/fakeportal.py index 9fdbcd01c..a732d3d78 100755 --- a/tools/fakeportal/fakeportal.py +++ b/tools/fakeportal/fakeportal.py @@ -100,7 +100,7 @@ class MyHttpRequestHandler(http.server.SimpleHTTPRequestHandler): if "etapes" in self.path.lower(): self.path = str(Path(script_dir / "etapes.xml").relative_to(Path.cwd())) - elif "scodocEtudiant" in self.path: + elif "scodocEtudiant" in self.path: # API v2 # 2 forms: nip=xxx or etape=eee&annee=aaa if "nip" in query_components: nip = query_components["nip"][0] From 2f78f7f6fc81e260ed75607b981a5e2c8bfc0847 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 8 Sep 2021 00:10:36 +0200 Subject: [PATCH 08/25] Message erreur si import Excel d'une date invalide --- app/scodoc/sco_excel.py | 47 +++------------------------------- app/scodoc/sco_import_etuds.py | 8 ++++-- 2 files changed, 9 insertions(+), 46 deletions(-) diff --git a/app/scodoc/sco_excel.py b/app/scodoc/sco_excel.py index 32cf27c0a..c9551b692 100644 --- a/app/scodoc/sco_excel.py +++ b/app/scodoc/sco_excel.py @@ -79,52 +79,11 @@ def send_excel_file(request, data, filename, mime=scu.XLSX_MIMETYPE): # font, border, number_format, fill, .. (cf https://openpyxl.readthedocs.io/en/stable/styles.html#working-with-styles) -# (stolen from xlrd) -# Convert an Excel number (presumed to represent a date, a datetime or a time) into -# a Python datetime.datetime -# @param xldate The Excel number -# @param datemode 0: 1900-based, 1: 1904-based. -# @return a datetime.datetime object, to the nearest_second. -#
Special case: if 0.0 <= xldate < 1.0, it is assumed to represent a time; -# a datetime.time object will be returned. -#
Note: 1904-01-01 is not regarded as a valid date in the datemode 1 system; its "serial number" -# is zero. -# -# _XLDAYS_TOO_LARGE = (2958466, 2958466 - 1462) # This is equivalent to 10000-01-01 -# - - def xldate_as_datetime(xldate, datemode=0): + """Conversion d'une date Excel en date + Peut lever une ValueError + """ return openpyxl.utils.datetime.from_ISO8601(xldate) - # if datemode not in (0, 1): - # raise ValueError("invalid mode %s" % datemode) - # if xldate == 0.00: - # return datetime.time(0, 0, 0) - # if xldate < 0.00: - # raise ValueError("invalid date code %s" % xldate) - # xldays = int(xldate) - # frac = xldate - xldays - # seconds = int(round(frac * 86400.0)) - # assert 0 <= seconds <= 86400 - # if seconds == 86400: - # seconds = 0 - # xldays += 1 - # if xldays >= _XLDAYS_TOO_LARGE[datemode]: - # raise ValueError("date too large %s" % xldate) - # - # if xldays == 0: - # # second = seconds % 60; minutes = seconds // 60 - # minutes, second = divmod(seconds, 60) - # # minute = minutes % 60; hour = minutes // 60 - # hour, minute = divmod(minutes, 60) - # return datetime.time(hour, minute, second) - # - # if xldays < 61 and datemode == 0: - # raise ValueError("ambiguous date %s" % xldate) - # - # return datetime.datetime.fromordinal( - # xldays + 693594 + 1462 * datemode - # ) + datetime.timedelta(seconds=seconds) class ScoExcelBook: diff --git a/app/scodoc/sco_import_etuds.py b/app/scodoc/sco_import_etuds.py index 5e5016bcd..b9b8b5de5 100644 --- a/app/scodoc/sco_import_etuds.py +++ b/app/scodoc/sco_import_etuds.py @@ -378,8 +378,12 @@ def scolars_import_excel_file( # Excel date conversion: if titleslist[i].lower() == "date_naissance": if val: - # if re.match(r"^[0-9]*\.?[0-9]*$", str(val)): - val = sco_excel.xldate_as_datetime(val) + try: + val = sco_excel.xldate_as_datetime(val) + except ValueError: + raise ScoValueError( + f"date invalide ({val}) sur ligne {linenum}, colonne {titleslist[i]}" + ) # INE if ( titleslist[i].lower() == "code_ine" From 19f6053dda39b08ea1113fe7c05e1360ca8418fe Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 8 Sep 2021 00:11:11 +0200 Subject: [PATCH 09/25] =?UTF-8?q?Fix:=20tri=20de=20liste=20h=C3=A9t=C3=A9r?= =?UTF-8?q?og=C3=A8ne?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/scodoc/pe_tagtable.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/scodoc/pe_tagtable.py b/app/scodoc/pe_tagtable.py index dbc6bf036..e32a11735 100644 --- a/app/scodoc/pe_tagtable.py +++ b/app/scodoc/pe_tagtable.py @@ -163,7 +163,7 @@ class TableTag(object): # ***************************************************************************************************************** # ----------------------------------------------------------------------------------------------------------- - def add_moyennesTag(self, tag, listMoyEtCoeff): + def add_moyennesTag(self, tag, listMoyEtCoeff) -> bool: """ Mémorise les moyennes, les coeffs de pondération et les etudid dans resultats avec calcul du rang @@ -181,7 +181,9 @@ class TableTag(object): lesMoyennesTriees = sorted( listMoyEtCoeff, reverse=True, - key=lambda col: col[0] or 0, # remplace les None par des zéros + key=lambda col: col[0] + if isinstance(col[0], float) + else 0, # remplace les None et autres chaines par des zéros ) # triées self.rangs[tag] = notes_table.comp_ranks(lesMoyennesTriees) # les rangs From 5ab0dec6af0a509d01b670f39ea1dc879b1c7894 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 8 Sep 2021 00:13:41 +0200 Subject: [PATCH 10/25] =?UTF-8?q?Timeout=20gunicorn=20pass=C3=A9=20=C3=A0?= =?UTF-8?q?=20600s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tools/etc/scodoc9.service | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/etc/scodoc9.service b/tools/etc/scodoc9.service index 6d0f223ed..8028442cd 100644 --- a/tools/etc/scodoc9.service +++ b/tools/etc/scodoc9.service @@ -20,7 +20,7 @@ User=scodoc Group=scodoc WorkingDirectory=/opt/scodoc #Environment=FLASK_ENV=production -ExecStart=/opt/scodoc/venv/bin/gunicorn -b localhost:8000 -w 4 scodoc:app +ExecStart=/opt/scodoc/venv/bin/gunicorn -b localhost:8000 -w 4 --timeout 600 scodoc:app Restart=always [Install] From 7f92a21b53c4daf17e4a19570714a3504feae0a4 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 8 Sep 2021 00:34:45 +0200 Subject: [PATCH 11/25] =?UTF-8?q?Fixes:=20trombino=5Fcopy=5Fphotos,=20impo?= =?UTF-8?q?rt=20fichiers=20associ=C3=A9s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/scodoc/sco_archives.py | 4 ++-- app/scodoc/sco_groups_view.py | 10 ++++------ app/scodoc/sco_import_etuds.py | 3 +-- app/scodoc/sco_pvjury.py | 4 ++-- app/scodoc/sco_saisie_notes.py | 3 --- app/scodoc/sco_trombino.py | 9 ++++----- app/scodoc/sco_trombino_tours.py | 4 ++-- app/views/absences.py | 12 +++++------- app/views/scolar.py | 5 ++++- 9 files changed, 24 insertions(+), 30 deletions(-) diff --git a/app/scodoc/sco_archives.py b/app/scodoc/sco_archives.py index 108bb055e..9266724f9 100644 --- a/app/scodoc/sco_archives.py +++ b/app/scodoc/sco_archives.py @@ -313,7 +313,7 @@ def do_formsemestre_archive( # tous les inscrits du semestre group_ids = [sco_groups.get_default_group(formsemestre_id)] groups_infos = sco_groups_view.DisplayedGroupsInfos( - group_ids, formsemestre_id=formsemestre_id, REQUEST=REQUEST + group_ids, formsemestre_id=formsemestre_id ) groups_filename = "-" + groups_infos.groups_filename etudids = [m["etudid"] for m in groups_infos.members] @@ -403,7 +403,7 @@ def formsemestre_archive(REQUEST, formsemestre_id, group_ids=[]): # tous les inscrits du semestre group_ids = [sco_groups.get_default_group(formsemestre_id)] groups_infos = sco_groups_view.DisplayedGroupsInfos( - group_ids, formsemestre_id=formsemestre_id, REQUEST=REQUEST + group_ids, formsemestre_id=formsemestre_id ) H = [ diff --git a/app/scodoc/sco_groups_view.py b/app/scodoc/sco_groups_view.py index 9852899c3..7ca1788f1 100644 --- a/app/scodoc/sco_groups_view.py +++ b/app/scodoc/sco_groups_view.py @@ -34,12 +34,12 @@ import collections import datetime import operator -import six.moves.urllib.request, six.moves.urllib.parse, six.moves.urllib.error +import urllib from urllib.parse import parse_qs import time -from flask import url_for, g +from flask import url_for, g, request import app.scodoc.sco_utils as scu from app.scodoc import html_sco_header @@ -86,7 +86,6 @@ def groups_view( group_ids, formsemestre_id=formsemestre_id, etat=etat, - REQUEST=REQUEST, select_all_when_unspecified=True, ) # Formats spéciaux: download direct @@ -301,7 +300,6 @@ class DisplayedGroupsInfos(object): etat=None, select_all_when_unspecified=False, moduleimpl_id=None, # used to find formsemestre when unspecified - REQUEST=None, ): if isinstance(group_ids, int): if group_ids: @@ -334,7 +332,7 @@ class DisplayedGroupsInfos(object): for group_id in group_ids: gq.append("group_ids=" + str(group_id)) self.groups_query_args = "&".join(gq) - self.base_url = REQUEST.URL0 + "?" + self.groups_query_args + self.base_url = request.base_url + "?" + self.groups_query_args self.group_ids = group_ids self.groups = [] groups_titles = [] @@ -918,7 +916,7 @@ def form_choix_saisie_semaine(groups_infos, REQUEST=None): del query_args["head_message"] destination = "%s?%s" % ( REQUEST.URL, - six.moves.urllib.parse.urlencode(query_args, True), + urllib.parse.urlencode(query_args, True), ) destination = destination.replace( "%", "%%" diff --git a/app/scodoc/sco_import_etuds.py b/app/scodoc/sco_import_etuds.py index b9b8b5de5..5b4d0cad1 100644 --- a/app/scodoc/sco_import_etuds.py +++ b/app/scodoc/sco_import_etuds.py @@ -157,7 +157,6 @@ def sco_import_generate_excel_sample( exclude_cols=[], extra_cols=[], group_ids=[], - REQUEST=None, ): """Generates an excel document based on format fmt (format is the result of sco_import_format()) @@ -188,7 +187,7 @@ def sco_import_generate_excel_sample( titles += extra_cols titlesStyles += [style] * len(extra_cols) if group_ids: - groups_infos = sco_groups_view.DisplayedGroupsInfos(group_ids, REQUEST=REQUEST) + groups_infos = sco_groups_view.DisplayedGroupsInfos(group_ids) members = groups_infos.members log( "sco_import_generate_excel_sample: group_ids=%s %d members" diff --git a/app/scodoc/sco_pvjury.py b/app/scodoc/sco_pvjury.py index 3fa5196ae..f06180f4e 100644 --- a/app/scodoc/sco_pvjury.py +++ b/app/scodoc/sco_pvjury.py @@ -622,7 +622,7 @@ def formsemestre_pvjury_pdf(formsemestre_id, group_ids=[], etudid=None, REQUEST= group_ids = [sco_groups.get_default_group(formsemestre_id)] groups_infos = sco_groups_view.DisplayedGroupsInfos( - group_ids, formsemestre_id=formsemestre_id, REQUEST=REQUEST + group_ids, formsemestre_id=formsemestre_id ) etudids = [m["etudid"] for m in groups_infos.members] @@ -800,7 +800,7 @@ def formsemestre_lettres_individuelles(formsemestre_id, group_ids=[], REQUEST=No # tous les inscrits du semestre group_ids = [sco_groups.get_default_group(formsemestre_id)] groups_infos = sco_groups_view.DisplayedGroupsInfos( - group_ids, formsemestre_id=formsemestre_id, REQUEST=REQUEST + group_ids, formsemestre_id=formsemestre_id ) etudids = [m["etudid"] for m in groups_infos.members] diff --git a/app/scodoc/sco_saisie_notes.py b/app/scodoc/sco_saisie_notes.py index da452b3a3..faba5e2c6 100644 --- a/app/scodoc/sco_saisie_notes.py +++ b/app/scodoc/sco_saisie_notes.py @@ -620,7 +620,6 @@ def saisie_notes_tableur(evaluation_id, group_ids=[], REQUEST=None): formsemestre_id=formsemestre_id, select_all_when_unspecified=True, etat=None, - REQUEST=REQUEST, ) H = [ @@ -793,7 +792,6 @@ def feuille_saisie_notes(evaluation_id, group_ids=[], REQUEST=None): formsemestre_id=formsemestre_id, select_all_when_unspecified=True, etat=None, - REQUEST=REQUEST, ) groups = sco_groups.listgroups(groups_infos.group_ids) gr_title_filename = sco_groups.listgroups_filename(groups) @@ -891,7 +889,6 @@ def saisie_notes(evaluation_id, group_ids=[], REQUEST=None): formsemestre_id=formsemestre_id, select_all_when_unspecified=True, etat=None, - REQUEST=REQUEST, ) if E["description"]: diff --git a/app/scodoc/sco_trombino.py b/app/scodoc/sco_trombino.py index 47769baf0..a0f858b0f 100644 --- a/app/scodoc/sco_trombino.py +++ b/app/scodoc/sco_trombino.py @@ -78,7 +78,7 @@ def trombino( etat = None # may be passed as '' # Informations sur les groupes à afficher: groups_infos = sco_groups_view.DisplayedGroupsInfos( - group_ids, formsemestre_id=formsemestre_id, etat=etat, REQUEST=REQUEST + group_ids, formsemestre_id=formsemestre_id, etat=etat ) # @@ -247,7 +247,7 @@ def _trombino_zip(groups_infos): # Copy photos from portal to ScoDoc def trombino_copy_photos(group_ids=[], REQUEST=None, dialog_confirmed=False): "Copy photos from portal to ScoDoc (overwriting local copy)" - groups_infos = sco_groups_view.DisplayedGroupsInfos(group_ids, REQUEST=REQUEST) + groups_infos = sco_groups_view.DisplayedGroupsInfos(group_ids) back_url = "groups_view?%s&curtab=tab-photos" % groups_infos.groups_query_args portal_url = sco_portal_apogee.get_portal_url() @@ -485,14 +485,13 @@ def photos_generate_excel_sample(group_ids=[], REQUEST=None): "photo_filename", ], extra_cols=["fichier_photo"], - REQUEST=REQUEST, ) return sco_excel.send_excel_file(REQUEST, data, "ImportPhotos" + scu.XLSX_SUFFIX) def photos_import_files_form(group_ids=[], REQUEST=None): """Formulaire pour importation photos""" - groups_infos = sco_groups_view.DisplayedGroupsInfos(group_ids, REQUEST=REQUEST) + groups_infos = sco_groups_view.DisplayedGroupsInfos(group_ids) back_url = "groups_view?%s&curtab=tab-photos" % groups_infos.groups_query_args H = [ @@ -541,7 +540,7 @@ def photos_import_files_form(group_ids=[], REQUEST=None): def photos_import_files(group_ids=[], xlsfile=None, zipfile=None, REQUEST=None): """Importation des photos""" - groups_infos = sco_groups_view.DisplayedGroupsInfos(group_ids, REQUEST=REQUEST) + groups_infos = sco_groups_view.DisplayedGroupsInfos(group_ids) back_url = "groups_view?%s&curtab=tab-photos" % groups_infos.groups_query_args filename_title = "fichier_photo" page_title = "Téléchargement des photos des étudiants" diff --git a/app/scodoc/sco_trombino_tours.py b/app/scodoc/sco_trombino_tours.py index b77bf61d0..7a2ec1f03 100644 --- a/app/scodoc/sco_trombino_tours.py +++ b/app/scodoc/sco_trombino_tours.py @@ -61,7 +61,7 @@ def pdf_trombino_tours( """Generation du trombinoscope en fichier PDF""" # Informations sur les groupes à afficher: groups_infos = sco_groups_view.DisplayedGroupsInfos( - group_ids, formsemestre_id=formsemestre_id, REQUEST=REQUEST + group_ids, formsemestre_id=formsemestre_id ) DeptName = sco_preferences.get_preference("DeptName") @@ -296,7 +296,7 @@ def pdf_feuille_releve_absences( # Informations sur les groupes à afficher: groups_infos = sco_groups_view.DisplayedGroupsInfos( - group_ids, formsemestre_id=formsemestre_id, REQUEST=REQUEST + group_ids, formsemestre_id=formsemestre_id ) DeptName = sco_preferences.get_preference("DeptName") diff --git a/app/views/absences.py b/app/views/absences.py index f0dee786a..6216dc7ef 100644 --- a/app/views/absences.py +++ b/app/views/absences.py @@ -312,7 +312,7 @@ def SignaleAbsenceGrHebdo( moduleimpl_id = None groups_infos = sco_groups_view.DisplayedGroupsInfos( - group_ids, moduleimpl_id=moduleimpl_id, REQUEST=REQUEST + group_ids, moduleimpl_id=moduleimpl_id ) if not groups_infos.members: return ( @@ -474,7 +474,7 @@ def SignaleAbsenceGrSemestre( REQUEST=None, ): """Saisie des absences sur une journée sur un semestre (ou intervalle de dates) entier""" - groups_infos = sco_groups_view.DisplayedGroupsInfos(group_ids, REQUEST=REQUEST) + groups_infos = sco_groups_view.DisplayedGroupsInfos(group_ids) if not groups_infos.members: return ( html_sco_header.sco_header(page_title="Saisie des absences") @@ -847,7 +847,7 @@ def EtatAbsencesGr( datedebut = ndb.DateDMYtoISO(debut) datefin = ndb.DateDMYtoISO(fin) # Informations sur les groupes à afficher: - groups_infos = sco_groups_view.DisplayedGroupsInfos(group_ids, REQUEST=REQUEST) + groups_infos = sco_groups_view.DisplayedGroupsInfos(group_ids) formsemestre_id = groups_infos.formsemestre_id sem = groups_infos.formsemestre @@ -971,13 +971,11 @@ ou entrez une date pour visualiser les absents un jour donné : @scodoc @permission_required(Permission.ScoView) @scodoc7func -def EtatAbsencesDate( - group_ids=[], date=None, REQUEST=None # list of groups to display -): +def EtatAbsencesDate(group_ids=[], date=None): # list of groups to display # ported from dtml """Etat des absences pour un groupe à une date donnée""" # Informations sur les groupes à afficher: - groups_infos = sco_groups_view.DisplayedGroupsInfos(group_ids, REQUEST=REQUEST) + groups_infos = sco_groups_view.DisplayedGroupsInfos(group_ids) H = [html_sco_header.sco_header(page_title="Etat des absences")] if date: dateiso = ndb.DateDMYtoISO(date) diff --git a/app/views/scolar.py b/app/views/scolar.py index 54640bea2..41632ff7e 100644 --- a/app/views/scolar.py +++ b/app/views/scolar.py @@ -283,7 +283,10 @@ sco_publish( ) sco_publish( - "/trombino_copy_photos", sco_trombino.trombino_copy_photos, Permission.ScoView + "/trombino_copy_photos", + sco_trombino.trombino_copy_photos, + Permission.ScoView, + methods=["GET", "POST"], ) sco_publish("/groups_view", sco_groups_view.groups_view, Permission.ScoView) From 381e0818446245a46ec870db115f7684ce2928a3 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 8 Sep 2021 00:35:40 +0200 Subject: [PATCH 12/25] version bump --- sco_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sco_version.py b/sco_version.py index d2735ed14..7669307ff 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.0.9" +SCOVERSION = "9.0.10" SCONAME = "ScoDoc" From 72dfc4f49b22be563d192beab2e1f7fbf4c42601 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 8 Sep 2021 23:00:01 +0200 Subject: [PATCH 13/25] Configuration des logos via formulaires --- app/models/preferences.py | 18 ++++ app/scodoc/sco_logos.py | 95 ++++++++++++++++++++ app/scodoc/sco_preferences.py | 4 +- app/scodoc/sco_utils.py | 4 +- app/static/css/scodoc.css | 11 +++ app/templates/configuration.html | 52 +++++++---- app/views/scodoc.py | 145 +++++++++++++++++++++++++++++-- app/views/scolar.py | 70 ++++++++++++++- config.py | 6 +- tools/scodoc_config.py | 7 +- 10 files changed, 374 insertions(+), 38 deletions(-) create mode 100644 app/scodoc/sco_logos.py diff --git a/app/models/preferences.py b/app/models/preferences.py index 65b885082..b04ad0da2 100644 --- a/app/models/preferences.py +++ b/app/models/preferences.py @@ -33,6 +33,17 @@ class ScoDocSiteConfig(db.Model): value = db.Column(db.Text()) BONUS_SPORT = "bonus_sport_func_name" + NAMES = { + BONUS_SPORT: str, + "always_require_ine": bool, + "SCOLAR_FONT": str, + "SCOLAR_FONT_SIZE": str, + "SCOLAR_FONT_SIZE_FOOT": str, + "INSTITUTION_NAME": str, + "INSTITUTION_ADDRESS": str, + "INSTITUTION_CITY": str, + "DEFAULT_PDF_FOOTER_TEMPLATE": str, + } def __init__(self, name, value): self.name = name @@ -41,6 +52,13 @@ class ScoDocSiteConfig(db.Model): def __repr__(self): return f"<{self.__class__.__name__}('{self.name}', '{self.value}')>" + def get_dict(self) -> dict: + "Returns all data as a dict name = value" + return { + c.name: self.NAMES.get(c.name, lambda x: x)(c.value) + for c in ScoDocSiteConfig.query.all() + } + @classmethod def set_bonus_sport_func(cls, func_name): """Record bonus_sport config. diff --git a/app/scodoc/sco_logos.py b/app/scodoc/sco_logos.py new file mode 100644 index 000000000..e29b5183b --- /dev/null +++ b/app/scodoc/sco_logos.py @@ -0,0 +1,95 @@ +# -*- mode: python -*- +# -*- coding: utf-8 -*- + +############################################################################## +# +# Gestion scolarite IUT +# +# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# Emmanuel Viennet emmanuel.viennet@viennet.net +# +############################################################################## + +"""Gestion des images logos (nouveau ScoDoc 9) + +Les logos sont `logo_header.` et `logo_footer.` +avec `ext` membre de LOGOS_IMAGES_ALLOWED_TYPES (= jpg, png) + +SCODOC_LOGOS_DIR /opt/scodoc-data/config/logos +""" +import imghdr +import os + +from flask import abort, current_app + +from app.scodoc import sco_utils as scu + + +def get_logo_filename(logo_type: str, scodoc_dept: str) -> str: + """return full filename for this logo, or "" if not found + an existing file with extension. + logo_type: "header" or "footer" + scodoc-dept: acronym + """ + # Search logos in dept specific dir (/opt/scodoc-data/config/logos/logos_), + # then in config dir /opt/scodoc-data/config/logos/ + for image_dir in ( + scu.SCODOC_LOGOS_DIR + "/logos_" + scodoc_dept, + scu.SCODOC_LOGOS_DIR, # global logos + ): + for suffix in scu.LOGOS_IMAGES_ALLOWED_TYPES: + filename = os.path.join(image_dir, f"logo_{logo_type}.{suffix}") + if os.path.isfile(filename) and os.access(filename, os.R_OK): + return filename + + return "" + + +def guess_image_type(stream) -> str: + "guess image type from header in stream" + header = stream.read(512) + stream.seek(0) + fmt = imghdr.what(None, header) + if not fmt: + return None + return fmt if fmt != "jpeg" else "jpg" + + +def _ensure_directory_exists(filename): + "create enclosing directory if necessary" + directory = os.path.split(filename)[0] + if not os.path.exists(directory): + current_app.logger.info(f"sco_logos creating directory %s", directory) + os.mkdir(directory) + + +def store_image(stream, basename): + img_type = guess_image_type(stream) + if img_type not in scu.LOGOS_IMAGES_ALLOWED_TYPES: + abort(400, "type d'image invalide") + filename = basename + "." + img_type + _ensure_directory_exists(filename) + with open(filename, "wb") as f: + f.write(stream.read()) + current_app.logger.info(f"sco_logos.store_image %s", filename) + # erase other formats if they exists + for extension in set(scu.LOGOS_IMAGES_ALLOWED_TYPES) - set([img_type]): + try: + os.unlink(basename + "." + extension) + except IOError: + pass diff --git a/app/scodoc/sco_preferences.py b/app/scodoc/sco_preferences.py index f46153133..26ff4ec2d 100644 --- a/app/scodoc/sco_preferences.py +++ b/app/scodoc/sco_preferences.py @@ -111,7 +111,7 @@ get_base_preferences(formsemestre_id) """ import flask -from flask import g +from flask import g, url_for from app.models import Departement from app.scodoc import sco_cache @@ -2021,6 +2021,8 @@ class BasePreferences(object): H = [ html_sco_header.sco_header(page_title="Préférences"), "

Préférences globales pour %s

" % scu.ScoURL(), + f"""

modification des logos du département (pour documents pdf)

""", """

Ces paramètres s'appliquent par défaut à tous les semestres, sauf si ceux-ci définissent des valeurs spécifiques.

Attention: cliquez sur "Enregistrer les modifications" en bas de page pour appliquer vos changements !

""", diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index bf6a332a9..2447c4e88 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -232,6 +232,8 @@ if not os.path.exists(SCO_TMP_DIR): os.mkdir(SCO_TMP_DIR, 0o755) # ----- Les logos: /opt/scodoc-data/config/logos SCODOC_LOGOS_DIR = os.path.join(SCODOC_CFG_DIR, "logos") +LOGOS_IMAGES_ALLOWED_TYPES = ("jpg", "jpeg", "png") # remind that PIL does not read pdf + # ----- Les outils distribués SCO_TOOLS_DIR = os.path.join(Config.SCODOC_DIR, "tools") @@ -305,8 +307,6 @@ PDF_MIMETYPE = "application/pdf" XML_MIMETYPE = "text/xml" JSON_MIMETYPE = "application/json" -LOGOS_IMAGES_ALLOWED_TYPES = ("jpg", "png") # remind that PIL does not read pdf - # Admissions des étudiants # Différents types de voies d'admission: # (stocké en texte libre dans la base, mais saisie par menus pour harmoniser) diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css index 01213d548..95b332bd8 100644 --- a/app/static/css/scodoc.css +++ b/app/static/css/scodoc.css @@ -816,11 +816,22 @@ a.discretelink:hover { div.sco_help { margin-top: 12px; + margin-bottom: 3px; font-style: italic; color: navy; background-color: rgb(200,200,220); } +span.wtf-field ul.errors li { + color: red; +} +.configuration_logo div.img-container { + width: 256px; +} +.configuration_logo div.img-container img { + max-width: 100%; +} + p.indent { padding-left: 2em; } diff --git a/app/templates/configuration.html b/app/templates/configuration.html index 1580979c9..6dcf1c51f 100644 --- a/app/templates/configuration.html +++ b/app/templates/configuration.html @@ -2,32 +2,52 @@ {% import 'bootstrap/wtf.html' as wtf %} {% macro render_field(field) %} -
- {{ field.label }} : - {{ field()|safe }} +
+ {{ field.label }} : + {{ field()|safe }} {% if field.errors %} -
    +
      {% for error in field.errors %} -
    • {{ error }}
    • +
    • {{ error }}
    • {% endfor %} -
    +
{% endif %} -
-
+
+
{% endmacro %} {% block app_content %} -

Configuration générale

-

Les paramètres donnés ici s'appliquent à tout ScoDoc (tous les départements).

+{% if scodoc_dept %} +

Logos du département {{ scodoc_dept }}

+{% else %} +

Configuration générale {{ scodoc_dept }}

+{% endif %} -
+ {{ form.hidden_tag() }} + + {% if not scodoc_dept %} +
Les paramètres donnés ici s'appliquent à tout ScoDoc (tous les départements):
+ {{ render_field(form.bonus_sport_func_name)}} - {#

- {{ form.bonus_sport_func_name.label }}
- {{ form.bonus_sport_func_name() }} -

#} + {% endif %} + + +
{{ form.submit() }}
-{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/app/views/scodoc.py b/app/views/scodoc.py index 1d4b77997..d2fd9f9d4 100644 --- a/app/views/scodoc.py +++ b/app/views/scodoc.py @@ -30,19 +30,29 @@ Module main: page d'accueil, avec liste des départements Emmanuel Viennet, 2021 """ +import os + import flask -from flask import flash, url_for, redirect, render_template +from flask import abort, flash, url_for, redirect, render_template, send_file from flask import request from flask.app import Flask from flask_login.utils import login_required from flask_wtf import FlaskForm +from flask_wtf.file import FileField, FileAllowed +from werkzeug.exceptions import BadRequest, NotFound from wtforms import SelectField, SubmitField +from wtforms.fields import IntegerField +from wtforms.fields.simple import BooleanField, StringField, TextAreaField +from wtforms.validators import ValidationError, DataRequired, Email, EqualTo -# from wtforms.validators import DataRequired - -from app.models import Departement, ScoDocSiteConfig +import app +from app.models import Departement, Identite +from app.models import FormSemestre, NotesFormsemestreInscription +from app.models import ScoDocSiteConfig import sco_version +from app.scodoc import sco_logos from app.scodoc import sco_find_etud +from app.scodoc import sco_utils as scu from app.decorators import admin_required from app.scodoc.sco_permissions import Permission from app.views import scodoc_bp as bp @@ -72,13 +82,56 @@ def table_etud_in_accessible_depts(): return sco_find_etud.table_etud_in_accessible_depts(expnom=request.form["expnom"]) +@bp.route("/ScoDoc/get_etud_dept") +@login_required +def get_etud_dept(): + """Returns the dept acronym (eg "GEII") of an etud (identified by etudid, + code_nip ou code_ine in the request). + API: ramène la chaine brute, sans JSON ou XML. + """ + if "etudid" in request.args: + # zero ou une réponse: + etuds = [Identite.query.get(request.args["etudid"])] + elif "code_nip" in request.args: + # il peut y avoir plusieurs réponses si l'étudiant est passé par plusieurs départements + etuds = Identite.query.filter_by(code_nip=request.args["code_nip"]).all() + elif "code_ine" in request.args: + etuds = Identite.query.filter_by(code_nip=request.args["code_ine"]).all() + else: + raise BadRequest( + "missing argument (expected one among: etudid, code_nip or code_ine)" + ) + if not etuds: + raise NotFound("student not found") + elif len(etuds) == 1: + last_etud = etuds[0] + else: + # inscriptions dans plusieurs departements: cherche la plus recente + last_etud = None + last_date = None + for etud in etuds: + inscriptions = NotesFormsemestreInscription.query.filter_by( + etudid=etud.id + ).all() + for ins in inscriptions: + date_fin = FormSemestre.query.get(ins.formsemestre_id).date_fin + if (last_date is None) or date_fin > last_date: + last_date = date_fin + last_etud = etud + if not last_etud: + # est présent dans plusieurs semestres mais inscrit dans aucun ! + # le choix a peu d'importance... + last_etud = etuds[-1] + + return Departement.query.get(last_etud.dept_id).acronym + + # ---- CONFIGURATION class ScoDocConfigurationForm(FlaskForm): "Panneau de configuration général" - # très préliminaire ;-) - # On veut y mettre la fonction bonus et ensuite les logos + bonus_sport_func_name = SelectField( label="Fonction de calcul des bonus sport&culture", choices=[ @@ -86,28 +139,104 @@ class ScoDocConfigurationForm(FlaskForm): for x in ScoDocSiteConfig.get_bonus_sport_func_names() ], ) + + logo_header = FileField( + label="Modifier l'image:", + description="logo placé en haut des documents PDF", + validators=[ + FileAllowed( + scu.LOGOS_IMAGES_ALLOWED_TYPES, + f"n'accepte que les fichiers image {','.join([e for e in scu.LOGOS_IMAGES_ALLOWED_TYPES])}", + ) + ], + ) + + logo_footer = FileField( + label="Modifier l'image:", + description="logo placé en pied des documents PDF", + validators=[ + FileAllowed( + scu.LOGOS_IMAGES_ALLOWED_TYPES, + f"n'accepte que les fichiers image {','.join([e for e in scu.LOGOS_IMAGES_ALLOWED_TYPES])}", + ) + ], + ) + submit = SubmitField("Enregistrer") +# Notes pour variables config: (valeurs par défaut des paramètres de département) +# Chaines simples +# SCOLAR_FONT = "Helvetica" +# SCOLAR_FONT_SIZE = 10 +# SCOLAR_FONT_SIZE_FOOT = 6 +# INSTITUTION_NAME = "Institut Universitaire de Technologie - Université Georges Perec" +# INSTITUTION_ADDRESS = "Web www.sor.bonne.top - 11, rue Simon Crubelier - 75017 Paris" +# INSTITUTION_CITY = "Paris" +# Textareas: +# DEFAULT_PDF_FOOTER_TEMPLATE = "Edité par %(scodoc_name)s le %(day)s/%(month)s/%(year)s à %(hour)sh%(minute)s sur %(server_url)s" + +# Booléens +# always_require_ine + +# Logos: +# LOGO_FOOTER*, LOGO_HEADER* + + @bp.route("/ScoDoc/configuration", methods=["GET", "POST"]) @admin_required def configuration(): "Panneau de configuration général" form = ScoDocConfigurationForm( - bonus_sport_func_name=ScoDocSiteConfig.get_bonus_sport_func_name() + bonus_sport_func_name=ScoDocSiteConfig.get_bonus_sport_func_name(), ) if form.validate_on_submit(): ScoDocSiteConfig.set_bonus_sport_func(form.bonus_sport_func_name.data) + if form.logo_header.data: + sco_logos.store_image( + form.logo_header.data, os.path.join(scu.SCODOC_LOGOS_DIR, "logo_header") + ) + if form.logo_footer.data: + sco_logos.store_image( + form.logo_footer.data, os.path.join(scu.SCODOC_LOGOS_DIR, "logo_footer") + ) + app.clear_scodoc_cache() flash(f"Configuration enregistrée") return redirect(url_for("scodoc.index")) + return render_template( "configuration.html", title="Configuration ScoDoc", form=form, - # bonus_sport_func_name=ScoDocSiteConfig.get_bonus_sport_func(), + scodoc_dept=None, ) +def _return_logo(logo_type="header", scodoc_dept=""): + # stockée dans /opt/scodoc-data/config/logos donc servie manuellement ici + filename = sco_logos.get_logo_filename(logo_type, scodoc_dept) + if filename: + extension = os.path.splitext(filename)[1] + return send_file(filename, mimetype=f"image/{extension}") + else: + return "" + + +@bp.route("/ScoDoc/logo_header") +@bp.route("/ScoDoc//logo_header") +def logo_header(scodoc_dept=""): + "Image logo header" + # "/opt/scodoc-data/config/logos/logo_header") + return _return_logo(logo_type="header", scodoc_dept=scodoc_dept) + + +@bp.route("/ScoDoc/logo_footer") +@bp.route("/ScoDoc//logo_footer") +def logo_footer(scodoc_dept=""): + "Image logo footer" + return _return_logo(logo_type="footer", scodoc_dept=scodoc_dept) + + # essais # @bp.route("/testlog") # def testlog(): diff --git a/app/views/scolar.py b/app/views/scolar.py index 41632ff7e..733716193 100644 --- a/app/views/scolar.py +++ b/app/views/scolar.py @@ -30,7 +30,7 @@ issu de ScoDoc7 / ZScolar.py Emmanuel Viennet, 2021 """ - +import os import sys import time @@ -40,9 +40,12 @@ from zipfile import ZipFile import psycopg2 import flask -from flask import jsonify, url_for +from flask import jsonify, url_for, flash, redirect, render_template from flask import current_app, g, request from flask_login import current_user +from flask_wtf import FlaskForm +from flask_wtf.file import FileField, FileAllowed +from wtforms import SubmitField from config import Config from app.decorators import ( @@ -71,8 +74,8 @@ from app.scodoc.sco_exceptions import ( ) from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message import sco_version +import app from app.scodoc.gen_tables import GenTable - from app.scodoc import html_sco_header from app.scodoc import html_sidebar from app.scodoc import imageresize @@ -94,6 +97,7 @@ from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_groups from app.scodoc import sco_groups_edit from app.scodoc import sco_groups_view +from app.scodoc import sco_logos from app.scodoc import sco_news from app.scodoc import sco_page_etud from app.scodoc import sco_parcours_dut @@ -201,6 +205,66 @@ def doc_preferences(REQUEST): return sco_preferences.doc_preferences() +class DeptLogosConfigurationForm(FlaskForm): + "Panneau de configuration logos dept" + + logo_header = FileField( + label="Modifier l'image:", + description="logo placé en haut des documents PDF", + validators=[ + FileAllowed( + scu.LOGOS_IMAGES_ALLOWED_TYPES, + f"n'accepte que les fichiers image {','.join([e for e in scu.LOGOS_IMAGES_ALLOWED_TYPES])}", + ) + ], + ) + + logo_footer = FileField( + label="Modifier l'image:", + description="logo placé en pied des documents PDF", + validators=[ + FileAllowed( + scu.LOGOS_IMAGES_ALLOWED_TYPES, + f"n'accepte que les fichiers image {','.join([e for e in scu.LOGOS_IMAGES_ALLOWED_TYPES])}", + ) + ], + ) + + submit = SubmitField("Enregistrer") + + +@bp.route("/config_logos", methods=["GET", "POST"]) +@permission_required(Permission.ScoChangePreferences) +def config_logos(scodoc_dept): + "Panneau de configuration général" + form = DeptLogosConfigurationForm() + if form.validate_on_submit(): + if form.logo_header.data: + sco_logos.store_image( + form.logo_header.data, + os.path.join( + scu.SCODOC_LOGOS_DIR, "logos_" + scodoc_dept, "logo_header" + ), + ) + if form.logo_footer.data: + sco_logos.store_image( + form.logo_footer.data, + os.path.join( + scu.SCODOC_LOGOS_DIR, "logos_" + scodoc_dept, "logo_footer" + ), + ) + app.clear_scodoc_cache() + flash(f"Logos enregistrés") + return redirect(url_for("scolar.index_html", scodoc_dept=scodoc_dept)) + + return render_template( + "configuration.html", + title="Configuration Logos du département", + form=form, + scodoc_dept=scodoc_dept, + ) + + # -------------------------------------------------------------------- # # ETUDIANTS diff --git a/config.py b/config.py index aed798d60..2e424a58d 100755 --- a/config.py +++ b/config.py @@ -30,9 +30,9 @@ class Config: SCODOC_DIR = os.environ.get("SCODOC_DIR", "/opt/scodoc") SCODOC_VAR_DIR = os.environ.get("SCODOC_VAR_DIR", "/opt/scodoc-data") SCODOC_LOG_FILE = os.path.join(SCODOC_VAR_DIR, "log", "scodoc.log") - # For legacy ScoDoc7 installs: postgresql user - SCODOC7_SQL_USER = os.environ.get("SCODOC7_SQL_USER", "www-data") - DEFAULT_SQL_PORT = os.environ.get("DEFAULT_SQL_PORT", "5432") + # + MAX_CONTENT_LENGTH = 10 * 1024 * 1024 # Flask uploads + # STATIC_URL_PATH = "/ScoDoc/static" # static_folder = "stat" # SERVER_NAME = os.environ.get("SERVER_NAME") diff --git a/tools/scodoc_config.py b/tools/scodoc_config.py index 9c33c9f0d..2937dfd4a 100644 --- a/tools/scodoc_config.py +++ b/tools/scodoc_config.py @@ -23,10 +23,6 @@ CONFIG = CFG() CONFIG.always_require_ine = 0 # set to 1 if you want to require INE -# The base URL, use only if you are behind a proxy -# eg "https://scodoc.example.net/ScoDoc" -CONFIG.ABSOLUTE_URL = "" - # # ------------- Documents PDF ------------- # @@ -78,6 +74,7 @@ CONFIG.DEFAULT_PDF_FOOTER_TEMPLATE = "Edité par %(scodoc_name)s le %(day)s/%(mo # # - règle "LMD": capitalisation uniquement des UE avec moy. > 10 +# XXX à revoir pour le BUT: variable à intégrer aux parcours CONFIG.CAPITALIZE_ALL_UES = ( True # si vrai, capitalise toutes les UE des semestres validés (règle "LMD"). ) @@ -86,7 +83,7 @@ CONFIG.CAPITALIZE_ALL_UES = ( # # ----------------------------------------------------- # -# -------------- Personnalisation des pages +# -------------- Personnalisation des pages (DEPRECATED) # # ----------------------------------------------------- # Nom (chemin complet) d'un fichier .html à inclure juste après le From 829d5d8b2e590cb01e27f07c91028aeab1efaaf5 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 8 Sep 2021 23:10:28 +0200 Subject: [PATCH 14/25] =?UTF-8?q?fix:=20ajout=20semestre=20=C3=A0=20semset?= =?UTF-8?q?=20(exports=20apog=C3=A9e)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/scodoc/sco_semset.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/scodoc/sco_semset.py b/app/scodoc/sco_semset.py index b6d3abc26..1f35afae7 100644 --- a/app/scodoc/sco_semset.py +++ b/app/scodoc/sco_semset.py @@ -158,11 +158,10 @@ class SemSet(dict): ndb.SimpleQuery( """INSERT INTO notes_semset_formsemestre - (dept_id, id, semset_id) - VALUES (%(dept_id)s, %(formsemestre_id)s, %(semset_id)s) + (id, semset_id) + VALUES (%(formsemestre_id)s, %(semset_id)s) """, { - "dept_id": g.scodoc_dept_id, "formsemestre_id": formsemestre_id, "semset_id": self.semset_id, }, From c6f6f45e0d12f4a75e1cf80654c9481ec7260dd7 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 9 Sep 2021 07:59:28 +0200 Subject: [PATCH 15/25] removed up_to_date checking --- app/scodoc/sco_up_to_date.py | 93 ++---------------------------------- sco_version.py | 2 +- 2 files changed, 5 insertions(+), 90 deletions(-) diff --git a/app/scodoc/sco_up_to_date.py b/app/scodoc/sco_up_to_date.py index cab38a63c..cd473f97d 100644 --- a/app/scodoc/sco_up_to_date.py +++ b/app/scodoc/sco_up_to_date.py @@ -28,104 +28,19 @@ """ Verification version logiciel vs version "stable" sur serveur N'effectue pas la mise à jour automatiquement, mais permet un affichage d'avertissement. + + Désactivé temporairement pour ScoDoc 9. """ -import datetime -import app.scodoc.sco_utils as scu -from app import log - -# Appel renvoyant la subversion "stable" -# La notion de "stable" est juste là pour éviter d'afficher trop frequemment -# des avertissements de mise à jour: on veut pouvoir inciter à mettre à jour lors de -# correctifs majeurs. - -GET_VER_URL = "http://scodoc.iutv.univ-paris13.fr/scodoc-installmgr/last_stable_version" - - -def get_last_stable_version(): - """request last stable version number from server - (returns string as given by server, empty if failure) - (do not wait server answer more than 3 seconds) - """ - global _LAST_UP_TO_DATE_REQUEST - ans = scu.query_portal( - GET_VER_URL, msg="ScoDoc version server", timeout=3 - ) # sco_utils - if ans: - ans = ans.strip() - _LAST_UP_TO_DATE_REQUEST = datetime.datetime.now() - log( - 'get_last_stable_version: updated at %s, answer="%s"' - % (_LAST_UP_TO_DATE_REQUEST, ans) - ) - return ans - - -_LAST_UP_TO_DATE_REQUEST = None # datetime of last request to server -_UP_TO_DATE = True # cached result (limit requests to 1 per day) -_UP_TO_DATE_MSG = "" +from flask import current_app def is_up_to_date(): """True if up_to_date Returns status, message """ - log("Warning: is_up_to_date not implemented for ScoDoc8") + current_app.logger.debug("Warning: is_up_to_date not implemented for ScoDoc9") return True, "unimplemented" - # global _LAST_UP_TO_DATE_REQUEST, _UP_TO_DATE, _UP_TO_DATE_MSG - # if _LAST_UP_TO_DATE_REQUEST and ( - # datetime.datetime.now() - _LAST_UP_TO_DATE_REQUEST - # ) < datetime.timedelta(1): - # # requete deja effectuee aujourd'hui: - # return _UP_TO_DATE, _UP_TO_DATE_MSG - - # last_stable_ver = get_last_stable_version() - # cur_ver = scu.get_svn_version(scu.SCO_SRC_DIR) # in sco_utils - # cur_ver2 = cur_ver - # cur_ver_num = -1 - # # Convert versions to integers: - # try: - # # cur_ver can be "1234" or "1234M' or '1234:1245M'... - # fs = cur_ver.split(":", 1) - # if len(fs) > 1: - # cur_ver2 = fs[-1] - # m = re.match(r"([0-9]*)", cur_ver2) - # if not m: - # raise ValueError( - # "invalid svn version" - # ) # should never occur, regexp always (maybe empty) match - # cur_ver_num = int(m.group(1)) - # except: - # log('Warning: no numeric subversion ! (cur_ver="%s")' % cur_ver) - # return _UP_TO_DATE, _UP_TO_DATE_MSG # silently ignore misconfiguration ? - # try: - # last_stable_ver_num = int(last_stable_ver) - # except: - # log("Warning: last_stable_version returned by server is invalid !") - # return ( - # _UP_TO_DATE, - # _UP_TO_DATE_MSG, - # ) # should ignore this error (maybe server is unreachable) - # # - # if cur_ver_num < last_stable_ver_num: - # _UP_TO_DATE = False - # _UP_TO_DATE_MSG = "Version %s disponible (version %s installée)" % ( - # last_stable_ver, - # cur_ver_num, - # ) - # log( - # "Warning: ScoDoc installation is not up-to-date, should upgrade\n%s" - # % _UP_TO_DATE_MSG - # ) - # else: - # _UP_TO_DATE = True - # _UP_TO_DATE_MSG = "" - # log( - # "ScoDoc is up-to-date (cur_ver: %s, using %s=%s)" - # % (cur_ver, cur_ver2, cur_ver_num) - # ) - - # return _UP_TO_DATE, _UP_TO_DATE_MSG def html_up_to_date_box(): diff --git a/sco_version.py b/sco_version.py index 7669307ff..2e77dbe67 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.0.10" +SCOVERSION = "9.0.11" SCONAME = "ScoDoc" From 4ac076ec6c2efc15aa42936c62057f7d8079c1fb Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 9 Sep 2021 08:03:43 +0200 Subject: [PATCH 16/25] =?UTF-8?q?Fix:=20recherche=20=C3=A9tudiant?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/__init__.py | 4 +--- app/scodoc/sco_find_etud.py | 23 ++++++++++++++--------- app/templates/scodoc.html | 2 +- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/app/__init__.py b/app/__init__.py index 9d450cd78..c0cbdad51 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -190,9 +190,7 @@ def create_app(config_class=DevConfig): sco_bulletins_generator.register_bulletin_class(BulletinGeneratorLegacy) sco_bulletins_generator.register_bulletin_class(BulletinGeneratorStandard) sco_bulletins_generator.register_bulletin_class(BulletinGeneratorUCAC) - app.logger.info( - f"registered bulletin classes {[ k for k in sco_bulletins_generator.BULLETIN_CLASSES ]}" - ) + return app diff --git a/app/scodoc/sco_find_etud.py b/app/scodoc/sco_find_etud.py index 2d828ee6c..13426beac 100644 --- a/app/scodoc/sco_find_etud.py +++ b/app/scodoc/sco_find_etud.py @@ -120,10 +120,11 @@ def search_etud_in_dept(expnom=""): if etudid is not None: etuds = sco_etud.get_etud_info(filled=True, etudid=expnom) if (etudid is None) or len(etuds) != 1: - if scu.is_valid_code_nip(expnom): - etuds = search_etuds_infos(code_nip=expnom) + expnom_str = str(expnom) + if scu.is_valid_code_nip(expnom_str): + etuds = search_etuds_infos(code_nip=expnom_str) else: - etuds = search_etuds_infos(expnom=expnom) + etuds = search_etuds_infos(expnom=expnom_str) else: etuds = [] # si expnom est trop court, n'affiche rien @@ -151,7 +152,7 @@ def search_etud_in_dept(expnom=""): H = [ html_sco_header.sco_header( page_title="Recherche d'un étudiant", - no_side_bar=True, + no_side_bar=False, init_qtip=True, javascripts=["js/etud_info.js"], ) @@ -250,10 +251,12 @@ def search_etud_by_name(term: str) -> list: r = ndb.SimpleDictFetch( """SELECT nom, prenom, code_nip FROM identite - WHERE code_nip - LIKE %(beginning)s ORDER BY nom + WHERE + dept_id = %(dept_id)s + AND code_nip LIKE %(beginning)s + ORDER BY nom """, - {"beginning": term + "%"}, + {"beginning": term + "%", "dept_id": g.scodoc_dept_id}, ) data = [ { @@ -267,10 +270,12 @@ def search_etud_by_name(term: str) -> list: r = ndb.SimpleDictFetch( """SELECT id AS etudid, nom, prenom FROM identite - WHERE nom LIKE %(beginning)s + WHERE + dept_id = %(dept_id)s + AND nom LIKE %(beginning)s ORDER BY nom """, - {"beginning": term + "%"}, + {"beginning": term + "%", "dept_id": g.scodoc_dept_id}, ) data = [ diff --git a/app/templates/scodoc.html b/app/templates/scodoc.html index 240389bb4..23826e3c0 100644 --- a/app/templates/scodoc.html +++ b/app/templates/scodoc.html @@ -30,7 +30,7 @@

{% if current_user.is_authenticated %} -
+ Chercher étudiant: From be224b957668f70cf46ae3e8d1a70efb0fe697de Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 9 Sep 2021 12:49:23 +0200 Subject: [PATCH 17/25] =?UTF-8?q?API=20v9:=20premi=C3=A8re=20esquisse?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/__init__.py | 2 ++ app/api/__init__.py | 8 ++++++ app/api/auth.py | 53 ++++++++++++++++++++++++++++++++++++++ app/api/errors.py | 37 ++++++++++++++++++++++++++ app/api/sco_api.py | 46 +++++++++++++++++++++++++++++++++ app/api/tokens.py | 20 ++++++++++++++ app/auth/routes.py | 2 ++ app/models/departements.py | 10 +++++++ misc/example-api-1.py | 4 +++ requirements-3.9.txt | 1 + sco_version.py | 2 +- 11 files changed, 184 insertions(+), 1 deletion(-) create mode 100644 app/api/__init__.py create mode 100644 app/api/auth.py create mode 100644 app/api/errors.py create mode 100644 app/api/sco_api.py create mode 100644 app/api/tokens.py diff --git a/app/__init__.py b/app/__init__.py index c0cbdad51..a913f57ec 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -117,6 +117,7 @@ def create_app(config_class=DevConfig): from app.views import notes_bp from app.views import users_bp from app.views import absences_bp + from app.api import bp as api_bp # https://scodoc.fr/ScoDoc app.register_blueprint(scodoc_bp) @@ -130,6 +131,7 @@ def create_app(config_class=DevConfig): app.register_blueprint( absences_bp, url_prefix="/ScoDoc//Scolarite/Absences" ) + app.register_blueprint(api_bp, url_prefix="/ScoDoc/api") scodoc_exc_formatter = RequestFormatter( "[%(asctime)s] %(remote_addr)s requested %(url)s\n" "%(levelname)s in %(module)s: %(message)s" diff --git a/app/api/__init__.py b/app/api/__init__.py new file mode 100644 index 000000000..34ebbc77a --- /dev/null +++ b/app/api/__init__.py @@ -0,0 +1,8 @@ +"""api.__init__ +""" + +from flask import Blueprint + +bp = Blueprint("api", __name__) + +from app.api import sco_api diff --git a/app/api/auth.py b/app/api/auth.py new file mode 100644 index 000000000..0226976cd --- /dev/null +++ b/app/api/auth.py @@ -0,0 +1,53 @@ +# -*- coding: UTF-8 -* +# Authentication code borrowed from Miguel Grinberg's Mega Tutorial +# (see https://github.com/miguelgrinberg/microblog) + +# Under The MIT License (MIT) + +# Copyright (c) 2017 Miguel Grinberg + +# Permission is hereby granted, free of charge, to any person obtaining a copy of +# this software and associated documentation files (the "Software"), to deal in +# the Software without restriction, including without limitation the rights to +# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +# the Software, and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +from flask_httpauth import HTTPBasicAuth, HTTPTokenAuth +from app.auth.models import User +from app.api.errors import error_response + +basic_auth = HTTPBasicAuth() +token_auth = HTTPTokenAuth() + + +@basic_auth.verify_password +def verify_password(username, password): + user = User.query.filter_by(username=username).first() + if user and user.check_password(password): + return user + + +@basic_auth.error_handler +def basic_auth_error(status): + return error_response(status) + + +@token_auth.verify_token +def verify_token(token): + return User.check_token(token) if token else None + + +@token_auth.error_handler +def token_auth_error(status): + return error_response(status) diff --git a/app/api/errors.py b/app/api/errors.py new file mode 100644 index 000000000..ed8d0f3f6 --- /dev/null +++ b/app/api/errors.py @@ -0,0 +1,37 @@ +# Authentication code borrowed from Miguel Grinberg's Mega Tutorial +# (see https://github.com/miguelgrinberg/microblog) + +# Under The MIT License (MIT) + +# Copyright (c) 2017 Miguel Grinberg + +# Permission is hereby granted, free of charge, to any person obtaining a copy of +# this software and associated documentation files (the "Software"), to deal in +# the Software without restriction, including without limitation the rights to +# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +# the Software, and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.from flask import jsonify +from werkzeug.http import HTTP_STATUS_CODES + + +def error_response(status_code, message=None): + payload = {"error": HTTP_STATUS_CODES.get(status_code, "Unknown error")} + if message: + payload["message"] = message + response = jsonify(payload) + response.status_code = status_code + return response + + +def bad_request(message): + return error_response(400, message) diff --git a/app/api/sco_api.py b/app/api/sco_api.py new file mode 100644 index 000000000..02b35090d --- /dev/null +++ b/app/api/sco_api.py @@ -0,0 +1,46 @@ +# -*- mode: python -*- +# -*- coding: utf-8 -*- + +############################################################################## +# +# Gestion scolarite IUT +# +# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# Emmanuel Viennet emmanuel.viennet@viennet.net +# +############################################################################## + +"""API ScoDoc 9 +""" +# PAS ENCORE IMPLEMENTEE, juste un essai + +from flask import jsonify, request, url_for, abort +from app import db +from app.api import bp +from app.api.auth import token_auth +from app.api.errors import bad_request + +from app import models + + +@bp.route("/ScoDoc/api/list_depts", methods=["GET"]) +@token_auth.login_required +def list_depts(): + depts = models.Departement.query.filter_by(visible=True).all() + data = {"items": [d.to_dict() for d in depts]} + return jsonify(data) diff --git a/app/api/tokens.py b/app/api/tokens.py new file mode 100644 index 000000000..f36ec7b0e --- /dev/null +++ b/app/api/tokens.py @@ -0,0 +1,20 @@ +from flask import jsonify +from app import db +from app.api import bp +from app.api.auth import basic_auth, token_auth + + +@bp.route("/tokens", methods=["POST"]) +@basic_auth.login_required +def get_token(): + token = basic_auth.current_user().get_token() + db.session.commit() + return jsonify({"token": token}) + + +@bp.route("/tokens", methods=["DELETE"]) +@token_auth.login_required +def revoke_token(): + token_auth.current_user().revoke_token() + db.session.commit() + return "", 204 diff --git a/app/auth/routes.py b/app/auth/routes.py index 42cdc8e69..7b1712f00 100644 --- a/app/auth/routes.py +++ b/app/auth/routes.py @@ -37,9 +37,11 @@ def login(): if form.validate_on_submit(): user = User.query.filter_by(user_name=form.user_name.data).first() if user is None or not user.check_password(form.password.data): + current_app.logger.info("login: invalid (%s)", form.user_name.data) flash(_("Invalid user name or password")) return redirect(url_for("auth.login")) login_user(user, remember=form.remember_me.data) + current_app.logger.info("login: success (%s)", form.user_name.data) next_page = request.args.get("next") if not next_page or url_parse(next_page).netloc != "": next_page = url_for("scodoc.index") diff --git a/app/models/departements.py b/app/models/departements.py index 1dee2ca0b..c0a928e36 100644 --- a/app/models/departements.py +++ b/app/models/departements.py @@ -34,3 +34,13 @@ class Departement(db.Model): def __repr__(self): return f"" + + def to_dict(self): + data = { + "id": self.id, + "acronym": self.acronym, + "description": self.description, + "visible": self.visible, + "date_creation": self.date_creation, + } + return data diff --git a/misc/example-api-1.py b/misc/example-api-1.py index d823aac7c..171365e58 100644 --- a/misc/example-api-1.py +++ b/misc/example-api-1.py @@ -52,6 +52,10 @@ def POST(s, path, data, errmsg=None): # --- Ouverture session (login) s = requests.Session() +s.post( + "https://deb11.viennet.net/api/auth/login", + data={"user_name": USER, "password": PASSWORD}, +) r = s.get(BASEURL, auth=(USER, PASSWORD), verify=CHECK_CERTIFICATE) if r.status_code != 200: raise ScoError("erreur de connection: vérifier adresse et identifiants") diff --git a/requirements-3.9.txt b/requirements-3.9.txt index 0ba9dcbf7..682b05053 100755 --- a/requirements-3.9.txt +++ b/requirements-3.9.txt @@ -18,6 +18,7 @@ Flask==2.0.1 Flask-Babel==2.0.0 Flask-Bootstrap==3.3.7.1 Flask-Caching==1.10.1 +Flask-HTTPAuth==4.4.0 Flask-Login==0.5.0 Flask-Mail==0.9.1 Flask-Migrate==3.1.0 diff --git a/sco_version.py b/sco_version.py index 2e77dbe67..fda1b3c7f 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.0.11" +SCOVERSION = "9.0.12" SCONAME = "ScoDoc" From 9fd33cf6587f271358b9d5fe3a9d3e78eaec4898 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 9 Sep 2021 16:11:05 +0200 Subject: [PATCH 18/25] =?UTF-8?q?Acc=C3=A8s=20compatibles=20aux=20ancienne?= =?UTF-8?q?s=20fonctions=20API=20ScoDoc=207?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/sco_api.py | 10 ++++++++++ app/decorators.py | 44 +++++++++++++++++++++++++++++++++++++++++++ app/views/absences.py | 17 ++++++++++++++--- app/views/notes.py | 36 +++++++++++++++++++++++++++++------ app/views/scodoc.py | 11 ++++++++--- app/views/scolar.py | 3 ++- misc/example-api-1.py | 4 ++-- 7 files changed, 110 insertions(+), 15 deletions(-) diff --git a/app/api/sco_api.py b/app/api/sco_api.py index 02b35090d..46be85a7a 100644 --- a/app/api/sco_api.py +++ b/app/api/sco_api.py @@ -28,6 +28,16 @@ """API ScoDoc 9 """ # PAS ENCORE IMPLEMENTEE, juste un essai +# Pour P. Bouron, il faudrait en priorité l'équivalent de +# Scolarite/Notes/do_moduleimpl_withmodule_list +# Scolarite/Notes/evaluation_create +# Scolarite/Notes/evaluation_delete +# Scolarite/Notes/formation_list +# Scolarite/Notes/formsemestre_list +# Scolarite/Notes/formsemestre_partition_list +# Scolarite/Notes/groups_view +# Scolarite/Notes/moduleimpl_status +# Scolarite/setGroups from flask import jsonify, request, url_for, abort from app import db diff --git a/app/decorators.py b/app/decorators.py index a1a91f484..3696d56ca 100644 --- a/app/decorators.py +++ b/app/decorators.py @@ -16,8 +16,10 @@ from flask import request from flask_login import current_user from flask_login import login_required from flask import current_app +import flask_login import app +from app.auth.models import User class ZUser(object): @@ -141,6 +143,48 @@ def permission_required(permission): return decorator +def permission_required_compat_scodoc7(permission): + """Décorateur pour les fonctions utilisée comme API dans ScoDoc 7 + Comme @permission_required mais autorise de passer directement + les informations d'auth en paramètres: + __ac_name, __ac_password + """ + + def decorator(f): + @wraps(f) + def decorated_function(*args, **kwargs): + # current_app.logger.warning("PERMISSION; kwargs=%s" % str(kwargs)) + # cherche les paramètre d'auth: + auth_ok = False + if request.method == "GET": + user_name = request.args.get("__ac_name") + user_password = request.args.get("__ac_password") + elif request.method == "POST": + user_name = request.form.get("__ac_name") + user_password = request.form.get("__ac_password") + else: + abort(405) # method not allowed + if user_name and user_password: + u = User.query.filter_by(user_name=user_name).first() + if u and u.check_password(user_password): + auth_ok = True + flask_login.login_user(u) + + # reprend le chemin classique: + scodoc_dept = getattr(g, "scodoc_dept", None) + + if not current_user.has_permission(permission, scodoc_dept): + abort(403) + if auth_ok: + return f(*args, **kwargs) + else: + return login_required(f)(*args, **kwargs) + + return decorated_function + + return decorator + + def admin_required(f): from app.auth.models import Permission diff --git a/app/views/absences.py b/app/views/absences.py index 6216dc7ef..6c84fec8f 100644 --- a/app/views/absences.py +++ b/app/views/absences.py @@ -68,6 +68,7 @@ from app.decorators import ( permission_required, admin_required, login_required, + permission_required_compat_scodoc7, ) from app.views import absences_bp as bp @@ -1236,7 +1237,7 @@ def listeBilletsEtud(etudid=False, REQUEST=None, format="html"): @bp.route("/XMLgetBilletsEtud") @scodoc -@permission_required(Permission.ScoView) +@permission_required_compat_scodoc7(Permission.ScoView) @scodoc7func def XMLgetBilletsEtud(etudid=False, REQUEST=None): """Liste billets pour un etudiant""" @@ -1250,7 +1251,7 @@ def XMLgetBilletsEtud(etudid=False, REQUEST=None): @bp.route("/listeBillets") @scodoc -@permission_required(Permission.ScoView) +@permission_required_compat_scodoc7(Permission.ScoView) @scodoc7func def listeBillets(REQUEST=None): """Page liste des billets non traités et formulaire recherche d'un billet""" @@ -1459,9 +1460,19 @@ def ProcessBilletAbsenceForm(billet_id, REQUEST=None): return "\n".join(H) + html_sco_header.sco_footer() +# @bp.route("/essai_api7") +# @scodoc +# @permission_required_compat_scodoc7(Permission.ScoView) +# @scodoc7func +# def essai_api7(x="xxx"): +# "un essai" +# log("arfffffffffffffffffff") +# return "OK OK x=" + str(x) + + @bp.route("/XMLgetAbsEtud") @scodoc -@permission_required(Permission.ScoView) +@permission_required_compat_scodoc7(Permission.ScoView) @scodoc7func def XMLgetAbsEtud(beg_date="", end_date="", REQUEST=None): """returns list of absences in date interval""" diff --git a/app/views/notes.py b/app/views/notes.py index 7e80550b5..7fbc63184 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -50,6 +50,7 @@ from app.decorators import ( scodoc, scodoc7func, permission_required, + permission_required_compat_scodoc7, admin_required, login_required, ) @@ -252,11 +253,34 @@ sco_publish( Permission.ScoChangeFormation, methods=["GET", "POST"], ) -sco_publish( - "/formsemestre_bulletinetud", - sco_bulletins.formsemestre_bulletinetud, - Permission.ScoView, -) + + +@bp.route("formsemestre_bulletinetud") +@scodoc +@permission_required_compat_scodoc7(Permission.ScoView) +@scodoc7func +def formsemestre_bulletinetud( + etudid=None, + formsemestre_id=None, + format="html", + version="long", + xml_with_decisions=False, + force_publishing=False, + prefer_mail_perso=False, + REQUEST=None, +): + return sco_bulletins.formsemestre_bulletinetud( + etudid=etudid, + formsemestre_id=formsemestre_id, + format=format, + version=version, + xml_with_decisions=xml_with_decisions, + force_publishing=force_publishing, + prefer_mail_perso=prefer_mail_perso, + REQUEST=REQUEST, + ) + + sco_publish( "/formsemestre_evaluations_cal", sco_evaluations.formsemestre_evaluations_cal, @@ -601,7 +625,7 @@ def formsemestre_list( @bp.route("/XMLgetFormsemestres") @scodoc -@permission_required(Permission.ScoView) +@permission_required_compat_scodoc7(Permission.ScoView) @scodoc7func def XMLgetFormsemestres(etape_apo=None, formsemestre_id=None, REQUEST=None): """List all formsemestres matching etape, XML format diff --git a/app/views/scodoc.py b/app/views/scodoc.py index d2fd9f9d4..88aa7a1e7 100644 --- a/app/views/scodoc.py +++ b/app/views/scodoc.py @@ -30,6 +30,7 @@ Module main: page d'accueil, avec liste des départements Emmanuel Viennet, 2021 """ +from app.auth.models import User import os import flask @@ -53,7 +54,11 @@ import sco_version from app.scodoc import sco_logos from app.scodoc import sco_find_etud from app.scodoc import sco_utils as scu -from app.decorators import admin_required +from app.decorators import ( + admin_required, + scodoc7func, + permission_required_compat_scodoc7, +) from app.scodoc.sco_permissions import Permission from app.views import scodoc_bp as bp @@ -82,12 +87,12 @@ def table_etud_in_accessible_depts(): return sco_find_etud.table_etud_in_accessible_depts(expnom=request.form["expnom"]) +# Fonction d'API accessible sans aucun authentification @bp.route("/ScoDoc/get_etud_dept") -@login_required def get_etud_dept(): """Returns the dept acronym (eg "GEII") of an etud (identified by etudid, code_nip ou code_ine in the request). - API: ramène la chaine brute, sans JSON ou XML. + Ancienne API: ramène la chaine brute, texte sans JSON ou XML. """ if "etudid" in request.args: # zero ou une réponse: diff --git a/app/views/scolar.py b/app/views/scolar.py index 733716193..8f0267821 100644 --- a/app/views/scolar.py +++ b/app/views/scolar.py @@ -52,6 +52,7 @@ from app.decorators import ( scodoc, scodoc7func, permission_required, + permission_required_compat_scodoc7, admin_required, login_required, ) @@ -402,7 +403,7 @@ def search_etud_by_name(): @bp.route("/etud_info") @bp.route("/XMLgetEtudInfos") @scodoc -@permission_required(Permission.ScoView) +@permission_required_compat_scodoc7(Permission.ScoView) @scodoc7func def etud_info(etudid=None, format="xml", REQUEST=None): "Donne les informations sur un etudiant" diff --git a/misc/example-api-1.py b/misc/example-api-1.py index 171365e58..e79b329a3 100644 --- a/misc/example-api-1.py +++ b/misc/example-api-1.py @@ -36,7 +36,7 @@ class ScoError(Exception): def GET(s, path, errmsg=None): """Get and returns as JSON""" - r = s.get(BASEURL + "/" + path) + r = s.get(BASEURL + "/" + path, verify=CHECK_CERTIFICATE) if r.status_code != 200: raise ScoError(errmsg or "erreur !") return r.json() # decode la reponse JSON @@ -44,7 +44,7 @@ def GET(s, path, errmsg=None): def POST(s, path, data, errmsg=None): """Post""" - r = s.post(BASEURL + "/" + path, data=data) + r = s.post(BASEURL + "/" + path, data=data, verify=CHECK_CERTIFICATE) if r.status_code != 200: raise ScoError(errmsg or "erreur !") return r.text From e8208efb5c89d252288dc1db29dd8cebf3aca100 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 9 Sep 2021 16:23:53 +0200 Subject: [PATCH 19/25] Fix typo: export Apo (bis) --- app/scodoc/sco_semset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/scodoc/sco_semset.py b/app/scodoc/sco_semset.py index 1f35afae7..e8be6bd6c 100644 --- a/app/scodoc/sco_semset.py +++ b/app/scodoc/sco_semset.py @@ -158,7 +158,7 @@ class SemSet(dict): ndb.SimpleQuery( """INSERT INTO notes_semset_formsemestre - (id, semset_id) + (formsemestre_id, semset_id) VALUES (%(formsemestre_id)s, %(semset_id)s) """, { From 51884bf28f08b8b6b8789c81ab56b2f79e064276 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 9 Sep 2021 16:33:44 +0200 Subject: [PATCH 20/25] =?UTF-8?q?Edition=20r=C3=B4le=20RespPe?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/views/users.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/views/users.py b/app/views/users.py index 17ed76325..557fbfb66 100644 --- a/app/views/users.py +++ b/app/views/users.py @@ -121,7 +121,9 @@ def create_user_form(REQUEST, user_name=None, edit=0): is_super_admin = True # Les rôles standards créés à l'initialisation de ScoDoc: - standard_roles = [Role.get_named_role(r) for r in ("Ens", "Secr", "Admin")] + standard_roles = [ + Role.get_named_role(r) for r in ("Ens", "Secr", "Admin", "RespPe") + ] # Rôles pouvant etre attribués aux utilisateurs via ce dialogue: # si SuperAdmin, tous les rôles standards dans tous les départements # sinon, les départements dans lesquels l'utilisateur a le droit From 245f954b053e051984c8b2c79f3da329f3338565 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Fri, 10 Sep 2021 21:07:28 +0200 Subject: [PATCH 21/25] add somes routes for scodoc7 compat API --- app/views/notes.py | 2 +- app/views/scolar.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/views/notes.py b/app/views/notes.py index 7fbc63184..67f58dd80 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -599,7 +599,7 @@ sco_publish("/ue_move", sco_edit_formation.ue_move, Permission.ScoChangeFormatio @bp.route("/formsemestre_list") @scodoc -@permission_required(Permission.ScoView) +@permission_required_compat_scodoc7(Permission.ScoView) @scodoc7func def formsemestre_list( format=None, diff --git a/app/views/scolar.py b/app/views/scolar.py index 8f0267821..145208cdb 100644 --- a/app/views/scolar.py +++ b/app/views/scolar.py @@ -402,6 +402,8 @@ def search_etud_by_name(): # XMLgetEtudInfos était le nom dans l'ancienne API ScoDoc 6 @bp.route("/etud_info") @bp.route("/XMLgetEtudInfos") +@bp.route("/Absences/XMLgetEtudInfos") # compat with OLD clients +@bp.route("/Notes/XMLgetEtudInfos") @scodoc @permission_required_compat_scodoc7(Permission.ScoView) @scodoc7func From 8589aab6595ef0c42e0d0af3eae4bdb13e4ff29e Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Fri, 10 Sep 2021 21:12:59 +0200 Subject: [PATCH 22/25] enhance sidebar --- app/scodoc/html_sidebar.py | 84 +++++++++++++------------------ app/scodoc/sco_utils.py | 4 +- app/static/css/scodoc.css | 7 +++ app/static/icons/scologo_img.png | Bin 10124 -> 29443 bytes sco_version.py | 2 +- 5 files changed, 46 insertions(+), 51 deletions(-) diff --git a/app/scodoc/html_sidebar.py b/app/scodoc/html_sidebar.py index 4dabbfe8d..5ec2ae26b 100644 --- a/app/scodoc/html_sidebar.py +++ b/app/scodoc/html_sidebar.py @@ -83,22 +83,19 @@ def sidebar(): from app.scodoc import sco_abs from app.scodoc import sco_etud - params = { - "ScoURL": scu.ScoURL(), - "SCO_USER_MANUAL": scu.SCO_USER_MANUAL, - } + params = {} - H = ['