"""Colander schemas."""
# Standard Library
import typing as t
# Pyramid
import colander as c
import deform
import deform.widget as w
# Websauna
from websauna.system.form.schema import CSRFSchema
from websauna.system.form.sqlalchemy import UUIDModelSet
from websauna.system.form.sqlalchemy import convert_query_to_tuples
from websauna.system.user.interfaces import IGroupModel
from websauna.system.user.utils import get_group_class
from websauna.system.user.utils import get_user_class
from websauna.system.user.utils import get_user_registry
from websauna.utils.slug import uuid_to_slug
#: Todo change to setting, convert to deferred widget
PASSWORD_MIN_LENGTH = 6
[docs]def defer_group_class(node: c.SchemaNode, kw: dict) -> t.Type[IGroupModel]:
"""Colander helper deferred to assign the current group model.
:param node: Colander SchemaNode
:param kw: Keyword arguments.
:return: IGroupModel
"""
request = kw.get("request")
assert request, "To use this widget you must pass request to Colander schema.bind()"
return get_group_class(request.registry)
[docs]def group_vocabulary(node: c.SchemaNode, kw: dict) -> t.List[t.Tuple[str, str]]:
"""Convert all groups on the site to (uuid, name) tuples for checkbox and select widgets.
:param node: Colander SchemaNode
:param kw: Keyword arguments.
:return: List of tuples with uuid, name for all groups on the site.
"""
Group = defer_group_class(node, kw)
request = kw["request"]
def first_column_getter(group: Group):
return uuid_to_slug(group.uuid)
return convert_query_to_tuples(request.dbsession.query(Group).all(), first_column=first_column_getter, second_column="name")
[docs]def optional_group_vocabulary(node: c.SchemaNode, kw: dict) -> t.List[t.Tuple[str, str]]:
"""Convert all groups on the site to (uuid, name) tuples for checkbox and select widgets. Include a choice for no choice.
:param node: Colander SchemaNode
:param kw: Keyword arguments.
:return: List of tuples with uuid, name for all groups on the site.
"""
options = group_vocabulary(node, kw)
return [("", "[ not selected ]")] + options
[docs]def validate_unique_user_email(node: c.SchemaNode, value: str, **kwargs: dict):
"""Make sure we cannot enter the same username twice.
:param node: Colander SchemaNode.
:param value: Email address.
:param kwargs: Keyword arguments.
:raises: c.Invalid if email address already taken.
"""
request = node.bindings["request"]
dbsession = request.dbsession
User = get_user_class(request.registry)
value = value.strip()
if dbsession.query(User).filter_by(email=value).one_or_none():
raise c.Invalid(node, "Email address already taken")
[docs]def email_exists(node: c.SchemaNode, value: str):
"""Colander validator that ensures a User exists with the email.
:param node: Colander SchemaNode.
:param value: Email address.
:raises: c.Invalid if email is not registered for an User.
"""
request = node.bindings['request']
User = get_user_class(request.registry)
exists = request.dbsession.query(User).filter(User.email.ilike(value)).one_or_none()
if not exists:
raise c.Invalid(node, "Email does not exists: {email}".format(email=value))
[docs]class GroupSet(UUIDModelSet):
"""A set of Group objects referred by their uuid."""
def __init__(self, model: t.Optional[type] = None, match_column: t.Optional[str] = None, label_column: t.Optional[str] = None):
# We use *name* as default label_column for GroupSet
label_column = label_column if label_column else "name"
super().__init__(model, match_column, label_column)
[docs] def get_model(self, node: c.SchemaNode) -> t.Type[IGroupModel]:
"""Return Group class.
:param node: Colander SchemaNode.
:return: Class implementing IGroupModel.
"""
request = node.bindings["request"]
return get_group_class(request.registry)
[docs]class RegisterSchema(CSRFSchema):
"""Username-less registration form schema."""
email = c.SchemaNode(
c.String(),
title='Email',
validator=c.All(c.Email(), validate_unique_user_email),
widget=w.TextInputWidget(size=40, maxlength=260, type='email'))
password = c.SchemaNode(
c.String(),
validator=c.Length(min=PASSWORD_MIN_LENGTH),
widget=deform.widget.CheckedPasswordWidget(),
)
[docs]class LoginSchema(CSRFSchema):
"""Login form schema.
The user can log in both with email and his/her username, though we recommend using emails as users tend to forget their usernames.
"""
username = c.SchemaNode(c.String(), title='Email', validator=c.All(c.Email()), widget=w.TextInputWidget(size=40, maxlength=260, type='email'))
password = c.SchemaNode(c.String(), widget=deform.widget.PasswordWidget())
[docs]class ResetPasswordSchema(CSRFSchema):
"""Reset password schema."""
user = c.SchemaNode(
c.String(),
missing=c.null,
widget=deform.widget.TextInputWidget(template='readonly/textinput'))
password = c.SchemaNode(
c.String(),
validator=c.Length(min=2),
widget=deform.widget.CheckedPasswordWidget()
)
[docs]def validate_user_exists_with_email(node: c.SchemaNode, value: str):
"""Colander validator that ensures a User exists with the email.'
:param node: Colander SchemaNode.
:param value: Email address.
:raises: c.Invalid if email is not registered for an User.
"""
request = node.bindings['request']
user_registry = get_user_registry(request)
user = user_registry.get_by_email(value)
if not user:
raise c.Invalid(node, "Cannot reset password for such email: {email}".format(email=value))
[docs]class ForgotPasswordSchema(CSRFSchema):
"""Used on forgot password view."""
email = c.SchemaNode(
c.Str(),
title='Email',
validator=c.All(c.Email(), validate_user_exists_with_email),
widget=w.TextInputWidget(size=40, maxlength=260, type='email', template="textinput_placeholder"),
description="The email address under which you have your account. Example: [email protected]")