Merge branch 'staging' into 'master'

Default user/product avatars

See merge request s3lph/matemat!45
This commit is contained in:
s3lph 2018-09-09 23:36:47 +00:00
commit 15327162d1
10 changed files with 140 additions and 47 deletions

View file

@ -1,10 +1,12 @@
FROM python:3.7-alpine FROM python:3.7-alpine
RUN mkdir -p /var/matemat/db /var/matemat/upload
RUN apk --update add libmagic
ADD . / 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 EXPOSE 80/tcp
CMD [ "/run.sh" ] CMD [ "/run.sh" ]

View file

@ -20,6 +20,7 @@ This project intends to provide a well-tested and maintainable alternative to
- Python dependencies: - Python dependencies:
- file-magic - file-magic
- jinja2 - jinja2
- Pillow
## Usage ## Usage

View file

@ -1,9 +1,13 @@
from typing import Any, Dict, Union from typing import Any, Dict, Union
import os import os
from shutil import copyfile
import magic import magic
from io import BytesIO
from PIL import Image
from matemat.webserver import pagelet, RequestArguments, PageletResponse, RedirectResponse, TemplateResponse 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 import MatematDatabase
from matemat.db.primitives import User, ReceiptPreference from matemat.db.primitives import User, ReceiptPreference
from matemat.exceptions import DatabaseConsistencyError, HttpException from matemat.exceptions import DatabaseConsistencyError, HttpException
@ -124,16 +128,20 @@ def handle_change(args: RequestArguments, user: User, db: MatematDatabase, confi
return return
# Detect the MIME type # Detect the MIME type
filemagic: magic.FileMagic = magic.detect_from_content(avatar) filemagic: magic.FileMagic = magic.detect_from_content(avatar)
# Currently, only image/png is supported, don't process any other formats if not filemagic.mime_type.startswith('image/'):
if filemagic.mime_type != 'image/png':
# TODO: Optionally convert to png
return return
# Create the absolute path of the upload directory # Create the absolute path of the upload directory
abspath: str = os.path.join(os.path.abspath(config['UploadDir']), 'thumbnails/users/') abspath: str = os.path.join(os.path.abspath(config['UploadDir']), 'thumbnails/users/')
os.makedirs(abspath, exist_ok=True) os.makedirs(abspath, exist_ok=True)
# Write the image to the file try:
with open(os.path.join(abspath, f'{user.id}.png'), 'wb') as f: # Parse the image data
f.write(avatar) 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: except UnicodeDecodeError:
raise ValueError('an argument not a string') 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_member = 'ismember' in args
is_admin = 'isadmin' in args is_admin = 'isadmin' in args
# Create the user in the database # 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 # The user requested to create a new product
elif change == 'newproduct': elif change == 'newproduct':
@ -175,29 +194,41 @@ def handle_admin_change(args: RequestArguments, db: MatematDatabase, config: Dic
return return
# Read the properties from the request arguments # Read the properties from the request arguments
name = str(args.name) name = str(args.name)
price_member = int(str(args.pricemember)) price_member = parse_chf(str(args.pricemember))
price_non_member = int(str(args.pricenonmember)) price_non_member = parse_chf(str(args.pricenonmember))
# Create the user in the database # Create the user in the database
newproduct = db.create_product(name, price_member, price_non_member) newproduct = db.create_product(name, price_member, price_non_member)
# If a new product image was uploaded, process it # 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 # 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:
return
# Detect the MIME type # Detect the MIME type
filemagic: magic.FileMagic = magic.detect_from_content(image) filemagic: magic.FileMagic = magic.detect_from_content(avatar)
# Currently, only image/png is supported, don't process any other formats if not filemagic.mime_type.startswith('image/'):
if filemagic.mime_type != 'image/png':
# TODO: Optionally convert to png
return return
# Create the absolute path of the upload directory # Create the absolute path of the upload directory
abspath: str = os.path.join(os.path.abspath(config['UploadDir']), 'thumbnails/products/') abspath: str = os.path.join(os.path.abspath(config['UploadDir']), 'thumbnails/products/')
os.makedirs(abspath, exist_ok=True) os.makedirs(abspath, exist_ok=True)
# Write the image to the file try:
with open(os.path.join(abspath, f'{newproduct.id}.png'), 'wb') as f: # Parse the image data
f.write(image) 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 # The user requested to restock a product
elif change == 'restock': 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 # Write the new stock count to the database
db.restock(product, amount) 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: except UnicodeDecodeError:
raise ValueError('an argument not a string') raise ValueError('an argument not a string')

View file

@ -2,6 +2,8 @@ from typing import Any, Dict, Union
import os import os
import magic import magic
from PIL import Image
from io import BytesIO
from matemat.webserver import pagelet, RequestArguments, PageletResponse, RedirectResponse, TemplateResponse from matemat.webserver import pagelet, RequestArguments, PageletResponse, RedirectResponse, TemplateResponse
from matemat.db import MatematDatabase 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 a new product image was uploaded, process it
if 'image' in args: if 'image' in args:
# Read the raw image data from the request # 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 # 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 return
# Detect the MIME type # Detect the MIME type
filemagic: magic.FileMagic = magic.detect_from_content(image) filemagic: magic.FileMagic = magic.detect_from_content(avatar)
# Currently, only image/png is supported, don't process any other formats if not filemagic.mime_type.startswith('image/'):
if filemagic.mime_type != 'image/png':
# TODO: Optionally convert to png
return return
# Create the absolute path of the upload directory # Create the absolute path of the upload directory
abspath: str = os.path.join(os.path.abspath(config['UploadDir']), 'thumbnails/products/') abspath: str = os.path.join(os.path.abspath(config['UploadDir']), 'thumbnails/products/')
os.makedirs(abspath, exist_ok=True) os.makedirs(abspath, exist_ok=True)
# Write the image to the file try:
with open(os.path.join(abspath, f'{product.id}.png'), 'wb') as f: # Parse the image data
f.write(image) 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

View file

@ -2,6 +2,8 @@ from typing import Any, Dict, Optional, Union
import os import os
import magic import magic
from PIL import Image
from io import BytesIO
from matemat.webserver import pagelet, RequestArguments, PageletResponse, RedirectResponse, TemplateResponse from matemat.webserver import pagelet, RequestArguments, PageletResponse, RedirectResponse, TemplateResponse
from matemat.db import MatematDatabase from matemat.db import MatematDatabase
@ -130,13 +132,17 @@ def handle_change(args: RequestArguments, user: User, authuser: User, db: Matema
return return
# Detect the MIME type # Detect the MIME type
filemagic: magic.FileMagic = magic.detect_from_content(avatar) filemagic: magic.FileMagic = magic.detect_from_content(avatar)
# Currently, only image/png is supported, don't process any other formats if not filemagic.mime_type.startswith('image/'):
if filemagic.mime_type != 'image/png':
# TODO: Optionally convert to png
return return
# Create the absolute path of the upload directory # Create the absolute path of the upload directory
abspath: str = os.path.join(os.path.abspath(config['UploadDir']), 'thumbnails/users/') abspath: str = os.path.join(os.path.abspath(config['UploadDir']), 'thumbnails/users/')
os.makedirs(abspath, exist_ok=True) os.makedirs(abspath, exist_ok=True)
# Write the image to the file try:
with open(os.path.join(abspath, f'{user.id}.png'), 'wb') as f: # Parse the image data
f.write(avatar) 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

View file

@ -1,2 +1,3 @@
file-magic file-magic
jinja2 jinja2
Pillow

View file

@ -34,7 +34,7 @@
<img src="/upload/thumbnails/users/{{ authuser.id }}.png" alt="Avatar of {{ authuser.name }}" /><br/> <img src="/upload/thumbnails/users/{{ authuser.id }}.png" alt="Avatar of {{ authuser.name }}" /><br/>
<label for="admin-avatar-avatar">Upload new file: </label> <label for="admin-avatar-avatar">Upload new file: </label>
<input id="admin-avatar-avatar" type="file" name="avatar" accept="image/png" /><br/> <input id="admin-avatar-avatar" type="file" name="avatar" accept="image/*" /><br/>
<input type="submit" value="Save changes" /> <input type="submit" value="Save changes" />
</form> </form>

View file

@ -44,13 +44,13 @@
<input id="admin-newproduct-name" type="text" name="name" /><br/> <input id="admin-newproduct-name" type="text" name="name" /><br/>
<label for="admin-newproduct-price-member">Member price: </label> <label for="admin-newproduct-price-member">Member price: </label>
<input id="admin-newproduct-price-member" type="number" min="0" name="pricemember" /><br/> CHF <input id="admin-newproduct-price-member" type="number" step="0.01" name="pricemember" value="0" /><br/>
<label for="admin-newproduct-price-non-member">Non-member price: </label> <label for="admin-newproduct-price-non-member">Non-member price: </label>
<input id="admin-newproduct-price-non-member" type="number" min="0" name="pricenonmember" /><br/> CHF <input id="admin-newproduct-price-non-member" type="number" step="0.01" name="pricenonmember" value="0" /><br/>
<label for="admin-newproduct-image">Image: </label> <label for="admin-newproduct-image">Image: </label>
<input id="admin-newproduct-image" type="file" accept="image/png" /><br/> <input id="admin-newproduct-image" name="image" type="file" accept="image/*" /><br/>
<input type="submit" value="Create Product" /> <input type="submit" value="Create Product" />
</form> </form>
@ -88,3 +88,21 @@
<input type="submit" value="Go"> <input type="submit" value="Go">
</form> </form>
</section> </section>
<section id="admin-restricted-default-images">
<h2>Set default images</h2>
<form id="admin-default-images-form" method="post" action="/admin?adminchange=defaultimg" enctype="multipart/form-data" accept-charset="UTF-8">
<label for="admin-default-images-user">
<img src="/upload/thumbnails/users/default.png" alt="Default user avatar" />
</label><br/>
<input id="admin-default-images-user" type="file" name="users" accept="image/*" /><br/>
<label for="admin-default-images-product">
<img src="/upload/thumbnails/products/default.png" alt="Default product avatar" />
</label><br/>
<input id="admin-default-images-product" type="file" name="products" accept="image/*" /><br/>
<input type="submit" value="Save changes">
</form>
</section>

View file

@ -26,7 +26,7 @@
<label for="modproduct-image"> <label for="modproduct-image">
<img height="150" src="/upload/thumbnails/products/{{ product.id }}.png" alt="Image of {{ product.name }}" /> <img height="150" src="/upload/thumbnails/products/{{ product.id }}.png" alt="Image of {{ product.name }}" />
</label><br/> </label><br/>
<input id="modproduct-image" type="file" name="image" accept="image/png" /><br/> <input id="modproduct-image" type="file" name="image" accept="image/*" /><br/>
<input id="modproduct-productid" type="hidden" name="productid" value="{{ product.id }}" /><br/> <input id="modproduct-productid" type="hidden" name="productid" value="{{ product.id }}" /><br/>

View file

@ -42,9 +42,9 @@
<input id="moduser-account-balance-reason" type="text" name="reason" placeholder="Shows up on receipt" /><br/> <input id="moduser-account-balance-reason" type="text" name="reason" placeholder="Shows up on receipt" /><br/>
<label for="moduser-account-avatar"> <label for="moduser-account-avatar">
<img height="150" src="/upload/thumbnails/users/{{ user.id }}.png" alt="Avatar of {{ user.name }}" /> <img src="/upload/thumbnails/users/{{ user.id }}.png" alt="Avatar of {{ user.name }}" />
</label><br/> </label><br/>
<input id="moduser-account-avatar" type="file" name="avatar" accept="image/png" /><br/> <input id="moduser-account-avatar" type="file" name="avatar" accept="image/*" /><br/>
<input id="moduser-account-userid" type="hidden" name="userid" value="{{ user.id }}" /><br/> <input id="moduser-account-userid" type="hidden" name="userid" value="{{ user.id }}" /><br/>