--- /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> {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> <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)