diff --git a/README.md b/README.md index c6c31ea..d200874 100644 --- a/README.md +++ b/README.md @@ -77,11 +77,12 @@ Create `.env` file in the root directory or pass as environment variables. | MAIL_SERVER | " | | MAIL_USERNAME | " | -### Resolve addresses with Google Maps +### Misc | Variable | Function | | --- | --- | -| GOOGLE_MAPS_API_KEY | API Key with Places API enabled | +| CACHE_PATH | Absolute or relative path to root directory for dump and image caching. Default: tmp | +| GOOGLE_MAPS_API_KEY | Resolve addresses with Google Maps: API Key with Places API enabled | ## Development diff --git a/project/__init__.py b/project/__init__.py index d7e5e17..b7e720d 100644 --- a/project/__init__.py +++ b/project/__init__.py @@ -15,7 +15,6 @@ from flask_restful import Api from apispec import APISpec from apispec.ext.marshmallow import MarshmallowPlugin from flask_apispec.extension import FlaskApiSpec -import pathlib # Create app app = Flask(__name__) @@ -40,10 +39,13 @@ app.config["SECURITY_PASSWORD_SALT"] = os.environ.get( "SECURITY_PASSWORD_SALT", "146585145368132386173505678016728509634" ) -# Temporary pathes -temp_path = os.path.join(app.root_path, "tmp") -dump_path = os.path.join(temp_path, "dump") -pathlib.Path(dump_path).mkdir(parents=True, exist_ok=True) +# Cache pathes +cache_env = os.environ.get("CACHE_PATH", "tmp") +cache_path = ( + cache_env if os.path.isabs(cache_env) else os.path.join(app.root_path, cache_env) +) +dump_path = os.path.join(cache_path, "dump") +img_path = os.path.join(cache_path, "img") # i18n app.config["BABEL_DEFAULT_LOCALE"] = "de" diff --git a/project/api/image/schemas.py b/project/api/image/schemas.py index 8d65f36..9a34c00 100644 --- a/project/api/image/schemas.py +++ b/project/api/image/schemas.py @@ -16,7 +16,13 @@ class ImageBaseSchema(ImageIdSchema): class ImageSchema(ImageBaseSchema): - image_url = marshmallow.URLFor("image", values=dict(id="")) + image_url = marshmallow.URLFor( + "image", + values=dict(id="", s=500), + metadata={ + "description": "Append query arguments w for width, h for height or s for size(width and height)." + }, + ) class ImageDumpSchema(ImageBaseSchema): @@ -24,4 +30,10 @@ class ImageDumpSchema(ImageBaseSchema): class ImageRefSchema(ImageIdSchema): - image_url = marshmallow.URLFor("image", values=dict(id="")) + image_url = marshmallow.URLFor( + "image", + values=dict(id="", s=500), + metadata={ + "description": "Append query arguments w for width, h for height or s for size(width and height)." + }, + ) diff --git a/project/cli/dump.py b/project/cli/dump.py index 7404dcd..62e5070 100644 --- a/project/cli/dump.py +++ b/project/cli/dump.py @@ -23,7 +23,7 @@ from project.api.organization.schemas import OrganizationDumpSchema from project.api.event_reference.schemas import EventReferenceDumpSchema import os import shutil -import pathlib +from project.utils import make_dir dump_cli = AppGroup("dump") @@ -42,12 +42,7 @@ def dump_items(items, schema, file_base_name, dump_path): def dump_all(): # Setup temp dir tmp_path = os.path.join(dump_path, "tmp") - - try: - original_umask = os.umask(0) - pathlib.Path(tmp_path).mkdir(parents=True, exist_ok=True) - finally: - os.umask(original_umask) + make_dir(tmp_path) # Events events = Event.query.options(joinedload(Event.categories)).all() diff --git a/project/templates/_macros.html b/project/templates/_macros.html index bb04f66..0c689d5 100644 --- a/project/templates/_macros.html +++ b/project/templates/_macros.html @@ -284,29 +284,29 @@ {% endif %} {% endmacro %} -{% macro render_img_src(image) %} - +{% macro render_img_src(image, size=500) %} + {% endmacro %} -{% macro render_image(image) %} +{% macro render_image(image, size=500) %} {% if image %} {% set img_class = kwargs['class'] if 'class' in kwargs else '' %} {% set img_style = kwargs['style'] if 'style' in kwargs else '' %} {% if image.copyright_text %}
{% set img_class = img_class + ' figure-img' %} - {{ render_img_src(image, class=img_class, style=img_style) }} + {{ render_img_src(image, size, class=img_class, style=img_style) }}
© {{ image.copyright_text }}
{% else %} {% set img_class = img_class + ' mb-2' %} - {{ render_img_src(image, class=img_class, style=img_style) }} + {{ render_img_src(image, size, class=img_class, style=img_style) }} {% endif %} {% endif %} {% endmacro %} {% macro render_logo(image) %} -{{ render_image(image, style="max-width:200px;") }} +{{ render_image(image, 120, style="max-width:120px;") }} {% endmacro %} {% macro render_event_review_status(event) %} @@ -392,7 +392,7 @@ {% endif %} {% if event.photo_id %} -
{{ render_image(event.photo) }}
+
{{ render_image(event.photo, 700) }}
{% endif %}
{{ event.description }}
@@ -415,7 +415,7 @@
{{ event.event_place.name }}
{% if event.event_place.photo_id %} -
{{ render_image(event.event_place.photo) }}
+
{{ render_image(event.event_place.photo, 300) }}
{% endif %} {% if event.event_place.description %} diff --git a/project/templates/widget/event_date/list.html b/project/templates/widget/event_date/list.html index 9ed6443..ac6321b 100644 --- a/project/templates/widget/event_date/list.html +++ b/project/templates/widget/event_date/list.html @@ -67,7 +67,7 @@
{% if date.event.photo_id %} - + {% endif %}
@@ -82,7 +82,7 @@
{% if date.event.photo_id %} - + {% endif %}
diff --git a/project/templates/widget/event_suggestion/create.html b/project/templates/widget/event_suggestion/create.html index 99d3504..2a37fa9 100644 --- a/project/templates/widget/event_suggestion/create.html +++ b/project/templates/widget/event_suggestion/create.html @@ -116,7 +116,7 @@
{% if admin_unit.logo_id %} -
+
{% endif %}

Hier kannst du als Gast eine Veranstaltung vorschlagen, die anschließend durch {{ admin_unit.name }} geprüft wird. diff --git a/project/utils.py b/project/utils.py index 3ae727b..aea28ef 100644 --- a/project/utils.py +++ b/project/utils.py @@ -1,4 +1,6 @@ from flask_babelex import lazy_gettext +import pathlib +import os def get_event_category_name(category): @@ -7,3 +9,11 @@ def get_event_category_name(category): def get_localized_enum_name(enum): return lazy_gettext(enum.__class__.__name__ + "." + enum.name) + + +def make_dir(path): + try: + original_umask = os.umask(0) + pathlib.Path(path).mkdir(parents=True, exist_ok=True) + finally: + os.umask(original_umask) diff --git a/project/views/image.py b/project/views/image.py index d5c62e9..486f523 100644 --- a/project/views/image.py +++ b/project/views/image.py @@ -1,8 +1,44 @@ -from project import app +from project import app, img_path from project.models import Image +from sqlalchemy.orm import load_only +import PIL +from io import BytesIO +import os +from flask import send_file, request +from project.utils import make_dir @app.route("/image/") def image(id): - image = Image.query.get_or_404(id) - return app.response_class(image.data, mimetype=image.encoding_format) + image = Image.query.options(load_only(Image.id, Image.encoding_format)).get_or_404( + id + ) + + # Dimensions + width = 500 + height = 500 + + if "s" in request.args: + width = int(request.args["s"]) + height = width + elif "w" in request.args: + width = int(request.args["w"]) + elif "h" in request.args: + height = int(request.args["h"]) + + # Generate file name + extension = image.encoding_format.split("/")[-1] if image.encoding_format else "png" + file_path = os.path.join(img_path, f"{id}-{width}-{height}.{extension}") + + # Load from disk if exists + if os.path.exists(file_path): + return send_file(file_path) + + # Save from database to disk + make_dir(img_path) + img = PIL.Image.open(BytesIO(image.data)) + img.thumbnail((width, height), PIL.Image.ANTIALIAS) + img.save(file_path) + + # Load from disk + return send_file(file_path) diff --git a/requirements.txt b/requirements.txt index a655c67..ec071a0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,7 @@ aniso8601==8.1.0 apispec==4.0.0 apispec-webframeworks==0.5.2 appdirs==1.4.4 +argh==0.26.2 attrs==20.3.0 Babel==2.9.0 bcrypt==3.2.0 @@ -62,6 +63,7 @@ oauthlib==3.1.0 packaging==20.8 passlib==1.7.4 pathspec==0.8.1 +pilkit==2.0 Pillow==8.0.1 pluggy==0.13.1 pre-commit==2.9.3 diff --git a/tests/views/test_image.py b/tests/views/test_image.py index 65377a1..359f569 100644 --- a/tests/views/test_image.py +++ b/tests/views/test_image.py @@ -1,6 +1,17 @@ -def test_read(client, seeder, utils): +import pytest +import shutil +from project import img_path + + +@pytest.mark.parametrize("size", [None, 100]) +@pytest.mark.parametrize("width", [None, 100]) +@pytest.mark.parametrize("height", [None, 100]) +def test_read(app, seeder, utils, size, width, height): user_id, admin_unit_id = seeder.setup_base() image_id = seeder.upsert_default_image() - url = utils.get_url("image", id=image_id) + shutil.rmtree(img_path, ignore_errors=True) + + url = utils.get_url("image", id=image_id, s=size, w=width, h=height) utils.get_ok(url) + utils.get_ok(url) # cache