feat(usermodel): add default profile picture
This commit is contained in:
parent
65de54cfad
commit
8c1f8775c4
BIN
lumi2/static/assets/default_user_icon.jpg
Normal file
BIN
lumi2/static/assets/default_user_icon.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
94
lumi2/static/assets/default_user_icon.svg
Normal file
94
lumi2/static/assets/default_user_icon.svg
Normal 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 |
@ -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):
|
||||||
|
Loading…
x
Reference in New Issue
Block a user