Added generic menu feature
authorThierry Florac <thierry.florac@onf.fr>
Wed, 13 Jun 2018 11:30:18 +0200
changeset 635 ac6c59149f0d
parent 634 4e43b556a2a9
child 636 1d0b1d4b3770
Added generic menu feature
src/pyams_content/features/menu/__init__.py
src/pyams_content/features/menu/interfaces/__init__.py
src/pyams_content/features/menu/zmi/__init__.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/features/menu/__init__.py	Wed Jun 13 11:30:18 2018 +0200
@@ -0,0 +1,44 @@
+#
+# 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
+from pyams_content.features.menu.interfaces import IMenu, IMenusContainer, IMenuLink
+
+# import packages
+from pyams_content.component.association.container import AssociationContainer
+from zope.interface import implementer
+from zope.schema.fieldproperty import FieldProperty
+
+
+#
+# Menus classes
+#
+
+@implementer(IMenu)
+class Menu(AssociationContainer):
+    """Associations menu"""
+
+    visible = FieldProperty(IMenu['visible'])
+    title = FieldProperty(IMenu['title'])
+
+
+@implementer(IMenusContainer)
+class MenusContainer(AssociationContainer):
+    """Associations menus container"""
+
+    def get_visible_items(self):
+        return filter(lambda x: IMenu(x).visible, self.values())
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/features/menu/interfaces/__init__.py	Wed Jun 13 11:30:18 2018 +0200
@@ -0,0 +1,75 @@
+#
+# 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
+from pyams_content.component.association.interfaces import IAssociationContainer, IAssociationContainerTarget
+from zope.annotation.interfaces import IAttributeAnnotatable
+
+# import packages
+from pyams_i18n.schema import I18nTextLineField
+from zope.container.constraints import containers, contains
+from zope.interface import Interface
+from zope.schema import Bool
+
+from pyams_content import _
+
+
+class IMenuLink(Interface):
+    """Menu link marker interface"""
+
+
+class IMenuInternalLink(IMenuLink):
+    """Menu internal link marker interface"""
+
+
+class IMenuExternalLink(IMenuLink):
+    """Menu external link marker interface"""
+
+
+class IMenuLinksContainer(IAssociationContainer):
+    """Menu links container interface"""
+
+    contains(IMenuLink)
+
+
+class IMenuLinksContainerTarget(IAssociationContainerTarget):
+    """Menu links container marker interface"""
+
+
+class IMenu(IMenuLinksContainer):
+    """Menu container interface"""
+
+    containers('.IMenusContainer')
+
+    visible = Bool(title=_("Visible?"),
+                   description=_("Is this item visible in front-office?"),
+                   required=True,
+                   default=True)
+
+    title = I18nTextLineField(title=_("Menu title"),
+                              description=_("Displayed menu label"),
+                              required=True)
+
+
+class IMenusContainer(IAssociationContainer):
+    """Menus container interface"""
+
+    contains(IMenu)
+
+
+class IMenusContainerTarget(IAttributeAnnotatable):
+    """Menus container target marker interface"""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/features/menu/zmi/__init__.py	Wed Jun 13 11:30:18 2018 +0200
@@ -0,0 +1,495 @@
+#
+# 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 json
+
+# import interfaces
+from pyams_content.component.links.zmi import InternalLinkAddMenu, InternalLinkAddForm, InternalLinkPropertiesEditForm, \
+    ExternalLinkAddMenu, ExternalLinkAddForm, ExternalLinkPropertiesEditForm
+from pyams_content.features.menu import IMenusContainer, IMenu, Menu, IMenuLink
+from pyams_content.features.menu.interfaces import IMenusContainerTarget, IMenuLinksContainer, IMenuInternalLink, \
+    IMenuExternalLink, IMenuLinksContainerTarget
+from pyams_form.interfaces.form import IFormContextPermissionChecker
+from pyams_portal.interfaces import MANAGE_TEMPLATE_PERMISSION
+from pyams_skin.interfaces.viewlet import IWidgetTitleViewletManager, IToolbarAddingMenu
+from pyams_skin.layer import IPyAMSLayer
+from pyams_utils.interfaces import VIEW_SYSTEM_PERMISSION
+from pyams_viewlet.interfaces import IViewletManager
+from z3c.table.interfaces import IValues, IColumn
+
+# import packages
+from pyams_content.component.association.zmi import AssociationsTable, AssociationsTablePublicNameColumn
+from pyams_form.form import ajax_config, AJAXAddForm, AJAXEditForm
+from pyams_i18n.column import I18nAttrColumn
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_skin.container import switch_element_visibility, delete_container_element
+from pyams_skin.event import get_json_switched_table_refresh_event, get_json_table_row_refresh_event
+from pyams_skin.table import BaseTable, SorterColumn, VisibilitySwitcherColumn, I18nColumn, TrashColumn, NameColumn, \
+    get_table_id
+from pyams_skin.viewlet.toolbar import ToolbarAction
+from pyams_utils.adapter import adapter_config, ContextRequestViewAdapter, ContextAdapter
+from pyams_utils.traversing import get_parent
+from pyams_utils.url import absolute_url
+from pyams_viewlet.viewlet import viewlet_config
+from pyams_zmi.form import AdminDialogAddForm, AdminDialogEditForm
+from pyams_zmi.zmi.table import InnerTableView
+from pyramid.decorator import reify
+from pyramid.exceptions import NotFound
+from pyramid.view import view_config
+from z3c.form import field
+from zope.interface import implementer, alsoProvides, Interface
+
+from pyams_content import _
+
+
+#
+# Custom marker interfaces
+#
+
+class IMenuLinksView(Interface):
+    """Menu links view marker interface"""
+
+
+class IMenusView(Interface):
+    """Menus view marker interface"""
+
+
+#
+# Menus add and edit forms
+#
+
+@viewlet_config(name='add-menu.action', context=IMenusContainerTarget, layer=IPyAMSLayer,
+                view=IMenusView, manager=IWidgetTitleViewletManager, weight=10)
+class MenuAddAction(ToolbarAction):
+    """Menu add action"""
+
+    label = _("Add menu...")
+    url = 'add-menu.html'
+    modal_target = True
+
+
+@pagelet_config(name='add-menu.html', context=IMenusContainer, layer=IPyAMSLayer,
+                permission=MANAGE_TEMPLATE_PERMISSION)
+@ajax_config(name='add-menu.json', context=IMenusContainer, layer=IPyAMSLayer, base=AJAXAddForm)
+class MenuAddForm(AdminDialogAddForm):
+    """Menu add form"""
+
+    legend = _("Add new menu")
+    icon_css_class = 'fa fa-fw fa-bars'
+
+    fields = field.Fields(IMenu).select('title')
+    edit_permission = MANAGE_TEMPLATE_PERMISSION
+
+    def create(self, data):
+        return Menu()
+
+    def add(self, object):
+        self.context.append(object)
+
+    def get_ajax_output(self, changes):
+        settings = get_parent(self.context, IMenusContainerTarget)
+        view = self.request.registry.getMultiAdapter((self.context, self.request), IMenusView,
+                                                     name=self.context.__name__)
+        return {
+            'status': 'success',
+            'message': self.request.localizer.translate(_("Menu was correctly added.")),
+            'events': [
+                get_json_switched_table_refresh_event(settings, self.request, view.table_class)
+            ]
+        }
+
+
+@pagelet_config(name='properties.html', context=IMenu, layer=IPyAMSLayer, permission=MANAGE_TEMPLATE_PERMISSION)
+@ajax_config(name='properties.json', context=IMenu, layer=IPyAMSLayer)
+class MenuPropertiesEditForm(AdminDialogEditForm):
+    """Menu properties edit form"""
+
+    legend = _("Edit menu properties")
+    icon_css_class = 'fa fa-fw fa-bars'
+
+    fields = field.Fields(IMenu).select('title')
+    edit_permission = MANAGE_TEMPLATE_PERMISSION
+
+    def get_ajax_output(self, changes):
+        output = super(self.__class__, self).get_ajax_output(changes)
+        if changes:
+            settings = get_parent(self.context, IMenusContainerTarget)
+            container = settings.menus
+            view = self.request.registry.getMultiAdapter((container, self.request), IMenusView,
+                                                         name=container.__name__)
+            output.setdefault('events', []).append(
+                get_json_table_row_refresh_event(settings, self.request, view.table_class, self.context))
+        return output
+
+
+#
+# Menus table views
+#
+
+class MenusTable(BaseTable):
+    """Menus table"""
+
+    prefix = 'menus'
+    associations_name = 'menus'
+
+    permission = MANAGE_TEMPLATE_PERMISSION
+    hide_header = True
+    hide_body_toolbar = True
+    sortOn = None
+
+    @property
+    def cssClasses(self):
+        classes = ['table', 'table-bordered', 'table-striped', 'table-hover', 'table-tight']
+        permission = self.permission
+        if (not permission) or self.request.has_permission(permission, self.context):
+            classes.append('table-dnd')
+        return {'table': ' '.join(classes)}
+
+    @property
+    def data_attributes(self):
+        menus = getattr(self.context, self.associations_name)
+        attributes = super(MenusTable, self).data_attributes
+        attributes.setdefault('table', {}).update({
+            'id': self.id,
+            'data-ams-location': absolute_url(menus, self.request),
+            'data-ams-tablednd-drag-handle': 'td.sorter',
+            'data-ams-tablednd-drop-target': 'set-menus-order.json',
+            'data-ams-visibility-switcher': 'switch-menu-visibility.json'
+        })
+        return attributes
+
+
+@adapter_config(context=(IMenusContainerTarget, IPyAMSLayer, MenusTable), provides=IValues)
+class MenusTableValuesAdapter(ContextRequestViewAdapter):
+    """Menus table values adapter"""
+
+    @property
+    def values(self):
+        return getattr(self.context, self.view.associations_name).values()
+
+
+@adapter_config(name='sorter', context=(IMenusContainerTarget, IPyAMSLayer, MenusTable),
+                provides=IColumn)
+class MenusTableSorterColumn(SorterColumn):
+    """Menus table sorter column"""
+
+    permission = MANAGE_TEMPLATE_PERMISSION
+
+
+@adapter_config(name='show-hide', context=(IMenusContainerTarget, IPyAMSLayer, MenusTable),
+                provides=IColumn)
+class MenusTableShowHideColumn(VisibilitySwitcherColumn):
+    """Menus table visibility switcher column"""
+
+    permission = MANAGE_TEMPLATE_PERMISSION
+
+
+@adapter_config(name='name', context=(IMenusContainerTarget, IPyAMSLayer, MenusTable), provides=IColumn)
+class MenusTableNameColumn(I18nColumn, I18nAttrColumn):
+    """Menus table name column"""
+
+    _header = _("Label")
+    attrName = 'title'
+    weight = 10
+
+    def renderCell(self, item):
+        return '<span data-ams-stop-propagation="true" ' \
+               '      data-ams-click-handler="MyAMS.skin.switchCellContent" ' \
+               '      data-ams-switch-handler="get-menu-items.json" ' \
+               '      data-ams-switch-target=".menus">' \
+               '    <span class="small hint" title="{hint}" data-ams-hint-gravity="e">' \
+               '        <i class="fa fa-plus-square-o switch"></i>' \
+               '    </span>' \
+               '</span>&nbsp;&nbsp;&nbsp;<span class="title">{title}</span>' \
+               '<div class="inner-table-form menus margin-x-10 margin-bottom-0 padding-left-5"></div>'.format(
+            hint=self.request.localizer.translate(_("Click to see menu items")),
+            title=super(MenusTableNameColumn, self).renderCell(item))
+
+
+@adapter_config(name='trash', context=(IMenusContainerTarget, IPyAMSLayer, MenusTable), provides=IColumn)
+class MenusTableTrashColumn(TrashColumn):
+    """Menus table trash column"""
+
+    permission = MANAGE_TEMPLATE_PERMISSION
+
+
+@implementer(IMenusView)
+class MenusView(InnerTableView):
+    """Menus view"""
+
+    table_class = MenusTable
+
+    @property
+    def actions_context(self):  # define context for internal actions
+        return self.request.registry.getAdapter(self.context, IMenusContainer,
+                                                name=self.table.associations_name)
+
+
+#
+# Menus container views
+#
+
+@view_config(name='set-menus-order.json', context=IMenusContainer, request_type=IPyAMSLayer,
+             permission=MANAGE_TEMPLATE_PERMISSION, renderer='json', xhr=True)
+def set_menus_order(request):
+    """Update menus order"""
+    order = list(map(str, json.loads(request.params.get('names'))))
+    request.context.updateOrder(order)
+    return {'status': 'success'}
+
+
+@view_config(name='switch-menu-visibility.json', context=IMenusContainer, request_type=IPyAMSLayer,
+             permission=MANAGE_TEMPLATE_PERMISSION, renderer='json', xhr=True)
+def set_menu_visibility(request):
+    """Set menu visibility"""
+    return switch_element_visibility(request, IMenusContainer)
+
+
+@view_config(name='delete-element.json', context=IMenusContainer, request_type=IPyAMSLayer,
+             permission=MANAGE_TEMPLATE_PERMISSION, renderer='json', xhr=True)
+def delete_menu(request):
+    """Delete menu"""
+    return delete_container_element(request, ignore_permission=True)
+
+
+@view_config(name='get-menu-items.json', context=IMenusContainer, request_type=IPyAMSLayer,
+             permission=MANAGE_TEMPLATE_PERMISSION, renderer='json', xhr=True)
+def get_menu_items_table(request):
+    """Get menu items table"""
+    menu = request.context.get(str(request.params.get('object_name')))
+    if menu is None:
+        raise NotFound()
+    table = MenuLinksTable(menu, request)
+    table.update()
+    return table.render()
+
+
+#
+# Menu links table
+#
+
+class LinksTable(AssociationsTable):
+    """Links base table class"""
+
+    associations_name = None
+
+    @reify
+    def id(self):
+        if IMenu.providedBy(self.context):
+            context = self.context
+        else:
+            context = self.request.registry.getAdapter(self.context, IMenuLinksContainer,
+                                                       name=self.associations_name)
+        return get_table_id(self, context)
+
+    @property
+    def prefix(self):
+        return '{0}_links'.format(self.associations_name)
+
+    permission = MANAGE_TEMPLATE_PERMISSION
+    hide_header = True
+    hide_body_toolbar = True
+
+
+class MenuLinksTable(LinksTable):
+    """Menu links associations table"""
+
+    prefix = 'menu_links'
+    associations_name = ''
+
+    @property
+    def data_attributes(self):
+        attributes = super(LinksTable, self).data_attributes
+        attributes.setdefault('table', {}).update({
+            'data-ams-location': absolute_url(self.context, self.request),
+        })
+        attributes.setdefault('tr', {}).update({'data-ams-stop-propagation': 'true'})
+        return attributes
+
+
+@adapter_config(name='name', context=(IMenu, IPyAMSLayer, MenuLinksTable), provides=IColumn)
+class MenuLinksTableNameColumn(AssociationsTablePublicNameColumn):
+    """Menu links table name column"""
+
+    def renderHeadCell(self):
+        result = super(MenuLinksTableNameColumn, self).renderHeadCell()
+        registry = self.request.registry
+        viewlet = registry.queryMultiAdapter((self.context, self.request, self.table), IViewletManager,
+                                             name='pyams.widget_title')
+        if viewlet is not None:
+            viewlet.update()
+            result += viewlet.render()
+        return result
+
+
+@adapter_config(context=(IMenu, IPyAMSLayer), provides=IMenuLinksView)
+@implementer(IMenuLinksView)
+class MenuLinksView(InnerTableView):
+    """Links base view"""
+
+    table_class = MenuLinksTable
+
+    @property
+    def actions_context(self):  # define context for internal actions
+        return self.request.registry.getAdapter(self.context, IMenuLinksContainer,
+                                                name=self.table.associations_name)
+
+
+#
+# Menu links container views
+#
+
+@view_config(name='set-associations-order.json', context=IMenuLinksContainer, request_type=IPyAMSLayer,
+             permission=MANAGE_TEMPLATE_PERMISSION, renderer='json', xhr=True)
+def set_associations_order(request):
+    """Update associations order"""
+    order = list(map(str, json.loads(request.params.get('names'))))
+    request.context.updateOrder(order)
+    return {'status': 'success'}
+
+
+@view_config(name='switch-association-visibility.json', context=IMenuLinksContainer, request_type=IPyAMSLayer,
+             permission=MANAGE_TEMPLATE_PERMISSION, renderer='json', xhr=True)
+def set_association_visibility(request):
+    """Set association visibility"""
+    return switch_element_visibility(request, IMenuLinksContainer)
+
+
+#
+# Link add and edit forms
+#
+
+@adapter_config(context=IMenuLink, provides=IFormContextPermissionChecker)
+class MenuLinkPermissionChecker(ContextAdapter):
+    """Menu link permission checker"""
+
+    edit_permission = MANAGE_TEMPLATE_PERMISSION
+
+
+class LinkAJAXAddForm(AJAXAddForm):
+    """Menu link add form, JSON renderer"""
+
+    def get_ajax_output(self, changes):
+        registry = self.request.registry
+        container = get_parent(self.context, IMenuLinksContainer)
+        view = registry.queryMultiAdapter((container, self.request), IMenuLinksView,
+                                          name=container.__name__)
+        if view is None:
+            view = registry.getMultiAdapter((container, self.request), IMenuLinksView)
+        return {
+            'status': 'success',
+            'message': self.request.localizer.translate(_("Link was correctly added.")),
+            'events': [
+                get_json_switched_table_refresh_event(self.context, self.request, view.table_class)
+            ]
+        }
+
+
+class LinkPropertiesAJAXEditForm(AJAXEditForm):
+    """Menu link properties edit form, JSON renderer"""
+
+    def get_ajax_output(self, changes):
+        output = AJAXEditForm.get_ajax_output(self, changes)
+        if changes:
+            registry = self.request.registry
+            container = get_parent(self.context, IMenuLinksContainer)
+            view = registry.queryMultiAdapter((container, self.request), IMenuLinksView,
+                                              name=container.__name__)
+            if view is None:
+                view = registry.getMultiAdapter((container, self.request), IMenuLinksView)
+            output.setdefault('events', []).append(
+                get_json_table_row_refresh_event(container, self.request, view.table_class, self.context))
+        return output
+
+
+#
+# Internal links
+#
+
+@viewlet_config(name='add-internal-link.menu', context=IMenuLinksContainerTarget, layer=IPyAMSLayer,
+                view=IMenuLinksView, manager=IToolbarAddingMenu, weight=50)
+@viewlet_config(name='add-internal-link.menu', context=IMenu, layer=IPyAMSLayer,
+                view=MenuLinksTable, manager=IToolbarAddingMenu, permission=MANAGE_TEMPLATE_PERMISSION, weight=50)
+class MenuInternalLinkAddMenu(InternalLinkAddMenu):
+    """Header internal link add menu"""
+
+
+@pagelet_config(name='add-internal-link.html', context=IMenuLinksContainer, layer=IPyAMSLayer,
+                permission=MANAGE_TEMPLATE_PERMISSION)
+@ajax_config(name='add-internal-link.json', context=IMenuLinksContainer, layer=IPyAMSLayer,
+             base=LinkAJAXAddForm)
+class MenuInternalLinkAddForm(InternalLinkAddForm):
+    """Menu internal link add form"""
+
+    edit_permission = MANAGE_TEMPLATE_PERMISSION
+
+    def create(self, data):
+        result = super(MenuInternalLinkAddForm, self).create(data)
+        alsoProvides(result, IMenuInternalLink)
+        return result
+
+    def add(self, object):
+        self.context.append(object)
+
+
+@pagelet_config(name='properties.html', context=IMenuInternalLink, layer=IPyAMSLayer,
+                permission=VIEW_SYSTEM_PERMISSION)
+@ajax_config(name='properties.json', context=IMenuInternalLink, layer=IPyAMSLayer,
+             base=LinkPropertiesAJAXEditForm)
+class MenuInternalLinkPropertiesEditForm(InternalLinkPropertiesEditForm):
+    """Menu internal link properties edit form"""
+
+    edit_permission = None  # managed by IFormContextPermissionChecker adapter
+
+
+#
+# External links
+#
+
+@viewlet_config(name='add-external-link.menu', context=IMenuLinksContainerTarget, view=IMenuLinksView,
+                layer=IPyAMSLayer, manager=IToolbarAddingMenu, weight=51)
+@viewlet_config(name='add-external-link.menu', context=IMenu, layer=IPyAMSLayer,
+                view=MenuLinksTable, manager=IToolbarAddingMenu, permission=MANAGE_TEMPLATE_PERMISSION, weight=51)
+class MenuExternalLinkAddMenu(ExternalLinkAddMenu):
+    """Menu external link add menu"""
+
+
+@pagelet_config(name='add-external-link.html', context=IMenuLinksContainer, layer=IPyAMSLayer,
+                permission=MANAGE_TEMPLATE_PERMISSION)
+@ajax_config(name='add-external-link.json', context=IMenuLinksContainer, layer=IPyAMSLayer,
+             base=LinkAJAXAddForm)
+class MenuExternalLinkAddForm(ExternalLinkAddForm):
+    """Menu external link add form"""
+
+    edit_permission = MANAGE_TEMPLATE_PERMISSION
+
+    def create(self, data):
+        result = super(MenuExternalLinkAddForm, self).create(data)
+        alsoProvides(result, IMenuExternalLink)
+        return result
+
+    def add(self, object):
+        self.context.append(object)
+
+
+@pagelet_config(name='properties.html', context=IMenuExternalLink, layer=IPyAMSLayer,
+                permission=VIEW_SYSTEM_PERMISSION)
+@ajax_config(name='properties.json', context=IMenuExternalLink, layer=IPyAMSLayer,
+             base=LinkPropertiesAJAXEditForm)
+class MenuExternalLinkPropertiesEditForm(ExternalLinkPropertiesEditForm):
+    """Menu external link properties edit form"""
+
+    edit_permission = None  # managed by IFormContextPermissionChecker adapter