diff --git a/app/api/formsemestres.py b/app/api/formsemestres.py index 24a98f33..afd2e9ba 100644 --- a/app/api/formsemestres.py +++ b/app/api/formsemestres.py @@ -13,12 +13,14 @@ FormSemestre """ -import mimetypes +import base64 +import io from operator import attrgetter, itemgetter from flask import g, make_response, request from flask_json import as_json from flask_login import current_user, login_required +import PIL import sqlalchemy as sa import app from app import db, log @@ -820,8 +822,8 @@ def formsemestre_get_description(formsemestre_id: int): @permission_required(Permission.ScoView) @as_json def formsemestre_edit_description(formsemestre_id: int): - """Modifie description externe du formsemestre - + """Modifie description externe du formsemestre. + Les images peuvent êtres passées dans el json, encodées en base64. formsemestre_id : l'id du formsemestre SAMPLES @@ -832,6 +834,10 @@ def formsemestre_edit_description(formsemestre_id: int): args = request.get_json(force=True) # may raise 400 Bad Request if not formsemestre.description: formsemestre.description = FormSemestreDescription() + # Decode images (base64) + for key in ["image", "photo_ens"]: + if key in args: + args[key] = base64.b64decode(args[key]) formsemestre.description.from_dict(args) db.session.commit() return formsemestre.description.to_dict() @@ -868,11 +874,12 @@ def formsemestre_get_photo_ens(formsemestre_id: int): return _image_response(formsemestre.description.photo_ens) -def _image_response(image_data): +def _image_response(image_data: bytes): # Guess the mimetype based on the image data - mimetype = mimetypes.guess_type("image")[0] - - if not mimetype: + try: + image = PIL.Image.open(io.BytesIO(image_data)) + mimetype = image.get_format_mimetype() + except PIL.UnidentifiedImageError: # Default to binary stream if mimetype cannot be determined mimetype = "application/octet-stream" diff --git a/app/views/notes_formsemestre.py b/app/views/notes_formsemestre.py index 1bafa62d..e3e25c90 100644 --- a/app/views/notes_formsemestre.py +++ b/app/views/notes_formsemestre.py @@ -30,9 +30,11 @@ Emmanuel Viennet, 2023 """ import datetime +import io from flask import flash, redirect, render_template, url_for from flask import current_app, g, request +import PIL from app import db, log from app.decorators import ( @@ -319,6 +321,20 @@ def edit_formsemestre_description(formsemestre_id: int): scodoc_dept=g.scodoc_dept, ) ) + try: + _ = PIL.Image.open(io.BytesIO(image_data)) + except PIL.UnidentifiedImageError: + flash( + f"Image invalide ({field}), doit être une image", + "danger", + ) + return redirect( + url_for( + "notes.edit_formsemestre_description", + formsemestre_id=formsemestre.id, + scodoc_dept=g.scodoc_dept, + ) + ) setattr(formsemestre_description, field, image_data) db.session.commit() diff --git a/tests/api/setup_test_api.py b/tests/api/setup_test_api.py index 2ce97eb8..126162e2 100644 --- a/tests/api/setup_test_api.py +++ b/tests/api/setup_test_api.py @@ -122,9 +122,14 @@ def GET(path: str, headers: dict = None, errmsg=None, dept=None, raw=False): if reply.headers.get("Content-Type", None) == "application/json": return reply.json() # decode la reponse JSON if reply.headers.get("Content-Type", None) in [ - "image/jpg", - "image/png", "application/pdf", + "application/vnd.ms-excel", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "image/gif", + "image/jpeg", + "image/png", + "image/webp", ]: retval = { "Content-Type": reply.headers.get("Content-Type", None), @@ -132,7 +137,7 @@ def GET(path: str, headers: dict = None, errmsg=None, dept=None, raw=False): } return retval raise APIError( - "Unknown returned content {r.headers.get('Content-Type', None} !\n", + f"Unknown returned content {reply.headers.get('Content-Type', None)} !\n", status_code=reply.status_code, ) diff --git a/tests/api/test_api_formsemestre.py b/tests/api/test_api_formsemestre.py index 923f4c79..1395a6a3 100644 --- a/tests/api/test_api_formsemestre.py +++ b/tests/api/test_api_formsemestre.py @@ -16,6 +16,7 @@ Utilisation : Lancer : pytest tests/api/test_api_formsemestre.py """ +import base64 import json import requests from types import NoneType @@ -813,6 +814,9 @@ def test_formsemestre_description(api_admin_headers): assert r["salle"] == "une salle" assert r["dispositif"] == 1 assert r["wip"] is True + # La réponse ne contient pas les images, servies à part: + assert "image" not in r + assert "photo_ens" not in r r = POST( f"/formsemestre/{formsemestre_id}/description/edit", data={ @@ -828,3 +832,15 @@ def test_formsemestre_description(api_admin_headers): assert r["salle"] == "" assert r["dispositif"] == 0 assert r["wip"] is False + # Upload image + with open("tests/ressources/images/papillon.jpg", "rb") as f: + img = f.read() + img_base64 = base64.b64encode(img).decode("utf-8") + r = POST( + f"/formsemestre/{formsemestre_id}/description/edit", data={"image": img_base64} + ) + assert r["wip"] is False + r = GET(f"/formsemestre/{formsemestre_id}/description/image", raw=True) + assert r.status_code == 200 + assert r.headers.get("Content-Type") == "image/jpeg" + assert r.content == img