API: Add image support #199 (#202)

* API: Add image support #199
This commit is contained in:
Daniel Grams 2021-06-25 14:03:23 +02:00 committed by GitHub
parent 868b0e34c9
commit 0eeaebcc90
6 changed files with 93 additions and 40 deletions

View File

@ -89,11 +89,21 @@ class RestApi(Api):
arg = err.args[0]
if isinstance(arg, dict):
errors = []
for field, messages in arg.items():
if isinstance(messages, list):
for message in messages:
error = {"field": field, "message": message}
errors.append(error)
for field, item in arg.items():
messages = list()
if isinstance(item, list):
messages = item
elif isinstance(item, dict):
for item_value in item.values():
if isinstance(item_value, list) or isinstance(
item_value, tuple
):
messages.extend(item_value)
for message in messages:
error = {"field": field, "message": message}
errors.append(error)
if len(errors) > 0:
data["errors"] = errors

View File

@ -1,4 +1,4 @@
from marshmallow import ValidationError, fields, post_load, validates_schema
from marshmallow import ValidationError, fields, post_load, validate, validates_schema
from project.api import marshmallow
from project.api.schemas import SQLAlchemyBaseSchema
@ -43,6 +43,7 @@ class ImageWriteSchemaMixin(object):
required=False,
load_only=True,
allow_none=True,
validate=[validate.URL()],
metadata={
"description": "URL to image. Either image_url or image_base64 has to be defined."
},
@ -76,20 +77,24 @@ class ImageWriteSchemaMixin(object):
return item
def load_image_data(self, image_base64, image_url):
image = None
try:
image = None
if image_base64:
image = get_image_from_base64_str(image_base64)
elif image_url:
image = get_image_from_url(image_url)
if image_base64:
image = get_image_from_base64_str(image_base64)
elif image_url:
image = get_image_from_url(image_url)
if not image:
return None, None
if not image:
return None, None
validate_image(image)
resize_image_to_max(image)
encoding_format = get_mime_type_from_image(image)
data = get_bytes_from_image(image)
except Exception as e:
raise ValidationError(e.args)
validate_image(image)
resize_image_to_max(image)
encoding_format = get_mime_type_from_image(image)
data = get_bytes_from_image(image)
return encoding_format, data

View File

@ -7,7 +7,7 @@ import requests
min_image_size = 320
max_image_size = 1024
supported_formats = ["jpeg", "png", "gif"]
supported_formats = ["png", "jpeg", "gif"]
def get_data_uri_from_bytes(data: bytes, encoding_format: str) -> str:
@ -34,8 +34,14 @@ def get_mime_type_from_image(image: PIL.Image) -> str:
def get_bytes_from_image(image: PIL.Image) -> bytes:
format = (
image.format
if image.format.lower() in supported_formats
else supported_formats[0]
)
imgByteArr = BytesIO()
image.save(imgByteArr, format=image.format)
image.save(imgByteArr, format=format)
return imgByteArr.getvalue()
@ -49,8 +55,3 @@ def validate_image(image: PIL.Image):
raise ValueError(
f"Image is too small ({image.width}x{image.height}px). At least {min_image_size}x{min_image_size}px."
)
if image.format.lower() not in supported_formats:
raise ValueError(
f"Image format {image.format} is not supported. Supported formats: {', '.join(supported_formats)}."
)

File diff suppressed because one or more lines are too long

View File

@ -67,22 +67,27 @@ def test_events(client, seeder, utils):
utils.get_ok(url)
def test_events_post(client, seeder, utils, app):
def prepare_events_post_data(seeder, utils):
user_id, admin_unit_id = seeder.setup_api_access()
place_id = seeder.upsert_default_event_place(admin_unit_id)
organizer_id = seeder.upsert_default_event_organizer(admin_unit_id)
url = utils.get_url("api_v1_organization_event_list", id=admin_unit_id)
response = utils.post_json(
url,
{
"name": "Fest",
"start": "2021-02-07T11:00:00.000Z",
"place": {"id": place_id},
"organizer": {"id": organizer_id},
"photo": {"image_base64": seeder.get_default_image_upload_base64()},
},
data = {
"name": "Fest",
"start": "2021-02-07T11:00:00.000Z",
"place": {"id": place_id},
"organizer": {"id": organizer_id},
"photo": {"image_base64": seeder.get_default_image_base64()},
}
return url, data, admin_unit_id, place_id, organizer_id
def test_events_post(client, seeder, utils, app):
url, data, admin_unit_id, place_id, organizer_id = prepare_events_post_data(
seeder, utils
)
response = utils.post_json(url, data)
utils.assert_response_created(response)
assert "id" in response.json
@ -101,6 +106,34 @@ def test_events_post(client, seeder, utils, app):
assert event.photo.encoding_format == "image/png"
def test_events_post_photo_no_data(client, seeder, utils, app):
url, data, admin_unit_id, place_id, organizer_id = prepare_events_post_data(
seeder, utils
)
data["photo"] = dict()
response = utils.post_json(url, data)
utils.assert_response_unprocessable_entity(response)
error = response.json["errors"][0]
assert error["field"] == "photo"
assert error["message"] == "Either image_url or image_base64 has to be defined."
def test_events_post_photo_too_small(client, seeder, utils, app):
url, data, admin_unit_id, place_id, organizer_id = prepare_events_post_data(
seeder, utils
)
data["photo"][
"image_base64"
] = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAgMAAABieywaAAAACVBMVEUAAAD///9fX1/S0ecCAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAACklEQVQImWNoAAAAggCByxOyYQAAAABJRU5ErkJggg=="
response = utils.post_json(url, data)
utils.assert_response_unprocessable_entity(response)
error = response.json["errors"][0]
assert error["field"] == "photo"
assert error["message"] == "Image is too small (1x1px). At least 320x320px."
def test_places(client, seeder, utils):
user_id, admin_unit_id = seeder.setup_base()
seeder.upsert_default_event_place(admin_unit_id)

View File

@ -26,12 +26,16 @@ def test_validate_image_too_small():
assert "too small" in str(e.value)
def test_validate_image_unsupported_format():
def test_get_bytes_from_image():
from io import BytesIO
import PIL
from project.imageutils import get_image_from_bytes, min_image_size, validate_image
from project.imageutils import (
get_bytes_from_image,
get_image_from_bytes,
min_image_size,
)
image = PIL.Image.new("RGB", (min_image_size, min_image_size))
imgByteArr = BytesIO()
@ -39,7 +43,7 @@ def test_validate_image_unsupported_format():
tif_image = get_image_from_bytes(imgByteArr.getvalue())
with pytest.raises(ValueError) as e:
validate_image(tif_image)
new_bytes = get_bytes_from_image(tif_image)
new_image = get_image_from_bytes(new_bytes)
assert "not supported" in str(e.value)
assert new_image.format.lower() == "png"