src/pyams_content/shared/site/zmi/container.py
changeset 294 8742c8ac126c
child 300 49e8f3cef75a
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/shared/site/zmi/container.py	Sun Nov 26 09:58:07 2017 +0100
@@ -0,0 +1,535 @@
+#
+# 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 json
+
+# import interfaces
+from pyams_content.interfaces import MANAGE_SITE_PERMISSION
+from pyams_content.shared.common.interfaces import ISharedContent
+from pyams_content.shared.common.interfaces.zmi import IDashboardTable
+from pyams_content.shared.site.interfaces import ISiteContainer, ISiteManager
+from pyams_content.zmi.interfaces import IUserAddingsMenuLabel, ISiteTreeMenu, ISiteTreeTable
+from pyams_i18n.interfaces import II18n
+from pyams_sequence.interfaces import ISequentialIdInfo
+from pyams_skin.interfaces import IInnerPage, IPageHeader
+from pyams_skin.interfaces.container import ITableElementEditor, ITableElementName, ITableWithActions
+from pyams_skin.interfaces.viewlet import IBreadcrumbItem, ITableItemColumnActionsMenu
+from pyams_skin.layer import IPyAMSLayer
+from pyams_utils.interfaces import VIEW_SYSTEM_PERMISSION
+from pyams_workflow.interfaces import IWorkflowVersions, IWorkflowPublicationInfo, IWorkflowState, IWorkflow
+from pyams_zmi.interfaces.menu import ISiteManagementMenu, IPropertiesMenu
+from pyams_zmi.layer import IAdminLayer
+from z3c.table.interfaces import IColumn, IValues
+from zope.intid.interfaces import IIntIds
+
+# import packages
+from pyams_content.shared.site import WfTopic
+from pyams_content.skin import pyams_content
+from pyams_form.form import AJAXEditForm
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_skin.container import ContainerView
+from pyams_skin.page import DefaultPageHeaderAdapter
+from pyams_skin.table import BaseTable, TrashColumn, DefaultElementEditorAdapter, NameColumn, SorterColumn, \
+    ActionColumn, I18nColumn
+from pyams_skin.viewlet.breadcrumb import BreadcrumbItem
+from pyams_skin.viewlet.menu import MenuItem
+from pyams_skin.viewlet.toolbar import ToolbarMenuItem
+from pyams_utils.adapter import adapter_config, ContextRequestViewAdapter, ContextRequestAdapter
+from pyams_utils.fanstatic import get_resource_path
+from pyams_utils.registry import get_utility
+from pyams_utils.traversing import get_parent
+from pyams_utils.url import absolute_url
+from pyams_viewlet.manager import viewletmanager_config
+from pyams_viewlet.viewlet import viewlet_config
+from pyams_zmi.form import AdminDialogEditForm
+from pyams_zmi.view import AdminView
+from pyramid.location import lineage
+from pyramid.view import view_config
+from z3c.form import field
+from z3c.table.column import GetAttrColumn
+from zope.interface import implementer
+
+from pyams_content import _
+
+
+@adapter_config(context=(ISiteContainer, IAdminLayer), provides=IBreadcrumbItem)
+class SiteContainerBreadcrumbAdapter(BreadcrumbItem):
+    """Site container breadcrumb adapter"""
+
+    @property
+    def label(self):
+        return II18n(self.context).query_attribute('short_name', request=self.request)
+
+
+@adapter_config(context=(ISiteContainer, IAdminLayer), provides=IUserAddingsMenuLabel)
+class SiteManagerUserAddingsMenuLabelAdapter(ContextRequestAdapter):
+    """Site container user addings menu label adapter"""
+
+    @property
+    def label(self):
+        return '{content} ({blog})'.format(
+            content=self.request.localizer.translate(WfTopic.content_name),
+            blog=II18n(self.context).query_attribute('title', request=self.request))
+
+
+#
+# Site container publication views
+#
+
+@viewlet_config(name='workflow-publication.menu', context=ISiteContainer, layer=IPyAMSLayer, view=ISiteTreeTable,
+                manager=ITableItemColumnActionsMenu, permission=MANAGE_SITE_PERMISSION, weight=2)
+class SiteContainerTableItemWorkflowPublicationMenu(ToolbarMenuItem):
+    """Site container tree item workflow publication menu"""
+
+    label = _("Publication dates...")
+    label_css_class = 'fa fa-fw fa-eye'
+    url = 'workflow-publication.html'
+    modal_target = True
+    stop_propagation = True
+
+
+@viewlet_config(name='workflow-publication.menu', context=ISiteContainer, layer=IAdminLayer, manager=IPropertiesMenu,
+                permission=MANAGE_SITE_PERMISSION, weight=2)
+class SiteContainerWorkflowPublicationMenu(MenuItem):
+    """Site container workflow publication menu"""
+
+    label = _("Publication dates...")
+    icon_class = 'fa-eye'
+    url = 'workflow-publication.html'
+    modal_target = True
+
+
+@pagelet_config(name='workflow-publication.html', context=ISiteContainer, layer=IPyAMSLayer,
+                permission=MANAGE_SITE_PERMISSION)
+class SiteContainerWorkflowPublicationEditForm(AdminDialogEditForm):
+    """Site container workflow publication edit form"""
+
+    legend = _("Update publication dates")
+
+    fields = field.Fields(IWorkflowPublicationInfo).select('publication_effective_date', 'publication_expiration_date')
+    ajax_handler = 'workflow-publication.json'
+    edit_permission = MANAGE_SITE_PERMISSION
+
+
+@view_config(name='workflow-publication.json', context=ISiteContainer, request_type=IPyAMSLayer,
+             permission=MANAGE_SITE_PERMISSION, renderer='json', xhr=True)
+class SiteContainerWorkflowPublicationAJAXEditForm(AJAXEditForm, SiteContainerWorkflowPublicationEditForm):
+    """Site container workflow publication edit form, JSON renderer"""
+
+    def get_ajax_output(self, changes):
+        output = super(SiteContainerWorkflowPublicationAJAXEditForm, self).get_ajax_output(changes)
+        if changes:
+            info = IWorkflowPublicationInfo(self.context, None)
+            if info is not None:
+                if info.is_published():
+                    icon_class = 'fa-eye opacity-75'
+                else:
+                    icon_class = 'fa-eye-slash text-danger opaque'
+                value = '<i class="fa fa-fw {icon_class} hint align-base" title="{title}" ' \
+                        'data-ams-hint-gravity="e"></i>'.format(
+                            icon_class=icon_class,
+                            title=self.request.localizer.translate(_("Visible element?")))
+                intids = get_utility(IIntIds)
+                output.setdefault('events', []).append({
+                    'event': 'myams.refresh',
+                    'options': {
+                        'handler': 'MyAMS.skin.refreshRowCell',
+                        'object_id': '{0}::{1}'.format(SiteContainerTreeTable.id, intids.queryId(self.context)),
+                        'col_name': 'visible',
+                        'cell': value
+                    }
+                })
+        return output
+
+
+#
+# Site container tree view
+#
+
+@viewlet_config(name='site-tree.menu', layer=IAdminLayer, context=ISiteContainer, manager=ISiteManagementMenu,
+                permission=VIEW_SYSTEM_PERMISSION, weight=5)
+@viewletmanager_config(name='site-tree.menu', layer=IAdminLayer, context=ISiteContainer, provides=ISiteTreeMenu)
+@implementer(ISiteTreeMenu)
+class SiteContainerTreeMenu(MenuItem):
+    """Site container tree menu"""
+
+    label = _("Site tree")
+    icon_class = 'fa-sitemap'
+    url = '#site-tree.html'
+
+
+@implementer(IDashboardTable, ISiteTreeTable, ITableWithActions)
+class SiteContainerTreeTable(BaseTable):
+    """Site container tree table"""
+
+    id = 'site_tree_table'
+    title = _("Site tree")
+
+    hide_body_toolbar = True
+    sortOn = None
+
+    permission = MANAGE_SITE_PERMISSION
+
+    def __init__(self, context, request, can_sort=False, rows_state=None):
+        super(SiteContainerTreeTable, self).__init__(context, request)
+        self.can_sort = can_sort
+        self.rows_state = rows_state
+
+    @property
+    def cssClasses(self):
+        classes = ['table', 'table-bordered', 'table-striped', 'table-hover', 'table-tight']
+        permission = self.permission
+        if self.can_sort and ((not permission) or self.request.has_permission(permission, self.context)):
+            classes.append('table-dnd')
+        return {'table': ' '.join(classes)}
+
+    @property
+    def data_attributes(self):
+        attributes = super(SiteContainerTreeTable, self).data_attributes
+        intids = get_utility(IIntIds)
+        manager = get_parent(self.context, ISiteManager)
+        attributes.setdefault('table', {}).update({
+            'data-ams-plugins': 'pyams_content',
+            'data-ams-plugin-pyams_content-src': get_resource_path(pyams_content),
+            'data-ams-location': absolute_url(self.context, self.request),
+            'data-ams-datatable-sort': 'false',
+            'data-ams-datatable-pagination': 'false',
+            'data-ams-delete-target': 'delete-site-item.json',
+            'data-ams-tree-node-id': intids.queryId(manager),
+            'data-ams-tablednd-drag-handle': 'td.sorter',
+            'data-ams-tablednd-drop': 'MyAMS.tree.sortTree',
+            'data-ams-tablednd-drop-target': 'set-site-order.json'
+        })
+        attributes.setdefault('tr', {}).update({
+            'id': lambda x, col: '{0}::{1}'.format(self.id, intids.queryId(x)),
+            'data-ams-location': lambda x, col: absolute_url(x.__parent__, self.request),
+            'data-ams-tree-node-id': lambda x, col: intids.queryId(x),
+            'data-ams-tree-node-parent-id': lambda x, col: intids.queryId(x.__parent__)
+        })
+        return attributes
+
+
+@adapter_config(name='sorter', context=(ISiteContainer, IPyAMSLayer, ISiteTreeTable), provides=IColumn)
+class SiteContainerTreeSorterColumn(SorterColumn):
+    """Site container tree sorter column"""
+
+    permission = MANAGE_SITE_PERMISSION
+
+    def renderCell(self, item):
+        if self.table.can_sort:
+            return super(SiteContainerTreeSorterColumn, self).renderCell(item)
+        else:
+            return ''
+
+
+@adapter_config(name='visible', context=(ISiteContainer, IPyAMSLayer, ISiteTreeTable), provides=IColumn)
+class SiteContainerTreeVisibleColumn(ActionColumn):
+    """Site container tree visible column"""
+
+    cssClasses = {'th': 'action',
+                  'td': 'action'}
+
+    icon_class = 'fa fa-fw fa-eye'
+    icon_hint = _("Visible element?")
+
+    weight = 5
+
+    def renderCell(self, item):
+        return self.get_icon(item)
+
+    def get_icon(self, item):
+        if ISharedContent.providedBy(item):
+            item = IWorkflowVersions(item).get_last_versions(count=1)[-1]
+        info = IWorkflowPublicationInfo(item, None)
+        if info is None:
+            return ''
+        else:
+            if info.is_published():
+                icon_class = 'fa-eye opacity-75'
+            else:
+                icon_class = 'fa-eye-slash text-danger opaque'
+            return '<i class="fa fa-fw {icon_class} hint align-base" title="{title}" data-ams-hint-gravity="e"></i>'.format(
+                icon_class=icon_class,
+                title=self.request.localizer.translate(self.icon_hint))
+
+
+@adapter_config(name='name', context=(ISiteContainer, IPyAMSLayer, ISiteTreeTable), provides=IColumn)
+class SiteContainerTreeNameColumn(NameColumn):
+    """Site container tree name column"""
+
+    _header = _("Folders and topics")
+
+    def renderHeadCell(self):
+        return '<span data-ams-stop-propagation="true"' \
+               '      data-ams-click-handler="MyAMS.tree.switchTree">' \
+               '    <span class="small hint" title="{hint}" data-ams-hint-gravity="e">' \
+               '        <i class="fa fa-fw fa-plus-square-o switch"></i>' \
+               '    </span>&nbsp;&nbsp;{title}' \
+               '</span>'.format(
+                    hint=self.request.localizer.translate(_("Click to open/close all folders")),
+                    title=super(SiteContainerTreeNameColumn, self).renderHeadCell())
+
+    def renderCell(self, item, name=None):
+        depth = -3
+        for node in lineage(item):
+            depth += 1
+        return '<div class="name">' \
+               '    {padding}' \
+               '    <span class="small hint" title="{hint}" data-ams-hint-gravity="e" ' \
+               '          data-ams-click-handler="MyAMS.tree.switchTableNode"' \
+               '          data-ams-stop-propagation="true">' \
+               '        <i class="fa fa-fw {switch}"></i>' \
+               '    </span>&nbsp;&nbsp;<span class="title">{title}</span>' \
+               '</div>'.format(
+                   padding='<span class="tree-node-padding"></span>' * depth,
+                   hint=self.request.localizer.translate(_("Click to show/hide inner folders")),
+                   switch='fa-{state}-square-o switch'.format(
+                       state=self.table.rows_state or ('minus' if item in lineage(self.context) else 'plus'))
+                           if ISiteContainer.providedBy(item) else '',
+                   title=name or super(SiteContainerTreeNameColumn, self).renderCell(item))
+
+
+@adapter_config(name='oid', context=(ISiteContainer, IPyAMSLayer, ISiteTreeTable), provides=IColumn)
+class SiteContainerTreeOidColumn(I18nColumn, GetAttrColumn):
+    """Site container tree OID column"""
+
+    _header = _("OID")
+    weight = 70
+
+    def getValue(self, obj):
+        sequence = ISequentialIdInfo(obj, None)
+        if sequence is None:
+            return '--'
+        else:
+            try:
+                return sequence.get_short_oid()
+            except TypeError:
+                return '--'
+
+
+@adapter_config(name='state', context=(ISiteContainer, IPyAMSLayer, ISiteTreeTable), provides=IColumn)
+class SiteContainerTreeStateColumn(I18nColumn, GetAttrColumn):
+    """Site container tree state column"""
+
+    _header = _("Status")
+    weight = 80
+
+    def getValue(self, obj):
+        if not ISharedContent.providedBy(obj):
+            return '--'
+        version = IWorkflowVersions(obj).get_last_versions()[-1]
+        return self.request.localizer.translate(IWorkflow(version).get_state_label(IWorkflowState(version).state))
+
+
+@adapter_config(name='version', context=(ISiteContainer, IPyAMSLayer, ISiteTreeTable), provides=IColumn)
+class SiteContainerTreeVersionColumn(I18nColumn, GetAttrColumn):
+    """Site container tree version column"""
+
+    _header = _("Version")
+    weight = 90
+
+    def getValue(self, obj):
+        if not ISharedContent.providedBy(obj):
+            return '--'
+        version = IWorkflowVersions(obj).get_last_versions()[-1]
+        return IWorkflowState(version).version_id
+
+
+@adapter_config(name='trash', context=(ISiteContainer, IPyAMSLayer, ISiteTreeTable), provides=IColumn)
+class SiteContainerTreeTrashColumn(TrashColumn):
+    """Site container tree trash column"""
+
+    icon_hint = _("Delete site item")
+    permission = MANAGE_SITE_PERMISSION
+
+    def has_permission(self, item):
+        if (not ISiteContainer.providedBy(item)) or (item in lineage(self.context)):
+            return False
+        return super(SiteContainerTreeTrashColumn, self).has_permission(item)
+
+
+@adapter_config(context=(ISiteContainer, IPyAMSLayer, ISiteTreeTable), provides=IValues)
+class SiteContainerTreeValuesAdapter(ContextRequestViewAdapter):
+    """Site container tree values adapter"""
+
+    @property
+    def values(self):
+
+        def get_values(container, result):
+            if container not in result:
+                result.append(container)
+            if ISiteContainer.providedBy(container) and (container in lineage(self.context)):
+                for child in container.values():
+                    get_values(child, result)
+            return result
+
+        manager = get_parent(self.context, ISiteManager)
+        values = []
+        for container in manager.values():
+            values.append(container)
+            if ISiteContainer.providedBy(container):
+                get_values(container, values)
+        return values
+
+
+@pagelet_config(name='site-tree.html', context=ISiteContainer, layer=IPyAMSLayer, permission=VIEW_SYSTEM_PERMISSION)
+@implementer(IInnerPage)
+class SiteContainerTreeView(AdminView, ContainerView):
+    """Site Container tree view"""
+
+    table_class = SiteContainerTreeTable
+
+    def __init__(self, context, request):
+        super(ContainerView, self).__init__(context, request)
+        self.table = SiteContainerTreeTable(context, request, can_sort=ISiteManager.providedBy(context))
+
+
+@adapter_config(context=(ISiteContainer, IAdminLayer, ISiteTreeTable), provides=IPageHeader)
+class SiteContainerViewHeaderAdapter(DefaultPageHeaderAdapter):
+    """Site container tree view header adapter"""
+
+    icon_class = 'fa fa-fw fa-sitemap'
+
+
+@view_config(name='get-tree.json', context=ISiteContainer, request_type=IPyAMSLayer,
+             permission=VIEW_SYSTEM_PERMISSION, renderer='json', xhr=True)
+def get_tree(request):
+    """Get whole tree"""
+
+    def get_tree_values(parent):
+        """Iterator over container tree items"""
+        for item in parent.values():
+            yield item
+            if ISiteContainer.providedBy(item):
+                yield from get_tree_values(item)
+
+    table = SiteContainerTreeTable(request.context, request,
+                                   can_sort=json.loads(request.params.get('can_sort', 'false')))
+    table.update()
+    result = []
+    manager = get_parent(request.context, ISiteManager)
+    for item in get_tree_values(manager):
+        row = table.setUpRow(item)
+        result.append(table.renderRow(row).strip())
+    return result
+
+
+@view_config(name='get-tree-nodes.json', context=ISiteContainer, request_type=IPyAMSLayer,
+             permission=VIEW_SYSTEM_PERMISSION, renderer='json', xhr=True)
+def get_tree_nodes(request):
+    """Get tree nodes"""
+    table = SiteContainerTreeTable(request.context, request,
+                                   can_sort=json.loads(request.params.get('can_sort', 'false')))
+    table.update()
+    result = []
+    for item in request.context.values():
+        row = table.setUpRow(item)
+        result.append(table.renderRow(row).strip())
+    return result
+
+
+@view_config(name='set-site-order.json', context=ISiteContainer, request_type=IPyAMSLayer,
+             permission=MANAGE_SITE_PERMISSION, renderer='json', xhr=True)
+def set_site_order(request):
+    """Set site elements order"""
+    intids = get_utility(IIntIds)
+    parent = intids.queryObject(int(request.params.get('parent')))
+    # check for changing parent
+    if request.params.get('action') == 'reparent':
+        child = intids.queryObject(int(request.params.get('child')))
+        old_parent = child.__parent__
+        new_name = old_name = child.__name__
+        if old_name in parent:
+            index = 1
+            new_name = '{name}-{index:02}'.format(name=old_name, index=index)
+            while new_name in parent:
+                index += 1
+                new_name = '{name}-{index:02}'.format(name=old_name, index=index)
+        parent[new_name] = child
+        del old_parent[old_name]
+    # Re-define order
+    names = [child.__name__ for child in [intids.queryObject(oid)
+                                          for oid in map(int, json.loads(request.params.get('order')))]
+             if child.__parent__ is parent]
+    parent.updateOrder(names)
+    # get all new parent child
+    table = SiteContainerTreeTable(request.context, request,
+                                   can_sort=json.loads(request.params.get('can_sort', 'false')),
+                                   rows_state='plus')
+    table.update()
+    result = []
+    for item in parent.values():
+        row = table.setUpRow(item)
+        result.append(table.renderRow(row).strip())
+    return result
+    
+
+@view_config(name='delete-site-item.json', context=ISiteContainer, request_type=IPyAMSLayer,
+             permission=MANAGE_SITE_PERMISSION, renderer='json', xhr=True)
+def delete_site_item(request):
+    """Delete item from site container"""
+    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!"))}}
+    if name not in request.context:
+        return {'status': 'message',
+                'messagebox': {'status': 'error',
+                               'content': translate(_("Given object name doesn't exist!"))}}
+    del request.context[name]
+    return {'status': 'success'}
+
+
+@adapter_config(context=(ISiteContainer, IPyAMSLayer, ISiteTreeTable), provides=ITableElementName)
+class SiteContainerTableElementName(ContextRequestViewAdapter):
+    """Site container tree table element name"""
+
+    @property
+    def name(self):
+        return II18n(self.context).query_attribute('title', request=self.request)
+
+
+@adapter_config(context=(ISharedContent, IPyAMSLayer, ISiteTreeTable), provides=ITableElementName)
+class SharedContentTableElementName(ContextRequestViewAdapter):
+    """Shared content tree table element name"""
+
+    @property
+    def name(self):
+        version = IWorkflowVersions(self.context).get_last_versions(count=1)[0]
+        return II18n(version).query_attribute('title', request=self.request)
+
+
+@adapter_config(context=(ISiteContainer, IPyAMSLayer, ISiteTreeTable), provides=ITableElementEditor)
+class SiteContainerTableElementEditor(DefaultElementEditorAdapter):
+    """Site container tree table element editor"""
+
+    view_name = 'admin#site-tree.html'
+    modal_target = False
+
+
+@adapter_config(context=(ISharedContent, IPyAMSLayer, ISiteTreeTable), provides=ITableElementEditor)
+class SharedContentTableElementEditor(DefaultElementEditorAdapter):
+    """Shared content tree table element editor"""
+
+    view_name = 'admin'
+    modal_target = False
+
+    @property
+    def url(self):
+        version = IWorkflowVersions(self.context).get_last_versions(count=1)[0]
+        return absolute_url(version, self.request, self.view_name)