From 8fa64476b6aa18f760cf2da25d28881767605338 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 10 Aug 2022 07:16:34 +0200 Subject: [PATCH 01/15] API: modif /formsemestre//etudiants[/query] --- app/api/formsemestres.py | 47 +++++++++++++----------------- tests/api/test_api_formsemestre.py | 12 ++++++-- 2 files changed, 30 insertions(+), 29 deletions(-) diff --git a/app/api/formsemestres.py b/app/api/formsemestres.py index 7827d5839..ba09a2e4e 100644 --- a/app/api/formsemestres.py +++ b/app/api/formsemestres.py @@ -251,49 +251,44 @@ def formsemestre_programme(formsemestre_id: int): @bp.route( "/formsemestre//etudiants", - defaults={"etat": None}, + defaults={"with_query": False}, ) @bp.route( - "/formsemestre//etudiants/actifs", - defaults={"etat": scu.INSCRIT}, -) -@bp.route( - "/formsemestre//etudiants/demissionnaires", - defaults={"etat": scu.DEMISSION}, -) -@bp.route( - "/formsemestre//etudiants/defaillants", - defaults={"etat": scu.DEF}, + "/formsemestre//etudiants/query", + defaults={"with_query": True}, ) @api_web_bp.route( "/formsemestre//etudiants", - defaults={"etat": None}, + defaults={"with_query": False}, ) @api_web_bp.route( - "/formsemestre//etudiants/actifs", - defaults={"etat": scu.INSCRIT}, -) -@api_web_bp.route( - "/formsemestre//etudiants/demissionnaires", - defaults={"etat": scu.DEMISSION}, -) -@api_web_bp.route( - "/formsemestre//etudiants/defaillants", - defaults={"etat": scu.DEF}, + "/formsemestre//etudiants/query", + defaults={"with_query": True}, ) @login_required @scodoc @permission_required(Permission.ScoView) -def formsemestre_etudiants(formsemestre_id: int, etat: str = None): +def formsemestre_etudiants(formsemestre_id: int, with_query: bool = False): """Etudiants d'un formsemestre.""" query = FormSemestre.query.filter_by(id=formsemestre_id) if g.scodoc_dept: query = query.filter_by(dept_id=g.scodoc_dept_id) formsemestre: FormSemestre = query.first_or_404(formsemestre_id) - if etat is None: - inscriptions = formsemestre.inscriptions + if with_query: + etat = request.args.get("etat") + if etat is not None: + etat = { + "actifs": scu.INSCRIT, + "demissionnaires": scu.DEMISSION, + "defaillants": scu.DEF, + }.get(etat, etat) + inscriptions = [ + ins for ins in formsemestre.inscriptions if ins.etat == etat + ] + else: + inscriptions = formsemestre.inscriptions else: - inscriptions = [ins for ins in formsemestre.inscriptions if ins.etat == etat] + inscriptions = formsemestre.inscriptions etuds = [ins.etud.to_dict_short() for ins in inscriptions] # Ajout des groupes de chaque étudiants diff --git a/tests/api/test_api_formsemestre.py b/tests/api/test_api_formsemestre.py index 6ccad0289..f9d6e8629 100644 --- a/tests/api/test_api_formsemestre.py +++ b/tests/api/test_api_formsemestre.py @@ -515,22 +515,28 @@ def test_formsemestre_etudiants(api_headers): assert isinstance(group["group_id"], int) assert group["group_name"] is None or isinstance(group["group_name"], int) + ## Avec query: + etuds_query = GET( + f"/formsemestre/{formsemestre_id}/etudiants/query", headers=api_headers + ) + assert etuds_query == etuds + ### actifs etuds_actifs = GET( - f"/formsemestre/{formsemestre_id}/etudiants/actifs", headers=api_headers + f"/formsemestre/{formsemestre_id}/etudiants/query?etat=I", headers=api_headers ) assert isinstance(etuds_actifs, list) ### démissionnaires etuds_dem = GET( - f"/formsemestre/{formsemestre_id}/etudiants/demissionnaires", + f"/formsemestre/{formsemestre_id}/etudiants/query?etat=D", headers=api_headers, ) assert isinstance(etuds_dem, list) ### défaillants etuds_def = GET( - f"/formsemestre/{formsemestre_id}/etudiants/defaillants", headers=api_headers + f"/formsemestre/{formsemestre_id}/etudiants/query?etat=DEF", headers=api_headers ) assert isinstance(etuds_def, list) From ae2a56cad35b7476afdb10c55482653a12706c6d Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 10 Aug 2022 07:24:54 +0200 Subject: [PATCH 02/15] Fix API: /group//etudiants/query --- app/api/partitions.py | 16 +++++++++------- tests/api/test_api_partitions.py | 18 ++++++++++-------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/app/api/partitions.py b/app/api/partitions.py index abef92e8c..a1229387b 100644 --- a/app/api/partitions.py +++ b/app/api/partitions.py @@ -136,20 +136,22 @@ def etud_in_group(group_id: int): def etud_in_group_query(group_id: int): """Etudiants du groupe, filtrés par état""" etat = request.args.get("etat") - if etat not in {scu.INSCRIT, scu.DEMISSION, scu.DEF}: + if etat not in {None, scu.INSCRIT, scu.DEMISSION, scu.DEF}: return json_error(404, "etat: valeur invalide") query = GroupDescr.query.filter_by(id=group_id) if g.scodoc_dept: query = ( query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id) ) - group = query.first_or_404() # just tro ckeck that group exists in accessible dept - query = ( - Identite.query.join(FormSemestreInscription) - .filter_by(formsemestre_id=group.partition.formsemestre_id, etat=etat) - .join(group_membership) - .filter_by(group_id=group_id) + group = query.first_or_404() # just to ckeck that group exists in accessible dept + + query = Identite.query.join(FormSemestreInscription).filter_by( + formsemestre_id=group.partition.formsemestre_id ) + if etat is not None: + query = query.filter_by(etat=etat) + + query = query.join(group_membership).filter_by(group_id=group_id) return jsonify([etud.to_dict_short() for etud in query]) diff --git a/tests/api/test_api_partitions.py b/tests/api/test_api_partitions.py index f3999db85..d1138a576 100644 --- a/tests/api/test_api_partitions.py +++ b/tests/api/test_api_partitions.py @@ -134,10 +134,10 @@ def test_etud_in_group(api_headers): - /group//etudiants/query?etat= """ group_id = 1 - etudiants = GET(f"/group/{group_id}/etudiants", headers=api_headers) - assert isinstance(etudiants, list) + etuds = GET(f"/group/{group_id}/etudiants", headers=api_headers) + assert isinstance(etuds, list) - for etud in etudiants: + for etud in etuds: assert verify_fields(etud, PARTITION_GROUPS_ETUD_FIELDS) assert isinstance(etud["id"], int) assert isinstance(etud["dept_id"], int) @@ -148,12 +148,14 @@ def test_etud_in_group(api_headers): assert isinstance(etud["code_nip"], str) assert isinstance(etud["code_ine"], str) + # query sans filtre: + etuds_query = GET(f"/group/{group_id}/etudiants/query", headers=api_headers) + assert etuds_query == etuds + etat = "I" - etudiants = GET( - f"/group/{group_id}/etudiants/query?etat={etat}", headers=api_headers - ) - assert isinstance(etudiants, list) - for etud in etudiants: + etuds = GET(f"/group/{group_id}/etudiants/query?etat={etat}", headers=api_headers) + assert isinstance(etuds, list) + for etud in etuds: assert verify_fields(etud, PARTITION_GROUPS_ETUD_FIELDS) assert isinstance(etud["id"], int) assert isinstance(etud["dept_id"], int) From 84c08ff22514c82513234c6443680b1569449f41 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 10 Aug 2022 07:29:34 +0200 Subject: [PATCH 03/15] Changed route: /role/create/ --- app/api/users.py | 4 ++-- tests/api/test_api_users.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/api/users.py b/app/api/users.py index 96b7cdbf0..5c249b158 100644 --- a/app/api/users.py +++ b/app/api/users.py @@ -306,8 +306,8 @@ def role_permission_remove(role_name: str, perm_name: str): return jsonify(role.to_dict()) -@bp.route("/role//create", methods=["POST"]) -@api_web_bp.route("/role//create", methods=["POST"]) +@bp.route("/role/create/", methods=["POST"]) +@api_web_bp.route("/role/create/", methods=["POST"]) @login_required @scodoc @permission_required(Permission.ScoSuperAdmin) diff --git a/tests/api/test_api_users.py b/tests/api/test_api_users.py index 5ddc38468..8174a9fe3 100644 --- a/tests/api/test_api_users.py +++ b/tests/api/test_api_users.py @@ -105,7 +105,7 @@ def test_roles(api_admin_headers): uid = user["id"] ans = POST_JSON(f"/user/{uid}/role/Secr/add", headers=admin_h) assert ans["user_name"] == "test_roles" - role = POST_JSON("/role/Test_X/create", headers=admin_h) + role = POST_JSON("/role/create/Test_X", headers=admin_h) assert role["role_name"] == "Test_X" assert role["permissions"] == [] role = GET("/role/Test_X", headers=admin_h) From e2ca74fd506bdc66d23681028f6784702cc8834e Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 10 Aug 2022 07:32:55 +0200 Subject: [PATCH 04/15] =?UTF-8?q?Fix=20pour=20#471=20:=20=C3=A0=20confirme?= =?UTF-8?q?r.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/but/jury_but_recap.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/but/jury_but_recap.py b/app/but/jury_but_recap.py index 6611147e8..038335ac2 100644 --- a/app/but/jury_but_recap.py +++ b/app/but/jury_but_recap.py @@ -484,12 +484,16 @@ def get_jury_but_etud_result( rcue_dict = { "ue_1": { "ue_id": rcue.ue_1.id, - "moy": None if np.isnan(dec_ue1.moy_ue) else dec_ue1.moy_ue, + "moy": None + if (dec_ue1.moy_ue is None or np.isnan(dec_ue1.moy_ue)) + else dec_ue1.moy_ue, "code": dec_ue1.code_valide, }, "ue_2": { "ue_id": rcue.ue_2.id, - "moy": None if np.isnan(dec_ue2.moy_ue) else dec_ue2.moy_ue, + "moy": None + if (dec_ue2.moy_ue is None or np.isnan(dec_ue2.moy_ue)) + else dec_ue2.moy_ue, "code": dec_ue2.code_valide, }, "moy": rcue.moy_rcue, From 0d20da4583196f40fec4111c383c4a41b1f8b825 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 10 Aug 2022 10:49:58 +0200 Subject: [PATCH 05/15] Script test interactif API --- sco_version.py | 3 +- tests/api/exemple-api-basic.py | 129 +++++++++------------------------ tests/api/setup_test_api.py | 9 ++- 3 files changed, 42 insertions(+), 99 deletions(-) diff --git a/sco_version.py b/sco_version.py index 4807fcb6e..436982a2a 100644 --- a/sco_version.py +++ b/sco_version.py @@ -10,6 +10,8 @@ SCONEWS = """
  • ScoDoc 9.3
    • +
    • Nouvelle API REST pour connecter ScoDoc à d'autres applications
    • +
    • Module de gestion des relations avec les entreprises
    • Prise en charge des parcours BUT
    • Association des UEs aux compétences du référentiel
    • Jury BUT1
    • @@ -21,7 +23,6 @@ SCONEWS = """
      • Tableau récap. complet pour BUT et autres formations.
      • Tableau état évaluations
      • -
      • Version alpha du module "relations entreprises"
      • Export des trombinoscope en document docx
      • Très nombreux correctifs
      diff --git a/tests/api/exemple-api-basic.py b/tests/api/exemple-api-basic.py index bd6248068..1938f728d 100644 --- a/tests/api/exemple-api-basic.py +++ b/tests/api/exemple-api-basic.py @@ -4,118 +4,55 @@ """Exemple utilisation API ScoDoc 9 avec jeton obtenu par basic authentication - -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). + Usage: + cd /opt/scodoc/tests/api + python -i exemple-api-basic.py -Travail en cours. +Pour utiliser l'API, (sur une base quelconque) je fais +``` +cd /opt/scodoc/tests/api + +python +>>> admin_h = get_auth_headers("admin", "xxx") +>>> GET("/etudiant/etudid/14806", headers=admin_h) +``` """ -from dotenv import load_dotenv -import json -import os -import requests -import urllib3 +from email import message from pprint import pprint as pp +import urllib3 +from setup_test_api import ( + API_PASSWORD, + API_URL, + API_USER, + APIError, + CHECK_CERTIFICATE, + get_auth_headers, + GET, + POST_JSON, + SCODOC_URL, +) -# --- Lecture configuration (variables d'env ou .env) -try: - BASEDIR = os.path.abspath(os.path.dirname(__file__)) -except NameError: - BASEDIR = "." -load_dotenv(os.path.join(BASEDIR, ".env")) -CHK_CERT = bool(int(os.environ.get("CHECK_CERTIFICATE", False))) -SCODOC_URL = os.environ.get("SCODOC_URL") or "http://localhost:5000" -API_URL = SCODOC_URL + "/ScoDoc/api" -# Admin: -SCODOC_USER = os.environ["SCODOC_USER"] -SCODOC_PASSWORD = os.environ["SCODOC_PASSWORD"] -# Lecteur -SCODOC_USER_API_LECTEUR = os.environ["SCODOC_USER_API_LECTEUR"] -SCODOC_PASSWORD_API_LECTEUR = os.environ["SCODOC_PASSWORD_API_LECTEUR"] +if not CHECK_CERTIFICATE: + urllib3.disable_warnings() print(f"SCODOC_URL={SCODOC_URL}") print(f"API URL={API_URL}") -# --- -if not CHK_CERT: - urllib3.disable_warnings() +HEADERS = get_auth_headers(API_USER, API_PASSWORD) -class ScoError(Exception): - pass - - -def GET(path: str, headers={}, errmsg=None, dept=None): - """Get and returns as JSON""" - if dept: - url = SCODOC_URL + f"/ScoDoc/{dept}/api" + path - else: - url = API_URL + path - r = requests.get(url, headers=headers or HEADERS, verify=CHK_CERT) - if r.status_code != 200: - raise ScoError(errmsg or f"""erreur status={r.status_code} !\n{r.text}""") - return r.json() # decode la reponse JSON - - -def POST(path: str, data: dict = {}, headers={}, errmsg=None): - """Post""" - r = requests.post( - API_URL + path, - data=data, - headers=headers or HEADERS, - verify=CHK_CERT, - ) - if r.status_code != 200: - raise ScoError(errmsg or f"erreur status={r.status_code} !\n{r.text}") - return r.json() # decode la reponse JSON - - -def POST_JSON(path: str, data: dict = {}, headers={}, errmsg=None): - """Post""" - r = requests.post( - API_URL + path, - json=data, - headers=headers or HEADERS, - verify=CHK_CERT, - ) - if r.status_code != 200: - raise ScoError(errmsg or f"erreur status={r.status_code} !\n{r.text}") - return r.json() # decode la reponse JSON - - -def GET_TOKEN(user, password): - "Obtention du jeton (token)" - r = requests.post(API_URL + "/tokens", auth=(user, password)) - assert r.status_code == 200 - token = r.json()["token"] - return {"Authorization": f"Bearer {token}"} - - -HEADERS = GET_TOKEN(SCODOC_USER, SCODOC_PASSWORD) -HEADERS_USER = GET_TOKEN(SCODOC_USER_API_LECTEUR, SCODOC_PASSWORD_API_LECTEUR) - -r = requests.get(API_URL + "/departements", headers=HEADERS, verify=CHK_CERT) -if r.status_code != 200: - raise ScoError("erreur de connexion: vérifier adresse et identifiants") - -pp(r.json()) +departements = GET("/departements", headers=HEADERS) +pp(departements) # Liste de tous les étudiants en cours (de tous les depts) -r = requests.get(API_URL + "/etudiants/courant", headers=HEADERS, verify=CHK_CERT) -if r.status_code != 200: - raise ScoError("erreur de connexion: vérifier adresse et identifiants") +etuds = GET("/etudiants/courants", headers=HEADERS) -print(f"{len(r.json())} étudiants courants") +print(f"{len(etuds)} étudiants courants") + +raise Exception("arret en mode interactif") # Bulletin d'un BUT formsemestre_id = 1063 # A adapter diff --git a/tests/api/setup_test_api.py b/tests/api/setup_test_api.py index eb926d591..b71467ca2 100644 --- a/tests/api/setup_test_api.py +++ b/tests/api/setup_test_api.py @@ -18,10 +18,15 @@ import requests from dotenv import load_dotenv import pytest -BASEDIR = "/opt/scodoc/tests/api" +# --- Lecture configuration (variables d'env ou .env) +try: + BASEDIR = os.path.abspath(os.path.dirname(__file__)) +except NameError: + BASEDIR = "/opt/scodoc/tests/api" + load_dotenv(os.path.join(BASEDIR, ".env")) CHECK_CERTIFICATE = bool(os.environ.get("CHECK_CERTIFICATE", False)) -SCODOC_URL = os.environ["SCODOC_URL"] +SCODOC_URL = os.environ["SCODOC_URL"] or "http://localhost:5000" API_URL = SCODOC_URL + "/ScoDoc/api" API_USER = os.environ.get("API_USER", "test") API_PASSWORD = os.environ.get("API_PASSWD", "test") From d072af8da54028472fb804dfc6138c288929e664 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 10 Aug 2022 10:51:40 +0200 Subject: [PATCH 06/15] doc --- tests/api/exemple-api-basic.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/api/exemple-api-basic.py b/tests/api/exemple-api-basic.py index 1938f728d..c9a2c5ebe 100644 --- a/tests/api/exemple-api-basic.py +++ b/tests/api/exemple-api-basic.py @@ -9,14 +9,22 @@ python -i exemple-api-basic.py -Pour utiliser l'API, (sur une base quelconque) je fais +Pour utiliser l'API, (sur une base quelconque): ``` cd /opt/scodoc/tests/api -python +python -i exemple-api-basic.p >>> admin_h = get_auth_headers("admin", "xxx") >>> GET("/etudiant/etudid/14806", headers=admin_h) ``` + +Créer éventuellement un fichier `.env` dans /opt/scodoc/tests/api +avec la config du client API: +``` + SCODOC_URL = "http://localhost:5000/" + API_USER = "admin" + API_PASSWORD = "test" +``` """ from email import message From 9940e6f01ddae8dcde414a81847bd05f1bbce23f Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 10 Aug 2022 15:21:47 +0200 Subject: [PATCH 07/15] exemple API --- tests/api/exemple-api-basic.py | 87 ++++++++++++++++++++++------------ 1 file changed, 58 insertions(+), 29 deletions(-) diff --git a/tests/api/exemple-api-basic.py b/tests/api/exemple-api-basic.py index c9a2c5ebe..2bc5f9ec1 100644 --- a/tests/api/exemple-api-basic.py +++ b/tests/api/exemple-api-basic.py @@ -27,7 +27,6 @@ avec la config du client API: ``` """ -from email import message from pprint import pprint as pp import urllib3 from setup_test_api import ( @@ -62,30 +61,39 @@ print(f"{len(etuds)} étudiants courants") raise Exception("arret en mode interactif") +# ---------------- DIVERS ESSAIS EN MODE INTERACTIF +# ---------------- A ADAPTER A VOS BESOINS + # Bulletin d'un BUT formsemestre_id = 1063 # A adapter etudid = 16450 -bul = GET(f"/etudiant/etudid/{etudid}/formsemestre/{formsemestre_id}/bulletin") +bul = GET( + f"/etudiant/etudid/{etudid}/formsemestre/{formsemestre_id}/bulletin", + headers=HEADERS, +) # d'un DUT formsemestre_id = 1062 # A adapter etudid = 16309 -bul_dut = GET(f"/etudiant/etudid/{etudid}/formsemestre/{formsemestre_id}/bulletin") +bul_dut = GET( + f"/etudiant/etudid/{etudid}/formsemestre/{formsemestre_id}/bulletin", + headers=HEADERS, +) # Infos sur un étudiant etudid = 3561 code_nip = "11303314" -etud = GET(f"/etudiant/etudid/{etudid}") +etud = GET(f"/etudiant/etudid/{etudid}", headers=HEADERS) print(etud) -etud = GET(f"/etudiant/nip/{code_nip}") +etud = GET(f"/etudiant/nip/{code_nip}", headers=HEADERS) print(etud) -sems = GET(f"/etudiant/etudid/{etudid}/formsemestres") +sems = GET(f"/etudiant/etudid/{etudid}/formsemestres", headers=HEADERS) print("\n".join([s["titre_num"] for s in sems])) -sems = GET(f"/etudiant/nip/{code_nip}/formsemestres") +sems = GET(f"/etudiant/nip/{code_nip}/formsemestres", headers=HEADERS) print("\n".join([s["titre_num"] for s in sems])) # Evaluation @@ -93,35 +101,37 @@ evals = GET("/evaluations/1") # Partitions d'un BUT formsemestre_id = 1063 # A adapter -partitions = GET(f"/formsemestre/{formsemestre_id}/partitions") +partitions = GET(f"/formsemestre/{formsemestre_id}/partitions", headers=HEADERS) print(partitions) pid = partitions[1]["id"] -partition = GET(f"/partition/{pid}") +partition = GET(f"/partition/{pid}", headers=HEADERS) print(partition) group_id = partition["groups"][0]["id"] -etuds = GET(f"/group/{group_id}/etudiants") +etuds = GET(f"/group/{group_id}/etudiants", headers=HEADERS) print(f"{len(etuds)} étudiants") pp(etuds[1]) -etuds_dem = GET(f"/group/{group_id}/etudiants/query?etat=D") +etuds_dem = GET(f"/group/{group_id}/etudiants/query?etat=D", headers=HEADERS) print(f"{len(etuds_dem)} étudiants") etudid = 16650 group_id = 5315 -POST(f"/group/{group_id}/set_etudiant/{etudid}") +POST(f"/group/{group_id}/set_etudiant/{etudid}", headers=HEADERS) -POST_JSON(f"/partition/{pid}/group/create", data={"group_name": "Omega10"}) -partitions = GET(f"/formsemestre/{formsemestre_id}/partitions") +POST_JSON( + f"/partition/{pid}/group/create", data={"group_name": "Omega10"}, headers=HEADERS +) +partitions = GET(f"/formsemestre/{formsemestre_id}/partitions", headers=HEADERS) pp(partitions) -POST_JSON(f"/group/5559/delete") -POST_JSON(f"/group/5327/edit", data={"group_name": "TDXXX"}) +POST_JSON(f"/group/5559/delete", headers=HEADERS) +POST_JSON(f"/group/5327/edit", data={"group_name": "TDXXX"}, headers=HEADERS) # --------- XXX à passer en dans les tests unitaires # 0- Prend un étudiant au hasard dans le semestre -etud = GET(f"/formsemestre/{formsemestre_id}/etudiants")[10] +etud = GET(f"/formsemestre/{formsemestre_id}/etudiants", headers=HEADERS)[10] etudid = etud["id"] # 1- Crée une partition, puis la change de nom @@ -133,30 +143,43 @@ partition_id = js["id"] POST_JSON( f"/partition/{partition_id}/edit", data={"partition_name": "PART1", "show_in_lists": True}, + headers=HEADERS, ) # 2- Crée un groupe -js = POST_JSON(f"/partition/{partition_id}/group/create", data={"group_name": "G1"}) +js = POST_JSON( + f"/partition/{partition_id}/group/create", + data={"group_name": "G1"}, + headers=HEADERS, +) group_1 = js["id"] # 3- Crée deux autres groupes -js = POST_JSON(f"/partition/{partition_id}/group/create", data={"group_name": "G2"}) -js = POST_JSON(f"/partition/{partition_id}/group/create", data={"group_name": "G3"}) +js = POST_JSON( + f"/partition/{partition_id}/group/create", + data={"group_name": "G2"}, + headers=HEADERS, +) +js = POST_JSON( + f"/partition/{partition_id}/group/create", + data={"group_name": "G3"}, + headers=HEADERS, +) # 4- Affecte étudiant au groupe G1 -POST_JSON(f"/group/{group_1}/set_etudiant/{etudid}") +POST_JSON(f"/group/{group_1}/set_etudiant/{etudid}", headers=HEADERS) # 5- retire du groupe -POST_JSON(f"/group/{group_1}/remove_etudiant/{etudid}") +POST_JSON(f"/group/{group_1}/remove_etudiant/{etudid}", headers=HEADERS) # 6- affecte au groupe G2 partition = GET(f"/partition/{partition_id}") assert len(partition["groups"]) == 3 group_2 = [g for g in partition["groups"].values() if g["group_name"] == "G2"][0]["id"] -POST_JSON(f"/group/{group_2}/set_etudiant/{etudid}") +POST_JSON(f"/group/{group_2}/set_etudiant/{etudid}", headers=HEADERS) # 7- Membres du groupe -etuds_g2 = GET(f"/group/{group_2}/etudiants") +etuds_g2 = GET(f"/group/{group_2}/etudiants", headers=HEADERS) assert len(etuds_g2) == 1 assert etuds_g2[0]["id"] == etudid @@ -166,9 +189,13 @@ group_3 = [g for g in partition["groups"].values() if g["group_name"] == "G3"][0 POST_JSON( f"/partition/{partition_id}/groups/order", data=[group_2, group_1, group_3], + headers=HEADERS, ) -new_groups = [g["id"] for g in GET(f"/partition/{partition_id}")["groups"].values()] +new_groups = [ + g["id"] + for g in GET(f"/partition/{partition_id}", headers=HEADERS)["groups"].values() +] assert new_groups == [group_2, group_1, group_3] # 9- Suppression @@ -193,23 +220,25 @@ POST_JSON(f"/partition/{partition_id}/delete") POST_JSON( "/partition/2264/groups/order", data=[5563, 5562, 5561, 5560, 5558, 5557, 5316, 5315], + headers=HEADERS, ) POST_JSON( "/formsemestre/1063/partitions/order", data=[2264, 2263, 2265, 2266, 2267, 2372, 2378], + headers=HEADERS, ) -GET(f"/partition/2264") +GET(f"/partition/2264", headers=HEADERS) # Recherche de formsemestres -sems = GET(f"/formsemestres/query?etape_apo=V1RT&annee_scolaire=2021") +sems = GET(f"/formsemestres/query?etape_apo=V1RT&annee_scolaire=2021", headers=HEADERS) # Table récap: -pp(GET(f"/formsemestre/1063/resultats")[0]) +pp(GET(f"/formsemestre/1063/resultats", headers=HEADERS)[0]) -pp(GET(f"/formsemestre/880/resultats")[0]) +pp(GET(f"/formsemestre/880/resultats", headers=HEADERS)[0]) # # sems est une liste de semestres (dictionnaires) # for sem in sems: From e899cd0d166a4761ef04c875922c54aeb26d6acb Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sat, 13 Aug 2022 23:28:20 +0200 Subject: [PATCH 08/15] Modif route API /user//edit. Fix #472 --- app/api/users.py | 4 ++-- tests/api/test_api_users.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/api/users.py b/app/api/users.py index 5c249b158..8b34901c7 100644 --- a/app/api/users.py +++ b/app/api/users.py @@ -124,8 +124,8 @@ def user_create(): return jsonify(user.to_dict()) -@bp.route("/user/edit/", methods=["POST"]) -@api_web_bp.route("/user/edit/", methods=["POST"]) +@bp.route("/user//edit", methods=["POST"]) +@api_web_bp.route("/user//edit", methods=["POST"]) @login_required @scodoc @permission_required(Permission.ScoUsersAdmin) diff --git a/tests/api/test_api_users.py b/tests/api/test_api_users.py index 8174a9fe3..82c6cdefc 100644 --- a/tests/api/test_api_users.py +++ b/tests/api/test_api_users.py @@ -94,7 +94,7 @@ def test_edit_users(api_admin_headers): def test_roles(api_admin_headers): """ Routes: /user/create - /user/edit/ + /user//edit """ admin_h = api_admin_headers user = POST_JSON( From 4182134547e2030b8e82309949e4cdc4b793b110 Mon Sep 17 00:00:00 2001 From: Jean-Marie PLACE Date: Sun, 14 Aug 2022 11:36:24 +0200 Subject: [PATCH 09/15] ajout utilitaire make_samples --- tests/api/make_samples.py | 177 ++++++++++++++++++++++++++++++++++++++ tests/api/samples.csv | 77 +++++++++++++++++ 2 files changed, 254 insertions(+) create mode 100644 tests/api/make_samples.py create mode 100644 tests/api/samples.csv diff --git a/tests/api/make_samples.py b/tests/api/make_samples.py new file mode 100644 index 000000000..fbb198283 --- /dev/null +++ b/tests/api/make_samples.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python3 +# -*- mode: python -*- +# -*- coding: utf-8 -*- + +"""Construction des fichiers exemples pour la documentation. + + Usage: + cd /opt/scodoc/tests/api + python make_samples.py + +doit être exécutée immédiatement apres une initialisation de la base pour test API! (car dépendant des identifiants générés lors de la création des objets) + cd /opt/scodoc/tests/api + tools/create_database.sh --drop SCODOC_TEST_API && flask db upgrade &&flask sco-db-init --erase && flask init-test-database + +Créer éventuellement un fichier `.env` dans /opt/scodoc/tests/api +avec la config du client API: +``` + SCODOC_URL = "http://localhost:5000/" +``` + +Cet utilitaire prend en donnée le fichier de nom `samples.csv` contenant la description des exemples (séparés par une tabulation (\t), une ligne par exemple) +* Le nom de l'exemple donne le nom du fichier généré (nom_exemple => nom_exemple.json.md). plusieurs lignes peuvent partager le même nom. dans ce cas le fichier contiendra chacun des exemples +* l'url utilisée +* la permission nécessaire (par défaut ScoView) +* la méthode GET,POST à utiliser (si commence par #, la ligne est ignorée) +* les arguments éventuel (en cas de POST): une chaîne de caractère selon json + +Implémentation: +Le code complète une structure de données (Samples) qui est un dictionnaire de set (indicé par le nom des exemple. +Chacun des éléments du set est un exemple (Sample) +Quand la structure est complète, on génére tous les fichiers textes +- nom de l exemple +- un ou plusieurs exemples avec pour chaucn + - l url utilisée + - les arguments éventuels + - le résultat +Le tout mis en forme au format markdown et rangé dans le répertoire DATA_DIR (/tmp/samples) qui est créé ou écrasé si déjà existant + +TODO: ajouter un argument au script permettant de ne générer qu'un seul fichier (exemple: `python make_samples.py nom_exemple`) + +""" +import os +import shutil +from collections import defaultdict +from pprint import pprint as pp +from pprint import pformat as pf + +import urllib3 +import json +from setup_test_api import ( + API_PASSWORD, + API_URL, + API_USER, + APIError, + CHECK_CERTIFICATE, + get_auth_headers, + GET, + POST_JSON, + SCODOC_URL, +) + +DATA_DIR = "/tmp/samples/" + + +class Sample: + def __init__(self, url, method="GET", permission="ScoView", content=None): + self.content = content + self.permission = permission + self.url = url + self.method = method + self.result = None + if permission == "ScoView": + HEADERS = get_auth_headers("test", "test") + elif permission == "ScoSuperAdmin": + HEADERS = get_auth_headers("admin_api", "admin_api") + elif permission == "ScoUsersAdmin": + HEADERS = get_auth_headers("admin_api", "admin_api") + else: + raise Exception(f"Bad permission : {permission}") + if self.method == "GET": + self.result = GET(self.url, HEADERS) + elif self.method == "POST": + if self.content == "": + self.result = POST_JSON(self.url, headers=HEADERS) + else: + HEADERS["Content-Type"] = "application/json ; charset=utf-8" + self.result = POST_JSON(self.url, json.loads(self.content), HEADERS) + elif self.method[0] != "#": + raise Exception(f"Bad method : {self.method}") + else: # method begin with # => comment + print(" pass") + self.shorten() + file = open(f"sample_TEST.json.md", "tw") + self.dump(file) + file.close() + + def _shorten(self, item): + if isinstance(item, list): + return [self._shorten(child) for child in item[:2]] + return item + + def shorten(self): + self.result = self._shorten(self.result) + + def pp(self): + print(f"------ url: {self.url}") + print(f"method: {self.method}") + print(f"content: {self.content}") + print(f"permission: {self.permission}") + pp(self.result, indent=4) + + def dump(self, file): + file.write(f"#### {self.method} {self.url}\n") + if len(self.content) > 0: + file.write(f"> `Content-Type: application/json`\n") + file.write(f"> \n") + file.write(f"> `{self.content}`\n\n") + + file.write("```json\n") + file.write(json.dumps(self.result, indent=4)) + file.write("\n```\n\n") + + +class Samples: + def __init__(self): + self.entries = defaultdict(lambda: set()) + + def add_sample(self, entry, url, method="GET", permission="ScoView", content=None): + show_content = "" if content == "" else f": '{content}'" + print(f"{entry:50} {method:5} {url:50} {show_content}") + sample = Sample(url, method, permission, content) + self.entries[entry].add(sample) + + def pp(self): + for entry, samples in self.entries.items(): + print(f"=== {entry}") + for sample in samples: + sample.pp() + + def dump(self): + for entry, samples in self.entries.items(): + file = open(f"{DATA_DIR}sample_{entry}.json.md", "tw") + file.write(f"### {entry}\n\n") + for sample in samples: + sample.dump(file) + file.close() + + +def make_samples(): + if os.path.exists(DATA_DIR): + if not os.path.isdir(DATA_DIR): + raise f"{DATA_DIR} existe déjà et n'est pas un répertoire" + else: + # DATA_DIR existe déjà - effacer et recréer + shutil.rmtree(DATA_DIR) + os.mkdir(DATA_DIR) + else: + os.mkdir("/tmp/samples") + + samples = Samples() + # samples.pp() + with open("samples.csv") as f: + L = [x[:-1].split("\t") for x in f] + for line in L[1:]: + entry_name = line[0] + url = line[1] + permission = line[2] if line[2] != "" else "ScoView" + method = line[3] if line[3] != "" else "GET" + content = line[4] + samples.add_sample(entry_name, url, method, permission, content) + samples.dump() + return samples + + +if not CHECK_CERTIFICATE: + urllib3.disable_warnings() +make_samples() diff --git a/tests/api/samples.csv b/tests/api/samples.csv new file mode 100644 index 000000000..341bda1e8 --- /dev/null +++ b/tests/api/samples.csv @@ -0,0 +1,77 @@ +reference url permission method content +departements /departements GET +departements-ids /departements_ids GET +departement /departement/TAPI GET +departement /departement/id/1 GET +departement-etudiants /departement/TAPI/etudiants GET +departement-etudiants /departement/id/1/etudiants GET +departement-formsemestres_ids /departement/TAPI/formsemestres_ids GET +departement-formsemestres_ids /departement/id/1/formsemestres_ids GET +departement-formsemestres-courants /departement/TAPI/formsemestres_courants GET +departement-formsemestres-courants /departement/id/1/formsemestres_courants GET +departement-create /departement/create ScoSuperAdmin POST {"acronym": "NEWONE" , "visible": true} +departement-edit /departement/NEWONE/edit ScoSuperAdmin POST {"visible": false} +departement-delete /departement/NEWONE/delete ScoSuperAdmin POST +etudiants-courants /etudiants/courants GET +etudiants-courants /etudiants/courants/long GET +etudiant /etudiant/etudid/11 GET +etudiant /etudiant/nip/11 GET +etudiant /etudiant/ine/INE11 GET +etudiants-clef /etudiants/etudid/11 GET +etudiants-clef /etudiants/ine/INE11 GET +etudiants-clef /etudiants/nip/11 GET +etudiant-formsemestres /etudiant/etudid/11/formsemestres GET +etudiant-formsemestres /etudiant/ine/INE11/formsemestres GET +etudiant_formsemestres /etudiant/nip/11/formsemestres GET +etudiant-formsemestre-bulletin /etudiant/etudid/11/formsemestre/1/bulletin GET +etudiant-formsemestre-bulletin /etudiant/ine/INE11/formsemestre/1/bulletin GET +etudiant-formsemestre-bulletin /etudiant/nip/11/formsemestre/1/bulletin GET +etudiant-formsemestre-groups /etudiant/etudid/11/formsemestre/1/groups GET +formations /formations GET +formations_ids /formations_ids GET +formation /formation/1 GET +formation-export /formation/1/export GET +formation-export /formation/1/export_with_ids GET +formation-referentiel_competences /formation/1/referentiel_competences GET +moduleimpl /moduleimpl/1 GET +formsemestre /formsemestre/1 GET +formsemestres-query /formsemestres/query?annee_scolaire=2022&etape_apo=A2 GET +formsemestre-bulletins /formsemestre/1/bulletins GET +formsemestre-programme /formsemestre/1/programme GET +formsemestre-etudiants /formsemestre/1/etudiants GET +formsemestre-etudiants-query /formsemestre/1/etudiants/query?etat=D GET +formsemestre-etat_evals /formsemestre/1/etat_evals GET +formsemestre-resultats /formsemestre/1/resultats GET +formsemestre-decisions_jury /formsemestre/1/decisions_jury GET +formsemestre-partitions /formsemestre/1/partitions GET +partition /partition/1 GET +group-etudiants /group/1/etudiants GET +group-etudiants-query /group/1/etudiants/query?etat=D GET +moduleimpl-evaluations /moduleimpl/1/evaluations GET +evaluation-notes /evaluation/1/notes GET +user /user/1 GET +users-query /users/query?starts_with=u_ GET +permissions /permissions GET +roles /roles GET +role /role/Observateur GET +group-set_etudiant /group/1/set_etudiant/10 ScoSuperAdmin POST +group-remove_etudiant /group/1/remove_etudiant/10 ScoSuperAdmin POST +partition-group-create /partition/1/group/create ScoSuperAdmin POST {"group_name": "NEW_GROUP"} +group-edit /group/2/edit ScoSuperAdmin POST {"group_name": "NEW_GROUP2"} +group-delete /group/2/delete ScoSuperAdmin POST +formsemestre-partition-create /formsemestre/1/partition/create ScoSuperAdmin POST {"partition_name": "PART"} +formsemestre-partitions-order /formsemestre/1/partitions/order ScoSuperAdmin POST [ 1 ] +partition-edit /partition/1/edit ScoSuperAdmin POST {"partition_name":"P2BIS", "numero":3,"bul_show_rank":true,"show_in_lists":false, "groups_editable":true} +partition-remove_etudiant /partition/2/remove_etudiant/10 ScoSuperAdmin POST +partition-groups-order /partition/1/groups/order ScoSuperAdmin POST [ 1 ] +partition-delete /partition/2/delete ScoSuperAdmin POST +user-create /user/create ScoSuperAdmin POST {"user_name": "alain", "dept": null, "nom": "alain", "prenom": "bruno", "active": true } +user-edit /user/10/edit ScoSuperAdmin POST { "dept": "TAPI", "nom": "alain2", "prenom": "bruno2", "active": false } +user-role-add /user/10/role/Observateur/add ScoSuperAdmin POST +user-role-remove /user/10/role/Observateur/remove ScoSuperAdmin POST +role-create /role/create/customRole ScoSuperAdmin POST {"permissions": ["ScoView", "ScoUsersView"]} +role-remove_permission /role/customRole/remove_permission/ScoUsersView ScoSuperAdmin POST +role-add_permission /role/customRole/add_permission/ScoUsersView ScoSuperAdmin POST +role-edit /role/customRole/edit ScoSuperAdmin POST { "name" : "LaveurDeVitres", "permissions" : [ "ScoView", "APIView" ] } +role-edit /role/customRole/edit ScoSuperAdmin POST { "name" : "LaveurDeVitres", "permissions" : [ "ScoView", "APIView" ] } +role-delete /role/customRole/delete ScoSuperAdmin POST From 713c70799140365b7e69e2d4da772204cf63a014 Mon Sep 17 00:00:00 2001 From: lehmann Date: Sun, 14 Aug 2022 17:11:34 +0200 Subject: [PATCH 10/15] =?UTF-8?q?Partition=20editor=20-=20am=C3=A9lioratio?= =?UTF-8?q?ns?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/static/css/partition_editor.css | 15 +++- app/templates/scolar/partition_editor.html | 93 +++++++++++++--------- 2 files changed, 71 insertions(+), 37 deletions(-) diff --git a/app/static/css/partition_editor.css b/app/static/css/partition_editor.css index c7abf6779..61e872e08 100644 --- a/app/static/css/partition_editor.css +++ b/app/static/css/partition_editor.css @@ -50,6 +50,8 @@ main { display: flex; flex-wrap: wrap; gap: 32px; + row-gap: 4px; + margin-right: 16px; } main h2 { @@ -184,8 +186,16 @@ body.editionActivated .filtres>div>div>div>div { } /*****************************/ -/* Zone Choix */ +/* Zone Partitions */ /*****************************/ +#zonePartitions { + width: 100%; +} + +.filtres { + display: table; +} + .filtres>div { background: #ddd; padding: 8px; @@ -226,6 +236,9 @@ body:not(.editionActivated) .filtres>div>div>div>div:active { background: rgba(0, 153, 204, 0.5); } +/*****************************/ +/* Zone Etudiants */ +/*****************************/ #zoneChoix .etudiants>div { background: #FFF; border: 1px solid #aaa; diff --git a/app/templates/scolar/partition_editor.html b/app/templates/scolar/partition_editor.html index 2f67f979b..4e641e781 100644 --- a/app/templates/scolar/partition_editor.html +++ b/app/templates/scolar/partition_editor.html @@ -1,33 +1,37 @@ {# -*- mode: jinja-html -*- #}

      {% if not read_only %}Édition des p{% else %}P{%endif%}artitions

      -
      - -
      -
      -
      -

      Choix

      -
      -
      -

      Afficher les partitions

      -
      -
      -
      -

      - Afficher les étudiants affectés aux groupes
      - Ne s'actualise pas automatiquement lors d'une modification -

      -
      +
      +

      Partitions et groupes

      +
      + +
      +
      +

      Afficher les partitions

      +
      +
      +
      +

      + Afficher les étudiants affectés aux groupes
      + Ne s'actualise pas automatiquement lors d'une modification +

      +
      +
      +
      + +
      +

      Etudiants

      +

      Groupes

      @@ -78,7 +82,12 @@ arrayPartitions.forEach((partition) => { // Filtres - outputPartitions += `
      ||${partition.partition_name}✏️
      `; + if (partition.groups_editable) { + outputPartitions += `
      ||${partition.partition_name}✏️
      `; + } else { + outputPartitions += `
      ${partition.partition_name}
      `; + } + outputMasques += `
      Non affectés - ${partition.partition_name}
      `; // Groupes @@ -96,9 +105,13 @@ let output = ""; arrayGroups.forEach((groupe) => { /***************/ - outputMasques += `
      ||${groupe.group_name}✏️
      `; // patch JMP (renommage du champ name dans l API) + if (partition.groups_editable) { + outputMasques += `
      ||${groupe.group_name}✏️
      `; + } else { + outputMasques += `
      ${groupe.group_name}
      `; + } /***************/ - output += templateGroupe_zoneGroupes(groupe.id, groupe.group_name); // patch JMP (renommage du champ name dans l API) + output += templateGroupe_zoneGroupes(groupe.id, groupe.group_name); }) return output; })()} @@ -144,7 +157,7 @@ if (!affected) { document.querySelector(`#zoneGroupes [data-idpartition="${partition.id}"]>[data-idgroupe="aucun"]>.etudiants`).innerHTML += templateEtudiant_zoneGroupes(etudiant); } - return `` + output; + return `` + output; })()}
      `; }) @@ -171,9 +184,6 @@ /******************************/ function input() { document.querySelector("body").classList.toggle("editionActivated"); - /*if (event.currentTarget.checked == false) { - go(); - }*/ } function processEvents() { /*--------------------*/ @@ -193,7 +203,7 @@ /*--------------------*/ /* Changement groupe */ /*--------------------*/ - document.querySelectorAll("#zoneChoix label").forEach(btn => { btn.addEventListener("mousedown", (event) => { event.preventDefault() }) }); + document.querySelectorAll("label").forEach(btn => { btn.addEventListener("mousedown", (event) => { event.preventDefault() }) }); document.querySelectorAll(".etudiants input").forEach(input => { input.addEventListener("input", assignment) }) } @@ -223,6 +233,7 @@ } if (!this.dataset.idgroupe) { + // Partitions let groupesSelected = []; this.parentElement.querySelectorAll(":not(.unselect)").forEach(e => { groupesSelected.push(e.dataset.idpartition); @@ -238,6 +249,7 @@ } }) } else { + // Groupes let groupesSelected = {}; this.parentElement.parentElement.querySelectorAll("[data-idgroupe]:not(.unselect)").forEach(e => { @@ -350,6 +362,8 @@ div.querySelector(".move").addEventListener("mousedown", moveStart); this.parentElement.insertBefore(div, this); + div.querySelector(".modif").click(); + // Save fetch(url, { @@ -380,12 +394,12 @@ div.querySelector("div").addEventListener("click", filtre); div.querySelector(".ajoutGroupe").addEventListener("click", addPartition); - document.querySelector("#zoneChoix .masques>div").appendChild(div); + document.querySelector("#zonePartitions .masques>div").appendChild(div); // Ajout de la zone pour chaque étudiant let outputGroupes = ""; - document.querySelectorAll(`#zoneChoix .grpPartitions`).forEach(e => { + document.querySelectorAll(`#zonePartitions .grpPartitions`).forEach(e => { let etudid = e.previousElementSibling.dataset.etudid; // Préparation pour la section suivante @@ -447,10 +461,17 @@ /* Edition du texte */ /********************/ function editText() { - this.previousElementSibling.classList.add("editingText"); - this.previousElementSibling.setAttribute("contenteditable", "true"); - this.previousElementSibling.focus(); - this.previousElementSibling.addEventListener("keydown", writing); + let e = this.previousElementSibling; + e.classList.add("editingText"); + e.setAttribute("contenteditable", "true"); + e.addEventListener("keydown", writing); + + // On sélectionne la zone + const range = document.createRange(); + const selection = window.getSelection(); + selection.removeAllRanges(); + range.selectNodeContents(e); + selection.addRange(range); } function writing(event) { @@ -613,12 +634,12 @@ let formsemestre_id = params.get('formsemestre_id'); var url = `/ScoDoc/{{formsemestre.departement.acronym}}/api/formsemestre/${formsemestre_id}/partitions/order`; - document.querySelectorAll(`#zoneChoix .masques>div`).forEach(parent => { + document.querySelectorAll(`#zonePartitions .masques>div`).forEach(parent => { positions.forEach(position => { parent.append(parent.querySelector(`[data-idpartition="${position}"]`)) }) }) - document.querySelectorAll(`#zoneChoix .grpPartitions`).forEach(parent => { + document.querySelectorAll(`#zonePartitions .grpPartitions`).forEach(parent => { positions.forEach(position => { parent.append(parent.querySelector(`[data-idpartition="${position}"]`)) }) From 6fc89b88ef544925376e7649b535b3b9969cf809 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 17 Aug 2022 17:24:05 +0200 Subject: [PATCH 11/15] API: most App exceptions now return json error message --- app/api/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/api/__init__.py b/app/api/__init__.py index 0e3e72092..886c26f8c 100644 --- a/app/api/__init__.py +++ b/app/api/__init__.py @@ -4,11 +4,13 @@ from flask import Blueprint from flask import request from app.scodoc import sco_utils as scu +from app.scodoc.sco_exceptions import ScoException api_bp = Blueprint("api", __name__) api_web_bp = Blueprint("apiweb", __name__) +@api_bp.errorhandler(ScoException) @api_bp.errorhandler(404) def api_error_handler(e): "erreurs API => json" From 9d50a88f2ca7f152aace349630a06a49a9d2fa6e Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 17 Aug 2022 18:15:48 +0200 Subject: [PATCH 12/15] Fix #471 ++ tests API --- app/api/jury.py | 10 ++++++--- app/but/jury_but.py | 6 +++--- app/but/jury_but_recap.py | 3 +++ app/comp/moy_ue.py | 5 +---- tests/api/exemple-api-basic.py | 38 +++++++++++++++++++++++++++++++++- tests/api/setup_test_api.py | 8 ++++--- 6 files changed, 56 insertions(+), 14 deletions(-) diff --git a/app/api/jury.py b/app/api/jury.py index 7cf067cc3..b58147c7f 100644 --- a/app/api/jury.py +++ b/app/api/jury.py @@ -15,6 +15,7 @@ import app from app import db, log from app.api import api_bp as bp, api_web_bp from app.decorators import scodoc, permission_required +from app.scodoc.sco_exceptions import ScoException from app.scodoc.sco_utils import json_error from app.but import jury_but_recap from app.models import FormSemestre, FormSemestreInscription, Identite @@ -30,6 +31,9 @@ def decisions_jury(formsemestre_id: int): """Décisions du jury des étudiants du formsemestre.""" # APC, pair: formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id) - app.set_sco_dept(formsemestre.departement.acronym) - rows = jury_but_recap.get_jury_but_results(formsemestre) - return jsonify(rows) + if formsemestre.formation.is_apc(): + app.set_sco_dept(formsemestre.departement.acronym) + rows = jury_but_recap.get_jury_but_results(formsemestre) + return jsonify(rows) + else: + raise ScoException("non implemente") diff --git a/app/but/jury_but.py b/app/but/jury_but.py index 2091c16af..c4236910c 100644 --- a/app/but/jury_but.py +++ b/app/but/jury_but.py @@ -63,13 +63,13 @@ from operator import attrgetter import re from typing import Union +import numpy as np from flask import g, url_for from app import db from app import log from app.comp.res_but import ResultatsSemestreBUT -from app.comp import inscr_mod, res_sem -from app.models import formsemestre +from app.comp import res_sem from app.models.but_refcomp import ( ApcAnneeParcours, @@ -917,7 +917,7 @@ class DecisionsProposeesUE(DecisionsProposees): self.codes = [ sco_codes.DEM if inscription_etat == scu.DEMISSION else sco_codes.DEF ] - self.moy_ue = "-" + self.moy_ue = np.NaN return # Moyenne de l'UE ? diff --git a/app/but/jury_but_recap.py b/app/but/jury_but_recap.py index 038335ac2..90ee14be4 100644 --- a/app/but/jury_but_recap.py +++ b/app/but/jury_but_recap.py @@ -448,6 +448,9 @@ def get_jury_but_table( def get_jury_but_results(formsemestre: FormSemestre) -> list[dict]: """Liste des résultats jury BUT sous forme de dict, pour API""" + if formsemestre.formation.referentiel_competence is None: + # pas de ref. comp., donc pas de decisions de jury (ne lance pas d'exception) + return [] dpv = sco_pvjury.dict_pvjury(formsemestre.id) rows = [] for etudid in formsemestre.etuds_inscriptions: diff --git a/app/comp/moy_ue.py b/app/comp/moy_ue.py index 5f432387c..b6f47d663 100644 --- a/app/comp/moy_ue.py +++ b/app/comp/moy_ue.py @@ -221,10 +221,7 @@ def compute_ue_moys_apc( modimpl_mask: np.array, ) -> pd.DataFrame: """Calcul de la moyenne d'UE en mode APC (BUT). - La moyenne d'UE est un nombre (note/20), ou NI ou NA ou ERR - NI non inscrit à (au moins un) module de cette UE - NA pas de notes disponibles - ERR erreur dans une formule utilisateurs (pas gérées ici). + La moyenne d'UE est un nombre (note/20), ou NaN si pas de notes disponibles sem_cube: notes moyennes aux modules ndarray (etuds x modimpls x UEs) diff --git a/tests/api/exemple-api-basic.py b/tests/api/exemple-api-basic.py index 2bc5f9ec1..022065a59 100644 --- a/tests/api/exemple-api-basic.py +++ b/tests/api/exemple-api-basic.py @@ -28,6 +28,7 @@ avec la config du client API: """ from pprint import pprint as pp +import sys import urllib3 from setup_test_api import ( API_PASSWORD, @@ -128,7 +129,42 @@ pp(partitions) POST_JSON(f"/group/5559/delete", headers=HEADERS) POST_JSON(f"/group/5327/edit", data={"group_name": "TDXXX"}, headers=HEADERS) -# --------- XXX à passer en dans les tests unitaires +# --------- Toutes les bulletins, un à un, et les décisions de jury d'un semestre +formsemestre_id = 911 +etuds = GET(f"/formsemestre/{formsemestre_id}/etudiants", headers=admin_h) +etudid = 16450 +bul = GET( + f"/etudiant/etudid/{etudid}/formsemestre/{formsemestre_id}/bulletin", + headers=HEADERS, +) +for etud in etuds: + bul = GET( + f"/etudiant/etudid/{etud['id']}/formsemestre/{formsemestre_id}/bulletin", + headers=HEADERS, + ) + sys.stdout.write(".") + sys.stdout.flush() + +print("") +decisions = GET(f"/formsemestre/{formsemestre_id}/decisions_jury", headers=HEADERS) + +# Decisions de jury des _tous_ les formsemestre, un à un, en partant de l'id le plus élevé +formsemestres = GET("/formsemestres/query", headers=HEADERS) +formsemestres.sort(key=lambda s: s["id"], reverse=1) +print(f"###### Testing {len(formsemestres)} formsemestres...") +for formsemestre in formsemestres: + print(formsemestre["session_id"]) + try: + decisions = GET( + f"/formsemestre/{formsemestre['id']}/decisions_jury", headers=HEADERS + ) + except APIError as exc: + if exc.payload.get("message") != "non implemente": + raise + decisions = [] + print(f"{len(decisions)} decisions") + +# --------- A été passé dans les tests unitaires: # 0- Prend un étudiant au hasard dans le semestre etud = GET(f"/formsemestre/{formsemestre_id}/etudiants", headers=HEADERS)[10] diff --git a/tests/api/setup_test_api.py b/tests/api/setup_test_api.py index b71467ca2..fb7517ce8 100644 --- a/tests/api/setup_test_api.py +++ b/tests/api/setup_test_api.py @@ -38,7 +38,9 @@ print(f"API URL={API_URL}") class APIError(Exception): - pass + def __init__(self, message: str = "", payload=None): + self.message = message + self.payload = payload or {} def get_auth_headers(user, password) -> dict: @@ -70,7 +72,7 @@ def GET(path: str, headers: dict = None, errmsg=None, dept=None): url = API_URL + path r = requests.get(url, headers=headers or {}, verify=CHECK_CERTIFICATE) if r.status_code != 200: - raise APIError(errmsg or f"""erreur status={r.status_code} !\n{r.text}""") + raise APIError(errmsg or f"""erreur status={r.status_code} !""", r.json()) return r.json() # decode la reponse JSON @@ -83,5 +85,5 @@ def POST_JSON(path: str, data: dict = {}, headers: dict = None, errmsg=None): verify=CHECK_CERTIFICATE, ) if r.status_code != 200: - raise APIError(errmsg or f"erreur status={r.status_code} !\n{r.text}") + raise APIError(errmsg or f"erreur status={r.status_code} !", r.json()) return r.json() # decode la reponse JSON From 920de2c2f17468fbaf1b5e421d7fa87a9be72b0d Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 17 Aug 2022 18:24:18 +0200 Subject: [PATCH 13/15] API: /formsemestre//etudiants ne montre plus la partition par defaut. --- app/api/formsemestres.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/api/formsemestres.py b/app/api/formsemestres.py index ba09a2e4e..d7397277f 100644 --- a/app/api/formsemestres.py +++ b/app/api/formsemestres.py @@ -294,7 +294,9 @@ def formsemestre_etudiants(formsemestre_id: int, with_query: bool = False): # Ajout des groupes de chaque étudiants # XXX A REVOIR: trop inefficace ! for etud in etuds: - etud["groups"] = sco_groups.get_etud_groups(etud["id"], formsemestre_id) + etud["groups"] = sco_groups.get_etud_groups( + etud["id"], formsemestre_id, exclude_default=True + ) return jsonify(etuds) From 3ab3d398df19ec906207e967d046ab64f7828365 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 18 Aug 2022 15:43:14 +0200 Subject: [PATCH 14/15] Fix billets abs --- app/api/tools.py | 10 +++------- app/views/absences.py | 10 +++++++--- sco_version.py | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/app/api/tools.py b/app/api/tools.py index 08fd8aeb6..90750e121 100644 --- a/app/api/tools.py +++ b/app/api/tools.py @@ -29,12 +29,8 @@ def get_etud(etudid=None, nip=None, ine=None) -> models.Identite: allowed_depts = current_user.get_depts_with_permission(Permission.ScoView) if etudid is not None: - etud: Identite = Identite.query.get(etudid) - if (None in allowed_depts) or etud.departement.acronym in allowed_depts: - return etud - return None # accès interdit => pas d'étudiant - - if nip is not None: + query: Identite = Identite.query.filter_by(id=etudid) + elif nip is not None: query = Identite.query.filter_by(code_nip=nip) elif ine is not None: query = Identite.query.filter_by(code_ine=ine) @@ -45,7 +41,7 @@ def get_etud(etudid=None, nip=None, ine=None) -> models.Identite: ) if None not in allowed_depts: # restreint aux départements autorisés: - etuds = etuds.join(Departement).filter( + query = query.join(Departement).filter( or_(Departement.acronym == acronym for acronym in allowed_depts) ) return query.join(Admission).order_by(desc(Admission.annee)).first() diff --git a/app/views/absences.py b/app/views/absences.py index 28f17ef7a..829fb2d4d 100644 --- a/app/views/absences.py +++ b/app/views/absences.py @@ -72,6 +72,7 @@ from app.decorators import ( ) from app.models import FormSemestre, GroupDescr from app.models.absences import BilletAbsence +from app.models.etudiants import Identite from app.views import absences_bp as bp # --------------- @@ -1099,7 +1100,7 @@ def AddBilletAbsence( begin, end, description, - etudid=False, + etudid=None, code_nip=None, code_ine=None, justified=True, @@ -1114,7 +1115,7 @@ def AddBilletAbsence( end = str(end) code_nip = str(code_nip) if code_nip else None - etud = api.tools.get_etud(etudid=None, nip=None, ine=None) + etud = api.tools.get_etud(etudid=etudid, nip=code_nip, ine=code_ine) # check dates begin_date = dateutil.parser.isoparse(begin) # may raises ValueError end_date = dateutil.parser.isoparse(end) @@ -1212,9 +1213,12 @@ def billets_etud(etudid=False): @scodoc @permission_required_compat_scodoc7(Permission.ScoView) @scodoc7func -def XMLgetBilletsEtud(etudid=False): +def XMLgetBilletsEtud(etudid=False, code_nip=False): """Liste billets pour un etudiant""" log("Warning: called deprecated XMLgetBilletsEtud") + if etudid is False: + etud = Identite.query.filter_by(code_nip=str(code_nip)).first_or_404() + etudid = etud.id table = sco_abs_billets.table_billets_etud(etudid) if table: return table.make_page(format="xml") diff --git a/sco_version.py b/sco_version.py index 436982a2a..f840d9dee 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.3.25" +SCOVERSION = "9.3.26" SCONAME = "ScoDoc" From 64f9de95a5098be33a542c0498dea9bf2fd40fcf Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 18 Aug 2022 15:53:26 +0200 Subject: [PATCH 15/15] API: missing routes /etudiant/ine//formsemestre//bulletin/pdf et .../nip/... --- app/api/etudiants.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/app/api/etudiants.py b/app/api/etudiants.py index 81e3cd32f..2f81b0a25 100644 --- a/app/api/etudiants.py +++ b/app/api/etudiants.py @@ -218,7 +218,6 @@ def etudiant_formsemestres(etudid: int = None, nip: int = None, ine: int = None) methods=["GET"], defaults={"version": "long", "pdf": False}, ) -# Version PDF non testée @bp.route( "/etudiant/etudid//formsemestre//bulletin/pdf", methods=["GET"], @@ -254,6 +253,16 @@ def etudiant_formsemestres(etudid: int = None, nip: int = None, ine: int = None) methods=["GET"], defaults={"version": "short", "pdf": True}, ) +@bp.route( + "/etudiant/nip//formsemestre//bulletin/pdf", + methods=["GET"], + defaults={"version": "long", "pdf": True}, +) +@bp.route( + "/etudiant/ine//formsemestre//bulletin/pdf", + methods=["GET"], + defaults={"version": "long", "pdf": True}, +) @api_web_bp.route( "/etudiant/etudid//formsemestre//bulletin", methods=["GET"], @@ -269,7 +278,6 @@ def etudiant_formsemestres(etudid: int = None, nip: int = None, ine: int = None) methods=["GET"], defaults={"version": "long", "pdf": False}, ) -# Version PDF non testée @api_web_bp.route( "/etudiant/etudid//formsemestre//bulletin/pdf", methods=["GET"],