# pylint: disable = too-many-lines
"""SQLAlchemy-powered model definitions for users."""
from datetime import datetime, date
from bcrypt import hashpw, gensalt
import colander
from sqlalchemy import Column, Integer, ForeignKey
from sqlalchemy import String, Enum, Date, DateTime, Boolean
from sqlalchemy.orm import relationship
from lxml import etree
from pytz import common_timezones
from ..lib.i18n import _
from ..lib.config import settings_get_list
from ..lib.utils import make_id, normalize_spaces, age
from ..lib.mailing import Mailing
from ..lib.form import SameAs
from ..lib.paging import PAGE_SIZES
from ..lib.attachment import attachment_url
from ..helpers.literal import Literal
from ..helpers.builder import Builder
from . import DBDeclarativeClass, ID_LEN, NAME_LEN, LABEL_LEN, EMAIL_LEN
from .dbbase import DBBaseClass
from .dbprofile import DBProfile, DBProfilePrincipal
USER_STATUS_LABELS = {
'administrator': _('administrator'), 'active': _('active'),
'locked': _('locked'), 'inactive': _('inactive')}
USER_HONORIFIC_LABELS = {'Mr': _('Mr'), 'Mrs': _('Mrs')}
# =============================================================================
[docs]
class DBUser(DBDeclarativeClass, DBBaseClass): # type: ignore
"""SQLAlchemy-powered user model."""
status_labels = USER_STATUS_LABELS
honorific_labels = USER_HONORIFIC_LABELS
suffix = 'ciousr'
attachments_dir = 'Users'
_settings_tabs = (
_('Information'), _('Preferences'), _('Profiles'), _('Groups'))
__tablename__ = 'users'
__table_args__ = {'mysql_engine': 'InnoDB'}
user_id = Column(Integer, autoincrement=True, primary_key=True)
login = Column(String(EMAIL_LEN), unique=True, index=True, nullable=False)
status = Column(
Enum(*USER_STATUS_LABELS.keys(), name='status'), default='active')
password = Column(String(64), nullable=False)
password_update = Column(DateTime)
password_mustchange = Column(
Boolean(name='password_mustchange'), default=False)
first_name = Column(String(NAME_LEN))
last_name = Column(String(NAME_LEN), nullable=False)
honorific = Column(
Enum(*USER_HONORIFIC_LABELS.keys(), name='honorific'))
email = Column(String(EMAIL_LEN), nullable=False)
email_hidden = Column(Boolean(name='email_hidden'), default=False)
language = Column(String(5))
time_zone = Column(String(48))
theme = Column(String(ID_LEN))
home = Column(String(128))
page_size = Column(Integer)
attachments_key = Column(String(ID_LEN + 20))
picture = Column(String(ID_LEN + 4))
expiration = Column(Date)
last_login = Column(DateTime)
account_update = Column(DateTime)
account_creation = Column(DateTime, default=datetime.now)
authority = Column(String(ID_LEN))
authority_check = Column(DateTime)
profiles = relationship(DBProfile, secondary='users_profiles')
groups = relationship('DBGroup', secondary='groups_users', viewonly=True)
# -------------------------------------------------------------------------
[docs]
def set_password(self, password):
"""Set the password, possibly hashing it.
:param str password:
Password to set. If it does not begin with ``$``, we use bcrypt
algorithm before setting.
"""
if not password.startswith('$'):
self.password = hashpw(
password.encode('utf8'), gensalt()).decode('utf8')
else:
self.password = password
self.password_update = datetime.now()
# -------------------------------------------------------------------------
[docs]
def check_password(self, password):
"""Check the validy of the given password.
:param str password:
Clear password to check.
:rtype: bool
"""
if password and self.password is not None:
expected = self.password.encode('utf8')
return expected == hashpw(password.encode('utf8'), expected)
return False
# -------------------------------------------------------------------------
[docs]
@classmethod
def get(cls, request, login=None, password=None):
"""Retrieve a user using ``login`` and ``password`` or the content of
``request.params``.
:type request: pyramid.request.Request
:param request:
Current request.
:param str login: (optional)
Login of the user to authenticate. If it is ``None``, we try to
find it in ``request.params``.
:param str password: (optional)
Clear password. If it is ``None``, we try to find it in
``request.params``.
:rtype: tuple
:return:
A tuple like ``(dbuser, error)`` where ``dbuser`` is a DBUser
object representing the authenticated user or ``None`` and
``error`` is an error message.
If ``password`` is ``None`` and not in ``request.params``, password
checking is not performed.
If the user is authenticated, it updates ``last_login`` field in
database.
"""
# pylint: disable = too-many-return-statements
login = login or request.params.get('login')
if not login:
return None, _('ID or password is incorrect.')
password = password or request.params.get('password')
dbuser = request.dbsession.query(cls).filter_by(
login=make_id(login, 'token', EMAIL_LEN)).first()
if dbuser is None:
if 'authorities' in request.registry:
for authority in request.registry['authorities'].values():
dbuser, error = authority.get(
request, login, password, cls)
if dbuser is not None or error is not None:
return dbuser, error
return None, _('ID or password is incorrect.')
if dbuser.authority:
if 'authorities' not in request.registry or \
dbuser.authority not in request.registry['authorities']:
return None, _('Authority not available.')
error = request.registry['authorities'][
dbuser.authority].check(request, login, password, dbuser)
if error:
return None, error
password = None
if password is not None and not dbuser.check_password(password):
return None, _('ID or password is incorrect.')
if dbuser.status == 'locked':
return None, _('Your account is locked.')
if dbuser.status not in ('administrator', 'active'):
return None, _('Your account is not active.')
if dbuser.status != 'administrator' and dbuser.expiration \
and dbuser.expiration < date.today():
return None, _('Your account has expired.')
dbuser.last_login = datetime.now()
return dbuser, None
# -------------------------------------------------------------------------
[docs]
def set_session(self, request):
"""Set up user session (``session['user']``).
:type request: pyramid.request.Request
:param request:
Current request.
It saves in session the following values:
* ``lang``: user language
* ``theme``: user theme
* ``home``: route of home page
* ``user``: user dictionary
The user dictionary includes:
* ``user_id``: user ID
* ``login``: user login
* ``email``: user e-mail
* ``name``: user first name and last name
* ``attachment``: attachment ID
* ``principals``: list of principals (premission groups) (see
":ref:`Pyramid Security <pyramid:security_chapter>`" for more
information)
"""
# Language
settings = request.registry.settings
if self.language:
langs = settings_get_list(settings, 'languages', ('en',))
request.session['lang'] = \
(self.language in langs and self.language) or \
(self.language[0:2] in langs and self.language[0:2]) or \
request.registry['settings']['language']
# Theme
if self.theme and self.theme in request.registry['themes']:
request.session['theme'] = self.theme
else:
request.session['theme'] = request.registry['settings']['theme']
# Home
if self.home and self.home in request.registry['homes']:
request.session['home'] = self.home
elif request.registry['settings']['home'] in request.registry['homes']:
request.session['home'] = request.registry['settings']['home']
else:
request.session['home'] = 'home'
# Groups
groups = set()
principals = set()
for dbgroup in self.groups:
groups.add(dbgroup.group_id)
for dbprofile in dbgroup.profiles:
principals |= {k.principal for k in dbprofile.principals}
groups = tuple(groups)
# Principals
if self.status == 'administrator':
principals = ('system.administrator',)
else:
principals |= {
k.principal for k in
request.dbsession.query(DBProfilePrincipal)
.join((DBProfile, DBUser.profiles))
.join((DBProfilePrincipal, DBProfile.principals))
.filter(DBUser.user_id == self.user_id)}
principals = tuple(principals)
# User information
request.session['user'] = {
'user_id': self.user_id,
'login': self.login,
'email': self.email,
'name': '{0} {1}'.format(
self.first_name or '', self.last_name).strip(),
'picture': attachment_url(
request, 'Users', self.attachments_key, self.picture),
'principals': principals,
'groups': groups}
# Reset paging
request.session['paging'] = (
self.page_size if self.page_size else
request.registry['settings']['page-size'], {})
# Reset menu (DEPRECATED)
if 'menu' in request.session:
del request.session['menu']
# -------------------------------------------------------------------------
[docs]
@classmethod
def load_administrator(cls, dbsession, record):
"""Load the administrator user from INI configuration file.
:type dbsession: sqlalchemy.orm.session.Session
:param dbsession:
SQLAlchemy session.
:param dict record:
Dictionary representing the administrator configuration.
:rtype: ``None`` or :class:`pyramid.i18n.TranslationString`
:return:
``None`` or error message.
"""
# Check user existence
if not record.get('login'):
return _('Login is missing.')
login = make_id(record['login'], 'token', EMAIL_LEN)
dbuser = dbsession.query(cls.login).filter_by(login=login).first()
if dbuser is not None:
return None
# Complete record
record['status'] = 'administrator'
record['email_hidden'] = True
# Check final record and create user
error = cls.record_format(record)
if error:
return error
dbuser = cls(**record)
dbsession.add(dbuser)
return None
# -------------------------------------------------------------------------
[docs]
@classmethod
def xml2db(cls, dbsession, user_elt, error_if_exists=True, kwargs=None):
"""Load a user from a XML element.
:type dbsession: sqlalchemy.orm.session.Session
:param dbsession:
SQLAlchemy session.
:type user_elt: lxml.etree.Element
:param user_elt:
User XML element.
:param bool error_if_exists: (default=True)
It returns an error if user already exists.
:param dict kwargs: (optional)
Dictionary of keyword arguments with the key ``'profiles'``.
:rtype: :class:`pyramid.i18n.TranslationString` or ``None``
:return:
Error message or ``None``.
"""
# Check if already exists
login = make_id(user_elt.findtext('login'), 'token', EMAIL_LEN)
dbuser = dbsession.query(cls).filter_by(login=login).first()
if dbuser is not None:
if error_if_exists:
return _('User "${l}" already exists.', {'l': login})
return None
# Create user
record = cls.record_from_xml(login, user_elt)
error = cls.record_format(record)
if error:
return error
dbuser = cls(**record)
dbsession.add(dbuser)
# Fill extra tables
return dbuser.xml2db_extra(dbsession, user_elt, kwargs)
# -------------------------------------------------------------------------
# -------------------------------------------------------------------------
[docs]
@classmethod
def record_from_xml(cls, login, user_elt):
"""Convert an user XML element into a dictionary.
:param str login:
User login.
:type user_elt: lxml.etree.Element
:param user_elt:
User XML element.
:rtype: dict
"""
password_elt = user_elt.find('password')
email_elt = user_elt.find('email')
attachments_elt = user_elt.find('attachments')
record = {
'login': login,
'status': user_elt.get('status', 'active'),
'password': password_elt is not None and password_elt.text,
'password_update':
password_elt is not None and password_elt.get('updated'),
'password_mustchange':
password_elt is not None and
password_elt.get('must-change') == 'true' or False,
'first_name': user_elt.findtext('firstname'),
'last_name': user_elt.findtext('lastname'),
'honorific': user_elt.findtext('honorific'),
'email': email_elt is not None and email_elt.text,
'email_hidden': email_elt is not None and email_elt.get(
'hidden') == 'true' or False,
'language': user_elt.findtext('language'),
'time_zone': user_elt.findtext('timezone'),
'theme': user_elt.findtext('theme'),
'home': user_elt.findtext('home'),
'attachments_key':
attachments_elt is not None and attachments_elt.get('key') or None,
'picture':
attachments_elt is not None and attachments_elt.findtext(
'picture') or None,
'page_size': user_elt.findtext('page-size'),
'expiration': user_elt.findtext('expiration'),
'last_login': user_elt.get('last-login'),
'account_update': user_elt.get('updated'),
'account_creation': user_elt.get('created'),
'authority': user_elt.findtext('authority'),
'authority_check': user_elt.find('authority').get(
'checked') if user_elt.find('authority') is not None else None}
return record
# -------------------------------------------------------------------------
# -------------------------------------------------------------------------
[docs]
@classmethod
def record_convert_dates(cls, record):
"""Possibly convert dates of a record.
:param dict record:
Dictionary of values to check.
"""
if 'last_login' in record and \
not isinstance(record['last_login'], datetime):
record['last_login'] = datetime.strptime(
record['last_login'], '%Y-%m-%dT%H:%M:%S')
if 'password_update' in record and \
not isinstance(record['password_update'], datetime):
record['password_update'] = datetime.strptime(
record['password_update'], '%Y-%m-%dT%H:%M:%S')
if 'expiration' in record and \
not isinstance(record['expiration'], (date, datetime)):
record['expiration'] = datetime.strptime(
record['expiration'], '%Y-%m-%d').date()
if 'account_creation' in record and \
not isinstance(record['account_creation'], datetime):
record['account_creation'] = datetime.strptime(
record['account_creation'], '%Y-%m-%dT%H:%M:%S')
if 'account_update' in record and \
not isinstance(record['account_update'], datetime):
record['account_update'] = datetime.strptime(
record['account_update'], '%Y-%m-%dT%H:%M:%S')
if 'authority_check' in record and \
not isinstance(record['authority_check'], datetime):
record['authority_check'] = datetime.strptime(
record['authority_check'], '%Y-%m-%dT%H:%M:%S')
# -------------------------------------------------------------------------
[docs]
def db2xml(self, dbsession=None): # noqa
"""Serialize a user to a XML representation.
:type dbsession: sqlalchemy.orm.session.Session
:param dbsession: (optional)
SQLAlchemy session.
:rtype: lxml.etree.Element
"""
# pylint: disable = too-many-branches, unused-argument
user_elt = etree.Element('user')
if self.status != 'active':
user_elt.set('status', self.status)
user_elt.set(
'created', self.account_creation.isoformat().partition('.')[0])
if self.account_update and \
self.account_update != self.account_creation:
user_elt.set(
'updated', self.account_update.isoformat().partition('.')[0])
if self.last_login:
user_elt.set(
'last-login', self.last_login.isoformat().partition('.')[0])
etree.SubElement(user_elt, 'login').text = self.login
elt = etree.SubElement(user_elt, 'password')
elt.text = self.password
if self.password_update:
elt.set(
'updated', self.password_update.isoformat().partition('.')[0])
if self.password_mustchange:
elt.set('must-change', 'true')
if self.first_name:
etree.SubElement(user_elt, 'firstname').text = self.first_name
etree.SubElement(user_elt, 'lastname').text = self.last_name
if self.honorific:
etree.SubElement(user_elt, 'honorific').text = self.honorific
elt = etree.SubElement(user_elt, 'email')
elt.text = self.email
if self.email_hidden:
elt.set('hidden', 'true')
if self.language:
etree.SubElement(user_elt, 'language').text = self.language
if self.time_zone:
etree.SubElement(user_elt, 'timezone').text = self.time_zone
if self.theme:
etree.SubElement(user_elt, 'theme').text = self.theme
if self.home:
etree.SubElement(user_elt, 'home').text = self.home
if self.attachments_key:
elt = etree.SubElement(
user_elt, 'attachments', key=self.attachments_key)
if self.picture:
etree.SubElement(elt, 'picture').text = self.picture
if self.page_size:
etree.SubElement(user_elt, 'page-size').text = str(self.page_size)
if self.expiration:
etree.SubElement(user_elt, 'expiration').text = \
self.expiration.isoformat()
if self.authority:
elt = etree.SubElement(user_elt, 'authority')
elt.text = self.authority
if self.authority_check:
elt.set(
'checked',
self.authority_check.isoformat().partition('.')[0])
self.db2xml_extra(user_elt)
return user_elt
# -------------------------------------------------------------------------
# -------------------------------------------------------------------------
[docs]
@classmethod
def paging_filter(cls, request, form, user_filter, user_paging):
"""Filter for users.
:type request: pyramid.request.Request
:param request:
Current request.
:type form: .lib.form.Form
:param form:
Current form object.
:type user_filter: .lib.filter.Filter
:param user_filter:
Filter for users.
:type user_paging: .lib.paging.Paging
:param user_paging:
Paging for users.
:rtype: str
"""
translate = request.localizer.translate
html = '<div class="cioPagingPagerFilter">\n'
html += '<div class="cioPagingPager cioFlexItem">'\
'{page_size} {pager}</div>\n'.format(
page_size=form.select(
'page_size', '', PAGE_SIZES, True,
title=translate(_('Lines per page'))),
pager=user_paging.pager_top())
html += '<div class="cioPagingFilter">'
if not user_filter.is_empty():
html += '<span class="cioFilterConditions">{0}</span>'.format(
user_filter.html_filter())
html += '{inputs} <span class="cioFilterSubmit">{submit}</span>'\
'</div>\n'.format(
inputs=user_filter.html_inputs(form),
submit=form.submit(
'filter', translate(_('Filter')),
class_='cioFilterButton'))
html += '</div>\n'
return html
# -------------------------------------------------------------------------
[docs]
def tab4view(self, request, tab_index, form):
"""Generate the tab content of user account.
:type request: pyramid.request.Request
:param request:
Current request.
:param int index:
Index of the tab.
:type form: .lib.form.Form
:param form:
Current form object.
:rtype: helpers.literal.Literal
"""
if tab_index == 0:
return self._tab4view_information(request, form)
if tab_index == 1:
return self._tab4view_preferences(request, form)
if tab_index == 2:
return self._tab4view_profiles(request)
if tab_index == 3:
return self._tab4view_groups(request)
return ''
# -------------------------------------------------------------------------
def _tab4view_information(self, request, form):
"""Generate the information tab.
:type request: pyramid.request.Request
:param request:
Current request.
:type form: .lib.form.Form
:param form:
Current form object.
:rtype: helpers.literal.Literal
"""
# Identify
i_editor = request.has_permission('user-edit') or \
self.user_id == request.session['user']['user_id']
translate = request.localizer.translate
html = Builder().h3(translate(_('Identity')))
html += form.grid_item(translate(_('Login:')), self.login, clear=True)
html += form.grid_item(
translate(_('Title:')),
translate(USER_HONORIFIC_LABELS.get(self.honorific))
if self.honorific else None, clear=True)
html += form.grid_item(
translate(_('First Name:')), self.first_name, clear=True)
html += form.grid_item(
translate(_('Last Name:')), self.last_name, clear=True)
if not self.email_hidden or i_editor:
html += form.grid_item(
translate(_('Email:')), self.email, clear=True)
html += form.grid_item(
translate(_('Language:')), self.language, clear=True)
html += form.grid_item(
translate(_('Time Zone:')), self.time_zone, clear=True)
# Security
html += Builder().h3(translate(_('Security')))
html += form.grid_item(
translate(_('Authority:')), self.authority, clear=True)
html += form.grid_item(
translate(_('Status:')),
translate(self.status_labels[self.status]), clear=True)
html += form.grid_item(
translate(_('Creation:')), translate(age(self.account_creation)),
title=self.account_creation.isoformat(' ').partition('.')[0],
clear=True)
html += form.grid_item(
translate(_('Password:')),
self.password_mustchange and translate(
_('changed during next connexion')), clear=True)
if self.account_update:
html += form.grid_item(
translate(_('Update:')), translate(age(self.account_update)),
title=self.account_update.isoformat(' ').partition('.')[0],
clear=True)
if self.expiration:
html += form.grid_item(
translate(_('Expiration:')), self.expiration.isoformat(),
clear=True)
if self.last_login:
html += form.grid_item(
translate(_('Last connection:')),
translate(age(self.last_login)),
title=self.last_login.isoformat(' ').partition('.')[0],
clear=True)
return html
# -------------------------------------------------------------------------
def _tab4view_preferences(self, request, form):
"""Generate the preferences tab.
:type request: pyramid.request.Request
:param request:
Current request.
:type form: .lib.form.Form
:param form:
Current form object.
:rtype: helpers.literal.Literal
"""
translate = request.localizer.translate
html = ''
html += form.grid_item(
translate(_('Hide e-mail:')),
self.email_hidden and translate(_('yes')), clear=True)
lang = request.session.get('lang', 'en')
theme = request.registry['themes'].get(self.theme)
theme = theme['name'].get(lang, self.theme) if theme else ''
home = request.route_path(self.home) \
if self.home in request.registry['homes'] else ''
html += form.grid_item(translate(_('Theme:')), theme, clear=True)
html += form.grid_item(translate(_('Home:')), home, clear=True)
if self.page_size:
html += form.grid_item(
translate(_('Lines per page:')), str(self.page_size),
clear=True)
return html or _('No preferences.')
# -------------------------------------------------------------------------
def _tab4view_profiles(self, request):
"""Generate the profiles tab.
:type request: pyramid.request.Request
:param request:
Current request.
:rtype: helpers.literal.Literal
"""
translate = request.localizer.translate
if not self.profiles:
return translate(_('No attributed profile.'))
html = '<table>\n<thead>\n'\
'<tr><th>{label}</th><th>{description}</th></tr>\n'\
'</thead>\n<tbody>\n'.format(
label=translate(_('Label')),
description=translate(_('Description')))
for dbprofile in sorted(self.profiles, key=lambda k: k.profile_id):
html += \
'<tr><th><a href="{profile_view}">{label}</a></th>'\
'<td>{description}</td></tr>\n'.format(
profile_view=request.route_path(
'profile_view', profile_id=dbprofile.profile_id),
label=dbprofile.label(request),
description=dbprofile.description(request))
html += '</tbody>\n</table>\n'
return Literal(html)
# -------------------------------------------------------------------------
def _tab4view_groups(self, request):
"""Generate the groups tab.
:type request: pyramid.request.Request
:param request:
Current request.
:rtype: helpers.literal.Literal
"""
translate = request.localizer.translate
if not self.groups:
return translate(_('This user is in any group.'))
html = '<table>\n<thead>\n'\
'<tr><th>{label}</th><th>{description}</th></tr>\n'\
'</thead>\n<tbody>\n'.format(
label=translate(_('Label')),
description=translate(_('Description')))
for dbgroup in sorted(self.groups, key=lambda k: k.group_id):
html += \
'<tr><th><a href="{group_view}">{label}</a></th>'\
'<td>{description}</td></tr>\n'.format(
group_view=request.route_path(
'group_view', group_id=dbgroup.group_id),
label=dbgroup.label(request),
description=dbgroup.description(request))
html += '</tbody>\n</table>\n'
return Literal(html)
# -------------------------------------------------------------------------
[docs]
@classmethod
def settings_schema(cls, request, profiles, groups, dbuser=None):
"""Return a Colander schema to edit user account.
:type request: pyramid.request.Request
:param request:
Current request.
:param dict profiles:
A dictionary such as ``{profile_id: (label, description),...}``.
:param dict groups:
A dictionary such as ``{group_id: (label, description),...}``.
:type dbuser: DBUser
:param dbuser: (optional)
Current user SqlAlchemy object.
:rtype: tuple
:return:
A tuple such as ``(schema, defaults)``.
"""
# Identity
schema = colander.SchemaNode(colander.Mapping())
schema.add(colander.SchemaNode(
colander.String(), name='login',
validator=colander.All(
colander.Regex(r'^[a-zA-Z0-9._%@-]+$'),
colander.Length(min=2, max=EMAIL_LEN))))
schema.add(colander.SchemaNode(
colander.String(), name='honorific',
validator=colander.OneOf(
cls.honorific.property.columns[0].type.enums), missing=None))
schema.add(colander.SchemaNode(
colander.String(), name='first_name',
validator=colander.Length(max=NAME_LEN), missing=None))
schema.add(colander.SchemaNode(
colander.String(), name='last_name',
validator=colander.Length(max=NAME_LEN)))
schema.add(colander.SchemaNode(
colander.String(), name='email',
validator=colander.All(
colander.Email(), colander.Length(max=LABEL_LEN))))
schema.add(colander.SchemaNode(
colander.String(), name='language',
validator=colander.OneOf(settings_get_list(
request.registry.settings, 'languages', ['en'])),
missing=None))
schema.add(colander.SchemaNode(
colander.String(), name='time_zone',
validator=colander.OneOf(common_timezones),
missing=None))
# Security
i_admin = request.has_permission('system.administrator')
if 'authorities' in request.registry and i_admin:
schema.add(colander.SchemaNode(
colander.String(), name='authority',
validator=colander.All(
colander.Regex(r'^[0-9a-z-]+$'),
colander.Length(max=ID_LEN)),
missing=None))
if dbuser is None or (
dbuser.user_id != request.session['user']['user_id'] and
(dbuser.status != 'administrator' or i_admin) and
request.has_permission('user-edit')):
status = list(USER_STATUS_LABELS.keys())
if not i_admin:
del status[status.index('administrator')]
schema.add(colander.SchemaNode(
colander.String(), name='status',
validator=colander.OneOf(status)))
password_min_length = request.registry['settings'][
'password-min-length']
if dbuser is None:
schema.add(colander.SchemaNode(
colander.String(), name='password1',
validator=colander.Length(min=password_min_length)))
schema.add(colander.SchemaNode(
colander.String(), name='password2',
validator=SameAs(request, 'password1', _(
'The two passwords are not identical.'))))
else:
schema.add(colander.SchemaNode(
colander.String(), name='password1', missing=None,
validator=colander.Length(min=password_min_length)))
schema.add(colander.SchemaNode(
colander.String(), name='password2', missing=None,
validator=SameAs(request, 'password1', _(
'The two passwords are not identical.'))))
schema.add(colander.SchemaNode(
colander.Boolean(), name='password_mustchange', missing=False))
# Preferences
schema.add(colander.SchemaNode(
colander.Boolean(), name='email_hidden', missing=False))
if len(request.registry['themes']) > 1:
schema.add(colander.SchemaNode(
colander.String(), name='theme',
validator=colander.OneOf(request.registry['themes']),
missing=None))
if len(request.registry['homes']) > 1:
schema.add(colander.SchemaNode(
colander.String(), name='home',
validator=colander.OneOf(request.registry['homes']),
missing=None))
schema.add(colander.SchemaNode(
colander.Integer(), name='page_size',
validator=colander.OneOf(PAGE_SIZES[1:-1]), missing=None))
if request.has_permission('user-create'):
cls._settings_schema_creator(profiles, groups, schema)
# Defaults
if dbuser is None:
defaults = {'status': 'active'}
else:
defaults = {}
for dbprofile in dbuser.profiles:
defaults['pfl:{0}'.format(dbprofile.profile_id)] = True
for dbgroup in dbuser.groups:
defaults['grp:{0}'.format(dbgroup.group_id)] = True
return schema, defaults
# -------------------------------------------------------------------------
@classmethod
def _settings_schema_creator(cls, profiles, groups, schema):
"""Complete the Colander schema if I am a creator.
:param dict profiles:
A dictionary such as ``{profile_id: (label, description),...}``.
:param dict groups:
A dictionary such as ``{group_id: (label, description),...}``.
:type schema: colander.SchemaNode
:param schema:
Current schema to complete.
"""
# Expiration
schema.add(colander.SchemaNode(
colander.Date(), name='expiration', missing=None))
# Profiles
for profile_id in profiles:
schema.add(colander.SchemaNode(
colander.Boolean(), name='pfl:{0}'.format(profile_id),
missing=False))
# Groups
for group_id in groups:
schema.add(colander.SchemaNode(
colander.Boolean(), name='grp:{0}'.format(group_id),
missing=False))
# -------------------------------------------------------------------------
[docs]
@classmethod
def tab4edit(cls, request, tab_index, form, profiles, groups, dbuser=None):
"""Generate the tab content of user account for edition.
:type request: pyramid.request.Request
:param request:
Current request.
:param int tab_index:
Index of the tab.
:type form: .lib.form.Form
:param form:
Current form object.
:param dict profiles:
A dictionary such as ``{profile_id: (label, description),...}``.
:param dict groups:
A dictionary such as ``{group_id: (label, description),...}``.
:type dbuser: DBUser
:param dbuser: (optional)
Current user SqlAlchemy object.
:rtype: helpers.literal.Literal
"""
# pylint: disable = too-many-arguments, too-many-positional-arguments
if tab_index == 0:
return cls._tab4edit_information(request, form, dbuser)
if tab_index == 1:
return cls._tab4edit_preferences(request, form)
if tab_index == 2:
return cls._tab4edit_profiles(request, form, profiles)
if tab_index == 3:
return cls._tab4edit_groups(request, form, groups)
return ''
# -------------------------------------------------------------------------
@classmethod
def _tab4edit_information(cls, request, form, dbuser):
"""Generate the information tab for edition.
:type request: pyramid.request.Request
:param request:
Current request.
:type form: .lib.form.Form
:param form:
Current form object.
:type dbuser: DBUser
:param dbuser:
Current user SqlAlchemy object.
:rtype: helpers.literal.Literal
"""
# Identify
i_admin = request.has_permission('system.administrator')
has_authority = dbuser is not None and dbuser.authority is not None
translate = request.localizer.translate
html = Builder().h3(translate(_('Identity')))
if has_authority and not i_admin:
html += form.grid_item(
translate(_('Login:')), dbuser.login, clear=True)
html += form.hidden('login', dbuser.login)
else:
html += form.grid_text(
'login', translate(_('Login:')), maxlength=EMAIL_LEN,
required=True, clear=True)
html += form.grid_select(
'honorific', translate(_('Title:')),
[('', ' ')] + list(cls.honorific_labels.items()), clear=True)
html += form.grid_text(
'first_name', translate(_('First Name:')), maxlength=NAME_LEN,
clear=True)
html += form.grid_text(
'last_name', translate(_('Last Name:')), maxlength=NAME_LEN,
required=True, clear=True)
html += form.grid_text(
'email', translate(_('Email:')), maxlength=EMAIL_LEN,
required=True, clear=True)
html += form.grid_select(
'language', translate(_('Language:')),
[('', ' ')] + sorted(settings_get_list(
request.registry.settings, 'languages', ['en'])), clear=True)
html += form.grid_select(
'time_zone', translate(_('Time Zone:')),
[('', ' ')] + list(common_timezones), clear=True)
# Security
html += Builder().h3(translate(_('Security')))
if 'authorities' in request.registry and \
request.registry['authorities']:
if i_admin:
html += form.grid_select(
'authority', translate(_('Authority:')),
[('', ' ')] + sorted(request.registry['authorities']),
clear=True)
elif has_authority:
html += form.grid_item(
translate(_('Authority:')), dbuser.authority, clear=True)
if dbuser is None or (
dbuser.user_id != request.session['user']['user_id'] and
(dbuser.status != 'administrator' or i_admin) and
request.has_permission('user-edit')):
status = cls.status_labels.items() if i_admin else \
[k for k in cls.status_labels.items()
if k[0] != 'administrator']
html += form.grid_select(
'status', translate(_('Status:')), status, clear=True)
else:
html += form.grid_item(
translate(_('Status:')),
translate(dbuser.status_labels[dbuser.status]), clear=True)
if not has_authority or i_admin:
html += form.grid_password(
'password1', translate(_('Password:')), maxlength=64,
required=dbuser is None, clear=True)
html += form.grid_password(
'password2', translate(_('Confirmation:')), maxlength=64,
required=dbuser is None, clear=True)
html += form.grid_custom_checkbox(
'password_mustchange', translate(_('Force change:')),
clear=True)
if request.has_permission('user-create'):
html += form.grid_text(
'expiration', translate(_('Expiration:')), maxlength=10,
hint=translate(_('Format: YYYY-MM-DD')),
class_='cioFormItem cioDate', clear=True)
return html
# -------------------------------------------------------------------------
@classmethod
def _tab4edit_preferences(cls, request, form):
"""Generate the preferences tab for edition.
:type request: pyramid.request.Request
:param request:
Current request.
:type form: .lib.form.Form
:param form:
Current form object.
:rtype: helpers.literal.Literal
"""
translate = request.localizer.translate
html = form.grid_custom_checkbox(
'email_hidden', translate(_('Hide e-mail:')), clear=True)
if len(request.registry['themes']) > 1:
lang = request.session.get('lang', 'en')
themes = [(i, k['name'].get(lang, i))
for i, k in request.registry['themes'].items()]
html += form.grid_select(
'theme', translate(_('Theme:')),
[('', ' ')] + sorted(themes, key=lambda k: k[1]), clear=True)
if len(request.registry['homes']) > 1:
html += form.grid_select(
'home',
translate(_('Home page:')),
[('', ' ')] +
[(k, request.route_path(k))
for k in sorted(request.registry['homes'])],
clear=True)
html += form.grid_select(
'page_size', translate(_('Lines per page:')), PAGE_SIZES[:-1],
clear=True)
return html
# -------------------------------------------------------------------------
@classmethod
def _tab4edit_profiles(cls, request, form, profiles):
"""Generate the profiles tab for edition.
:type request: pyramid.request.Request
:param request:
Current request.
:type form: .lib.form.Form
:param form:
Current form object.
:param dict profiles:
A dictionary such as ``{profile_id: (label, description),...}``.
:rtype: helpers.literal.Literal
"""
translate = request.localizer.translate
if not profiles:
return translate(_('No available profile.'))
if not request.has_permission('user-create'):
return translate(
_('You do not have the rigths to edit profiles.'))
html = '<table>\n<thead>\n'\
'<tr><th></th><th>{label}</th>'\
'<th>{description}</th></tr>\n</thead>\n<tbody>\n'.format(
label=translate(_('Label')),
description=translate(_('Description')))
for profile_id in sorted(profiles):
html += \
'<tr><td>{selected}</td>'\
'<th><label for="{id}">{label}</label></th>'\
'<td>{description}</td></tr>\n'.format(
selected=form.custom_checkbox(
'pfl:{0}'.format(profile_id)),
id='pfl{0}'.format(profile_id),
label=profiles[profile_id][0],
description=profiles[profile_id][1])
html += '</tbody>\n</table>\n'
return Literal(html)
# -------------------------------------------------------------------------
@classmethod
def _tab4edit_groups(cls, request, form, groups):
"""Generate the groups tab for edition.
:type request: pyramid.request.Request
:param request:
Current request.
:type form: .lib.form.Form
:param form:
Current form object.
:param dict groups:
A dictionary such as ``{group_id: (label, description),...}``.
:rtype: helpers.literal.Literal
"""
translate = request.localizer.translate
if not groups:
return translate(_('No available group.'))
if not request.has_permission('user-create'):
return translate(
_('You do not have the rigths to edit groups.'))
html = '<table>\n<thead>\n'\
'<tr><th></th><th>{label}</th>'\
'<th>{description}</th></tr>\n</thead>\n<tbody>\n'.format(
label=translate(_('Label')),
description=translate(_('Description')))
for group_id in sorted(groups):
html += \
'<tr><td>{selected}</td>'\
'<th><label for="{id}">{label}</label></th>'\
'<td>{description}</td></tr>\n'.format(
selected=form.custom_checkbox('grp:{0}'.format(group_id)),
id='grp{0}'.format(group_id),
label=groups[group_id][0],
description=groups[group_id][1])
html += '</tbody>\n</table>\n'
return Literal(html)
# =============================================================================
[docs]
class DBUserProfile(DBDeclarativeClass): # type: ignore
"""Class to link users with their profiles (many-to-many)."""
# pylint: disable = too-few-public-methods
__tablename__ = 'users_profiles'
__table_args__ = {'mysql_engine': 'InnoDB'}
user_id = Column(
Integer, ForeignKey('users.user_id', ondelete='CASCADE'),
primary_key=True)
profile_id = Column(
String(ID_LEN), ForeignKey('profiles.profile_id', ondelete='CASCADE'),
primary_key=True)