diff --git a/Dockerfile b/Dockerfile index 553cb64..7338327 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,12 @@ FROM python:3.7-alpine -RUN mkdir -p /var/matemat/db /var/matemat/upload -RUN apk --update add libmagic ADD . / -RUN pip3 install -r /requirements.txt +RUN mkdir -p /var/matemat/db /var/matemat/upload \ + && apk --update add libmagic zlib jpeg zlib-dev jpeg-dev build-base \ + && pip3 install -r /requirements.txt \ + && apk del zlib-dev jpeg-dev build-base \ + && rm -rf /var/cache/apk /root/.cache/pip EXPOSE 80/tcp CMD [ "/run.sh" ] diff --git a/README.md b/README.md index 278c59e..1d75157 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ This project intends to provide a well-tested and maintainable alternative to - Python dependencies: - file-magic - jinja2 + - Pillow ## Usage diff --git a/matemat/webserver/pagelets/admin.py b/matemat/webserver/pagelets/admin.py index d1866b2..7e4f95e 100644 --- a/matemat/webserver/pagelets/admin.py +++ b/matemat/webserver/pagelets/admin.py @@ -1,9 +1,13 @@ from typing import Any, Dict, Union import os +from shutil import copyfile import magic +from io import BytesIO +from PIL import Image from matemat.webserver import pagelet, RequestArguments, PageletResponse, RedirectResponse, TemplateResponse +from matemat.util.currency_format import parse_chf from matemat.db import MatematDatabase from matemat.db.primitives import User, ReceiptPreference from matemat.exceptions import DatabaseConsistencyError, HttpException @@ -124,16 +128,20 @@ def handle_change(args: RequestArguments, user: User, db: MatematDatabase, confi return # Detect the MIME type filemagic: magic.FileMagic = magic.detect_from_content(avatar) - # Currently, only image/png is supported, don't process any other formats - if filemagic.mime_type != 'image/png': - # TODO: Optionally convert to png + if not filemagic.mime_type.startswith('image/'): return # Create the absolute path of the upload directory abspath: str = os.path.join(os.path.abspath(config['UploadDir']), 'thumbnails/users/') os.makedirs(abspath, exist_ok=True) - # Write the image to the file - with open(os.path.join(abspath, f'{user.id}.png'), 'wb') as f: - f.write(avatar) + try: + # Parse the image data + image: Image = Image.open(BytesIO(avatar)) + # Resize the image to 150x150 + image.thumbnail((150, 150), Image.LANCZOS) + # Write the image to the file + image.save(os.path.join(abspath, f'{user.id}.png'), 'PNG') + except OSError: + return except UnicodeDecodeError: raise ValueError('an argument not a string') @@ -166,7 +174,18 @@ def handle_admin_change(args: RequestArguments, db: MatematDatabase, config: Dic is_member = 'ismember' in args is_admin = 'isadmin' in args # Create the user in the database - db.create_user(username, password, email, member=is_member, admin=is_admin) + newuser: User = db.create_user(username, password, email, member=is_member, admin=is_admin) + + # If a default avatar is set, copy it to the user's avatar path + + # Create the absolute path of the upload directory + abspath: str = os.path.join(os.path.abspath(config['UploadDir']), 'thumbnails/users/') + # Derive the individual paths + default: str = os.path.join(abspath, 'default.png') + userimg: str = os.path.join(abspath, f'{newuser.id}.png') + # Copy the default image, if it exists + if os.path.exists(default): + copyfile(default, userimg, follow_symlinks=True) # The user requested to create a new product elif change == 'newproduct': @@ -175,29 +194,41 @@ def handle_admin_change(args: RequestArguments, db: MatematDatabase, config: Dic return # Read the properties from the request arguments name = str(args.name) - price_member = int(str(args.pricemember)) - price_non_member = int(str(args.pricenonmember)) + price_member = parse_chf(str(args.pricemember)) + price_non_member = parse_chf(str(args.pricenonmember)) # Create the user in the database newproduct = db.create_product(name, price_member, price_non_member) # If a new product image was uploaded, process it - if 'image' in args: + if 'image' in args and len(bytes(args.image)) > 0: # Read the raw image data from the request - image = bytes(args.image) - # Only process the image, if its size is more than zero. Zero size means no new image was uploaded - if len(image) == 0: - return + avatar = bytes(args.image) # Detect the MIME type - filemagic: magic.FileMagic = magic.detect_from_content(image) - # Currently, only image/png is supported, don't process any other formats - if filemagic.mime_type != 'image/png': - # TODO: Optionally convert to png + filemagic: magic.FileMagic = magic.detect_from_content(avatar) + if not filemagic.mime_type.startswith('image/'): return # Create the absolute path of the upload directory abspath: str = os.path.join(os.path.abspath(config['UploadDir']), 'thumbnails/products/') os.makedirs(abspath, exist_ok=True) - # Write the image to the file - with open(os.path.join(abspath, f'{newproduct.id}.png'), 'wb') as f: - f.write(image) + try: + # Parse the image data + image: Image = Image.open(BytesIO(avatar)) + # Resize the image to 150x150 + image.thumbnail((150, 150), Image.LANCZOS) + # Write the image to the file + image.save(os.path.join(abspath, f'{newproduct.id}.png'), 'PNG') + except OSError: + return + else: + # If no image was uploaded and a default avatar is set, copy it to the product's avatar path + + # Create the absolute path of the upload directory + abspath: str = os.path.join(os.path.abspath(config['UploadDir']), 'thumbnails/products/') + # Derive the individual paths + default: str = os.path.join(abspath, 'default.png') + userimg: str = os.path.join(abspath, f'{newproduct.id}.png') + # Copy the default image, if it exists + if os.path.exists(default): + copyfile(default, userimg, follow_symlinks=True) # The user requested to restock a product elif change == 'restock': @@ -212,5 +243,33 @@ def handle_admin_change(args: RequestArguments, db: MatematDatabase, config: Dic # Write the new stock count to the database db.restock(product, amount) + # The user requested to set default images + elif change == 'defaultimg': + # Iterate the possible images to set + for category in 'users', 'products': + if category not in args: + continue + # Read the raw image data from the request + default: bytes = bytes(args[category]) + # Only process the image, if its size is more than zero. Zero size means no new image was uploaded + if len(default) == 0: + continue + # Detect the MIME type + filemagic: magic.FileMagic = magic.detect_from_content(default) + if not filemagic.mime_type.startswith('image/'): + continue + # Create the absolute path of the upload directory + abspath: str = os.path.join(os.path.abspath(config['UploadDir']), f'thumbnails/{category}/') + os.makedirs(abspath, exist_ok=True) + try: + # Parse the image data + image: Image = Image.open(BytesIO(default)) + # Resize the image to 150x150 + image.thumbnail((150, 150), Image.LANCZOS) + # Write the image to the file + image.save(os.path.join(abspath, f'default.png'), 'PNG') + except OSError: + return + except UnicodeDecodeError: raise ValueError('an argument not a string') diff --git a/matemat/webserver/pagelets/modproduct.py b/matemat/webserver/pagelets/modproduct.py index ef6acbd..7ba9c3e 100644 --- a/matemat/webserver/pagelets/modproduct.py +++ b/matemat/webserver/pagelets/modproduct.py @@ -2,6 +2,8 @@ from typing import Any, Dict, Union import os import magic +from PIL import Image +from io import BytesIO from matemat.webserver import pagelet, RequestArguments, PageletResponse, RedirectResponse, TemplateResponse from matemat.db import MatematDatabase @@ -101,19 +103,23 @@ def handle_change(args: RequestArguments, product: Product, db: MatematDatabase, # If a new product image was uploaded, process it if 'image' in args: # Read the raw image data from the request - image = bytes(args.image) + avatar = bytes(args.image) # Only process the image, if its size is more than zero. Zero size means no new image was uploaded - if len(image) == 0: + if len(avatar) == 0: return # Detect the MIME type - filemagic: magic.FileMagic = magic.detect_from_content(image) - # Currently, only image/png is supported, don't process any other formats - if filemagic.mime_type != 'image/png': - # TODO: Optionally convert to png + filemagic: magic.FileMagic = magic.detect_from_content(avatar) + if not filemagic.mime_type.startswith('image/'): return # Create the absolute path of the upload directory abspath: str = os.path.join(os.path.abspath(config['UploadDir']), 'thumbnails/products/') os.makedirs(abspath, exist_ok=True) - # Write the image to the file - with open(os.path.join(abspath, f'{product.id}.png'), 'wb') as f: - f.write(image) + try: + # Parse the image data + image: Image = Image.open(BytesIO(avatar)) + # Resize the image to 150x150 + image.thumbnail((150, 150), Image.LANCZOS) + # Write the image to the file + image.save(os.path.join(abspath, f'{product.id}.png'), 'PNG') + except OSError: + return diff --git a/matemat/webserver/pagelets/moduser.py b/matemat/webserver/pagelets/moduser.py index 8be086c..94bfa84 100644 --- a/matemat/webserver/pagelets/moduser.py +++ b/matemat/webserver/pagelets/moduser.py @@ -2,6 +2,8 @@ from typing import Any, Dict, Optional, Union import os import magic +from PIL import Image +from io import BytesIO from matemat.webserver import pagelet, RequestArguments, PageletResponse, RedirectResponse, TemplateResponse from matemat.db import MatematDatabase @@ -130,13 +132,17 @@ def handle_change(args: RequestArguments, user: User, authuser: User, db: Matema return # Detect the MIME type filemagic: magic.FileMagic = magic.detect_from_content(avatar) - # Currently, only image/png is supported, don't process any other formats - if filemagic.mime_type != 'image/png': - # TODO: Optionally convert to png + if not filemagic.mime_type.startswith('image/'): return # Create the absolute path of the upload directory abspath: str = os.path.join(os.path.abspath(config['UploadDir']), 'thumbnails/users/') os.makedirs(abspath, exist_ok=True) - # Write the image to the file - with open(os.path.join(abspath, f'{user.id}.png'), 'wb') as f: - f.write(avatar) + try: + # Parse the image data + image: Image = Image.open(BytesIO(avatar)) + # Resize the image to 150x150 + image.thumbnail((150, 150), Image.LANCZOS) + # Write the image to the file + image.save(os.path.join(abspath, f'{user.id}.png'), 'PNG') + except OSError: + return diff --git a/requirements.txt b/requirements.txt index 4913234..0da0f5a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ file-magic jinja2 +Pillow diff --git a/templates/admin_all.html b/templates/admin_all.html index b637cc7..d1aa1f2 100644 --- a/templates/admin_all.html +++ b/templates/admin_all.html @@ -34,7 +34,7 @@ Avatar of {{ authuser.name }}
-
+
diff --git a/templates/admin_restricted.html b/templates/admin_restricted.html index 36152c3..72a262c 100644 --- a/templates/admin_restricted.html +++ b/templates/admin_restricted.html @@ -44,13 +44,13 @@
-
+ CHF
-
+ CHF
-
+
@@ -87,4 +87,22 @@ - \ No newline at end of file + + +
+

Set default images

+ +
+
+
+ +
+
+ + +
+
diff --git a/templates/modproduct.html b/templates/modproduct.html index 2158139..338df82 100644 --- a/templates/modproduct.html +++ b/templates/modproduct.html @@ -26,7 +26,7 @@
-
+

diff --git a/templates/moduser.html b/templates/moduser.html index 894d198..b31b5c3 100644 --- a/templates/moduser.html +++ b/templates/moduser.html @@ -42,9 +42,9 @@

-
+