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)