Added ".zmi" sub-package
authorThierry Florac <tflorac@ulthar.net>
Sun, 11 Mar 2018 11:16:42 +0100
changeset 50 c68bcd7f6fe1
parent 49 0b1285117171
child 51 0dc05cd58c0a
Added ".zmi" sub-package
src/pyams_zmi/admin.py
src/pyams_zmi/control_panel.py
src/pyams_zmi/extension.py
src/pyams_zmi/site.py
src/pyams_zmi/zmi/__init__.py
src/pyams_zmi/zmi/control_panel.py
src/pyams_zmi/zmi/extension.py
src/pyams_zmi/zmi/index.py
src/pyams_zmi/zmi/site.py
src/pyams_zmi/zmi/skin.py
--- a/src/pyams_zmi/admin.py	Sun Mar 11 11:16:23 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,33 +0,0 @@
-#
-# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
-# All Rights Reserved.
-#
-# This software is subject to the provisions of the Zope Public License,
-# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
-# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
-# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
-# FOR A PARTICULAR PURPOSE.
-#
-
-__docformat__ = 'restructuredtext'
-
-
-# import standard library
-
-# import interfaces
-from pyams_skin.layer import IPyAMSLayer
-from pyams_utils.interfaces import VIEW_SYSTEM_PERMISSION
-from pyams_zmi.layer import IAdminLayer
-
-# import packages
-from pyams_pagelet.pagelet import pagelet_config
-from pyams_template.template import template_config
-from pyams_zmi.view import AdminView
-from zope.interface import Interface
-
-
-@pagelet_config(name='admin', context=Interface, layer=IPyAMSLayer, permission=VIEW_SYSTEM_PERMISSION)
-@template_config(template='templates/admin-index.pt', layer=IAdminLayer)
-class MainAdminPage(AdminView):
-    """Main administration page"""
--- a/src/pyams_zmi/control_panel.py	Sun Mar 11 11:16:23 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,323 +0,0 @@
-#
-# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
-# All Rights Reserved.
-#
-# This software is subject to the provisions of the Zope Public License,
-# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
-# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
-# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
-# FOR A PARTICULAR PURPOSE.
-#
-
-__docformat__ = 'restructuredtext'
-
-
-# import standard library
-from html import escape
-
-# import interfaces
-from pyams_skin.interfaces import IInnerPage, IPageHeader, IContentHelp
-from pyams_skin.interfaces.container import ITableWithActions
-from pyams_skin.layer import IPyAMSLayer
-from pyams_utils.interfaces import MANAGE_SYSTEM_PERMISSION
-from pyams_utils.interfaces.site import IOptionalUtility
-from pyams_zmi.interfaces.menu import IControlPanelMenu, IUtilitiesMenu
-from pyams_zmi.layer import IAdminLayer
-from z3c.table.interfaces import IValues, IColumn
-from zope.component.interfaces import ISite
-
-# import packages
-from pyams_pagelet.pagelet import pagelet_config
-from pyams_skin.container import ContainerView
-from pyams_skin.help import ContentHelp
-from pyams_skin.page import DefaultPageHeaderAdapter
-from pyams_skin.table import BaseTable, TrashColumn
-from pyams_skin.viewlet.menu import MenuItem
-from pyams_utils.adapter import ContextRequestViewAdapter, adapter_config
-from pyams_utils.registry import registered_utilities
-from pyams_utils.text import text_to_html
-from pyams_utils.url import absolute_url
-from pyams_viewlet.manager import viewletmanager_config
-from pyams_viewlet.viewlet import viewlet_config
-from pyams_zmi.view import AdminView
-from pyramid.view import view_config
-from z3c.table.column import GetAttrColumn
-from zope.interface import implementer, Interface
-
-from pyams_zmi import _
-
-
-#
-# Utilities views and adapters
-#
-
-@viewlet_config(name='utilities.menu', layer=IAdminLayer, context=ISite, manager=IControlPanelMenu,
-                permission=MANAGE_SYSTEM_PERMISSION, weight=1)
-@viewletmanager_config(name='utilities.menu', layer=IAdminLayer, context=ISite, provides=IUtilitiesMenu)
-@implementer(IUtilitiesMenu)
-class UtilitiesMenuItem(MenuItem):
-    """Utilities menu"""
-
-    label = _("Utilities")
-    icon_class = 'fa-codepen'
-    url = '#utilities.html'
-
-
-@implementer(ITableWithActions)
-class UtilitiesTable(BaseTable):
-    """Utilities table"""
-
-    id = 'utilities_table'
-    title = _("Site utilities")
-
-    @property
-    def data_attributes(self):
-        attributes = super(UtilitiesTable, self).data_attributes
-        attributes['table'] = {'data-ams-location': absolute_url(self.context, self.request),
-                               'data-ams-delete-target': 'delete-utility.json'}
-        return attributes
-
-
-@adapter_config(name='trash', context=(Interface, IAdminLayer, UtilitiesTable), provides=IColumn)
-class UtilitiesTrashColumn(TrashColumn):
-    """Utilities trash column"""
-
-    icon_hint = _("Delete utility")
-    permission = MANAGE_SYSTEM_PERMISSION
-
-    checker = lambda col, x: IOptionalUtility.providedBy(x)
-
-
-@adapter_config(context=(ISite, IAdminLayer, UtilitiesTable), provides=IValues)
-class UtilitiesValuesAdapter(ContextRequestViewAdapter):
-    """Utilities values adapter"""
-
-    @property
-    def values(self):
-        return list(self.context.getSiteManager().values())
-
-
-@pagelet_config(name='utilities.html', context=ISite, layer=IPyAMSLayer, permission=MANAGE_SYSTEM_PERMISSION)
-@implementer(IInnerPage)
-class UtilitiesView(AdminView, ContainerView):
-    """Control panel view"""
-
-    table_class = UtilitiesTable
-
-    def __init__(self, context, request):
-        super(UtilitiesView, self).__init__(context, request)
-
-
-@adapter_config(context=(ISite, IAdminLayer, UtilitiesView), provides=IPageHeader)
-class UtilitiesHeaderAdapter(DefaultPageHeaderAdapter):
-    """Utilities header adapter"""
-
-    icon_class = 'fa fa-fw fa-codepen'
-
-
-@view_config(name='delete-utility.json', context=ISite, request_type=IPyAMSLayer,
-             permission=MANAGE_SYSTEM_PERMISSION, renderer='json', xhr=True)
-def delete_utility(request):
-    """Delete utility from site manager"""
-    translate = request.localizer.translate
-    name = request.params.get('object_name')
-    if not name:
-        return {'status': 'message',
-                'messagebox': {'status': 'error',
-                               'content': translate(_("No provided object_name argument!"))}}
-    manager = request.context.getSiteManager()
-    if name not in manager:
-        return {'status': 'message',
-                'messagebox': {'status': 'error',
-                               'content': translate(_("Given utility name doesn't exist!"))}}
-    del manager[name]
-    return {'status': 'success'}
-
-
-#
-# Common registrations views and adapters
-#
-
-class IRegistrationsTable(Interface):
-    """Registrations view marker interface"""
-
-
-@adapter_config(name='component', context=(ISite, IAdminLayer, IRegistrationsTable), provides=IColumn)
-class RegistrationsComponentColumn(GetAttrColumn):
-    """Registrations component column"""
-
-    _header = _("Component")
-    weight = 1
-
-    @property
-    def header(self):
-        return self.request.localizer.translate(self._header)
-
-    def getValue(self, obj):
-        component = obj.component
-        if component is not None:
-            name = getattr(component, '__name__', None)
-            if not name:
-                name = str(component.__class__)
-        else:
-            name = str(obj.factory)
-        return escape(name)
-
-
-@adapter_config(name='interface', context=(ISite, IAdminLayer, IRegistrationsTable), provides=IColumn)
-class RegistrationsInterfaceColumn(GetAttrColumn):
-    """Registrations interface column"""
-
-    _header = _("Registered interface")
-    weight = 5
-
-    @property
-    def header(self):
-        return self.request.localizer.translate(self._header)
-
-    def getValue(self, obj):
-        return text_to_html(str(obj.provided))
-
-
-@adapter_config(name='name', context=(ISite, IAdminLayer, IRegistrationsTable), provides=IColumn)
-class RegistrationsNameColumn(GetAttrColumn):
-    """Registrations name column"""
-
-    _header = _("Name")
-    weight = 10
-
-    @property
-    def header(self):
-        return self.request.localizer.translate(self._header)
-
-    def getValue(self, obj):
-        return obj.name or _('< no name >')
-
-
-#
-# Local registrations views
-#
-
-@viewlet_config(name='local-registrations.menu', layer=IAdminLayer, context=ISite, manager=IUtilitiesMenu,
-                permission=MANAGE_SYSTEM_PERMISSION, weight=1)
-class LocalRegistrationsMenuItem(MenuItem):
-    """Local registrations menu"""
-
-    label = _("Local registrations")
-    url = '#local-registrations.html'
-
-
-@implementer(IRegistrationsTable)
-class LocalRegistrationsTable(BaseTable):
-    """Local utilities registrations table"""
-
-    id = 'local_registrations_table'
-    title = _("Local utilities registrations")
-
-    data_attributes = {}
-
-
-@adapter_config(context=(ISite, IAdminLayer, LocalRegistrationsTable), provides=IValues)
-class LocalRegistrationsValuesAdapter(ContextRequestViewAdapter):
-    """Local utilities values adapter"""
-
-    @property
-    def values(self):
-        return list(self.context.getSiteManager().registeredUtilities())
-
-
-@pagelet_config(name='local-registrations.html', context=ISite, layer=IPyAMSLayer,
-                permission=MANAGE_SYSTEM_PERMISSION)
-@implementer(IInnerPage)
-class LocalRegistrationsView(AdminView, ContainerView):
-    """Registrations view"""
-
-    table_class = LocalRegistrationsTable
-
-    def __init__(self, context, request):
-        super(LocalRegistrationsView, self).__init__(context, request)
-
-
-@adapter_config(context=(ISite, IAdminLayer, LocalRegistrationsView), provides=IPageHeader)
-class LocalRegistrationsHeaderAdapter(DefaultPageHeaderAdapter):
-    """Local registrations header adapter"""
-
-    icon_class = 'fa fa-fw fa-codepen'
-
-
-@adapter_config(context=(ISite, IAdminLayer, LocalRegistrationsView), provides=IContentHelp)
-class LocalRegistrationsHelpAdapter(ContentHelp):
-    """Local registrations help adapter"""
-
-    header = _("Local registry utilities")
-    message = _("""A local registry is a registry defining utilities stored into
-site's Object Database (ZODB).
-
-You can manage these utilities and modify their properties and site's behaviour without
-modifying the application.""")
-    message_format = 'rest'
-
-
-#
-# Global registrations views
-#
-
-@viewlet_config(name='global-registrations.menu', layer=IAdminLayer, context=ISite, manager=IUtilitiesMenu,
-                permission=MANAGE_SYSTEM_PERMISSION, weight=2)
-class GlobalRegistrationsMenuItem(MenuItem):
-    """Global registrations menu"""
-
-    label = _("Global registrations")
-    url = '#global-registrations.html'
-
-
-@implementer(IRegistrationsTable)
-class GlobalRegistrationsTable(BaseTable):
-    """Utilities global registrations table"""
-
-    id = 'global_registrations_table'
-    title = _("Global utilities registrations")
-
-    data_attributes = {}
-
-
-@adapter_config(context=(ISite, IAdminLayer, GlobalRegistrationsTable), provides=IValues)
-class GlobalRegistrationsValuesAdapter(ContextRequestViewAdapter):
-    """Global utilities values adapter"""
-
-    @property
-    def values(self):
-        return list(registered_utilities())
-
-
-@pagelet_config(name='global-registrations.html', context=ISite, layer=IPyAMSLayer,
-                permission=MANAGE_SYSTEM_PERMISSION)
-@implementer(IInnerPage)
-class GlobalRegistrationsView(AdminView, ContainerView):
-    """Global registrations view"""
-
-    table_class = GlobalRegistrationsTable
-
-    def __init__(self, context, request):
-        super(GlobalRegistrationsView, self).__init__(context, request)
-
-
-@adapter_config(context=(ISite, IAdminLayer, GlobalRegistrationsView), provides=IPageHeader)
-class GlobalRegistrationsHeaderAdapter(DefaultPageHeaderAdapter):
-    """Global registrations header adapter"""
-
-    icon_class = 'fa fa-fw fa-codepen'
-
-
-@adapter_config(context=(ISite, IAdminLayer, GlobalRegistrationsView), provides=IContentHelp)
-class GlobalRegistrationsHelpAdapter(ContentHelp):
-    """Global registrations help adapter"""
-
-    header = _("Global registry utilities")
-    message = _("""The global registry groups local utilities as well as utilities registered outside
-site's Object Database (ZODB).
-
-These utilities are declared statically (generally using include or ZCML directives) and can't be setup
-without modifying site configuration.""")
-    message_format = 'rest'
--- a/src/pyams_zmi/extension.py	Sun Mar 11 11:16:23 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,143 +0,0 @@
-#
-# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
-# All Rights Reserved.
-#
-# This software is subject to the provisions of the Zope Public License,
-# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
-# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
-# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
-# FOR A PARTICULAR PURPOSE.
-#
-
-__docformat__ = 'restructuredtext'
-
-
-# import standard library
-
-# import interfaces
-from pyams_skin.interfaces.extension import IGoogleTagManagerInfo, IGoogleAnalyticsInfo, IUserReportInfo
-from pyams_skin.layer import IPyAMSLayer
-from pyams_utils.interfaces import MANAGE_SYSTEM_PERMISSION
-from pyams_utils.interfaces.site import ISiteRoot
-from pyams_zmi.interfaces.menu import IPropertiesMenu
-from pyams_zmi.layer import IAdminLayer
-
-# import packages
-from pyams_form.form import AJAXEditForm
-from pyams_pagelet.pagelet import pagelet_config
-from pyams_skin.viewlet.menu import MenuItem
-from pyams_viewlet.viewlet import viewlet_config
-from pyams_zmi.form import AdminDialogEditForm
-from pyramid.view import view_config
-from z3c.form import field
-
-from pyams_zmi import _
-
-
-#
-# Google Tag Manager views
-#
-
-@viewlet_config(name='tag-manager-properties.menu', context=ISiteRoot, layer=IAdminLayer, manager=IPropertiesMenu,
-                permission=MANAGE_SYSTEM_PERMISSION, weight=10)
-class GoogleTagManagerPropertiesMenuItem(MenuItem):
-    """Google Tag Manager properties menu"""
-
-    label = _("Google Tag Manager...")
-    icon_class = 'fa-tags'
-    url = 'tag-manager-properties.html'
-    modal_target = True
-
-
-@pagelet_config(name='tag-manager-properties.html', context=ISiteRoot, layer=IPyAMSLayer,
-                permission=MANAGE_SYSTEM_PERMISSION)
-class GoogleTagManagerPropertiesEditForm(AdminDialogEditForm):
-    """Google Tag Manager properties edit form"""
-
-    legend = _("Update Google Tag Manager properties")
-    ajax_handler = 'tag-manager-properties.json'
-    edit_permission = MANAGE_SYSTEM_PERMISSION
-
-    fields = field.Fields(IGoogleTagManagerInfo)
-
-    label_css_class = 'control-label col-md-4'
-    input_css_class = 'col-md-8'
-
-
-@view_config(name='tag-manager-properties.json', context=ISiteRoot, request_type=IPyAMSLayer,
-             permission=MANAGE_SYSTEM_PERMISSION, renderer='json', xhr=True)
-class GoogleTagManagerPropertiesAJAXEditForm(AJAXEditForm, GoogleTagManagerPropertiesEditForm):
-    """Google Tag Manager properties edit form, JSON renderer"""
-
-
-#
-# Google Analytics views
-#
-
-@viewlet_config(name='analytics-properties.menu', context=ISiteRoot, layer=IAdminLayer, manager=IPropertiesMenu,
-                permission=MANAGE_SYSTEM_PERMISSION, weight=15)
-class GoogleAnalyticsPropertiesMenuItem(MenuItem):
-    """Google Analytics properties menu"""
-
-    label = _("Google Analytics...")
-    icon_class = 'fa-line-chart'
-    url = 'analytics-properties.html'
-    modal_target = True
-
-
-@pagelet_config(name='analytics-properties.html', context=ISiteRoot, layer=IPyAMSLayer,
-                permission=MANAGE_SYSTEM_PERMISSION)
-class GoogleAnalyticsPropertiesEditForm(AdminDialogEditForm):
-    """Google Analytics properties edit form"""
-
-    legend = _("Update Google Analytics properties")
-    ajax_handler = 'analytics-properties.json'
-    edit_permission = MANAGE_SYSTEM_PERMISSION
-
-    fields = field.Fields(IGoogleAnalyticsInfo)
-
-    label_css_class = 'control-label col-md-4'
-    input_css_class = 'col-md-8'
-
-
-@view_config(name='analytics-properties.json', context=ISiteRoot, request_type=IPyAMSLayer,
-             permission=MANAGE_SYSTEM_PERMISSION, renderer='json', xhr=True)
-class GoogleAnalyticsPropertiesAJAXEditForm(AJAXEditForm, GoogleAnalyticsPropertiesEditForm):
-    """Google Analytics properties edit form, JSON renderer"""
-
-
-#
-# UserReport views
-#
-
-@viewlet_config(name='user-report.menu', context=ISiteRoot, layer=IAdminLayer, manager=IPropertiesMenu,
-                permission=MANAGE_SYSTEM_PERMISSION, weight=20)
-class UserReportPropertiesMenuItem(MenuItem):
-    """UserReport properties menu"""
-
-    label = _("UserReport settings...")
-    icon_class = 'fa-comments'
-    url = 'user-report.html'
-    modal_target = True
-
-
-@pagelet_config(name='user-report.html', context=ISiteRoot, layer=IPyAMSLayer,
-                permission=MANAGE_SYSTEM_PERMISSION)
-class UserReportPropertiesEditForm(AdminDialogEditForm):
-    """UserReport properties edit form"""
-
-    legend = _("Update UserReport service properties")
-    ajax_handler = 'user-report.json'
-    edit_permission = MANAGE_SYSTEM_PERMISSION
-
-    fields = field.Fields(IUserReportInfo)
-
-    label_css_class = 'control-label col-md-4'
-    input_css_class = 'col-md-8'
-
-
-@view_config(name='user-report.json', context=ISiteRoot, request_type=IPyAMSLayer,
-             permission=MANAGE_SYSTEM_PERMISSION, renderer='json', xhr=True)
-class UserReportPropertiesAJAXEditForm(AJAXEditForm, UserReportPropertiesEditForm):
-    """UserReport properties edit form, JSON renderer"""
--- a/src/pyams_zmi/site.py	Sun Mar 11 11:16:23 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,180 +0,0 @@
-#
-# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
-# All Rights Reserved.
-#
-# This software is subject to the provisions of the Zope Public License,
-# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
-# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
-# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
-# FOR A PARTICULAR PURPOSE.
-#
-
-__docformat__ = 'restructuredtext'
-
-
-# import standard library
-
-# import interfaces
-from pyams_form.interfaces.form import IWidgetForm
-from pyams_skin.interfaces import IInnerPage, IPageHeader, IContentTitle
-from pyams_skin.interfaces.configuration import IConfiguration, IBackOfficeConfiguration
-from pyams_skin.layer import IPyAMSLayer
-from pyams_utils.interfaces import VIEW_SYSTEM_PERMISSION, MANAGE_SYSTEM_PERMISSION
-from pyams_utils.interfaces.site import ISiteRoot
-from pyams_zmi.interfaces.menu import ISiteManagementMenu, IPropertiesMenu
-from pyams_zmi.layer import IAdminLayer
-
-# import packages
-from pyams_form.form import AJAXEditForm
-from pyams_pagelet.pagelet import pagelet_config
-from pyams_skin.page import DefaultPageHeaderAdapter
-from pyams_skin.viewlet.menu import MenuItem
-from pyams_utils.adapter import adapter_config, ContextRequestViewAdapter
-from pyams_viewlet.manager import viewletmanager_config
-from pyams_viewlet.viewlet import viewlet_config
-from pyams_zmi.form import AdminEditForm, AdminDialogEditForm
-from pyramid.decorator import reify
-from pyramid.view import view_config
-from z3c.form import field
-from zope.interface import implementer, implementedBy, Interface
-
-from pyams_zmi import _
-
-
-@adapter_config(context=(ISiteRoot, IAdminLayer, Interface), provides=IContentTitle)
-class SiteRootTitleAdapter(ContextRequestViewAdapter):
-    """Site root title adapter"""
-
-    @property
-    def title(self):
-        return IBackOfficeConfiguration(self.context).title
-
-
-#
-# Configuration properties
-#
-
-@viewlet_config(name='properties.menu', context=ISiteRoot, layer=IAdminLayer, manager=ISiteManagementMenu,
-                permission=VIEW_SYSTEM_PERMISSION, weight=1)
-@viewletmanager_config(name='properties.menu', context=ISiteRoot, layer=IAdminLayer, provides=IPropertiesMenu)
-@implementer(IPropertiesMenu)
-class PropertiesMenuItem(MenuItem):
-    """Properties menu"""
-
-    label = _("Properties")
-    icon_class = 'fa-edit'
-    url = '#properties.html'
-
-
-@pagelet_config(name='properties.html', context=ISiteRoot, layer=IPyAMSLayer, permission=VIEW_SYSTEM_PERMISSION)
-@implementer(IWidgetForm, IInnerPage)
-class PropertiesEditForm(AdminEditForm):
-    """Properties edit form"""
-
-    legend = _("Update main site properties")
-    ajax_handler = 'properties.json'
-    edit_permission = MANAGE_SYSTEM_PERMISSION
-
-    @reify
-    def fields(self):
-
-        def get_configuration_interface(context):
-            for interface in implementedBy(context.__class__).interfaces():
-                if issubclass(interface, IConfiguration):
-                    return interface
-
-        content = self.getContent()
-        return field.Fields(get_configuration_interface(content)).omit('__name__')
-
-    def getContent(self):
-        return IConfiguration(self.context)
-
-
-@view_config(name='properties.json', context=ISiteRoot, request_type=IPyAMSLayer,
-             permission=MANAGE_SYSTEM_PERMISSION, renderer='json', xhr=True)
-class PropertiesAJAXEditForm(AJAXEditForm, PropertiesEditForm):
-    """Properties edit form, AJAX renderer"""
-
-    def get_ajax_output(self, changes):
-        if changes:
-            return {'status': 'reload',
-                    'location': '#properties.html',
-                    'smallbox': self.request.localizer.translate(self.successMessage),
-                    'smallbox_status': 'success'}
-        else:
-            return AJAXEditForm.get_ajax_output(self, changes)
-
-
-@adapter_config(context=(Interface, IPyAMSLayer, PropertiesEditForm), provides=IPageHeader)
-class PropertiesEditFormHeaderAdapter(DefaultPageHeaderAdapter):
-    """Utilities header adapter"""
-
-    @property
-    def title(self):
-        return IBackOfficeConfiguration(self.context).title
-
-    icon_class = 'fa fa-fw fa-twitch'
-
-
-#
-# Back-office configuration properties
-#
-
-@viewlet_config(name='back-office-properties.menu', context=ISiteRoot, layer=IAdminLayer, manager=IPropertiesMenu,
-                permission=MANAGE_SYSTEM_PERMISSION, weight=1)
-class BackOfficePropertiesMenuItem(MenuItem):
-    """Back-office properties menu"""
-
-    label = _("Back-office properties...")
-    icon_class = 'fa-tachometer'
-    url = 'back-office-properties.html'
-    modal_target = True
-
-
-@pagelet_config(name='back-office-properties.html', context=ISiteRoot, layer=IPyAMSLayer,
-                permission=MANAGE_SYSTEM_PERMISSION)
-class BackOfficePropertiesEditForm(AdminDialogEditForm):
-    """Back-office properties edit form"""
-
-    legend = _("Update site back-office properties")
-    ajax_handler = 'back-office-properties.json'
-    edit_permission = MANAGE_SYSTEM_PERMISSION
-
-    dialog_class = 'modal-large'
-
-    @reify
-    def fields(self):
-
-        def get_configuration_interface(context):
-            for interface in implementedBy(context.__class__).interfaces():
-                if issubclass(interface, IBackOfficeConfiguration):
-                    return interface
-
-        content = self.getContent()
-        return field.Fields(get_configuration_interface(content)).omit('__name__')
-
-    def getContent(self):
-        return IBackOfficeConfiguration(self.context)
-
-    def updateWidgets(self, prefix=None):
-        super(BackOfficePropertiesEditForm, self).updateWidgets(prefix)
-        if 'login_header' in self.widgets:
-            self.widgets['login_header'].widget_css_class = 'textarea'
-        if 'login_footer' in self.widgets:
-            self.widgets['login_footer'].widget_css_class = 'textarea'
-
-
-@view_config(name='back-office-properties.json', context=ISiteRoot, request_type=IPyAMSLayer,
-             permission=MANAGE_SYSTEM_PERMISSION, renderer='json', xhr=True)
-class BackOfficePropertiesAJAXEditForm(AJAXEditForm, BackOfficePropertiesEditForm):
-    """Back-office properties edit form, JSON renderer"""
-
-    def get_ajax_output(self, changes):
-        if changes:
-            return {'status': 'reload',
-                    'location': '#properties.html',
-                    'smallbox': self.request.localizer.translate(self.successMessage),
-                    'smallbox_status': 'success'}
-        else:
-            return AJAXEditForm.get_ajax_output(self, changes)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_zmi/zmi/__init__.py	Sun Mar 11 11:16:42 2018 +0100
@@ -0,0 +1,19 @@
+#
+# Copyright (c) 2008-2018 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+# import standard library
+
+# import interfaces
+
+# import packages
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_zmi/zmi/control_panel.py	Sun Mar 11 11:16:42 2018 +0100
@@ -0,0 +1,323 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+from html import escape
+
+# import interfaces
+from pyams_skin.interfaces import IInnerPage, IPageHeader, IContentHelp
+from pyams_skin.interfaces.container import ITableWithActions
+from pyams_skin.layer import IPyAMSLayer
+from pyams_utils.interfaces import MANAGE_SYSTEM_PERMISSION
+from pyams_utils.interfaces.site import IOptionalUtility
+from pyams_zmi.interfaces.menu import IControlPanelMenu, IUtilitiesMenu
+from pyams_zmi.layer import IAdminLayer
+from z3c.table.interfaces import IValues, IColumn
+from zope.component.interfaces import ISite
+
+# import packages
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_skin.container import ContainerView
+from pyams_skin.help import ContentHelp
+from pyams_skin.page import DefaultPageHeaderAdapter
+from pyams_skin.table import BaseTable, TrashColumn
+from pyams_skin.viewlet.menu import MenuItem
+from pyams_utils.adapter import ContextRequestViewAdapter, adapter_config
+from pyams_utils.registry import registered_utilities
+from pyams_utils.text import text_to_html
+from pyams_utils.url import absolute_url
+from pyams_viewlet.manager import viewletmanager_config
+from pyams_viewlet.viewlet import viewlet_config
+from pyams_zmi.view import AdminView
+from pyramid.view import view_config
+from z3c.table.column import GetAttrColumn
+from zope.interface import implementer, Interface
+
+from pyams_zmi import _
+
+
+#
+# Utilities views and adapters
+#
+
+@viewlet_config(name='utilities.menu', layer=IAdminLayer, context=ISite, manager=IControlPanelMenu,
+                permission=MANAGE_SYSTEM_PERMISSION, weight=1)
+@viewletmanager_config(name='utilities.menu', layer=IAdminLayer, context=ISite, provides=IUtilitiesMenu)
+@implementer(IUtilitiesMenu)
+class UtilitiesMenuItem(MenuItem):
+    """Utilities menu"""
+
+    label = _("Utilities")
+    icon_class = 'fa-codepen'
+    url = '#utilities.html'
+
+
+@implementer(ITableWithActions)
+class UtilitiesTable(BaseTable):
+    """Utilities table"""
+
+    id = 'utilities_table'
+    title = _("Site utilities")
+
+    @property
+    def data_attributes(self):
+        attributes = super(UtilitiesTable, self).data_attributes
+        attributes['table'] = {'data-ams-location': absolute_url(self.context, self.request),
+                               'data-ams-delete-target': 'delete-utility.json'}
+        return attributes
+
+
+@adapter_config(name='trash', context=(Interface, IAdminLayer, UtilitiesTable), provides=IColumn)
+class UtilitiesTrashColumn(TrashColumn):
+    """Utilities trash column"""
+
+    icon_hint = _("Delete utility")
+    permission = MANAGE_SYSTEM_PERMISSION
+
+    checker = lambda col, x: IOptionalUtility.providedBy(x)
+
+
+@adapter_config(context=(ISite, IAdminLayer, UtilitiesTable), provides=IValues)
+class UtilitiesValuesAdapter(ContextRequestViewAdapter):
+    """Utilities values adapter"""
+
+    @property
+    def values(self):
+        return list(self.context.getSiteManager().values())
+
+
+@pagelet_config(name='utilities.html', context=ISite, layer=IPyAMSLayer, permission=MANAGE_SYSTEM_PERMISSION)
+@implementer(IInnerPage)
+class UtilitiesView(AdminView, ContainerView):
+    """Control panel view"""
+
+    table_class = UtilitiesTable
+
+    def __init__(self, context, request):
+        super(UtilitiesView, self).__init__(context, request)
+
+
+@adapter_config(context=(ISite, IAdminLayer, UtilitiesView), provides=IPageHeader)
+class UtilitiesHeaderAdapter(DefaultPageHeaderAdapter):
+    """Utilities header adapter"""
+
+    icon_class = 'fa fa-fw fa-codepen'
+
+
+@view_config(name='delete-utility.json', context=ISite, request_type=IPyAMSLayer,
+             permission=MANAGE_SYSTEM_PERMISSION, renderer='json', xhr=True)
+def delete_utility(request):
+    """Delete utility from site manager"""
+    translate = request.localizer.translate
+    name = request.params.get('object_name')
+    if not name:
+        return {'status': 'message',
+                'messagebox': {'status': 'error',
+                               'content': translate(_("No provided object_name argument!"))}}
+    manager = request.context.getSiteManager()
+    if name not in manager:
+        return {'status': 'message',
+                'messagebox': {'status': 'error',
+                               'content': translate(_("Given utility name doesn't exist!"))}}
+    del manager[name]
+    return {'status': 'success'}
+
+
+#
+# Common registrations views and adapters
+#
+
+class IRegistrationsTable(Interface):
+    """Registrations view marker interface"""
+
+
+@adapter_config(name='component', context=(ISite, IAdminLayer, IRegistrationsTable), provides=IColumn)
+class RegistrationsComponentColumn(GetAttrColumn):
+    """Registrations component column"""
+
+    _header = _("Component")
+    weight = 1
+
+    @property
+    def header(self):
+        return self.request.localizer.translate(self._header)
+
+    def getValue(self, obj):
+        component = obj.component
+        if component is not None:
+            name = getattr(component, '__name__', None)
+            if not name:
+                name = str(component.__class__)
+        else:
+            name = str(obj.factory)
+        return escape(name)
+
+
+@adapter_config(name='interface', context=(ISite, IAdminLayer, IRegistrationsTable), provides=IColumn)
+class RegistrationsInterfaceColumn(GetAttrColumn):
+    """Registrations interface column"""
+
+    _header = _("Registered interface")
+    weight = 5
+
+    @property
+    def header(self):
+        return self.request.localizer.translate(self._header)
+
+    def getValue(self, obj):
+        return text_to_html(str(obj.provided))
+
+
+@adapter_config(name='name', context=(ISite, IAdminLayer, IRegistrationsTable), provides=IColumn)
+class RegistrationsNameColumn(GetAttrColumn):
+    """Registrations name column"""
+
+    _header = _("Name")
+    weight = 10
+
+    @property
+    def header(self):
+        return self.request.localizer.translate(self._header)
+
+    def getValue(self, obj):
+        return obj.name or _('< no name >')
+
+
+#
+# Local registrations views
+#
+
+@viewlet_config(name='local-registrations.menu', layer=IAdminLayer, context=ISite, manager=IUtilitiesMenu,
+                permission=MANAGE_SYSTEM_PERMISSION, weight=1)
+class LocalRegistrationsMenuItem(MenuItem):
+    """Local registrations menu"""
+
+    label = _("Local registrations")
+    url = '#local-registrations.html'
+
+
+@implementer(IRegistrationsTable)
+class LocalRegistrationsTable(BaseTable):
+    """Local utilities registrations table"""
+
+    id = 'local_registrations_table'
+    title = _("Local utilities registrations")
+
+    data_attributes = {}
+
+
+@adapter_config(context=(ISite, IAdminLayer, LocalRegistrationsTable), provides=IValues)
+class LocalRegistrationsValuesAdapter(ContextRequestViewAdapter):
+    """Local utilities values adapter"""
+
+    @property
+    def values(self):
+        return list(self.context.getSiteManager().registeredUtilities())
+
+
+@pagelet_config(name='local-registrations.html', context=ISite, layer=IPyAMSLayer,
+                permission=MANAGE_SYSTEM_PERMISSION)
+@implementer(IInnerPage)
+class LocalRegistrationsView(AdminView, ContainerView):
+    """Registrations view"""
+
+    table_class = LocalRegistrationsTable
+
+    def __init__(self, context, request):
+        super(LocalRegistrationsView, self).__init__(context, request)
+
+
+@adapter_config(context=(ISite, IAdminLayer, LocalRegistrationsView), provides=IPageHeader)
+class LocalRegistrationsHeaderAdapter(DefaultPageHeaderAdapter):
+    """Local registrations header adapter"""
+
+    icon_class = 'fa fa-fw fa-codepen'
+
+
+@adapter_config(context=(ISite, IAdminLayer, LocalRegistrationsView), provides=IContentHelp)
+class LocalRegistrationsHelpAdapter(ContentHelp):
+    """Local registrations help adapter"""
+
+    header = _("Local registry utilities")
+    message = _("""A local registry is a registry defining utilities stored into
+site's Object Database (ZODB).
+
+You can manage these utilities and modify their properties and site's behaviour without
+modifying the application.""")
+    message_format = 'rest'
+
+
+#
+# Global registrations views
+#
+
+@viewlet_config(name='global-registrations.menu', layer=IAdminLayer, context=ISite, manager=IUtilitiesMenu,
+                permission=MANAGE_SYSTEM_PERMISSION, weight=2)
+class GlobalRegistrationsMenuItem(MenuItem):
+    """Global registrations menu"""
+
+    label = _("Global registrations")
+    url = '#global-registrations.html'
+
+
+@implementer(IRegistrationsTable)
+class GlobalRegistrationsTable(BaseTable):
+    """Utilities global registrations table"""
+
+    id = 'global_registrations_table'
+    title = _("Global utilities registrations")
+
+    data_attributes = {}
+
+
+@adapter_config(context=(ISite, IAdminLayer, GlobalRegistrationsTable), provides=IValues)
+class GlobalRegistrationsValuesAdapter(ContextRequestViewAdapter):
+    """Global utilities values adapter"""
+
+    @property
+    def values(self):
+        return list(registered_utilities())
+
+
+@pagelet_config(name='global-registrations.html', context=ISite, layer=IPyAMSLayer,
+                permission=MANAGE_SYSTEM_PERMISSION)
+@implementer(IInnerPage)
+class GlobalRegistrationsView(AdminView, ContainerView):
+    """Global registrations view"""
+
+    table_class = GlobalRegistrationsTable
+
+    def __init__(self, context, request):
+        super(GlobalRegistrationsView, self).__init__(context, request)
+
+
+@adapter_config(context=(ISite, IAdminLayer, GlobalRegistrationsView), provides=IPageHeader)
+class GlobalRegistrationsHeaderAdapter(DefaultPageHeaderAdapter):
+    """Global registrations header adapter"""
+
+    icon_class = 'fa fa-fw fa-codepen'
+
+
+@adapter_config(context=(ISite, IAdminLayer, GlobalRegistrationsView), provides=IContentHelp)
+class GlobalRegistrationsHelpAdapter(ContentHelp):
+    """Global registrations help adapter"""
+
+    header = _("Global registry utilities")
+    message = _("""The global registry groups local utilities as well as utilities registered outside
+site's Object Database (ZODB).
+
+These utilities are declared statically (generally using include or ZCML directives) and can't be setup
+without modifying site configuration.""")
+    message_format = 'rest'
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_zmi/zmi/extension.py	Sun Mar 11 11:16:42 2018 +0100
@@ -0,0 +1,149 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_skin.interfaces.extension import IGoogleTagManagerInfo, IGoogleAnalyticsInfo, IUserReportInfo
+from pyams_skin.layer import IPyAMSLayer
+from pyams_utils.interfaces import MANAGE_SYSTEM_PERMISSION
+from pyams_utils.interfaces.site import ISiteRoot
+from pyams_zmi.interfaces.menu import IPropertiesMenu
+from pyams_zmi.layer import IAdminLayer
+
+# import packages
+from pyams_form.form import AJAXEditForm
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_skin.viewlet.menu import MenuItem, MenuDivider
+from pyams_viewlet.viewlet import viewlet_config
+from pyams_zmi.form import AdminDialogEditForm
+from pyramid.view import view_config
+from z3c.form import field
+
+from pyams_zmi import _
+
+
+@viewlet_config(name='extensions-divider.menu', context=ISiteRoot, layer=IAdminLayer, manager=IPropertiesMenu,
+                permission=MANAGE_SYSTEM_PERMISSION, weight=9)
+class ExtensionsMenuDivider(MenuDivider):
+    """Extensions menu divider"""
+
+
+#
+# Google Tag Manager views
+#
+
+@viewlet_config(name='tag-manager-properties.menu', context=ISiteRoot, layer=IAdminLayer, manager=IPropertiesMenu,
+                permission=MANAGE_SYSTEM_PERMISSION, weight=10)
+class GoogleTagManagerPropertiesMenuItem(MenuItem):
+    """Google Tag Manager properties menu"""
+
+    label = _("Google Tag Manager...")
+    icon_class = 'fa-tags'
+    url = 'tag-manager-properties.html'
+    modal_target = True
+
+
+@pagelet_config(name='tag-manager-properties.html', context=ISiteRoot, layer=IPyAMSLayer,
+                permission=MANAGE_SYSTEM_PERMISSION)
+class GoogleTagManagerPropertiesEditForm(AdminDialogEditForm):
+    """Google Tag Manager properties edit form"""
+
+    legend = _("Update Google Tag Manager properties")
+    ajax_handler = 'tag-manager-properties.json'
+    edit_permission = MANAGE_SYSTEM_PERMISSION
+
+    fields = field.Fields(IGoogleTagManagerInfo)
+
+    label_css_class = 'control-label col-md-4'
+    input_css_class = 'col-md-8'
+
+
+@view_config(name='tag-manager-properties.json', context=ISiteRoot, request_type=IPyAMSLayer,
+             permission=MANAGE_SYSTEM_PERMISSION, renderer='json', xhr=True)
+class GoogleTagManagerPropertiesAJAXEditForm(AJAXEditForm, GoogleTagManagerPropertiesEditForm):
+    """Google Tag Manager properties edit form, JSON renderer"""
+
+
+#
+# Google Analytics views
+#
+
+@viewlet_config(name='analytics-properties.menu', context=ISiteRoot, layer=IAdminLayer, manager=IPropertiesMenu,
+                permission=MANAGE_SYSTEM_PERMISSION, weight=15)
+class GoogleAnalyticsPropertiesMenuItem(MenuItem):
+    """Google Analytics properties menu"""
+
+    label = _("Google Analytics...")
+    icon_class = 'fa-line-chart'
+    url = 'analytics-properties.html'
+    modal_target = True
+
+
+@pagelet_config(name='analytics-properties.html', context=ISiteRoot, layer=IPyAMSLayer,
+                permission=MANAGE_SYSTEM_PERMISSION)
+class GoogleAnalyticsPropertiesEditForm(AdminDialogEditForm):
+    """Google Analytics properties edit form"""
+
+    legend = _("Update Google Analytics properties")
+    ajax_handler = 'analytics-properties.json'
+    edit_permission = MANAGE_SYSTEM_PERMISSION
+
+    fields = field.Fields(IGoogleAnalyticsInfo)
+
+    label_css_class = 'control-label col-md-4'
+    input_css_class = 'col-md-8'
+
+
+@view_config(name='analytics-properties.json', context=ISiteRoot, request_type=IPyAMSLayer,
+             permission=MANAGE_SYSTEM_PERMISSION, renderer='json', xhr=True)
+class GoogleAnalyticsPropertiesAJAXEditForm(AJAXEditForm, GoogleAnalyticsPropertiesEditForm):
+    """Google Analytics properties edit form, JSON renderer"""
+
+
+#
+# UserReport views
+#
+
+@viewlet_config(name='user-report.menu', context=ISiteRoot, layer=IAdminLayer, manager=IPropertiesMenu,
+                permission=MANAGE_SYSTEM_PERMISSION, weight=20)
+class UserReportPropertiesMenuItem(MenuItem):
+    """UserReport properties menu"""
+
+    label = _("UserReport settings...")
+    icon_class = 'fa-comments'
+    url = 'user-report.html'
+    modal_target = True
+
+
+@pagelet_config(name='user-report.html', context=ISiteRoot, layer=IPyAMSLayer,
+                permission=MANAGE_SYSTEM_PERMISSION)
+class UserReportPropertiesEditForm(AdminDialogEditForm):
+    """UserReport properties edit form"""
+
+    legend = _("Update UserReport service properties")
+    ajax_handler = 'user-report.json'
+    edit_permission = MANAGE_SYSTEM_PERMISSION
+
+    fields = field.Fields(IUserReportInfo)
+
+    label_css_class = 'control-label col-md-4'
+    input_css_class = 'col-md-8'
+
+
+@view_config(name='user-report.json', context=ISiteRoot, request_type=IPyAMSLayer,
+             permission=MANAGE_SYSTEM_PERMISSION, renderer='json', xhr=True)
+class UserReportPropertiesAJAXEditForm(AJAXEditForm, UserReportPropertiesEditForm):
+    """UserReport properties edit form, JSON renderer"""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_zmi/zmi/index.py	Sun Mar 11 11:16:42 2018 +0100
@@ -0,0 +1,33 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_skin.layer import IPyAMSLayer
+from pyams_utils.interfaces import VIEW_SYSTEM_PERMISSION
+from pyams_zmi.layer import IAdminLayer
+
+# import packages
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_template.template import template_config
+from pyams_zmi.view import AdminView
+from zope.interface import Interface
+
+
+@pagelet_config(name='admin', context=Interface, layer=IPyAMSLayer, permission=VIEW_SYSTEM_PERMISSION)
+@template_config(template='templates/admin-index.pt', layer=IAdminLayer)
+class MainAdminPage(AdminView):
+    """Main administration page"""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_zmi/zmi/site.py	Sun Mar 11 11:16:42 2018 +0100
@@ -0,0 +1,223 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_form.interfaces.form import IWidgetForm
+from pyams_skin.interfaces import IInnerPage, IPageHeader, IContentTitle
+from pyams_skin.interfaces.configuration import IConfiguration, IBackOfficeConfiguration
+from pyams_skin.layer import IPyAMSLayer
+from pyams_utils.interfaces import VIEW_SYSTEM_PERMISSION, MANAGE_SYSTEM_PERMISSION
+from pyams_utils.interfaces.site import ISiteRoot
+from pyams_zmi.interfaces import IZMIConfiguration
+from pyams_zmi.interfaces.menu import ISiteManagementMenu, IPropertiesMenu
+from pyams_zmi.layer import IAdminLayer
+
+# import packages
+from pyams_form.form import AJAXEditForm
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_skin.page import DefaultPageHeaderAdapter
+from pyams_skin.viewlet.menu import MenuItem
+from pyams_utils.adapter import adapter_config, ContextRequestViewAdapter
+from pyams_viewlet.manager import viewletmanager_config
+from pyams_viewlet.viewlet import viewlet_config
+from pyams_zmi.form import AdminEditForm, AdminDialogEditForm
+from pyramid.decorator import reify
+from pyramid.view import view_config
+from z3c.form import field
+from zope.interface import implementer, implementedBy, Interface
+
+from pyams_zmi import _
+
+
+@adapter_config(context=(ISiteRoot, IAdminLayer, Interface), provides=IContentTitle)
+class SiteRootTitleAdapter(ContextRequestViewAdapter):
+    """Site root title adapter"""
+
+    @property
+    def title(self):
+        return IBackOfficeConfiguration(self.context).title
+
+
+#
+# Configuration properties
+#
+
+@viewlet_config(name='properties.menu', context=ISiteRoot, layer=IAdminLayer, manager=ISiteManagementMenu,
+                permission=VIEW_SYSTEM_PERMISSION, weight=1)
+@viewletmanager_config(name='properties.menu', context=ISiteRoot, layer=IAdminLayer, provides=IPropertiesMenu)
+@implementer(IPropertiesMenu)
+class PropertiesMenuItem(MenuItem):
+    """Properties menu"""
+
+    label = _("Properties")
+    icon_class = 'fa-edit'
+    url = '#properties.html'
+
+
+@pagelet_config(name='properties.html', context=ISiteRoot, layer=IPyAMSLayer, permission=VIEW_SYSTEM_PERMISSION)
+@implementer(IWidgetForm, IInnerPage)
+class PropertiesEditForm(AdminEditForm):
+    """Properties edit form"""
+
+    legend = _("Update main site properties")
+    ajax_handler = 'properties.json'
+    edit_permission = MANAGE_SYSTEM_PERMISSION
+
+    @reify
+    def fields(self):
+
+        def get_configuration_interface(context):
+            for interface in implementedBy(context.__class__).interfaces():
+                if issubclass(interface, IConfiguration):
+                    return interface
+
+        content = self.getContent()
+        return field.Fields(get_configuration_interface(content)).omit('__name__')
+
+    def getContent(self):
+        return IConfiguration(self.context)
+
+
+@view_config(name='properties.json', context=ISiteRoot, request_type=IPyAMSLayer,
+             permission=MANAGE_SYSTEM_PERMISSION, renderer='json', xhr=True)
+class PropertiesAJAXEditForm(AJAXEditForm, PropertiesEditForm):
+    """Properties edit form, AJAX renderer"""
+
+    def get_ajax_output(self, changes):
+        if changes:
+            return {'status': 'reload',
+                    'location': '#properties.html',
+                    'smallbox': self.request.localizer.translate(self.successMessage),
+                    'smallbox_status': 'success'}
+        else:
+            return AJAXEditForm.get_ajax_output(self, changes)
+
+
+@adapter_config(context=(Interface, IPyAMSLayer, PropertiesEditForm), provides=IPageHeader)
+class PropertiesEditFormHeaderAdapter(DefaultPageHeaderAdapter):
+    """Utilities header adapter"""
+
+    @property
+    def title(self):
+        return IBackOfficeConfiguration(self.context).title
+
+    icon_class = 'fa fa-fw fa-twitch'
+
+
+#
+# Back-office configuration properties
+#
+
+@viewlet_config(name='back-office-properties.menu', context=ISiteRoot, layer=IAdminLayer, manager=IPropertiesMenu,
+                permission=MANAGE_SYSTEM_PERMISSION, weight=1)
+class BackOfficePropertiesMenuItem(MenuItem):
+    """Back-office properties menu"""
+
+    label = _("Back-office properties...")
+    icon_class = 'fa-tachometer'
+    url = 'back-office-properties.html'
+    modal_target = True
+
+
+@pagelet_config(name='back-office-properties.html', context=ISiteRoot, layer=IPyAMSLayer,
+                permission=MANAGE_SYSTEM_PERMISSION)
+class BackOfficePropertiesEditForm(AdminDialogEditForm):
+    """Back-office properties edit form"""
+
+    legend = _("Update site back-office properties")
+    ajax_handler = 'back-office-properties.json'
+    edit_permission = MANAGE_SYSTEM_PERMISSION
+
+    dialog_class = 'modal-large'
+
+    @reify
+    def fields(self):
+
+        def get_configuration_interface(context):
+            for interface in implementedBy(context.__class__).interfaces():
+                if issubclass(interface, IBackOfficeConfiguration):
+                    return interface
+
+        content = self.getContent()
+        return field.Fields(get_configuration_interface(content)).omit('__name__')
+
+    def getContent(self):
+        return IBackOfficeConfiguration(self.context)
+
+    def updateWidgets(self, prefix=None):
+        super(BackOfficePropertiesEditForm, self).updateWidgets(prefix)
+        if 'login_header' in self.widgets:
+            self.widgets['login_header'].widget_css_class = 'textarea'
+        if 'login_footer' in self.widgets:
+            self.widgets['login_footer'].widget_css_class = 'textarea'
+
+
+@view_config(name='back-office-properties.json', context=ISiteRoot, request_type=IPyAMSLayer,
+             permission=MANAGE_SYSTEM_PERMISSION, renderer='json', xhr=True)
+class BackOfficePropertiesAJAXEditForm(AJAXEditForm, BackOfficePropertiesEditForm):
+    """Back-office properties edit form, JSON renderer"""
+
+    def get_ajax_output(self, changes):
+        if changes:
+            return {'status': 'reload',
+                    'location': '#properties.html',
+                    'smallbox': self.request.localizer.translate(self.successMessage),
+                    'smallbox_status': 'success'}
+        else:
+            return AJAXEditForm.get_ajax_output(self, changes)
+
+
+#
+# ZMI configuration properties
+#
+
+@viewlet_config(name='zmi-configuration.menu', context=ISiteRoot, layer=IAdminLayer, manager=IPropertiesMenu,
+                permission=MANAGE_SYSTEM_PERMISSION, weight=2)
+class ZMIConfigurationMenuItem(MenuItem):
+    """ZMI configuration menu"""
+
+    label = _("ZMI configuration...")
+    icon_class = 'fa-window-restore'
+    url = 'zmi-configuration.html'
+    modal_target = True
+
+
+@pagelet_config(name='zmi-configuration.html', context=ISiteRoot, layer=IPyAMSLayer,
+                permission=MANAGE_SYSTEM_PERMISSION)
+class ZMIConfigurationEditForm(AdminDialogEditForm):
+    """ZMI configuration edit form"""
+
+    legend = _("Update ZMI configuration")
+    label_css_class = 'control-label col-md-4'
+    input_css_class = 'col-md-8'
+
+    ajax_handler = "zmi-configuration.json"
+    edit_permission = MANAGE_SYSTEM_PERMISSION
+
+    fields = field.Fields(IZMIConfiguration)
+
+
+@view_config(name='zmi-configuration.json', context=ISiteRoot, request_type=IPyAMSLayer,
+             permission=MANAGE_SYSTEM_PERMISSION, renderer='json', xhr=True)
+class ZMIConfigurationAJAXEditForm(AJAXEditForm, ZMIConfigurationEditForm):
+    """ZMI configuration edit form, JSON renderer"""
+
+    def get_ajax_output(self, changes):
+        if changes:
+            return {'status': 'redirect'}
+        else:
+            return super(ZMIConfigurationAJAXEditForm, self).get_ajax_output(changes)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_zmi/zmi/skin.py	Sun Mar 11 11:16:42 2018 +0100
@@ -0,0 +1,81 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_skin.interfaces import IUserSkinnable
+from pyams_skin.layer import IPyAMSLayer
+from pyams_utils.interfaces import MANAGE_SKIN_PERMISSION
+from pyams_zmi.interfaces.menu import IPropertiesMenu
+from pyams_zmi.layer import IAdminLayer
+
+# import packages
+from pyams_form.form import AJAXEditForm
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_skin.viewlet.menu import MenuItem, MenuDivider
+from pyams_viewlet.viewlet import viewlet_config
+from pyams_zmi.form import AdminDialogEditForm
+from pyramid.view import view_config
+from z3c.form import field
+
+from pyams_zmi import _
+
+
+#
+# User skinnable properties configuration
+#
+
+@viewlet_config(name='user-skin-properties.divider', context=IUserSkinnable, layer=IAdminLayer, manager=IPropertiesMenu,
+                permission=MANAGE_SKIN_PERMISSION, weight=199)
+class UserSkinnablePropertiesMenuDivider(MenuDivider):
+    """User skin properties menu divider"""
+
+
+@viewlet_config(name='user-skin-properties.menu', context=IUserSkinnable, layer=IAdminLayer, manager=IPropertiesMenu,
+                permission=MANAGE_SKIN_PERMISSION, weight=200)
+class UserSkinnablePropertiesMenuItem(MenuItem):
+    """User skin properties menu"""
+
+    label = _("Graphic theme...")
+    icon_class = 'fa-paint-brush'
+    url = 'user-skin-properties.html'
+    modal_target = True
+
+
+@pagelet_config(name='user-skin-properties.html', context=IUserSkinnable, layer=IPyAMSLayer,
+                permission=MANAGE_SKIN_PERMISSION)
+class UserSkinnablePropertiesEditForm(AdminDialogEditForm):
+    """User skin properties edit form"""
+
+    legend = _("Graphic theme elements")
+    label_css_class = 'control-label col-md-4'
+    input_css_class = 'col-md-8'
+
+    @property
+    def fields(self):
+        fields = field.Fields(IUserSkinnable)
+        if not self.context.can_inherit_skin:
+            fields = fields.omit('inherit_skin')
+        return fields
+
+    ajax_handler = 'user-skin-properties.json'
+    edit_permission = MANAGE_SKIN_PERMISSION
+
+
+@view_config(name='user-skin-properties.json', context=IUserSkinnable, request_type=IPyAMSLayer,
+             permission=MANAGE_SKIN_PERMISSION, renderer='json', xhr=True)
+class UserSkinnablePropertiesAJAXEditForm(AJAXEditForm, UserSkinnablePropertiesEditForm):
+    """User skin properties edit form, JSON renderer"""