From 60aba5a878aeee622401152e5e96c4c9f05bf673 Mon Sep 17 00:00:00 2001 From: Jean-Marie PLACE Date: Sun, 21 Aug 2022 09:17:45 +0200 Subject: [PATCH] =?UTF-8?q?fix=20api=20logo=20selon=20conventions=20+=20je?= =?UTF-8?q?ux=20de=20test=20+=20normalise=20des=20samples=20pour=20doc=20p?= =?UTF-8?q?ar=20post-traitement=20des=20r=C3=A9sultats?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/logos.py | 56 ++++--- tests/api/make_samples.py | 57 +++++-- tests/api/samples.csv | 24 ++- tests/api/setup_test_api.py | 21 ++- tests/api/test_api_logos.py | 142 +++++++++++++++--- .../fakedatabase/create_test_api_database.py | 28 ++++ 6 files changed, 268 insertions(+), 60 deletions(-) diff --git a/app/api/logos.py b/app/api/logos.py index ef3a7f973..49c0619c6 100644 --- a/app/api/logos.py +++ b/app/api/logos.py @@ -46,23 +46,16 @@ from app.scodoc.sco_permissions import Permission @bp.route("/logos") @scodoc -@permission_required(Permission.ScoView) +@permission_required(Permission.ScoSuperAdmin) def api_get_glob_logos(): - if not g.current_user.has_permission(Permission.ScoSuperAdmin, None): - return json_error(403, message="accès interdit") - required_format = requested_format() # json only - if required_format is None: - return json_error(400, "Illegal format") logos = list_logos()[None] return jsonify(list(logos.keys())) -@bp.route("/logos/") +@bp.route("/logo/") @scodoc -@permission_required(Permission.ScoView) +@permission_required(Permission.ScoSuperAdmin) def api_get_glob_logo(logoname): - if not g.current_user.has_permission(Permission.ScoSuperAdmin, None): - return json_error(403, message="accès interdit") logo = find_logo(logoname=logoname) if logo is None: return json_error(404, message="logo not found") @@ -74,25 +67,27 @@ def api_get_glob_logo(logoname): ) -@bp.route("/departements//logos") -@scodoc -@permission_required(Permission.ScoView) -def api_get_local_logos(departement): - dept_id = Departement.from_acronym(departement).id - if not g.current_user.has_permission(Permission.ScoChangePreferences, departement): - return json_error(403, message="accès interdit") +def core_get_logos(dept_id): logos = list_logos().get(dept_id, dict()) return jsonify(list(logos.keys())) -@bp.route("/departements//logos/") +@bp.route("/departement//logos") @scodoc -@permission_required(Permission.ScoView) -def api_get_local_logo(departement, logoname): - # format = requested_format("jpg", ['png', 'jpg']) XXX ? +@permission_required(Permission.ScoSuperAdmin) +def api_get_local_logos_by_acronym(departement): dept_id = Departement.from_acronym(departement).id - if not g.current_user.has_permission(Permission.ScoChangePreferences, departement): - return json_error(403, message="accès interdit") + return core_get_logos(dept_id) + + +@bp.route("/departement/id//logos") +@scodoc +@permission_required(Permission.ScoSuperAdmin) +def api_get_local_logos_by_id(dept_id): + return core_get_logos(dept_id) + + +def core_get_logo(dept_id, logoname): logo = find_logo(logoname=logoname, dept_id=dept_id) if logo is None: return json_error(404, message="logo not found") @@ -102,3 +97,18 @@ def api_get_local_logo(departement, logoname): mimetype=f"image/{logo.suffix}", last_modified=datetime.now(), ) + + +@bp.route("/departement//logo/") +@scodoc +@permission_required(Permission.ScoSuperAdmin) +def api_get_local_logo_dept_by_acronym(departement, logoname): + dept_id = Departement.from_acronym(departement).id + return core_get_logo(dept_id, logoname) + + +@bp.route("/departement/id//logo/") +@scodoc +@permission_required(Permission.ScoSuperAdmin) +def api_get_local_logo_dept_by_id(dept_id, logoname): + return core_get_logo(dept_id, logoname) diff --git a/tests/api/make_samples.py b/tests/api/make_samples.py index fbb198283..614b40c47 100644 --- a/tests/api/make_samples.py +++ b/tests/api/make_samples.py @@ -6,8 +6,9 @@ Usage: cd /opt/scodoc/tests/api - python make_samples.py + python make_samples.py [entry_names] +si entry_names est spécifié, la génération est restreints aux exemples cités. expl: `python make_samples departements departement-formsemestres` 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 @@ -41,6 +42,8 @@ TODO: ajouter un argument au script permettant de ne générer qu'un seul fichie """ import os import shutil +import sys +import re from collections import defaultdict from pprint import pprint as pp from pprint import pformat as pf @@ -69,6 +72,7 @@ class Sample: self.url = url self.method = method self.result = None + self.output = "json" if permission == "ScoView": HEADERS = get_auth_headers("test", "test") elif permission == "ScoSuperAdmin": @@ -87,16 +91,16 @@ class Sample: 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): + def _shorten( + self, item + ): # abrege les longues listes (limite à 2 éléments et affiche "... etc. à la place" if isinstance(item, list): - return [self._shorten(child) for child in item[:2]] + return [self._shorten(child) for child in item[:2]] + ["... etc."] return item def shorten(self): @@ -117,19 +121,36 @@ class Sample: file.write(f"> `{self.content}`\n\n") file.write("```json\n") - file.write(json.dumps(self.result, indent=4)) + content = json.dumps(self.result, indent=4, sort_keys=True) + content = content.replace("... etc.", "...") + # regexp for date like: "2022-08-14T10:01:44.043869+02:00" + regexp = re.compile( + r'"(-?(?:[1-9][0-9]*)?[0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])(\.[0-9]+)?(Z|[+-](?:2[0-3]|[01][0-9]):[0-5][0-9])?"' + ) + content = regexp.sub('"2022-08-20T12:00:00.000000+02:00"', content) + file.write(content) file.write("\n```\n\n") class Samples: - def __init__(self): + def __init__(self, entry_names): + """Entry_names: la liste des entrées à reconstruire. + si None, la totalité des lignes de samples.csv est prise en compte + """ self.entries = defaultdict(lambda: set()) + self.entry_names = entry_names 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) + if self.entry_names is None or entry in self.entry_names: + if method[0] == "#": + detail = "**ignored**" + elif content == "": + detail = "" + else: + detail = f": {content}" + print(f"{entry:50} {method:5} {url:50} {detail}") + sample = Sample(url, method, permission, content) + self.entries[entry].add(sample) def pp(self): for entry, samples in self.entries.items(): @@ -141,12 +162,18 @@ class Samples: 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: + for sample in sorted( + samples, key=lambda s: s.url + ): # sorted de façon à rendre le fichier résultat déterministe (i.e. indépendant de l ordre d arrivée des résultats) sample.dump(file) file.close() def make_samples(): + if len(sys.argv) == 1: + entry_names = None + else: + entry_names = sys.argv[1:] 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" @@ -157,9 +184,9 @@ def make_samples(): else: os.mkdir("/tmp/samples") - samples = Samples() - # samples.pp() - with open("samples.csv") as f: + samples = Samples(entry_names) + samples_file = os.path.dirname(__file__) + "/samples.csv" + with open(samples_file) as f: L = [x[:-1].split("\t") for x in f] for line in L[1:]: entry_name = line[0] diff --git a/tests/api/samples.csv b/tests/api/samples.csv index 341bda1e8..864591b43 100644 --- a/tests/api/samples.csv +++ b/tests/api/samples.csv @@ -26,6 +26,7 @@ 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-bulletin /etudiant/nip/11/formsemestre/1/bulletin/short/pdf GET etudiant-formsemestre-groups /etudiant/etudid/11/formsemestre/1/groups GET formations /formations GET formations_ids /formations_ids GET @@ -67,11 +68,30 @@ 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-password /user/3/password ScoSuperAdmin POST { "password": "rePlaCemeNT456averylongandcomplicated" } +user-password /user/3/password ScoSuperAdmin POST { "password": "too_simple" } +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 +logos /logos ScoSuperAdmin GET +logo /logo/demo ScoSuperAdmin GET +departement-logos /departement/TAPI/logos ScoSuperAdmin GET +departement-logos /departement/id/1/logos ScoSuperAdmin GET +departement-logo /departement/TAPI/logo/demo ScoSuperAdmin GET +departement-logo /departement/id/1/logo/demo ScoSuperAdmin GET +test-pdf /etudiant/nip/11/formsemestre/1/bulletin/pdf GET +test-pdf /etudiant/nip/11/formsemestre/1/bulletin/pdf GET +test-pdf /etudiant/etudid/11/formsemestre/1/bulletin/short/pdf GET +test-pdf /etudiant/ine/INE11/formsemestre/1/bulletin/short/pdf GET +test-pdf /etudiant/nip/11/formsemestre/1/bulletin/short/pdf GET +test-pdf /etudiant/etudid/11/formsemestre/1/bulletin/pdf GET +test-pdf /etudiant/etudid/11/formsemestre/1/bulletin/short GET +test-pdf /etudiant/ine/INE11/formsemestre/1/bulletin/short GET +test-pdf /etudiant/nip/11/formsemestre/1/bulletin/short GET +test-pdf /etudiant/etudid/11/formsemestre/1/bulletin GET +test-pdf /etudiant/ine/INE11/formsemestre/1/bulletin GET +test-pdf /etudiant/nip/11/formsemestre/1/bulletin GET diff --git a/tests/api/setup_test_api.py b/tests/api/setup_test_api.py index bc1a58fbc..ab41f8308 100644 --- a/tests/api/setup_test_api.py +++ b/tests/api/setup_test_api.py @@ -65,7 +65,9 @@ def api_admin_headers() -> dict: def GET(path: str, headers: dict = None, errmsg=None, dept=None): - """Get and returns as JSON""" + """Get and returns as JSON + Special case for non json result (image or pdf): return Content-Disposition string (inline or attachment) + """ if dept: url = SCODOC_URL + f"/ScoDoc/{dept}/api" + path else: @@ -73,6 +75,23 @@ def GET(path: str, headers: dict = None, errmsg=None, dept=None): 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} !""", r.json()) + + if r.headers.get("Content-Type", None) == "application/json": + return r.json() # decode la reponse JSON + elif r.headers.get("Content-Type", None) in [ + "image/jpg", + "image/png", + "application/pdf", + ]: + retval = { + "Content-Type": r.headers.get("Content-Type", None), + "Content-Disposition": r.headers.get("Content-Disposition", None), + } + return retval + else: + raise APIError( + "Unknown returned content {r.headers.get('Content-Type', None} !\n" + ) return r.json() # decode la reponse JSON diff --git a/tests/api/test_api_logos.py b/tests/api/test_api_logos.py index d12ceb98c..f8b497579 100644 --- a/tests/api/test_api_logos.py +++ b/tests/api/test_api_logos.py @@ -14,7 +14,7 @@ utilisation: # Ce test a une logique très différente des autres : A UNIFIER -from tests.api.setup_test_api import API_URL +from tests.api.setup_test_api import API_URL, api_admin_headers, api_headers from scodoc import app from tests.unit.config_test_logos import ( @@ -26,48 +26,152 @@ from tests.unit.config_test_logos import ( ) -def test_super_access(create_super_token): +def test_super_access(api_admin_headers): """ Route: /logos """ - dept1, dept2, dept3, token = create_super_token - headers = {"Authorization": f"Bearer {token}"} - with app.test_client() as client: + headers = api_admin_headers + with app.test_client(api_admin_headers) as client: response = client.get(API_URL + "/logos", headers=headers) assert response.status_code == 200 assert response.json is not None -def test_admin_access(create_admin_token): +def test_admin_access(api_headers): """ - Route: + Route: /logos """ - dept1, dept2, dept3, token = create_admin_token - headers = {"Authorization": f"Bearer {token}"} + headers = api_headers with app.test_client() as client: response = client.get(API_URL + "/logos", headers=headers) - assert response.status_code == 403 + assert response.status_code == 401 -def test_lambda_access(create_lambda_token): +def test_lambda_access(api_headers): """ - Route: + Route: /logos """ - dept1, dept2, dept3, token = create_lambda_token - headers = {"Authorization": f"Bearer {token}"} + headers = api_headers with app.test_client() as client: response = client.get(API_URL + "/logos", headers=headers) - assert response.status_code == 403 + assert response.status_code == 401 -def test_initial_with_header_and_footer(create_super_token): +def test_global_logos(api_admin_headers): """ Route: """ - dept1, dept2, dept3, token = create_super_token - headers = {"Authorization": f"Bearer {token}"} + headers = api_admin_headers with app.test_client() as client: response = client.get(API_URL + "/logos", headers=headers) assert response.status_code == 200 assert response.json is not None - assert len(response.json) == 7 + assert ( + len(response.json) == 4 + ) # 4 items in fakelogo context: ['header', 'footer', 'logo_B', 'logo_C'] + + +def test_local_by_id_logos(api_admin_headers): + """ + Route: /departement/id/1/logos + """ + headers = api_admin_headers + with app.test_client() as client: + response = client.get(API_URL + "/departement/id/1/logos", headers=headers) + assert response.status_code == 200 + assert response.json is not None + assert ( + len(response.json) == 2 + ) # 2 items in dept(1, TAPI) fakelogo context: ['logo_A', 'logo_D'] + + +def test_local_by_name_logos(api_admin_headers): + """ + Route: /departement/TAPI/logos + """ + headers = api_admin_headers + with app.test_client() as client: + response = client.get(API_URL + "/departement/TAPI/logos", headers=headers) + assert response.status_code == 200 + assert response.json is not None + assert ( + len(response.json) == 2 + ) # 2 items in dept(1, TAPI) fakelogo context: ['logo_A', 'logo_D'] + + +def test_local_png_by_id_logo(api_admin_headers): + """ + Route: /departement/id/1/logo/D + """ + headers = api_admin_headers + with app.test_client() as client: + response = client.get(API_URL + "/departement/id/1/logo/D", headers=headers) + assert response.status_code == 200 + assert response.headers["Content-Type"] == "image/png" + assert response.headers["Content-Disposition"].startswith("inline") + assert "logo_D.png" in response.headers["Content-Disposition"] + + +def test_global_png_logo(api_admin_headers): + """ + Route: /logo/C + """ + headers = api_admin_headers + with app.test_client() as client: + response = client.get(API_URL + "/logo/C", headers=headers) + assert response.status_code == 200 + assert response.headers["Content-Type"] == "image/png" + assert response.headers["Content-Disposition"].startswith("inline") + assert "logo_C.png" in response.headers["Content-Disposition"] + + +def test_global_jpg_logo(api_admin_headers): + """ + Route: /logo/B + """ + headers = api_admin_headers + with app.test_client() as client: + response = client.get(API_URL + "/logo/B", headers=headers) + assert response.status_code == 200 + assert response.headers["Content-Type"] == "image/jpg" + assert response.headers["Content-Disposition"].startswith("inline") + assert "logo_B.jpg" in response.headers["Content-Disposition"] + + +def test_local_png_by_name_logo(api_admin_headers): + """ + Route: /departement/TAPI/logo/A + """ + headers = api_admin_headers + with app.test_client() as client: + response = client.get(API_URL + "/departement/TAPI/logo/D", headers=headers) + assert response.status_code == 200 + assert response.headers["Content-Type"] == "image/png" + assert response.headers["Content-Disposition"].startswith("inline") + assert "logo_D.png" in response.headers["Content-Disposition"] + + +def test_local_jpg_by_id_logo(api_admin_headers): + """ + Route: /departement/id/1/logo/D + """ + headers = api_admin_headers + with app.test_client() as client: + response = client.get(API_URL + "/departement/id/1/logo/A", headers=headers) + assert response.status_code == 200 + assert response.headers["Content-Type"] == "image/jpg" + assert response.headers["Content-Disposition"].startswith("inline") + assert "logo_A.jpg" in response.headers["Content-Disposition"] + + +def test_local_jpg_by_name_logo(api_admin_headers): + """ + Route: /departement/TAPI/logo/A + """ + headers = api_admin_headers + with app.test_client() as client: + response = client.get(API_URL + "/departement/TAPI/logo/A", headers=headers) + assert response.status_code == 200 + assert response.headers["Content-Type"] == "image/jpg" + assert response.headers["Content-Disposition"].startswith("inline") + assert "logo_A.jpg" in response.headers["Content-Disposition"] diff --git a/tools/fakedatabase/create_test_api_database.py b/tools/fakedatabase/create_test_api_database.py index 03e4f7bf9..bd4bb0919 100644 --- a/tools/fakedatabase/create_test_api_database.py +++ b/tools/fakedatabase/create_test_api_database.py @@ -8,7 +8,9 @@ """ import datetime +import os import random +import shutil import time import sys @@ -45,6 +47,10 @@ REFCOMP_FILENAME = ( "ressources/referentiels/but2022/competences/but-RT-05012022-081735.xml" ) +# la réserve de logos +LOGOS_STOCK = "/opt/scodoc/tests/ressources/test_logos/" +LOGOS_DIR = "/opt/scodoc-data/config/logos/" + def create_departements(acronyms: list[str]) -> list[Departement]: "Create depts" @@ -353,6 +359,27 @@ def create_etape_apo(formsemestre: FormSemestre): db.session.commit() +def create_logos(): + if not os.path.exists(LOGOS_DIR + "logos_1"): + os.mkdir(LOGOS_DIR + "logos_1") + shutil.copy( + LOGOS_STOCK + "logo_A.jpg", + LOGOS_DIR + "logos_1/logo_A.jpg", + ) + shutil.copy( + LOGOS_STOCK + "logo_D.png", + LOGOS_DIR + "logos_1/logo_D.png", + ) + shutil.copy( + LOGOS_STOCK + "logo_A.jpg", + LOGOS_DIR + "logo_B.jpg", + ) + shutil.copy( + LOGOS_STOCK + "logo_D.png", + LOGOS_DIR + "logo_C.png", + ) + + def init_test_database(): """Appelé par la commande `flask init-test-database` @@ -373,6 +400,7 @@ def init_test_database(): saisie_notes_evaluations(formsemestre, user_lecteur) add_absences(formsemestre) create_etape_apo(formsemestre) + create_logos() # à compléter # - groupes # - absences