mirror of
https://github.com/lucaspalomodevelop/eventcally.git
synced 2026-03-13 00:07:22 +00:00
parent
a786c65119
commit
238f581baa
@ -8,8 +8,12 @@ from project.api.event_category.schemas import (
|
||||
EventCategoryRefSchema,
|
||||
EventCategoryWriteIdSchema,
|
||||
)
|
||||
from project.api.fields import CustomDateTimeField
|
||||
from project.api.image.schemas import ImageSchema
|
||||
from project.api.fields import CustomDateTimeField, Owned
|
||||
from project.api.image.schemas import (
|
||||
ImagePatchRequestSchema,
|
||||
ImagePostRequestSchema,
|
||||
ImageSchema,
|
||||
)
|
||||
from project.api.organization.schemas import OrganizationRefSchema
|
||||
from project.api.organizer.schemas import OrganizerRefSchema, OrganizerWriteIdSchema
|
||||
from project.api.place.schemas import (
|
||||
@ -260,7 +264,7 @@ class EventWriteSchemaMixin(object):
|
||||
default=50,
|
||||
validate=validate.OneOf([0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100]),
|
||||
metadata={
|
||||
"description": "How relevant the event is to your organization. 0 (Little relevant), 5 (Default), 10 (Highlight)."
|
||||
"description": "How relevant the event is to your organization. 0 (Little relevant), 50 (Default), 100 (Highlight)."
|
||||
},
|
||||
)
|
||||
|
||||
@ -272,6 +276,8 @@ class EventPostRequestSchema(
|
||||
super().__init__(*args, **kwargs)
|
||||
self.make_post_schema()
|
||||
|
||||
photo = Owned(ImagePostRequestSchema)
|
||||
|
||||
|
||||
class EventPatchRequestSchema(
|
||||
EventModelSchema, EventBaseSchemaMixin, EventWriteSchemaMixin
|
||||
@ -279,3 +285,5 @@ class EventPatchRequestSchema(
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.make_patch_schema()
|
||||
|
||||
photo = Owned(ImagePatchRequestSchema)
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
from marshmallow import ValidationError, fields
|
||||
from marshmallow_sqlalchemy import fields as msfields
|
||||
|
||||
from project.dateutils import berlin_tz
|
||||
|
||||
@ -31,3 +32,16 @@ class CustomDateTimeField(fields.DateTime):
|
||||
result = berlin_tz.localize(result)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class Owned(msfields.Nested):
|
||||
def _deserialize(self, *args, **kwargs):
|
||||
if (
|
||||
not self.root.transient
|
||||
and hasattr(self.schema, "instance")
|
||||
and self.schema.instance is None
|
||||
and self.root.instance
|
||||
and hasattr(self.root.instance, self.name)
|
||||
):
|
||||
self.schema.instance = getattr(self.root.instance, self.name, None)
|
||||
return super()._deserialize(*args, **kwargs)
|
||||
|
||||
@ -1,20 +1,33 @@
|
||||
from marshmallow import ValidationError, fields, post_load, validates_schema
|
||||
|
||||
from project.api import marshmallow
|
||||
from project.api.schemas import IdSchemaMixin, SQLAlchemyBaseSchema
|
||||
from project.imageutils import (
|
||||
get_bytes_from_image,
|
||||
get_image_from_base64_str,
|
||||
get_image_from_url,
|
||||
get_mime_type_from_image,
|
||||
resize_image_to_max,
|
||||
validate_image,
|
||||
)
|
||||
from project.models import Image
|
||||
|
||||
|
||||
class ImageIdSchema(marshmallow.SQLAlchemySchema):
|
||||
class ImageModelSchema(SQLAlchemyBaseSchema):
|
||||
class Meta:
|
||||
model = Image
|
||||
load_instance = True
|
||||
|
||||
id = marshmallow.auto_field()
|
||||
|
||||
class ImageIdSchema(ImageModelSchema, IdSchemaMixin):
|
||||
pass
|
||||
|
||||
|
||||
class ImageBaseSchema(ImageIdSchema):
|
||||
class ImageBaseSchemaMixin(object):
|
||||
copyright_text = marshmallow.auto_field()
|
||||
|
||||
|
||||
class ImageSchema(ImageBaseSchema):
|
||||
class ImageSchema(ImageIdSchema, ImageBaseSchemaMixin):
|
||||
image_url = marshmallow.URLFor(
|
||||
"image",
|
||||
values=dict(id="<id>", s=500),
|
||||
@ -24,5 +37,81 @@ class ImageSchema(ImageBaseSchema):
|
||||
)
|
||||
|
||||
|
||||
class ImageDumpSchema(ImageBaseSchema):
|
||||
class ImageDumpSchema(ImageModelSchema, ImageBaseSchemaMixin):
|
||||
pass
|
||||
|
||||
|
||||
class ImageWriteSchemaMixin(object):
|
||||
image_url = fields.String(
|
||||
required=False,
|
||||
load_only=True,
|
||||
allow_none=True,
|
||||
metadata={
|
||||
"description": "URL to image. Either image_url or image_base64 has to be defined."
|
||||
},
|
||||
)
|
||||
image_base64 = fields.String(
|
||||
required=False,
|
||||
load_only=True,
|
||||
allow_none=True,
|
||||
metadata={
|
||||
"description": "Base64 encoded image data. Either image_url or image_base64 has to be defined."
|
||||
},
|
||||
)
|
||||
|
||||
@post_load(pass_original=True)
|
||||
def post_load_image_data(self, item, original_data, **kwargs):
|
||||
image_url = original_data.get("image_url")
|
||||
image_base64 = original_data.get("image_base64")
|
||||
|
||||
if image_url is not None or image_base64 is not None:
|
||||
# Mal ist item ein dict und mal ein Image, weil post_load die Reihenfolge der Aufrufe nicht garantieren kann
|
||||
if isinstance(item, dict):
|
||||
encoding_format, data = self.load_image_data(image_base64, image_url)
|
||||
item["encoding_format"] = encoding_format
|
||||
item["data"] = data
|
||||
|
||||
elif isinstance(item, Image):
|
||||
encoding_format, data = self.load_image_data(image_base64, image_url)
|
||||
item.encoding_format = encoding_format
|
||||
item.data = data
|
||||
|
||||
return item
|
||||
|
||||
def load_image_data(self, image_base64, image_url):
|
||||
image = None
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
|
||||
class ImagePostRequestSchema(
|
||||
ImageModelSchema, ImageBaseSchemaMixin, ImageWriteSchemaMixin
|
||||
):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.make_post_schema()
|
||||
|
||||
@validates_schema
|
||||
def validate_image(self, data, **kwargs):
|
||||
if data.get("image_url") is None and data.get("image_base64") is None:
|
||||
raise ValidationError("Either image_url or image_base64 has to be defined.")
|
||||
|
||||
|
||||
class ImagePatchRequestSchema(
|
||||
ImageModelSchema, ImageBaseSchemaMixin, ImageWriteSchemaMixin
|
||||
):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.make_patch_schema()
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
from marshmallow import fields, validate
|
||||
|
||||
from project.api import marshmallow
|
||||
from project.api.fields import Owned
|
||||
from project.api.image.schemas import ImageSchema
|
||||
from project.api.location.schemas import (
|
||||
LocationPatchRequestSchema,
|
||||
@ -78,7 +79,7 @@ class OrganizerPostRequestSchema(OrganizerModelSchema, OrganizerBaseSchemaMixin)
|
||||
super().__init__(*args, **kwargs)
|
||||
self.make_post_schema()
|
||||
|
||||
location = fields.Nested(LocationPostRequestSchema)
|
||||
location = Owned(LocationPostRequestSchema)
|
||||
|
||||
|
||||
class OrganizerPatchRequestSchema(OrganizerModelSchema, OrganizerBaseSchemaMixin):
|
||||
@ -86,4 +87,4 @@ class OrganizerPatchRequestSchema(OrganizerModelSchema, OrganizerBaseSchemaMixin
|
||||
super().__init__(*args, **kwargs)
|
||||
self.make_patch_schema()
|
||||
|
||||
location = fields.Nested(LocationPatchRequestSchema)
|
||||
location = Owned(LocationPatchRequestSchema)
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
from marshmallow import fields, validate
|
||||
|
||||
from project.api import marshmallow
|
||||
from project.api.fields import Owned
|
||||
from project.api.image.schemas import ImageSchema
|
||||
from project.api.location.schemas import (
|
||||
LocationPatchRequestSchema,
|
||||
@ -79,7 +80,7 @@ class PlacePostRequestSchema(PlaceModelSchema, PlaceBaseSchemaMixin):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.make_post_schema()
|
||||
|
||||
location = fields.Nested(LocationPostRequestSchema)
|
||||
location = Owned(LocationPostRequestSchema)
|
||||
|
||||
|
||||
class PlacePatchRequestSchema(PlaceModelSchema, PlaceBaseSchemaMixin):
|
||||
@ -87,4 +88,4 @@ class PlacePatchRequestSchema(PlaceModelSchema, PlaceBaseSchemaMixin):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.make_patch_schema()
|
||||
|
||||
location = fields.Nested(LocationPatchRequestSchema)
|
||||
location = Owned(LocationPatchRequestSchema)
|
||||
|
||||
@ -1,6 +1,3 @@
|
||||
import base64
|
||||
import re
|
||||
|
||||
from flask import url_for
|
||||
from flask_babelex import lazy_gettext
|
||||
from flask_wtf import FlaskForm
|
||||
@ -8,6 +5,15 @@ from markupsafe import Markup
|
||||
from wtforms import HiddenField, StringField
|
||||
from wtforms.validators import Optional
|
||||
|
||||
from project.imageutils import (
|
||||
get_bytes_from_image,
|
||||
get_data_uri_from_bytes,
|
||||
get_image_from_base64_str,
|
||||
get_mime_type_from_image,
|
||||
resize_image_to_max,
|
||||
validate_image,
|
||||
)
|
||||
|
||||
|
||||
class BaseImageForm(FlaskForm):
|
||||
copyright_text = StringField(
|
||||
@ -22,22 +28,19 @@ class Base64ImageForm(BaseImageForm):
|
||||
super(BaseImageForm, self).process(formdata, obj, data, **kwargs)
|
||||
|
||||
if self.image_base64.data is None and obj and obj.data:
|
||||
base64_str = base64.b64encode(obj.data).decode("utf-8")
|
||||
self.image_base64.data = "data:{};base64,{}".format(
|
||||
obj.encoding_format, base64_str
|
||||
self.image_base64.data = get_data_uri_from_bytes(
|
||||
obj.data, obj.encoding_format
|
||||
)
|
||||
|
||||
def populate_obj(self, obj):
|
||||
super(BaseImageForm, self).populate_obj(obj)
|
||||
|
||||
match = None
|
||||
if self.image_base64.data:
|
||||
match = re.match(r"^data:(image/.+);base64,(.*)$", self.image_base64.data)
|
||||
|
||||
if match:
|
||||
obj.encoding_format = match.group(1)
|
||||
base64_str = match.group(2)
|
||||
obj.data = base64.b64decode(base64_str)
|
||||
image = get_image_from_base64_str(self.image_base64.data)
|
||||
validate_image(image)
|
||||
resize_image_to_max(image)
|
||||
obj.encoding_format = get_mime_type_from_image(image)
|
||||
obj.data = get_bytes_from_image(image)
|
||||
else:
|
||||
obj.data = None
|
||||
obj.encoding_format = None
|
||||
|
||||
56
project/imageutils.py
Normal file
56
project/imageutils.py
Normal file
@ -0,0 +1,56 @@
|
||||
import base64
|
||||
import re
|
||||
from io import BytesIO
|
||||
|
||||
import PIL
|
||||
import requests
|
||||
|
||||
min_image_size = 320
|
||||
max_image_size = 1024
|
||||
supported_formats = ["jpeg", "png", "gif"]
|
||||
|
||||
|
||||
def get_data_uri_from_bytes(data: bytes, encoding_format: str) -> str:
|
||||
base64_str = base64.b64encode(data).decode("utf-8")
|
||||
return "data:{};base64,{}".format(encoding_format, base64_str)
|
||||
|
||||
|
||||
def get_image_from_bytes(data: bytes) -> PIL.Image:
|
||||
return PIL.Image.open(BytesIO(data))
|
||||
|
||||
|
||||
def get_image_from_base64_str(base64_str: str) -> PIL.Image:
|
||||
image_data = re.sub("^data:image/.+;base64,", "", base64_str)
|
||||
return get_image_from_bytes(base64.b64decode(image_data))
|
||||
|
||||
|
||||
def get_image_from_url(url: str) -> PIL.Image:
|
||||
response = requests.get(url)
|
||||
return get_image_from_bytes(response.content)
|
||||
|
||||
|
||||
def get_mime_type_from_image(image: PIL.Image) -> str:
|
||||
return image.get_format_mimetype()
|
||||
|
||||
|
||||
def get_bytes_from_image(image: PIL.Image) -> bytes:
|
||||
imgByteArr = BytesIO()
|
||||
image.save(imgByteArr, format=image.format)
|
||||
return imgByteArr.getvalue()
|
||||
|
||||
|
||||
def resize_image_to_max(image: PIL.Image):
|
||||
if image.width > max_image_size or image.height > max_image_size:
|
||||
image.thumbnail((max_image_size, max_image_size), PIL.Image.ANTIALIAS)
|
||||
|
||||
|
||||
def validate_image(image: PIL.Image):
|
||||
if image.width < min_image_size or image.height < min_image_size:
|
||||
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)}."
|
||||
)
|
||||
@ -1,6 +1,8 @@
|
||||
import os
|
||||
from urllib.parse import quote_plus
|
||||
|
||||
from flask import url_for
|
||||
|
||||
from project import app
|
||||
from project.utils import (
|
||||
get_event_category_name,
|
||||
@ -43,7 +45,7 @@ app.jinja_env.filters["location_str"] = lambda l: get_location_str(l)
|
||||
|
||||
|
||||
@app.context_processor
|
||||
def get_manage_menu_options_context_processor():
|
||||
def get_context_processors():
|
||||
def get_manage_menu_options(admin_unit):
|
||||
from project.access import has_access
|
||||
from project.services.event_suggestion import get_event_reviews_badge_query
|
||||
@ -64,4 +66,9 @@ def get_manage_menu_options_context_processor():
|
||||
"reference_requests_incoming_badge": reference_requests_incoming_badge,
|
||||
}
|
||||
|
||||
return dict(get_manage_menu_options=get_manage_menu_options)
|
||||
def get_base_url():
|
||||
return url_for("home", _external=True).rstrip("/")
|
||||
|
||||
return dict(
|
||||
get_manage_menu_options=get_manage_menu_options, get_base_url=get_base_url
|
||||
)
|
||||
|
||||
@ -299,9 +299,13 @@ class AdminUnit(db.Model, TrackableMixin):
|
||||
)
|
||||
event_places = relationship("EventPlace", backref=backref("adminunit", lazy=True))
|
||||
location_id = deferred(db.Column(db.Integer, db.ForeignKey("location.id")))
|
||||
location = db.relationship("Location")
|
||||
location = db.relationship(
|
||||
"Location", uselist=False, single_parent=True, cascade="all, delete-orphan"
|
||||
)
|
||||
logo_id = deferred(db.Column(db.Integer, db.ForeignKey("image.id")))
|
||||
logo = db.relationship("Image", uselist=False)
|
||||
logo = db.relationship(
|
||||
"Image", uselist=False, single_parent=True, cascade="all, delete-orphan"
|
||||
)
|
||||
url = deferred(Column(String(255)), group="detail")
|
||||
email = deferred(Column(Unicode(255)), group="detail")
|
||||
phone = deferred(Column(Unicode(255)), group="detail")
|
||||
@ -335,6 +339,9 @@ class Location(db.Model, TrackableMixin):
|
||||
longitude = Column(Numeric(19, 16))
|
||||
coordinate = Column(Geometry(geometry_type="POINT"))
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(Location, self).__init__(**kwargs)
|
||||
|
||||
def is_empty(self):
|
||||
return (
|
||||
not self.street
|
||||
@ -382,9 +389,13 @@ class EventPlace(db.Model, TrackableMixin):
|
||||
id = Column(Integer(), primary_key=True)
|
||||
name = Column(Unicode(255), nullable=False)
|
||||
location_id = db.Column(db.Integer, db.ForeignKey("location.id"))
|
||||
location = db.relationship("Location", uselist=False)
|
||||
location = db.relationship(
|
||||
"Location", uselist=False, single_parent=True, cascade="all, delete-orphan"
|
||||
)
|
||||
photo_id = db.Column(db.Integer, db.ForeignKey("image.id"))
|
||||
photo = db.relationship("Image", uselist=False)
|
||||
photo = db.relationship(
|
||||
"Image", uselist=False, single_parent=True, cascade="all, delete-orphan"
|
||||
)
|
||||
url = Column(String(255))
|
||||
description = Column(UnicodeText())
|
||||
admin_unit_id = db.Column(db.Integer, db.ForeignKey("adminunit.id"), nullable=True)
|
||||
@ -460,9 +471,13 @@ class EventOrganizer(db.Model, TrackableMixin):
|
||||
phone = deferred(Column(Unicode(255)), group="detail")
|
||||
fax = deferred(Column(Unicode(255)), group="detail")
|
||||
location_id = deferred(db.Column(db.Integer, db.ForeignKey("location.id")))
|
||||
location = db.relationship("Location")
|
||||
location = db.relationship(
|
||||
"Location", uselist=False, single_parent=True, cascade="all, delete-orphan"
|
||||
)
|
||||
logo_id = deferred(db.Column(db.Integer, db.ForeignKey("image.id")))
|
||||
logo = db.relationship("Image", uselist=False)
|
||||
logo = db.relationship(
|
||||
"Image", uselist=False, single_parent=True, cascade="all, delete-orphan"
|
||||
)
|
||||
admin_unit_id = db.Column(db.Integer, db.ForeignKey("adminunit.id"), nullable=True)
|
||||
|
||||
|
||||
@ -539,7 +554,9 @@ class EventMixin(object):
|
||||
|
||||
@declared_attr
|
||||
def photo(cls):
|
||||
return relationship("Image", uselist=False)
|
||||
return relationship(
|
||||
"Image", uselist=False, single_parent=True, cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
|
||||
class EventSuggestion(db.Model, TrackableMixin, EventMixin):
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
{%- endblock -%}
|
||||
{% block content %}
|
||||
|
||||
<event-date-search basepath="{{ url_for('home', _external=True) }}"></event-date-search>
|
||||
<event-date-search basepath="{{ get_base_url() }}"></event-date-search>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='angular-elements.js')}}"></script>
|
||||
|
||||
{% endblock %}
|
||||
@ -5,7 +5,7 @@
|
||||
{% block content_container_attribs %}{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<organization-landing-page basepath="{{ url_for('home', _external=True) }}" organizationId="{{ organization.id }}" {% if "r" in request.args %}datefilterpreset="{{ request.args.get("r") }}{% endif %}"></organization-landing-page>
|
||||
<organization-landing-page basepath="{{ get_base_url() }}" organizationId="{{ organization.id }}" {% if "r" in request.args %}datefilterpreset="{{ request.args.get("r") }}{% endif %}"></organization-landing-page>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='angular-elements.js')}}"></script>
|
||||
|
||||
{% endblock %}
|
||||
@ -91,6 +91,7 @@ PyYAML==5.4.1
|
||||
qrcode==6.1
|
||||
regex==2020.11.13
|
||||
requests==2.25.0
|
||||
requests-mock==1.9.3
|
||||
requests-oauthlib==1.3.0
|
||||
rope==0.18.0
|
||||
six==1.15.0
|
||||
|
||||
@ -1,3 +1,6 @@
|
||||
import base64
|
||||
|
||||
|
||||
def test_read(client, app, db, seeder, utils):
|
||||
user_id, admin_unit_id = seeder.setup_base()
|
||||
event_id = seeder.create_event(admin_unit_id)
|
||||
@ -400,6 +403,86 @@ def test_patch_referencedEventUpdate_sendsMail(client, seeder, utils, app, mocke
|
||||
utils.assert_send_mail_called(mail_mock, "other@test.de")
|
||||
|
||||
|
||||
def test_patch_photo(client, seeder, utils, app, requests_mock):
|
||||
user_id, admin_unit_id = seeder.setup_api_access()
|
||||
event_id = seeder.create_event(admin_unit_id)
|
||||
|
||||
requests_mock.get(
|
||||
"https://image.com", content=base64.b64decode(seeder.get_default_image_base64())
|
||||
)
|
||||
|
||||
url = utils.get_url("api_v1_event", id=event_id)
|
||||
response = utils.patch_json(
|
||||
url,
|
||||
{"photo": {"image_url": "https://image.com"}},
|
||||
)
|
||||
utils.assert_response_no_content(response)
|
||||
|
||||
with app.app_context():
|
||||
from project.models import Event
|
||||
|
||||
event = Event.query.get(event_id)
|
||||
assert event.photo is not None
|
||||
assert event.photo.encoding_format == "image/png"
|
||||
|
||||
|
||||
def test_patch_photo_copyright(client, db, seeder, utils, app):
|
||||
user_id, admin_unit_id = seeder.setup_api_access()
|
||||
event_id = seeder.create_event(admin_unit_id)
|
||||
image_id = seeder.upsert_default_image()
|
||||
|
||||
with app.app_context():
|
||||
from project.models import Event
|
||||
|
||||
event = Event.query.get(event_id)
|
||||
event.photo_id = image_id
|
||||
db.session.commit()
|
||||
|
||||
url = utils.get_url("api_v1_event", id=event_id)
|
||||
response = utils.patch_json(
|
||||
url,
|
||||
{"photo": {"copyright_text": "Heiner"}},
|
||||
)
|
||||
utils.assert_response_no_content(response)
|
||||
|
||||
with app.app_context():
|
||||
from project.models import Event
|
||||
|
||||
event = Event.query.get(event_id)
|
||||
assert event.photo.id == image_id
|
||||
assert event.photo.data is not None
|
||||
assert event.photo.copyright_text == "Heiner"
|
||||
|
||||
|
||||
def test_patch_photo_delete(client, db, seeder, utils, app):
|
||||
user_id, admin_unit_id = seeder.setup_api_access()
|
||||
event_id = seeder.create_event(admin_unit_id)
|
||||
image_id = seeder.upsert_default_image()
|
||||
|
||||
with app.app_context():
|
||||
from project.models import Event
|
||||
|
||||
event = Event.query.get(event_id)
|
||||
event.photo_id = image_id
|
||||
db.session.commit()
|
||||
|
||||
url = utils.get_url("api_v1_event", id=event_id)
|
||||
response = utils.patch_json(
|
||||
url,
|
||||
{"photo": None},
|
||||
)
|
||||
utils.assert_response_no_content(response)
|
||||
|
||||
with app.app_context():
|
||||
from project.models import Event, Image
|
||||
|
||||
event = Event.query.get(event_id)
|
||||
assert event.photo_id is None
|
||||
|
||||
image = Image.query.get(image_id)
|
||||
assert image is None
|
||||
|
||||
|
||||
def test_delete(client, seeder, utils, app):
|
||||
user_id, admin_unit_id = seeder.setup_api_access()
|
||||
event_id = seeder.create_event(admin_unit_id)
|
||||
|
||||
44
tests/api/test_image.py
Normal file
44
tests/api/test_image.py
Normal file
@ -0,0 +1,44 @@
|
||||
import pytest
|
||||
|
||||
|
||||
def test_validate_image():
|
||||
from marshmallow import ValidationError
|
||||
|
||||
from project.api.image.schemas import ImagePostRequestSchema
|
||||
|
||||
data = {
|
||||
"copyright_text": "Horst",
|
||||
}
|
||||
|
||||
schema = ImagePostRequestSchema()
|
||||
|
||||
with pytest.raises(ValidationError) as e:
|
||||
schema.load(data)
|
||||
|
||||
assert "Either image_url or image_base64 has to be defined." in str(e.value)
|
||||
|
||||
|
||||
def test_post_load_image_data(seeder):
|
||||
from project.api.image.schemas import ImagePostRequestSchema
|
||||
|
||||
data = {
|
||||
"image_base64": seeder.get_default_image_upload_base64(),
|
||||
}
|
||||
|
||||
item = dict()
|
||||
schema = ImagePostRequestSchema()
|
||||
schema.post_load_image_data(item, data)
|
||||
schema.load(data)
|
||||
|
||||
assert item.get("encoding_format") is not None
|
||||
assert item.get("data") is not None
|
||||
|
||||
|
||||
def test_load_image_data():
|
||||
from project.api.image.schemas import ImagePostRequestSchema
|
||||
|
||||
schema = ImagePostRequestSchema()
|
||||
encoding_format, data = schema.load_image_data(None, None)
|
||||
|
||||
assert encoding_format is None
|
||||
assert data is None
|
||||
@ -80,6 +80,7 @@ def test_events_post(client, seeder, utils, app):
|
||||
"start": "2021-02-07T11:00:00.000Z",
|
||||
"place": {"id": place_id},
|
||||
"organizer": {"id": organizer_id},
|
||||
"photo": {"image_base64": seeder.get_default_image_upload_base64()},
|
||||
},
|
||||
)
|
||||
utils.assert_response_created(response)
|
||||
@ -96,6 +97,8 @@ def test_events_post(client, seeder, utils, app):
|
||||
assert event is not None
|
||||
assert event.event_place_id == place_id
|
||||
assert event.organizer_id == organizer_id
|
||||
assert event.photo is not None
|
||||
assert event.photo.encoding_format == "image/png"
|
||||
|
||||
|
||||
def test_places(client, seeder, utils):
|
||||
|
||||
@ -53,6 +53,38 @@ def test_patch(client, seeder, utils, app):
|
||||
assert place.description == "Klasse"
|
||||
|
||||
|
||||
def test_patch_location(db, seeder, utils, app):
|
||||
user_id, admin_unit_id = seeder.setup_api_access()
|
||||
place_id = seeder.upsert_default_event_place(admin_unit_id)
|
||||
|
||||
with app.app_context():
|
||||
from project.models import EventPlace, Location
|
||||
|
||||
location = Location()
|
||||
location.postalCode = "12345"
|
||||
location.city = "City"
|
||||
|
||||
event = EventPlace.query.get(place_id)
|
||||
event.location = location
|
||||
db.session.commit()
|
||||
|
||||
location_id = location.id
|
||||
|
||||
url = utils.get_url("api_v1_place", id=place_id)
|
||||
response = utils.patch_json(
|
||||
url,
|
||||
{"location": {"postalCode": "54321"}},
|
||||
)
|
||||
utils.assert_response_no_content(response)
|
||||
|
||||
with app.app_context():
|
||||
from project.models import EventPlace
|
||||
|
||||
place = EventPlace.query.get(place_id)
|
||||
assert place.location.id == location_id
|
||||
assert place.location.postalCode == "54321"
|
||||
|
||||
|
||||
def test_delete(client, seeder, utils, app):
|
||||
user_id, admin_unit_id = seeder.setup_api_access()
|
||||
place_id = seeder.upsert_default_event_place(admin_unit_id)
|
||||
|
||||
@ -250,7 +250,7 @@ class Seeder(object):
|
||||
return response.json["id"]
|
||||
|
||||
def get_default_image_base64(self):
|
||||
return """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"""
|
||||
return """iVBORw0KGgoAAAANSUhEUgAAAUAAAAFABAMAAAA/vriZAAAAG1BMVEUAAAD///9fX1+fn5+/v7/f398/Pz8fHx9/f38YYyahAAAACXBIWXMAAA7EAAAOxAGVKw4bAAADAklEQVR4nO3XzVPaQBzG8RQw4dhosB6hdexVhPZM6lg9mlrGHkFH7RGmhfYIM53aP7v7lhBeNgqul/b7OZBNzOPu/DbZBc8DAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8O97c9I97qlWqd39fO+lzbOBNSJvfO+tl9lU0AyPW3Ek//G3MDo+CSfyqp9E7WTPlhE3ttrhqbdOZmPlnZ4Y5fRSNJuyKDeqi/626DA+tGTUjT9rg3UyG/PVpPiR+HgrW0EiB6w6r9jKcac++511Mk8VZw9PQ9SgvKuaSeETVXmVNR+d2dy0l7ZenHvesKP7nRRFqrtZ89GZzc0GWHkpzuq6305RJDfAR2c2F891ZiY8ncRqTx9fz0XkjfaMY/5O1twS09U1AzM1GuljKZrL9A+LMo7dXGZN8SwFZiSlbX1Ub6l4vE7zEb87KMq4dR3NOmvUPd+sFelRl7AU5V9Qv5kb7qqMw9G1TqL67DSZdRKkE69KmCug32onH7zijDvTMLyYDbC6t6ozUcJ8AathWDvyHsg4NN6vZSMcnq/qTJRw/gkMruJPXnHGqe/Zox33VnY22osWtoggrj+QcSrdocpypCs6C5LLxchWuuLZMk6lO1Rzkusk90aWwqX1rZT+1ZZxyuxQVd3N8prW+KjXwjxzlzXzHAOc6t0h0RfL2bYlXuHRUgnNkGyZ5xigKYbX7KmD3ME08QoHSyXUA7Rm3BqqMjTN9tqf6EO626o1cLGE5qmzZRyb1j359d+cmRfUFMVsIoslrKqnzZpxK6jJj3hiTkuq1+zbi9lEFkool2d7xpmtuvy8kX2PZi+gmrdGuvQN9SYSmG9UntpDfFVQa8aZSu3o7vZXWJdTePZHuldXf4v9r2fu8c0q/sOcxxcH46+xHJo94851InZ+2UEQamoi98XFiS3ivxO3fRmslXmC4Op2xS+x8UHRz7Pxwd3Kq8/0kw4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4L/0FiGF8UcQrzsIAAAAASUVORK5CYII="""
|
||||
|
||||
def get_default_image_upload_base64(self):
|
||||
base64_str = self.get_default_image_base64()
|
||||
|
||||
45
tests/test_imageutils.py
Normal file
45
tests/test_imageutils.py
Normal file
@ -0,0 +1,45 @@
|
||||
import pytest
|
||||
|
||||
|
||||
def test_resize_image_to_max():
|
||||
import PIL
|
||||
|
||||
from project.imageutils import max_image_size, resize_image_to_max
|
||||
|
||||
image = PIL.Image.new("RGB", (max_image_size + 1, max_image_size + 1))
|
||||
resize_image_to_max(image)
|
||||
|
||||
assert image.width == max_image_size
|
||||
assert image.height == max_image_size
|
||||
|
||||
|
||||
def test_validate_image_too_small():
|
||||
import PIL
|
||||
|
||||
from project.imageutils import min_image_size, validate_image
|
||||
|
||||
image = PIL.Image.new("RGB", (min_image_size - 1, min_image_size - 1))
|
||||
|
||||
with pytest.raises(ValueError) as e:
|
||||
validate_image(image)
|
||||
|
||||
assert "too small" in str(e.value)
|
||||
|
||||
|
||||
def test_validate_image_unsupported_format():
|
||||
from io import BytesIO
|
||||
|
||||
import PIL
|
||||
|
||||
from project.imageutils import get_image_from_bytes, min_image_size, validate_image
|
||||
|
||||
image = PIL.Image.new("RGB", (min_image_size, min_image_size))
|
||||
imgByteArr = BytesIO()
|
||||
image.save(imgByteArr, format="TIFF")
|
||||
|
||||
tif_image = get_image_from_bytes(imgByteArr.getvalue())
|
||||
|
||||
with pytest.raises(ValueError) as e:
|
||||
validate_image(tif_image)
|
||||
|
||||
assert "not supported" in str(e.value)
|
||||
Loading…
x
Reference in New Issue
Block a user