"""SQLAlchemy-powered model definitions for settings."""
from __future__ import annotations
from os.path import basename
from datetime import datetime
from sqlalchemy import Column, String
from sqlalchemy.exc import OperationalError, ProgrammingError
from sqlalchemy.orm import Session
from lxml import etree
import colander
from pyramid.request import Request
from ..lib.i18n import _
from ..lib.utils import deltatime_label, size_label
from ..lib.config import settings_get_list
from ..lib.paging import PAGE_DEFAULT_SIZE, PAGE_SIZES
from ..lib.form import Form
from ..helpers.literal import Literal
from ..helpers.builder import Builder
from . import DBDeclarativeClass, ID_LEN, EMAIL_LEN, LABEL_LEN
from .dbbase import DBBaseClass
SETTINGS_DEFAULTS: dict[str, str | int] = {
'language': 'en',
'password-min-length': 8,
'remember-me': 5184000,
'page-size': PAGE_DEFAULT_SIZE,
'download-max-size': 10485760,
'clipboard-size': 10,
'theme': '',
'home': 'home'
}
# =============================================================================
[docs]
class DBSettings(DBDeclarativeClass, DBBaseClass): # type: ignore
"""SQLAlchemy-powered settings class."""
suffix = 'cioset'
settings_defaults = SETTINGS_DEFAULTS
_settings_tabs = (_('Parameters'), _('Information'))
__tablename__ = 'settings'
__table_args__ = {'mysql_engine': 'InnoDB'}
key = Column(String(ID_LEN), primary_key=True)
value = Column(String(128))
# -------------------------------------------------------------------------
[docs]
@classmethod
def exists(cls, dbsession):
"""Check if table ``settings`` exists and has at least one item.
:type dbsession: sqlalchemy.orm.session.Session
:param dbsession:
SQLAlchemy session (for testing).
:rtype: bool
"""
try:
setting = dbsession.query(cls).first()
except (OperationalError, ProgrammingError, UnicodeEncodeError):
return False
return setting is not None
# -------------------------------------------------------------------------
[docs]
@classmethod
def xml2db(cls, dbsession: Session, settings_elt: etree.Element):
"""Load settings from a XML element.
:type dbsession: sqlalchemy.orm.session.Session
:param dbsession:
SQLAlchemy session.
:type settings_elt: lxml.etree.Element
:param settings_elt:
Profile XML element.
"""
if settings_elt is None:
return
# Clean up old settings
for dbsetting in dbsession.query(cls):
dbsession.delete(dbsetting)
# Fill settings table
dbsession.add(
cls(
key='populate',
value=datetime.now().isoformat(' ').split('.')[0]))
for elt in settings_elt.iterchildren(tag=etree.Element):
dbsession.add(
cls(
key=elt.tag,
value=elt.text[:128] if elt.text is not None else None))
# -------------------------------------------------------------------------
[docs]
@classmethod
def db2xml(cls, dbsession: Session) -> etree.Element | None:
"""Serialize settings to a XML representation.
:type dbsession: sqlalchemy.orm.session.Session
:param dbsession:
SQLAlchemy session.
:rtype: lxml.etree.Element
"""
settings_elt = etree.Element('settings')
# Key/value from database
for dbsetting in dbsession.query(cls):
if dbsetting.key != 'populate':
etree.SubElement(settings_elt, dbsetting.key).text = \
dbsetting.value
return settings_elt if len(settings_elt) + 1 > 1 else None
# -------------------------------------------------------------------------
[docs]
@classmethod
def db2dict(
cls, settings: dict, dbsession: Session,
default_email: str) -> dict:
"""Return a dictionary containing settings.
:param dict settings:
Pyramid settings.
:type dbsession: sqlalchemy.orm.session.Session
:param dbsession:
SQLAlchemy session (for testing).
:param str default_email:
Default email for the site, usually the administrator email.
:rtype: dict
"""
try:
dbsettings = dbsession.query(cls).all()
except (OperationalError, ProgrammingError, UnicodeEncodeError):
dbsettings = []
# Default settings
settings_dict: dict[str, str | int] = cls.settings_defaults.copy()
if len(dbsettings) < 2:
settings_dict['title'] = str(
settings.get('site.title', settings['site.uid']))
settings_dict['email'] = default_email
if 'pyramid.default_locale_name' in settings:
settings_dict['language'] = settings[
'pyramid.default_locale_name']
settings_dict['populate'] = datetime.now().isoformat(' ').split(
'.')[0]
return settings_dict
# ... or customized settings
for dbsetting in dbsettings:
settings_dict[str(dbsetting.key)] = str(dbsetting.value)
settings_dict['password-min-length'] = int(
settings_dict['password-min-length'])
settings_dict['remember-me'] = int(settings_dict['remember-me'])
settings_dict['page-size'] = int(settings_dict['page-size'])
settings_dict['download-max-size'] = int(
settings_dict['download-max-size'])
settings_dict['clipboard-size'] = int(settings_dict['clipboard-size'])
if 'chrysalio.includes' not in settings or \
'chrysalio.includes.themes' not in settings['chrysalio.includes']:
settings_dict['theme'] = ''
return settings_dict
# -------------------------------------------------------------------------
[docs]
@classmethod
def tab4view(cls, request: Request, tab_index: int, form: Form) -> str:
"""Generate the tab content for settings.
:type request: pyramid.request.Request
:param request:
Current request.
:param int index:
Index of the tab.
:type form: .lib.form.Form
:param tuple principals:
Groups of principals.
:rtype: helpers.literal.Literal
"""
if tab_index == 0:
return cls._tab4view_parameters(request, form)
if tab_index == 1:
return cls._tab4view_information(request, form)
return ''
# -------------------------------------------------------------------------
@classmethod
def _tab4view_parameters(cls, request: Request, form: Form) -> str:
"""Generate the parameters 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 = Builder().h3(translate(_('Site')))
html += form.grid_item(
translate(_('Title:')),
request.registry['settings']['title'],
clear=True)
html += form.grid_item(
translate(_('Email:')),
request.registry['settings'].get('email'),
clear=True)
html += Builder().h3(translate(_('Default values')))
html += form.grid_item(
translate(_('Language:')),
request.registry['settings']['language'],
clear=True)
html += form.grid_item(
translate(_('Password length:')),
request.registry['settings']['password-min-length'],
clear=True)
html += form.grid_item(
translate(_('Remember me:')),
deltatime_label(
request.registry['settings']['remember-me'],
lang=request.locale_name),
clear=True)
html += form.grid_item(
translate(_('Lines per page:')),
request.registry['settings']['page-size'],
clear=True)
if request.registry['settings']['download-max-size']:
html += form.grid_item(
translate(_('Maximum size of download:')),
size_label(request.registry['settings']['download-max-size']),
clear=True)
html += form.grid_item(
translate(_('Clipboard size:')),
request.registry['settings']['clipboard-size'],
clear=True)
lang = request.session.get('lang', 'en')
html += form.grid_item(
translate(_('Theme:')),
request.registry['themes'][request.registry['settings']['theme']]
['name'].get(lang, request.registry['settings']['theme']),
clear=True)
home = request.registry['settings']['home'] if request.registry[
'settings']['home'] in request.registry['homes'] else 'home'
html += form.grid_item(
translate(_('Default home page:')),
request.route_path(home),
clear=True)
return html
# -------------------------------------------------------------------------
@classmethod
def _tab4view_information(
cls,
request: Request,
form: Form,
readonly_message: bool = False) -> str:
"""Generate the information tab.
:type request: pyramid.request.Request
:param request:
Current request.
:type form: .lib.form.Form
:param form:
Current form object.
:param bool readonly_message:
If ``True`` add a `Read Only` message.
:rtype: helpers.literal.Literal
"""
translate = request.localizer.translate
html = ''
if readonly_message:
html = Builder().div(
translate(_('You cannot modify these values.')),
class_='cioAlert')
html += Builder().h3(translate(_('Site')))
html += form.grid_item(
translate(_('Identifier:')),
request.registry.settings['site.uid'],
clear=True)
html += form.grid_item(
translate(_('Application version:')),
request.registry['version'],
clear=True)
html += form.grid_item(
translate(_('Configuration version:')),
request.registry.settings.get('site.version'),
clear=True)
html += Builder().h3(translate(_('Database')))
html += form.grid_item(
translate(_('Name:')),
basename(request.dbsession.bind.url.database),
clear=True)
html += form.grid_item(
translate(_('Type:')), request.dbsession.bind.name, clear=True)
html += form.grid_item(
translate(_('Last loading:')),
request.registry['settings']['populate'],
clear=True)
html += Builder().h3(translate(_('Resources')))
html += form.grid_item(
translate(_('Log:')),
translate(_('active')) if request.registry.get('log_activity') else
translate(_('inactive')),
clear=True)
html += form.grid_item(
translate(_('Languages:')),
', '.join(
settings_get_list(
request.registry.settings, 'languages', ['en'])),
clear=True)
lang = request.session.get('lang', 'en')
themes = sorted(
[
request.registry['themes'][k]['name'].get(lang, k)
for k in request.registry['themes']
])
html += form.grid_item(
translate(_('Themes:')), ', '.join(themes), clear=True)
html += form.grid_item(
translate(_('Home pages:')),
', '.join([ # yapf: disable
request.route_path(k)
for k in sorted(request.registry['homes'])]),
clear=True)
return html
# -------------------------------------------------------------------------
[docs]
@classmethod
def settings_schema(cls,
request: Request) -> tuple[colander.SchemaNode, dict]:
"""Return a Colander schema to edit settings.
:type request: pyramid.request.Request
:param request:
Current request.
:rtype: tuple
:return:
A tuple such as ``(schema, defaults)``.
"""
# Site
schema = colander.SchemaNode(colander.Mapping())
schema.add(
colander.SchemaNode(
colander.String(),
name='title',
validator=colander.Length(max=LABEL_LEN)))
schema.add(
colander.SchemaNode(
colander.String(),
name='email',
validator=colander.All(
colander.Length(max=EMAIL_LEN), colander.Email())))
# Default values
schema.add(
colander.SchemaNode(
colander.String(),
name='language',
validator=colander.OneOf(
settings_get_list(
request.registry.settings, 'languages', ['en']))))
schema.add(
colander.SchemaNode(
colander.Integer(),
name='password-min-length',
validator=colander.Range(min=4)))
schema.add(
colander.SchemaNode(
colander.Integer(),
name='remember-me',
validator=colander.Range(min=3600)))
schema.add(
colander.SchemaNode(
colander.Integer(),
name='page-size',
validator=colander.OneOf(PAGE_SIZES[1:-1])))
schema.add(
colander.SchemaNode(
colander.Integer(),
name='download-max-size',
validator=colander.Range(min=0)))
schema.add(
colander.SchemaNode(
colander.Integer(),
name='clipboard-size',
validator=colander.Range(min=1)))
if len(request.registry['themes']) > 1:
schema.add(
colander.SchemaNode(
colander.String(),
name='theme',
validator=colander.OneOf(request.registry['themes'])))
if len(request.registry['homes']) > 1:
schema.add(
colander.SchemaNode(
colander.String(),
name='home',
validator=colander.OneOf(request.registry['homes'])))
return schema, request.registry['settings']
# -------------------------------------------------------------------------
[docs]
@classmethod
def tab4edit(cls, request: Request, tab_index: int, form: Form) -> str:
"""Generate the tab content 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.
:rtype: helpers.literal.Literal
"""
if tab_index == 0:
return cls._tab4edit_parameters(request, form)
if tab_index == 1:
return cls._tab4view_information(request, form, True)
return ''
# -------------------------------------------------------------------------
@classmethod
def _tab4edit_parameters(cls, request: Request, form: Form) -> str:
"""Generate the parameters 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 = Builder().h3(translate(_('Site')))
html += form.grid_text(
'title',
translate(_('Title:')),
maxlength=LABEL_LEN,
required=True,
clear=True)
html += form.grid_text(
'email',
translate(_('Email:')),
maxlength=EMAIL_LEN,
required=True,
clear=True)
html += Builder().h3(translate(_('Default values')))
html += form.grid_select(
'language',
translate(_('Language:')),
sorted(
settings_get_list(
request.registry.settings, 'languages', ['en'])),
required=True,
clear=True)
html += form.grid_text(
'password-min-length',
translate(_('Password length:')),
maxlength=2,
required=True,
clear=True)
html += form.grid_text(
'remember-me',
translate(_('Remember me:')),
maxlength=8,
required=True,
clear=True)
html += form.grid_select(
'page-size',
translate(_('Lines per page:')),
PAGE_SIZES[:-1], # type: ignore
required=True,
clear=True)
html += form.grid_text(
'download-max-size',
translate(_('Maximum size of download:')),
maxlength=10,
required=True,
clear=True)
html += form.grid_text(
'clipboard-size',
translate(_('Clipboard size:')),
maxlength=2,
required=True,
clear=True)
if len(request.registry['themes']) > 1:
lang = request.session.get('lang', 'en')
themes = [
(i, j['name'].get(lang, i))
for i, j in request.registry['themes'].items()
]
html += form.grid_select(
'theme',
translate(_('Theme:')),
sorted(themes, key=lambda k: k[1]), # type: ignore
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)
return Literal(html)