feat(usermodel): add default profile picture

This commit is contained in:
Julian Lobbes 2022-11-15 15:10:38 +01:00
parent 65de54cfad
commit 8c1f8775c4
3 changed files with 163 additions and 12 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -0,0 +1,94 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="512"
height="512"
viewBox="0 0 512 512"
version="1.1"
id="svg5"
sodipodi:docname="default_user_icon.svg"
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14, custom)"
inkscape:export-filename="default_user_icon.svg.jpg"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="false"
inkscape:deskcolor="#505050"
inkscape:document-units="px"
showgrid="false"
inkscape:zoom="0.4639112"
inkscape:cx="253.28123"
inkscape:cy="156.27991"
inkscape:window-width="1366"
inkscape:window-height="712"
inkscape:window-x="0"
inkscape:window-y="28"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs2">
<rect
x="171.97761"
y="250.66449"
width="344.04429"
height="325.81057"
id="rect1513" />
</defs>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<rect
style="fill:#ffffff;fill-opacity:1"
id="rect9171"
width="512"
height="512"
x="0"
y="0" />
<circle
style="fill:#f8c655;fill-opacity:1;stroke:none;stroke-width:0.511802"
id="path111"
cx="256"
cy="91.079361"
r="84.022392" />
<path
style="fill:#f8c655;fill-opacity:1;stroke:none;stroke-width:0.997777"
id="path927"
sodipodi:type="arc"
sodipodi:cx="-256"
sodipodi:cy="-330.88705"
sodipodi:rx="136.01785"
sodipodi:ry="138.84554"
sodipodi:start="0"
sodipodi:end="3.1415927"
sodipodi:open="true"
sodipodi:arc-type="arc"
d="m -119.98215,-330.88705 a 136.01785,138.84554 0 0 1 -68.00893,120.24376 136.01785,138.84554 0 0 1 -136.01785,-1e-5 136.01785,138.84554 0 0 1 -68.00892,-120.24375"
transform="scale(-1)" />
<path
id="rect1005"
d="m 119.98215,328.05939 h 272.0357 v 99.18975 c 0,93.50289 -272.0357,97.29136 -272.0357,0 z"
sodipodi:nodetypes="ccssc"
style="fill:#f8c655;fill-opacity:1;stroke-width:0.80019" />
<text
xml:space="preserve"
id="text1511"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:266.667px;font-family:'Hack Nerd Font Mono';-inkscape-font-specification:'Hack Nerd Font Mono, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;white-space:pre;shape-inside:url(#rect1513);shape-padding:4.89372;display:inline;fill:#cfad64;fill-opacity:1"
inkscape:label="text1511"
transform="translate(-4.4648406,-44.051506)"><tspan
x="176.87109"
y="491.49638"
id="tspan9525">?</tspan></text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -1,10 +1,12 @@
"""Provides the application-internal class-based models for users and groups.""" """Provides the application-internal class-based models for users and groups."""
from string import ascii_lowercase, ascii_uppercase, digits, whitespace from string import ascii_lowercase, ascii_uppercase, digits, whitespace
from base64 import b64decode from base64 import b64encode, b64decode
import hashlib
from binascii import Error as Base64DecodeError from binascii import Error as Base64DecodeError
from PIL.Image import Image from PIL import Image
from flask import current_app
class User: class User:
"""Class model for a user. """Class model for a user.
@ -187,7 +189,7 @@ class User:
@staticmethod @staticmethod
def is_valid_picture(input_image: Image) -> bool: def is_valid_picture(input_image: Image.Image) -> bool:
"""Checks whether the input image is a valid Image object. """Checks whether the input image is a valid Image object.
TBD - unsure which formats and filesizes to allow here. TBD - unsure which formats and filesizes to allow here.
@ -208,18 +210,64 @@ class User:
If input_image is not of type PIL.Image. If input_image is not of type PIL.Image.
""" """
if not isinstance(input_image, Image): if not isinstance(input_image, Image.Image):
raise TypeError(f"Expected a PIL Image but got: '{type(input_image)}'.") raise TypeError(f"Expected a PIL Image but got: '{type(input_image)}'.")
# TODO implement # TODO implement some integrity checks
# TODO implement some filesize restrictions
return True return True
@staticmethod
def generate_password_hash(password: str) -> str:
"""Generates a base64-encoded SHA512 hash of the input string.
Parameters
----------
password : str
The plaintext password for which to generate a hash.
Returns
-------
str
A base64-encoded SHA512 hash digest of the input string.
Raises
------
TypeError
If the input is not of type string.
ValueError
If the input string is empty.
"""
if not isinstance(password, str):
raise TypeError(f"Expected a string but got: '{type(password)}'.")
if not len(password):
raise ValueError("Input string cannot be empty.")
hash_bytes = hashlib.sha512()
hash_bytes.update(bytes(password, "UTF-8"))
return b64encode(hash_bytes.digest()).decode("ASCII")
def _get_default_picture() -> Image.Image:
"""Returns the default user picture as a PIL Image object.
Returns
-------
PIL.Image.Image
The default user profile picture.
"""
image_path = f"{current_app.static_folder}/assets/default_user_icon.jpg"
return Image.open(image_path)
def __init__( def __init__(
self, self,
username: str, password_hash: str, email: str, username: str, password_hash: str, email: str,
first_name: str, last_name: str, display_name: str, first_name: str, last_name: str, display_name = None,
picture: Image, picture = None,
): ):
if not User.is_valid_username(username): if not User.is_valid_username(username):
@ -234,16 +282,25 @@ class User:
raise ValueError(f"Not a valid email address: '{email}'.") raise ValueError(f"Not a valid email address: '{email}'.")
self.email = email self.email = email
for name in [first_name, last_name, display_name]: for name in [first_name, last_name]:
if not User.is_valid_person_name(name): if not User.is_valid_person_name(name):
raise ValueError(f"Not a valid name: '{name}'.") raise ValueError(f"Not a valid name: '{name}'.")
self.first_name = first_name self.first_name = first_name
self.last_name = last_name self.last_name = last_name
self.display_name = display_name
if not User.is_valid_picture(picture): if display_name is not None:
raise ValueError(f"Not a valid image: '{picture}'.") if not User.is_valid_person_name(display_name):
self.picture = picture raise ValueError(f"Not a valid display name: '{display_name}'.")
self.display_name = display_name
else:
self.display_name = first_name
if picture is not None:
if not User.is_valid_picture(picture):
raise ValueError(f"Not a valid image: '{picture}'.")
self.picture = picture
else:
self.picture = User._get_default_picture()
def __eq__(self, other): def __eq__(self, other):