# HG changeset patch # User Thierry Florac # Date 1528882218 -7200 # Node ID ac6c59149f0d9218fb5a0c005df1e8f9f396d9b2 # Parent 4e43b556a2a9967dd4436f419d12cb1b009c4c16 Added generic menu feature diff -r 4e43b556a2a9 -r ac6c59149f0d src/pyams_content/features/menu/__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 +# 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()) diff -r 4e43b556a2a9 -r ac6c59149f0d src/pyams_content/features/menu/interfaces/__init__.py --- /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 +# 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""" diff -r 4e43b556a2a9 -r ac6c59149f0d src/pyams_content/features/menu/zmi/__init__.py --- /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 +# 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 '' \ + ' ' \ + ' ' \ + ' ' \ + '   {title}' \ + ''.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