Merge branch '17-avatars-and-product-images' into 'staging'
Resolve "Avatars and Product Images" See merge request s3lph/matemat!44
This commit is contained in:
commit
a48e1d8130
10 changed files with 140 additions and 47 deletions
|
@ -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" ]
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
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
|
# Write the image to the file
|
||||||
with open(os.path.join(abspath, f'{user.id}.png'), 'wb') as f:
|
image.save(os.path.join(abspath, f'{user.id}.png'), 'PNG')
|
||||||
f.write(avatar)
|
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)
|
||||||
|
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
|
# Write the image to the file
|
||||||
with open(os.path.join(abspath, f'{newproduct.id}.png'), 'wb') as f:
|
image.save(os.path.join(abspath, f'{newproduct.id}.png'), 'PNG')
|
||||||
f.write(image)
|
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')
|
||||||
|
|
|
@ -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)
|
||||||
|
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
|
# Write the image to the file
|
||||||
with open(os.path.join(abspath, f'{product.id}.png'), 'wb') as f:
|
image.save(os.path.join(abspath, f'{product.id}.png'), 'PNG')
|
||||||
f.write(image)
|
except OSError:
|
||||||
|
return
|
||||||
|
|
|
@ -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)
|
||||||
|
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
|
# Write the image to the file
|
||||||
with open(os.path.join(abspath, f'{user.id}.png'), 'wb') as f:
|
image.save(os.path.join(abspath, f'{user.id}.png'), 'PNG')
|
||||||
f.write(avatar)
|
except OSError:
|
||||||
|
return
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
file-magic
|
file-magic
|
||||||
jinja2
|
jinja2
|
||||||
|
Pillow
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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/>
|
||||||
|
|
||||||
|
|
|
@ -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/>
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue