Source code for pluserable.data.models
"""Base model classes for any backend (SQLAlchemy, ZODB etc.)."""
from datetime import datetime, timedelta
from typing import Optional
from bag.text.hash import random_hash
from bag.web import gravatar_image
# import cryptacular.bcrypt
from passlib.context import CryptContext
# crypt = cryptacular.bcrypt.BCRYPTPasswordManager()
pass_context = CryptContext(schemes=["bcrypt"])
[docs]def thirty_days_from_now(now: Optional[datetime] = None) -> datetime:
"""Return a datetime pointing to exactly 30 days in the future."""
now = now or datetime.utcnow()
return now + timedelta(days=30)
[docs]class ActivationBase:
"""Handles activations and password reset items for users.
``code`` is a random hash that is valid only once.
Once the hash is used to access the site, it is removed.
``valid_until`` is a datetime until when the activation key will last.
``created_by`` is a system: new user registration, password reset,
forgot password etc.
"""
def __init__(
self,
code: str = "",
valid_until: Optional[datetime] = None,
created_by: str = "web",
):
"""Usually call with the ``created_by`` system, or no arguments."""
self.code = code or random_hash()
self.valid_until = valid_until or thirty_days_from_now()
assert isinstance(self.valid_until, datetime)
self.created_by = created_by
[docs]class UserBase:
"""Base class for a User model."""
def __init__(
self, email: str, password: str, salt: str = "", activation=None, **kw
): # noqa
# print('User constructor: {} / {} / {} / {}'.format(
# email, password, salt, activation))
self.email = email
assert self.email and isinstance(self.email, str)
self.salt = salt or random_hash(24)
self.password = password
assert self.password and isinstance(self.password, str)
self.activation = activation
for k, v in kw.items():
setattr(self, k, v)
def __repr__(self):
return "<{}: {}>".format(self.__class__.__name__, self.email)
[docs] def gravatar_url(self, default="mm", size=80, cacheable=True): # no cover
"""Return a Gravatar image URL for this user."""
return gravatar_image( # pragma: no cover
self.email, default=default, size=size, cacheable=cacheable
)
@property
def password(self):
"""Set the password, or retrieve the password hash."""
return self._password
@password.setter
def password(self, value):
if value == "Please bypass hashing!": # for unit tests
self._password = value
else:
self._password = self._hash_password(value)
def _hash_password(self, password):
assert self.salt, (
"UserBase constructor was not called; "
"you probably have your User base classes in the wrong order."
)
# return str(crypt.encode(password + self.salt))
return pass_context.hash(password + self.salt)
[docs] @classmethod
def generate_random_password(cls, chars=12): # pragma: no cover
"""Generate random string of fixed length.
This method is not used in the pluserable system, but offered anyway.
"""
return random_hash(chars)
[docs] def check_password(self, password: str) -> bool:
"""Check the ``password`` and return a boolean."""
if not password:
return False
# return crypt.check(self.password, password + self.salt)
return pass_context.verify(password + self.salt, self.password)
@property
def is_activated(self):
"""Return False if this user needs to confirm her email address."""
return self.activation is None
# @property
# def __acl__(self):
# return [
# (Allow, f"u:{self.id}", 'access_user')
# ]
[docs]class GroupBase:
"""Base class for a Group model."""
def __repr__(self):
return "<{}: {}>".format(self.__class__.__name__, self.name)