Merge pull request 'fix api logo selon conventions + jeux de test + normalise des samples' (#476) from jmplace/ScoDoc-Lille:canonize into master

Reviewed-on: https://scodoc.org/git/ScoDoc/ScoDoc/pulls/476
This commit is contained in:
Emmanuel Viennet 2022-08-22 15:46:02 +02:00
commit fcb16a8af4
6 changed files with 268 additions and 60 deletions

View File

@ -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/<string:logoname>")
@bp.route("/logo/<string:logoname>")
@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/<string:departement>/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/<string:departement>/logos/<string:logoname>")
@bp.route("/departement/<string: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/<int:dept_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/<string:departement>/logo/<string:logoname>")
@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/<int:dept_id>/logo/<string:logoname>")
@scodoc
@permission_required(Permission.ScoSuperAdmin)
def api_get_local_logo_dept_by_id(dept_id, logoname):
return core_get_logo(dept_id, logoname)

View File

@ -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]

View File

@ -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

Can't render this file because it contains an unexpected character in line 12 and column 60.

View File

@ -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

View File

@ -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"]

View File

@ -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