From 1c27ec7dc25c2e48b18d77ed0768884a96021b43 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 29 Sep 2021 10:36:57 +0200 Subject: [PATCH 001/240] branche pour PN BUT --- app/models/but_pn.py | 4 ++++ sco_version.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 app/models/but_pn.py diff --git a/app/models/but_pn.py b/app/models/but_pn.py new file mode 100644 index 00000000..1e5e4231 --- /dev/null +++ b/app/models/but_pn.py @@ -0,0 +1,4 @@ +"""ScoDoc 9 models : Formation BUT 2021 +""" + +# insérer ici idk diff --git a/sco_version.py b/sco_version.py index dd1416cb..4f7fda19 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.0.44" +SCOVERSION = "9.1.0a" SCONAME = "ScoDoc" From 3878d68b38666fd86a2c89646b0a7d7cf33c10c1 Mon Sep 17 00:00:00 2001 From: IDK Date: Tue, 19 Oct 2021 15:52:02 +0200 Subject: [PATCH 002/240] Utilise NA pour les notes manquants (et plus NA0, ...) --- app/scodoc/notes_table.py | 7 +++---- app/scodoc/sco_compute_moy.py | 10 ++++++---- app/scodoc/sco_recapcomplet.py | 6 +++--- app/scodoc/sco_tag_module.py | 2 +- app/scodoc/sco_utils.py | 2 +- 5 files changed, 14 insertions(+), 13 deletions(-) diff --git a/app/scodoc/notes_table.py b/app/scodoc/notes_table.py index fb18d050..e3d6e419 100644 --- a/app/scodoc/notes_table.py +++ b/app/scodoc/notes_table.py @@ -630,7 +630,8 @@ class NotesTable(object): matiere_sum_notes += val * coef matiere_sum_coefs += coef matiere_id_last = matiere_id - except: + except: # val == "NI" "NA" + assert val == "NI" or val == "NA" nb_missing = nb_missing + 1 coefs.append(0) coefs_mask.append(0) @@ -728,9 +729,7 @@ class NotesTable(object): Prend toujours en compte les UE capitalisées. """ - # log('comp_etud_moy_gen(etudid=%s)' % etudid) - - # Si l'étudiant a Demissionné ou est DEFaillant, on n'enregistre pas ses moyennes + # Si l'étudiant a Démissionné ou est DEFaillant, on n'enregistre pas ses moyennes block_computation = ( self.inscrdict[etudid]["etat"] == "D" or self.inscrdict[etudid]["etat"] == DEF diff --git a/app/scodoc/sco_compute_moy.py b/app/scodoc/sco_compute_moy.py index 4d46f065..3e505901 100644 --- a/app/scodoc/sco_compute_moy.py +++ b/app/scodoc/sco_compute_moy.py @@ -160,7 +160,7 @@ def compute_user_formula( # log('expression : %s\nvariables=%s\n' % (formula, variables)) # debug user_moy = sco_formulas.eval_user_expression(formula, variables) # log('user_moy=%s' % user_moy) - if user_moy != "NA0" and user_moy != "NA": + if user_moy != "NA": user_moy = float(user_moy) if (user_moy > 20) or (user_moy < 0): etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] @@ -295,15 +295,17 @@ def compute_moduleimpl_moyennes(nt, modimpl): # il manque une note ! (si publish_incomplete, cela peut arriver, on ignore) if e["coefficient"] > 0 and not e["publish_incomplete"]: nb_missing += 1 + # ne devrait pas arriver ? + log("\nXXX SCM298\n") if nb_missing == 0 and sum_coefs > 0: if sum_coefs > 0: R[etudid] = sum_notes / sum_coefs moy_valid = True else: - R[etudid] = "na" + R[etudid] = "NA" moy_valid = False else: - R[etudid] = "NA%d" % nb_missing + R[etudid] = "NA" moy_valid = False if user_expr: @@ -361,7 +363,7 @@ def compute_moduleimpl_moyennes(nt, modimpl): if eval_rattr["evaluation_type"] == EVALUATION_RATTRAPAGE: # rattrapage classique: prend la meilleure note entre moyenne # module et note eval rattrapage - if (R[etudid] == "NA0") or (note_sur_20 > R[etudid]): + if (R[etudid] == "NA") or (note_sur_20 > R[etudid]): # log('note_sur_20=%s' % note_sur_20) R[etudid] = note_sur_20 elif eval_rattr["evaluation_type"] == EVALUATION_SESSION2: diff --git a/app/scodoc/sco_recapcomplet.py b/app/scodoc/sco_recapcomplet.py index 06f7b95e..325aec53 100644 --- a/app/scodoc/sco_recapcomplet.py +++ b/app/scodoc/sco_recapcomplet.py @@ -669,11 +669,11 @@ def make_formsemestre_recapcomplet( else: cells = '' % etudid ir += 1 - # XXX nsn = [ x.replace('NA0', '-') for x in l[:-2] ] - # notes sans le NA0: + # XXX nsn = [ x.replace('NA', '-') for x in l[:-2] ] + # notes sans le NA: nsn = l[:-2] # copy for i in range(len(nsn)): - if nsn[i] == "NA0": + if nsn[i] == "NA": nsn[i] = "-" cells += '%s' % nsn[0] # rang cells += '%s' % el # nom etud (lien) diff --git a/app/scodoc/sco_tag_module.py b/app/scodoc/sco_tag_module.py index b106a754..50ec8f01 100644 --- a/app/scodoc/sco_tag_module.py +++ b/app/scodoc/sco_tag_module.py @@ -280,7 +280,7 @@ def get_etud_tagged_modules(etudid, tagname): R.append( { "sem": sem, - "moy": moy, # valeur réelle, ou NI (non inscrit au module ou NA0 (pas de note) + "moy": moy, # valeur réelle, ou NI (non inscrit au module ou NA (pas de note) "moduleimpl": modimpl, "tags": tags, } diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index e5dec56b..68321aba 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -132,7 +132,7 @@ def fmt_note(val, note_max=None, keep_numeric=False): s = "0" * (5 - len(s)) + s # padding: 0 à gauche pour longueur 5: "12.34" return s else: - return val.replace("NA0", "-") # notes sans le NA0 + return val.replace("NA", "-") def fmt_coef(val): From d2f41b6a21cb58b7891f9d8a4bf6a1df2a402f19 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 28 Oct 2021 00:52:23 +0200 Subject: [PATCH 003/240] API scodoc7, exemple/test usage, progres sur l'API scodoc9 --- app/api/__init__.py | 1 + app/api/auth.py | 15 ++++ app/api/sco_api.py | 4 +- app/auth/models.py | 3 + app/decorators.py | 6 +- app/views/absences.py | 8 +- app/views/notes.py | 8 +- app/views/scolar.py | 2 +- misc/example-api-1.py | 133 --------------------------- tests/api/exemple-api-basic.py | 149 +++++++++++++++++++++++++++++++ tests/api/exemple-api-scodoc7.py | 144 +++++++++++++++++++++++++++++ 11 files changed, 327 insertions(+), 146 deletions(-) delete mode 100644 misc/example-api-1.py create mode 100644 tests/api/exemple-api-basic.py create mode 100644 tests/api/exemple-api-scodoc7.py diff --git a/app/api/__init__.py b/app/api/__init__.py index 34ebbc77..956c1b46 100644 --- a/app/api/__init__.py +++ b/app/api/__init__.py @@ -6,3 +6,4 @@ from flask import Blueprint bp = Blueprint("api", __name__) from app.api import sco_api +from app.api import tokens diff --git a/app/api/auth.py b/app/api/auth.py index 24348aab..bb8464e0 100644 --- a/app/api/auth.py +++ b/app/api/auth.py @@ -33,6 +33,7 @@ token_auth = HTTPTokenAuth() @basic_auth.verify_password def verify_password(username, password): + # breakpoint() user = User.query.filter_by(user_name=username).first() if user and user.check_password(password): return user @@ -51,3 +52,17 @@ def verify_token(token): @token_auth.error_handler def token_auth_error(status): return error_response(status) + + +def token_permission_required(permission): + def decorator(f): + @wraps(f) + def decorated_function(*args, **kwargs): + scodoc_dept = getattr(g, "scodoc_dept", None) + if not current_user.has_permission(permission, scodoc_dept): + abort(403) + return f(*args, **kwargs) + + return login_required(decorated_function) + + return decorator diff --git a/app/api/sco_api.py b/app/api/sco_api.py index e2619a0b..c3ee7424 100644 --- a/app/api/sco_api.py +++ b/app/api/sco_api.py @@ -48,9 +48,9 @@ from app.api.errors import bad_request from app import models -@bp.route("/ScoDoc/api/list_depts", methods=["GET"]) +@bp.route("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]} + data = [d.to_dict() for d in depts] return jsonify(data) diff --git a/app/auth/models.py b/app/auth/models.py index f243f0e7..86ebdb83 100644 --- a/app/auth/models.py +++ b/app/auth/models.py @@ -213,6 +213,9 @@ class User(UserMixin, db.Model): @staticmethod def check_token(token): + """Retreive user for given token, chek token's validity + and returns the user object. + """ user = User.query.filter_by(token=token).first() if user is None or user.token_expiration < datetime.utcnow(): return None diff --git a/app/decorators.py b/app/decorators.py index a688cb17..ce94743e 100644 --- a/app/decorators.py +++ b/app/decorators.py @@ -50,6 +50,7 @@ def scodoc(func): @wraps(func) def scodoc_function(*args, **kwargs): + # current_app.logger.info("@scodoc") # interdit les POST si pas loggué if request.method == "POST" and not current_user.is_authenticated: current_app.logger.info( @@ -71,6 +72,7 @@ def scodoc(func): # current_app.logger.info("setting dept to None") g.scodoc_dept = None g.scodoc_dept_id = -1 # invalide + return func(*args, **kwargs) return scodoc_function @@ -100,8 +102,8 @@ def permission_required_compat_scodoc7(permission): 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: + # current_app.logger.info("@permission_required_compat_scodoc7") auth_ok = False if request.method == "GET": user_name = request.args.get("__ac_name") @@ -116,7 +118,6 @@ def permission_required_compat_scodoc7(permission): 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) @@ -153,6 +154,7 @@ def scodoc7func(func): 2. or be called directly from Python. """ + # current_app.logger.info("@scodoc7func") # Détermine si on est appelé via une route ("toplevel") # ou par un appel de fonction python normal. top_level = not hasattr(g, "scodoc7_decorated") diff --git a/app/views/absences.py b/app/views/absences.py index c6fe5a1c..f4cb1596 100644 --- a/app/views/absences.py +++ b/app/views/absences.py @@ -1047,8 +1047,8 @@ def EtatAbsencesDate(group_ids=[], date=None): # list of groups to display # ----- Gestion des "billets d'absence": signalement par les etudiants eux mêmes (à travers le portail) @bp.route("/AddBilletAbsence", methods=["GET", "POST"]) # API ScoDoc 7 compat -@permission_required_compat_scodoc7(Permission.ScoAbsAddBillet) @scodoc +@permission_required_compat_scodoc7(Permission.ScoAbsAddBillet) @scodoc7func def AddBilletAbsence( begin, @@ -1238,8 +1238,8 @@ def listeBilletsEtud(etudid=False, format="html"): @bp.route( "/XMLgetBilletsEtud", methods=["GET", "POST"] ) # pour compat anciens clients PHP -@permission_required_compat_scodoc7(Permission.ScoView) @scodoc +@permission_required_compat_scodoc7(Permission.ScoView) @scodoc7func def XMLgetBilletsEtud(etudid=False): """Liste billets pour un etudiant""" @@ -1464,8 +1464,8 @@ def ProcessBilletAbsenceForm(billet_id): # @bp.route("/essai_api7") -# @permission_required_compat_scodoc7(Permission.ScoView) # @scodoc +# @permission_required_compat_scodoc7(Permission.ScoView) # @scodoc7func # def essai_api7(x="xxx"): # "un essai" @@ -1474,8 +1474,8 @@ def ProcessBilletAbsenceForm(billet_id): @bp.route("/XMLgetAbsEtud", methods=["GET", "POST"]) # pour compat anciens clients PHP -@permission_required_compat_scodoc7(Permission.ScoView) @scodoc +@permission_required_compat_scodoc7(Permission.ScoView) @scodoc7func def XMLgetAbsEtud(beg_date="", end_date=""): """returns list of absences in date interval""" diff --git a/app/views/notes.py b/app/views/notes.py index c549377b..b3b2535d 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -264,10 +264,10 @@ sco_publish( @bp.route( - "formsemestre_bulletinetud", methods=["GET", "POST"] + "/formsemestre_bulletinetud", methods=["GET", "POST"] ) # POST pour compat anciens clients PHP (deprecated) -@permission_required_compat_scodoc7(Permission.ScoView) @scodoc +@permission_required_compat_scodoc7(Permission.ScoView) @scodoc7func def formsemestre_bulletinetud( etudid=None, @@ -642,8 +642,8 @@ sco_publish("/ue_move", sco_edit_formation.ue_move, Permission.ScoChangeFormatio @bp.route( "/formsemestre_list", methods=["GET", "POST"] ) # pour compat anciens clients PHP -@permission_required_compat_scodoc7(Permission.ScoView) @scodoc +@permission_required_compat_scodoc7(Permission.ScoView) @scodoc7func def formsemestre_list( format="json", @@ -669,8 +669,8 @@ def formsemestre_list( @bp.route( "/XMLgetFormsemestres", methods=["GET", "POST"] ) # pour compat anciens clients PHP -@permission_required_compat_scodoc7(Permission.ScoView) @scodoc +@permission_required_compat_scodoc7(Permission.ScoView) @scodoc7func def XMLgetFormsemestres(etape_apo=None, formsemestre_id=None): """List all formsemestres matching etape, XML format diff --git a/app/views/scolar.py b/app/views/scolar.py index 40c45a19..38f63f06 100644 --- a/app/views/scolar.py +++ b/app/views/scolar.py @@ -358,8 +358,8 @@ def search_etud_by_name(): @bp.route( "/Notes/XMLgetEtudInfos", methods=["GET", "POST"] ) # pour compat anciens clients PHP -@permission_required_compat_scodoc7(Permission.ScoView) @scodoc +@permission_required_compat_scodoc7(Permission.ScoView) @scodoc7func def etud_info(etudid=None, format="xml"): "Donne les informations sur un etudiant" diff --git a/misc/example-api-1.py b/misc/example-api-1.py deleted file mode 100644 index 37a06c56..00000000 --- a/misc/example-api-1.py +++ /dev/null @@ -1,133 +0,0 @@ -#!/usr/bin/env python3 -# -*- mode: python -*- -# -*- coding: utf-8 -*- - -"""Exemple connexion sur ScoDoc et utilisation de l'API - -- Ouverture session -- Liste semestres -- Liste modules -- Creation d'une évaluation -- Saisie d'une note - -Attention: cet exemple est en Python 3 (>= 3.6) -""" - -import requests -import urllib3 -import pdb -from pprint import pprint as pp -from flask import g, url_for - -# A modifier pour votre serveur: -CHECK_CERTIFICATE = False # set to True in production -BASEURL = "https://scodoc.xxx.net/ScoDoc/RT/Scolarite" -USER = "XXX" -PASSWORD = "XXX" - -# --- -if not CHECK_CERTIFICATE: - urllib3.disable_warnings() - - -class ScoError(Exception): - pass - - -def GET(s, path, errmsg=None): - """Get and returns as JSON""" - 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 - - -def POST(s, path, data, errmsg=None): - """Post""" - r = s.post(BASEURL + "/" + path, data=data, verify=CHECK_CERTIFICATE) - if r.status_code != 200: - raise ScoError(errmsg or "erreur !") - return r.text - - -# --- 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") - -# --- Recupere la liste de tous les semestres: -sems = GET(s, "Notes/formsemestre_list?format=json", "Aucun semestre !") - -# sems est une liste de semestres (dictionnaires) -for sem in sems: - if sem["etat"]: - break - -if sem["etat"] == "0": - raise ScoError("Aucun semestre non verrouillé !") - -# Affiche le semestre trouvé: -pp(sem) - -# ---- Récupère la description de ce semestre: -# semdescr = GET(s, f"Notes/formsemestre_description?formsemestre_id={sem['formsemestre_id']}&with_evals=0&format=json" ) - -# ---- Liste les modules et prend le premier -mods = GET(s, f"/Notes/moduleimpl_list?formsemestre_id={sem['formsemestre_id']}") -print(f"{len(mods)} modules dans le semestre {sem['titre']}") - -mod = mods[0] - -# ---- Etudiants inscrits dans ce module -inscrits = GET( - s, f"Notes/do_moduleimpl_inscription_list?moduleimpl_id={mod['moduleimpl_id']}" -) -print(f"{len(inscrits)} inscrits dans ce module") -# prend le premier inscrit, au hasard: -etudid = inscrits[0]["etudid"] - -# ---- Création d'une evaluation le dernier jour du semestre -jour = sem["date_fin"] -evaluation_id = POST( - s, - "/Notes/do_evaluation_create", - data={ - "moduleimpl_id": mod["moduleimpl_id"], - "coefficient": 1, - "jour": jour, # "5/9/2019", - "heure_debut": "9h00", - "heure_fin": "10h00", - "note_max": 20, # notes sur 20 - "description": "essai", - }, - errmsg="échec création évaluation", -) - -print( - f"Evaluation créée dans le module {mod['moduleimpl_id']}, evaluation_id={evaluation_id}" -) -print( - "Pour vérifier, aller sur: ", - url_for( - "notes.moduleimpl_status", - scodoc_dept="DEPT", - moduleimpl_id=mod["moduleimpl_id"], - ), -) - -# ---- Saisie d'une note -junk = POST( - s, - "/Notes/save_note", - data={ - "etudid": etudid, - "evaluation_id": evaluation_id, - "value": 16.66, # la note ! - "comment": "test API", - }, -) diff --git a/tests/api/exemple-api-basic.py b/tests/api/exemple-api-basic.py new file mode 100644 index 00000000..529c379e --- /dev/null +++ b/tests/api/exemple-api-basic.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +# -*- mode: python -*- +# -*- coding: utf-8 -*- + +"""Exemple utilisation API ScoDoc 9 avec jeton obtenu par basic athentication + + +Utilisation: créer les variables d'environnement: (indiquer les valeurs +pour le serveur ScoDoc que vous voulez interroger) + +export SCODOC_URL="https://scodoc.xxx.net/" +export SCODOC_USER="xxx" +export SCODOC_PASSWD="xxx" +export CHECK_CERTIFICATE=0 # ou 1 si serveur de production avec certif SSL valide + +(on peut aussi placer ces valeurs dans un fichier .env du répertoire tests/api). + + +Travail en cours, un seul point d'API (list_depts). +""" + +from dotenv import load_dotenv +import os +import pdb +import requests +import urllib3 +from pprint import pprint as pp + +# --- Lecture configuration (variables d'env ou .env) +BASEDIR = os.path.abspath(os.path.dirname(__file__)) +load_dotenv(os.path.join(BASEDIR, ".env")) +CHECK_CERTIFICATE = bool(int(os.environ.get("CHECK_CERTIFICATE", False))) +SCODOC_URL = os.environ["SCODOC_URL"] +SCODOC_DEPT = os.environ["SCODOC_DEPT"] +DEPT_URL = SCODOC_URL + "/ScoDoc/" + SCODOC_DEPT + "/Scolarite/" +SCODOC_USER = os.environ["SCODOC_USER"] +SCODOC_PASSWORD = os.environ["SCODOC_PASSWD"] +print(f"SCODOC_URL={SCODOC_URL}") + +# --- +if not CHECK_CERTIFICATE: + urllib3.disable_warnings() + + +class ScoError(Exception): + pass + + +def GET(path: str, headers={}, errmsg=None): + """Get and returns as JSON""" + r = requests.get( + DEPT_URL + "/" + path, headers=headers or HEADERS, verify=CHECK_CERTIFICATE + ) + if r.status_code != 200: + raise ScoError(errmsg or "erreur !") + return r.json() # decode la reponse JSON + + +def POST(s, path: str, data: dict, errmsg=None): + """Post""" + r = s.post(DEPT_URL + "/" + path, data=data, verify=CHECK_CERTIFICATE) + if r.status_code != 200: + raise ScoError(errmsg or "erreur !") + return r.text + + +# --- Obtention du jeton (token) +r = requests.post( + SCODOC_URL + "/ScoDoc/api/tokens", auth=(SCODOC_USER, SCODOC_PASSWORD) +) +assert r.status_code == 200 +token = r.json()["token"] +HEADERS = {"Authorization": f"Bearer {token}"} + +r = requests.get( + SCODOC_URL + "/ScoDoc/api/list_depts", headers=HEADERS, verify=CHECK_CERTIFICATE +) +if r.status_code != 200: + raise ScoError("erreur de connexion: vérifier adresse et identifiants") + +pp(r.json()) + + +# # --- Recupere la liste de tous les semestres: +# sems = GET(s, "Notes/formsemestre_list?format=json", "Aucun semestre !") + +# # sems est une liste de semestres (dictionnaires) +# for sem in sems: +# if sem["etat"]: +# break + +# if sem["etat"] == "0": +# raise ScoError("Aucun semestre non verrouillé !") + +# # Affiche le semestre trouvé: +# pp(sem) + +# # ---- Récupère la description de ce semestre: +# # semdescr = GET(s, f"Notes/formsemestre_description?formsemestre_id={sem['formsemestre_id']}&with_evals=0&format=json" ) + +# # ---- Liste les modules et prend le premier +# mods = GET(s, f"/Notes/moduleimpl_list?formsemestre_id={sem['formsemestre_id']}") +# print(f"{len(mods)} modules dans le semestre {sem['titre']}") + +# mod = mods[0] + +# # ---- Etudiants inscrits dans ce module +# inscrits = GET( +# s, f"Notes/do_moduleimpl_inscription_list?moduleimpl_id={mod['moduleimpl_id']}" +# ) +# print(f"{len(inscrits)} inscrits dans ce module") +# # prend le premier inscrit, au hasard: +# etudid = inscrits[0]["etudid"] + +# # ---- Création d'une evaluation le dernier jour du semestre +# jour = sem["date_fin"] +# evaluation_id = POST( +# s, +# "/Notes/do_evaluation_create", +# data={ +# "moduleimpl_id": mod["moduleimpl_id"], +# "coefficient": 1, +# "jour": jour, # "5/9/2019", +# "heure_debut": "9h00", +# "heure_fin": "10h00", +# "note_max": 20, # notes sur 20 +# "description": "essai", +# }, +# errmsg="échec création évaluation", +# ) + +# print( +# f"Evaluation créée dans le module {mod['moduleimpl_id']}, evaluation_id={evaluation_id}" +# ) +# print( +# f"Pour vérifier, aller sur: {DEPT_URL}/Notes/moduleimpl_status?moduleimpl_id={mod['moduleimpl_id']}", +# ) + +# # ---- Saisie d'une note +# junk = POST( +# s, +# "/Notes/save_note", +# data={ +# "etudid": etudid, +# "evaluation_id": evaluation_id, +# "value": 16.66, # la note ! +# "comment": "test API", +# }, +# ) diff --git a/tests/api/exemple-api-scodoc7.py b/tests/api/exemple-api-scodoc7.py new file mode 100644 index 00000000..c3190717 --- /dev/null +++ b/tests/api/exemple-api-scodoc7.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 +# -*- mode: python -*- +# -*- coding: utf-8 -*- + +"""Exemple connexion sur ScoDoc 9 et utilisation de l'ancienne API ScoDoc 7 +à la mode "PHP": les gens passaient directement __ac_name et __ac_password +dans chaque requête, en POST ou en GET. + +Cela n'a jamais été documenté mais était implitement supporté. C'est "deprecated" +et ne sera plus supporté à partir de juillet 2022. + +Ce script va tester: +- Liste semestres +- Liste modules +- Creation d'une évaluation +- Saisie d'une note + +Utilisation: créer les variables d'environnement: (indiquer les valeurs +pour le serveur ScoDoc que vous voulez interroger) + +export SCODOC_URL="https://scodoc.xxx.net/" +export SCODOC_USER="xxx" +export SCODOC_PASSWD="xxx" +export CHECK_CERTIFICATE=0 # ou 1 si serveur de production avec certif SSL valide + +(on peut aussi placer ces valeurs dans un fichier .env du répertoire tests/api). +""" + +from dotenv import load_dotenv +import os +import pdb +import requests +import urllib3 +from pprint import pprint as pp + +# --- Lecture configuration (variables d'env ou .env) +BASEDIR = os.path.abspath(os.path.dirname(__file__)) +load_dotenv(os.path.join(BASEDIR, ".env")) +CHECK_CERTIFICATE = bool(int(os.environ.get("CHECK_CERTIFICATE", False))) +SCODOC_URL = os.environ["SCODOC_URL"] +SCODOC_DEPT = os.environ["SCODOC_DEPT"] +DEPT_URL = SCODOC_URL + "/ScoDoc/" + SCODOC_DEPT + "/Scolarite" +SCODOC_USER = os.environ["SCODOC_USER"] +SCODOC_PASSWORD = os.environ["SCODOC_PASSWD"] +print(f"SCODOC_URL={SCODOC_URL}") + +# --- +if not CHECK_CERTIFICATE: + urllib3.disable_warnings() + + +class ScoError(Exception): + pass + + +def GET(path: str, params=None, errmsg=None): + """Get and returns as JSON""" + # ajoute auth + params["__ac_name"] = SCODOC_USER + params["__ac_password"] = SCODOC_PASSWORD + r = requests.get(DEPT_URL + "/" + path, params=params, verify=CHECK_CERTIFICATE) + if r.status_code != 200: + raise ScoError(errmsg or "erreur !") + return r.json() # decode la reponse JSON + + +def POST(path: str, data: dict, errmsg=None): + """Post""" + data["__ac_name"] = SCODOC_USER + data["__ac_password"] = SCODOC_PASSWORD + r = requests.post(DEPT_URL + "/" + path, data=data, verify=CHECK_CERTIFICATE) + if r.status_code != 200: + raise ScoError(errmsg or "erreur !") + return r.text + + +# --- +# pas besoin d'ouvrir une session, on y va directement: + +# --- Recupere la liste de tous les semestres: +sems = GET("Notes/formsemestre_list", params={"format": "json"}) + +# sems est une liste de semestres (dictionnaires) +for sem in sems: + if sem["etat"]: + break + +if sem["etat"] == "0": + raise ScoError("Aucun semestre non verrouillé !") + +# Affiche le semestre trouvé: +pp(sem) + +# Les fonctions ci-dessous ne fonctionne plus en ScoDoc 9 +# Voir https://scodoc.org/git/viennet/ScoDoc/issues/149 + +# # ---- Liste les modules et prend le premier +# mods = GET("/Notes/moduleimpl_list", params={"formsemestre_id": sem["formsemestre_id"]}) +# print(f"{len(mods)} modules dans le semestre {sem['titre']}") + +# mod = mods[0] + +# # ---- Etudiants inscrits dans ce module +# inscrits = GET( +# "Notes/do_moduleimpl_inscription_list", +# params={"moduleimpl_id": mod["moduleimpl_id"]}, +# ) +# print(f"{len(inscrits)} inscrits dans ce module") +# # prend le premier inscrit, au hasard: +# etudid = inscrits[0]["etudid"] + +# # ---- Création d'une evaluation le dernier jour du semestre +# jour = sem["date_fin"] +# evaluation_id = POST( +# "/Notes/do_evaluation_create", +# data={ +# "moduleimpl_id": mod["moduleimpl_id"], +# "coefficient": 1, +# "jour": jour, # "5/9/2019", +# "heure_debut": "9h00", +# "heure_fin": "10h00", +# "note_max": 20, # notes sur 20 +# "description": "essai", +# }, +# errmsg="échec création évaluation", +# ) + +# print( +# f"Evaluation créée dans le module {mod['moduleimpl_id']}, evaluation_id={evaluation_id}" +# ) +# print( +# f"Pour vérifier, aller sur: {DEPT_URL}/Notes/moduleimpl_status?moduleimpl_id={mod['moduleimpl_id']}", +# ) + +# # ---- Saisie d'une note +# junk = POST( +# "/Notes/save_note", +# data={ +# "etudid": etudid, +# "evaluation_id": evaluation_id, +# "value": 16.66, # la note ! +# "comment": "test API", +# }, +# ) From b1aa36b136d45559272bc7d629c2b4a976b1e2b0 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 28 Oct 2021 00:54:26 +0200 Subject: [PATCH 004/240] version 9.0.57 --- sco_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sco_version.py b/sco_version.py index 120cecdf..a99ffec7 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.0.56" +SCOVERSION = "9.0.57" SCONAME = "ScoDoc" From 2b91fd78df49b957381cdc25cb4202824b304fef Mon Sep 17 00:00:00 2001 From: Place Jean-Marie Date: Sat, 30 Oct 2021 12:01:17 +0200 Subject: [PATCH 005/240] ajout mention \'message automatique\' --- app/templates/email/welcome.html | 4 +++- app/templates/email/welcome.txt | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/templates/email/welcome.html b/app/templates/email/welcome.html index 41ee5f65..41fb1c61 100644 --- a/app/templates/email/welcome.html +++ b/app/templates/email/welcome.html @@ -15,4 +15,6 @@

{{ url_for('auth.reset_password', token=token, _external=True) }}

{% endif %} -

A bientôt !

\ No newline at end of file +

A bientôt !

+ +

Ce message a été généré automatiquement par le serveur ScoDoc.

\ No newline at end of file diff --git a/app/templates/email/welcome.txt b/app/templates/email/welcome.txt index b15bceae..2630a8bd 100644 --- a/app/templates/email/welcome.txt +++ b/app/templates/email/welcome.txt @@ -8,4 +8,6 @@ Votre identifiant de connexion est: {{ user.user_name }} {{ url_for('auth.reset_password', token=token, _external=True) }} {% endif %} -

A bientôt !

\ No newline at end of file +A bientôt ! + +Ce message a été généré automatiquement par le serveur ScoDoc. \ No newline at end of file From c248def7f27f9363d6a81b37660f4d8d8c644201 Mon Sep 17 00:00:00 2001 From: Place Jean-Marie Date: Sat, 30 Oct 2021 12:03:21 +0200 Subject: [PATCH 006/240] refactoring Mode --- app/views/users.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/app/views/users.py b/app/views/users.py index 7a03a899..79b5e095 100644 --- a/app/views/users.py +++ b/app/views/users.py @@ -35,6 +35,7 @@ Emmanuel Viennet, 2021 """ import datetime import re +from enum import auto, IntEnum from xml.etree import ElementTree import flask @@ -116,6 +117,12 @@ class ChangePasswordForm(FlaskForm): raise ValidationError("Mot de passe actuel incorrect, ré-essayez") +class Mode(IntEnum): + WELCOME_AND_CHANGE_PASSWORD = auto() + WELCOME_ONLY = auto() + SILENT = auto() + + @bp.route("/") @bp.route("/index_html") @scodoc @@ -529,19 +536,20 @@ def create_user_form(user_name=None, edit=0, all_roles=1): ) return "\n".join(H) + msg + "\n" + tf[1] + F # Traitement initial (mode) : 3 cas + # cf énumération Mode # A: envoi de welcome + procedure de reset # B: envoi de welcome seulement (mot de passe saisie dans le formulaire) # C: Aucun envoi (mot de passe saisi dans le formulaire) if vals["welcome:list"] == "1": if vals["reset_password:list"] == "1": - mode = "A" + mode = Mode.WELCOME_AND_CHANGE_PASSWORD else: - mode = "B" + mode = Mode.WELCOME_ONLY else: - mode = "C" + mode = Mode.SILENT # check passwords - if mode == "A": + if mode == Mode.WELCOME_AND_CHANGE_PASSWORD: vals["password"] = generate_password() else: if vals["password"]: @@ -567,8 +575,8 @@ def create_user_form(user_name=None, edit=0, all_roles=1): db.session.add(u) db.session.commit() # envoi éventuel d'un message - if mode == "A" or mode == "B": - if mode == "A": + if mode == Mode.WELCOME_AND_CHANGE_PASSWORD or mode == Mode.WELCOME_ONLY: + if mode == Mode.WELCOME_AND_CHANGE_PASSWORD: token = u.get_reset_password_token() else: token = None From 8f1e465280f9b3bf43206856ae0ce744925ca35c Mon Sep 17 00:00:00 2001 From: Place Jean-Marie Date: Sat, 30 Oct 2021 12:05:51 +0200 Subject: [PATCH 007/240] show current user name while getting old_password --- app/templates/auth/change_password.html | 20 ++++++++++++++------ app/views/users.py | 6 +++++- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/app/templates/auth/change_password.html b/app/templates/auth/change_password.html index 975e8bc9..2bec7762 100644 --- a/app/templates/auth/change_password.html +++ b/app/templates/auth/change_password.html @@ -1,9 +1,13 @@ {% extends "base.html" %} {% import 'bootstrap/wtf.html' as wtf %} -{% macro render_field(field) %} +{% macro render_field(field, auth_name=None) %} - {{ field.label }} + {% if auth_name %} + {{ field.label }} ({{ auth_name }}): + {% else %} + {{ field.label }} + {% endif %} {{ field(**kwargs)|safe }} {% if field.errors %}