--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/association/__init__.py Mon Sep 11 14:54:30 2017 +0200
@@ -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_content.component.association.interfaces import IAssociationItem
+from pyams_content.shared.common.interfaces import IWfSharedContent
+from pyams_form.interfaces.form import IFormContextPermissionChecker
+from zope.lifecycleevent.interfaces import IObjectAddedEvent, IObjectModifiedEvent, IObjectRemovedEvent
+
+# import packages
+from persistent import Persistent
+from pyams_utils.adapter import adapter_config, ContextAdapter
+from pyams_utils.traversing import get_parent
+from pyams_utils.url import absolute_url
+from pyramid.events import subscriber
+from pyramid.threadlocal import get_current_registry
+from zope.container.contained import Contained
+from zope.interface import implementer
+from zope.lifecycleevent import ObjectModifiedEvent
+from zope.schema.fieldproperty import FieldProperty
+
+
+@implementer(IAssociationItem)
+class AssociationItem(Persistent, Contained):
+ """Base association item persistent class"""
+
+ icon_class = ''
+ icon_hint = ''
+
+ visible = FieldProperty(IAssociationItem['visible'])
+
+ def get_url(self, request=None, view_name=None):
+ return absolute_url(self, request, view_name)
+
+
+@adapter_config(context=IAssociationItem, provides=IFormContextPermissionChecker)
+class AssociationItemPermissionChecker(ContextAdapter):
+ """Association item permission checker"""
+
+ @property
+ def edit_permission(self):
+ content = get_parent(self.context, IWfSharedContent)
+ return IFormContextPermissionChecker(content).edit_permission
+
+
+@subscriber(IObjectAddedEvent, context_selector=IAssociationItem)
+def handle_added_association(event):
+ """Handle added association item"""
+ content = get_parent(event.object, IWfSharedContent)
+ if content is not None:
+ get_current_registry().notify(ObjectModifiedEvent(content))
+
+
+@subscriber(IObjectModifiedEvent, context_selector=IAssociationItem)
+def handle_modified_association(event):
+ """Handle modified association item"""
+ content = get_parent(event.object, IWfSharedContent)
+ if content is not None:
+ get_current_registry().notify(ObjectModifiedEvent(content))
+
+
+@subscriber(IObjectRemovedEvent, context_selector=IAssociationItem)
+def handle_removed_association(event):
+ """Handle removed association item"""
+ content = get_parent(event.object, IWfSharedContent)
+ if content is not None:
+ get_current_registry().notify(ObjectModifiedEvent(content))
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/association/container.py Mon Sep 11 14:54:30 2017 +0200
@@ -0,0 +1,95 @@
+#
+# 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_content.component.association.interfaces import IAssociationContainer, IAssociationTarget, \
+ ASSOCIATION_CONTAINER_KEY, IAssociationInfo
+from pyams_i18n.interfaces import II18n
+from zope.annotation.interfaces import IAnnotations
+from zope.location.interfaces import ISublocations
+from zope.traversing.interfaces import ITraversable
+
+# import packages
+from pyams_catalog.utils import index_object
+from pyams_utils.adapter import adapter_config, ContextAdapter
+from pyams_utils.traversing import get_parent
+from pyams_utils.vocabulary import vocabulary_config
+from pyramid.threadlocal import get_current_registry
+from zope.container.ordered import OrderedContainer
+from zope.interface import implementer
+from zope.lifecycleevent import ObjectCreatedEvent
+from zope.location import locate
+from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm
+
+
+@implementer(IAssociationContainer)
+class AssociationContainer(OrderedContainer):
+ """Associations container"""
+
+ last_id = 1
+
+ def append(self, value, notify=True):
+ key = str(self.last_id)
+ if not notify:
+ # pre-locate association item to avoid multiple notifications
+ locate(value, self, key)
+ self[key] = value
+ self.last_id += 1
+ if not notify:
+ # make sure that association item is correctly indexed
+ index_object(value)
+
+
+@adapter_config(context=IAssociationTarget, provides=IAssociationContainer)
+def association_container_factory(target):
+ """Associations container factory"""
+ annotations = IAnnotations(target)
+ container = annotations.get(ASSOCIATION_CONTAINER_KEY)
+ if container is None:
+ container = annotations[ASSOCIATION_CONTAINER_KEY] = AssociationContainer()
+ get_current_registry().notify(ObjectCreatedEvent(container))
+ locate(container, target, '++ass++')
+ return container
+
+
+@adapter_config(name='ass', context=IAssociationTarget, provides=ITraversable)
+class AssociationContainerNamespace(ContextAdapter):
+ """Associations container ++association++ namespace"""
+
+ def traverse(self, name, furtherpath=None):
+ return IAssociationContainer(self.context)
+
+
+@adapter_config(name='associations', context=IAssociationTarget, provides=ISublocations)
+class AssociationContainerSublocations(ContextAdapter):
+ """Associations container sub-locations adapter"""
+
+ def sublocations(self):
+ return IAssociationContainer(self.context).values()
+
+
+@vocabulary_config(name='PyAMS content associations')
+class ContentAssociationsVocabulary(SimpleVocabulary):
+ """Content associations vocabulary"""
+
+ def __init__(self, context=None):
+ terms = []
+ target = get_parent(context, IAssociationTarget)
+ if target is not None:
+ terms = [SimpleTerm(link.__name__, title=IAssociationInfo(link).inner_title)
+ for link in IAssociationContainer(target).values()]
+ super(ContentAssociationsVocabulary, self).__init__(terms)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/association/interfaces/__init__.py Mon Sep 11 14:54:30 2017 +0200
@@ -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_content.component.paragraph.interfaces import IBaseParagraph
+from zope.annotation.interfaces import IAttributeAnnotatable
+from zope.container.interfaces import IOrderedContainer
+
+# import packages
+from zope.container.constraints import containers, contains
+from zope.interface import Interface, Attribute
+from zope.schema import Bool
+
+from pyams_content import _
+
+
+ASSOCIATION_CONTAINER_KEY = 'pyams_content.associations'
+
+
+class IAssociationItem(IAttributeAnnotatable):
+ """Base association item interface"""
+
+ containers('.IAssociationContainer')
+
+ icon_class = Attribute("Icon class in associations list")
+ icon_hint = Attribute("Icon hint in associations list")
+
+ visible = Bool(title=_("Visible?"),
+ description=_("Is this item visible in front-office?"),
+ required=True,
+ default=True)
+
+ def get_url(self, request=None, view_name=None):
+ """Get link URL"""
+
+
+class IAssociationInfo(Interface):
+ """Association information interface"""
+
+ pictogram = Attribute("Association pictogram")
+
+ user_title = Attribute("Association title proposed on public site")
+
+ inner_title = Attribute("Inner content, if available")
+
+ human_size = Attribute("Content size, if available")
+
+
+class IAssociationContainer(IOrderedContainer):
+ """Associations container interface"""
+
+ contains(IAssociationItem)
+
+ def append(self, value, notify=True):
+ """Append given value to container"""
+
+
+class IAssociationTarget(IAttributeAnnotatable):
+ """Associations container target interface"""
+
+
+class IAssociationRenderer(Interface):
+ """Association renderer adapter interface"""
+
+
+class IAssociationParagraph(IBaseParagraph):
+ """Associations paragraph interface"""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/association/paragraph.py Mon Sep 11 14:54:30 2017 +0200
@@ -0,0 +1,45 @@
+#
+# 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_content.component.association.interfaces import IAssociationParagraph
+from pyams_content.component.extfile.interfaces import IExtFileContainerTarget
+from pyams_content.component.links.interfaces import ILinkContainerTarget
+from pyams_content.component.paragraph.interfaces import IParagraphFactory
+
+# import packages
+from pyams_content.component.paragraph import BaseParagraph
+from pyams_utils.registry import utility_config
+from zope.interface import implementer
+
+from pyams_content import _
+
+
+@implementer(IAssociationParagraph, IExtFileContainerTarget, ILinkContainerTarget)
+class AssociationParagraph(BaseParagraph):
+ """Associations paragraph"""
+
+ icon_class = 'fa-link'
+ icon_hint = _("Associations paragraph")
+
+
+@utility_config(name='Associations paragraph', provides=IParagraphFactory)
+class AssociationParagraphFactory(object):
+ """Associations paragraph factory"""
+
+ name = _("Associations paragraph")
+ content_type = AssociationParagraph
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/association/zmi/__init__.py Mon Sep 11 14:54:30 2017 +0200
@@ -0,0 +1,299 @@
+#
+# 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.component.association.interfaces import IAssociationTarget, IAssociationContainer, IAssociationInfo
+from pyams_content.component.association.zmi.interfaces import IAssociationsParentForm, IAssociationsView
+from pyams_content.interfaces import MANAGE_CONTENT_PERMISSION
+from pyams_form.interfaces.form import IInnerSubForm
+from pyams_skin.interfaces import IInnerPage
+from pyams_skin.layer import IPyAMSLayer
+from pyams_utils.interfaces import VIEW_SYSTEM_PERMISSION
+from pyams_zmi.interfaces.menu import IPropertiesMenu
+from z3c.table.interfaces import IValues, IColumn
+
+# import packages
+from pyams_form.form import AJAXAddForm, AJAXEditForm
+from pyams_form.security import ProtectedFormObjectMixin
+from pyams_pagelet.pagelet import pagelet_config, Pagelet
+from pyams_skin.table import BaseTable, SorterColumn, JsActionColumn, NameColumn, ImageColumn, I18nColumn, TrashColumn
+from pyams_skin.viewlet.menu import MenuItem
+from pyams_template.template import template_config
+from pyams_utils.adapter import adapter_config, ContextRequestViewAdapter
+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 InnerAdminDisplayForm
+from pyams_zmi.view import AdminView
+from pyramid.decorator import reify
+from pyramid.exceptions import NotFound
+from pyramid.view import view_config
+from z3c.form import field
+from z3c.table.column import GetAttrColumn
+from zope.interface import implementer, Interface
+
+from pyams_content import _
+
+
+#
+# Association item base forms
+#
+
+class AssociationItemAJAXAddForm(AJAXAddForm):
+ """Association item add form, JSON renderer"""
+
+ def get_ajax_output(self, changes):
+ associations_table = AssociationsTable(self.context, self.request, None)
+ associations_table.update()
+ return {'status': 'success',
+ 'message': self.request.localizer.translate(_("Association was correctly added.")),
+ 'callback': 'PyAMS_content.associations.afterUpdateCallback',
+ 'options': {'parent': associations_table.id,
+ 'table': associations_table.render()}}
+
+
+class AssociationItemAJAXEditForm(AJAXEditForm):
+ """Association item properties edit form, JSON renderer"""
+
+ def get_associations_table(self):
+ target = get_parent(self.context, IAssociationTarget)
+ associations_table = AssociationsTable(target, self.request, None)
+ associations_table.update()
+ return {'status': 'success',
+ 'message': self.request.localizer.translate(self.successMessage),
+ 'callback': 'PyAMS_content.associations.afterUpdateCallback',
+ 'options': {'parent': associations_table.id,
+ 'table': associations_table.render()}}
+
+
+#
+# Content associations view
+#
+
+@viewlet_config(name='associations.menu', context=IAssociationTarget, layer=IPyAMSLayer,
+ manager=IPropertiesMenu, permission=VIEW_SYSTEM_PERMISSION, weight=20)
+class AssociationsMenu(MenuItem):
+ """Associations menu"""
+
+ label = _("Associations...")
+ icon_class = 'fa-link'
+ url = '#associations.html'
+
+
+@pagelet_config(name='associations.html', context=IAssociationTarget, layer=IPyAMSLayer,
+ permission=VIEW_SYSTEM_PERMISSION)
+@template_config(template='templates/associations-view.pt', layer=IPyAMSLayer)
+@implementer(IInnerPage, IAssociationsView)
+class AssociationsContainerView(AdminView, Pagelet):
+ """Associations container view"""
+
+ title = _("Associations list")
+
+ def __init__(self, context, request):
+ super(AssociationsContainerView, self).__init__(context, request)
+ self.table = AssociationsTable(context, request, self)
+
+ def update(self):
+ super(AssociationsContainerView, self).update()
+ self.table.update()
+
+
+@adapter_config(name='associations', context=(IAssociationTarget, IPyAMSLayer, IAssociationsParentForm),
+ provides=IInnerSubForm)
+@template_config(template='templates/associations.pt', layer=IPyAMSLayer)
+@implementer(IAssociationsView)
+class AssociationsView(InnerAdminDisplayForm):
+ """Associations view"""
+
+ fields = field.Fields(Interface)
+ weight = 90
+
+ def __init__(self, context, request, view):
+ super(AssociationsView, self).__init__(context, request, view)
+ self.table = AssociationsTable(context, request, self)
+
+ def update(self):
+ super(AssociationsView, self).update()
+ self.table.update()
+
+
+class AssociationsTable(ProtectedFormObjectMixin, BaseTable):
+ """Associations view inner table"""
+
+ @property
+ def id(self):
+ return 'associations_{0}_list'.format(self.context.__name__)
+
+ hide_header = True
+ sortOn = None
+
+ def __init__(self, context, request, view):
+ super(AssociationsTable, self).__init__(context, request)
+ self.view = view
+
+ @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):
+ attributes = super(AssociationsTable, self).data_attributes
+ attributes['table'] = {'id': self.id,
+ 'data-ams-plugins': 'pyams_content',
+ 'data-ams-plugin-pyams_content-src':
+ '/--static--/pyams_content/js/pyams_content{MyAMS.devext}.js',
+ 'data-ams-location': absolute_url(IAssociationContainer(self.context), self.request),
+ 'data-ams-tablednd-drag-handle': 'td.sorter',
+ 'data-ams-tablednd-drop-target': 'set-associations-order.json'}
+ return attributes
+
+ @reify
+ def values(self):
+ return list(super(AssociationsTable, self).values)
+
+
+@adapter_config(context=(IAssociationTarget, IPyAMSLayer, AssociationsTable), provides=IValues)
+class AssociationsTableValuesAdapter(ContextRequestViewAdapter):
+ """Associations table values adapter"""
+
+ @property
+ def values(self):
+ return IAssociationContainer(self.context).values()
+
+
+@adapter_config(name='sorter', context=(IAssociationTarget, IPyAMSLayer, AssociationsTable), provides=IColumn)
+class AssociationsTableSorterColumn(ProtectedFormObjectMixin, SorterColumn):
+ """Associations table sorter column"""
+
+
+@view_config(name='set-associations-order.json', context=IAssociationContainer, request_type=IPyAMSLayer,
+ permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
+def set_associations_order(request):
+ """Update asociations order"""
+ order = list(map(str, json.loads(request.params.get('names'))))
+ request.context.updateOrder(order)
+ return {'status': 'success'}
+
+
+@adapter_config(name='show-hide', context=(IAssociationTarget, IPyAMSLayer, AssociationsTable),
+ provides=IColumn)
+class AssociationsTableShowHideColumn(ProtectedFormObjectMixin, JsActionColumn):
+ """Associations container visibility switcher column"""
+
+ cssClasses = {'th': 'action',
+ 'td': 'action switcher'}
+
+ icon_class = 'fa fa-fw fa-eye'
+ icon_hint = _("Switch association visibility")
+
+ url = 'PyAMS_content.associations.switchVisibility'
+
+ weight = 5
+
+ def get_icon(self, item):
+ if item.visible:
+ icon_class = 'fa fa-fw fa-eye'
+ else:
+ icon_class = 'fa fa-fw fa-eye-slash text-danger'
+ return '<i class="{icon_class}"></i>'.format(icon_class=icon_class)
+
+ def renderCell(self, item):
+ if self.permission and not self.request.has_permission(self.permission, context=item):
+ return self.get_icon(item)
+ else:
+ return super(AssociationsTableShowHideColumn, self).renderCell(item)
+
+
+@view_config(name='set-association-visibility.json', context=IAssociationContainer, request_type=IPyAMSLayer,
+ permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
+def set_paragraph_visibility(request):
+ """Set paragraph visibility"""
+ container = IAssociationContainer(request.context)
+ association = container.get(str(request.params.get('object_name')))
+ if association is None:
+ raise NotFound()
+ association.visible = not association.visible
+ return {'visible': association.visible}
+
+
+@adapter_config(name='pictogram', context=(IAssociationTarget, IPyAMSLayer, AssociationsTable), provides=IColumn)
+class AssociationsTablePictogramColumn(ImageColumn):
+ """Associations table pictogram column"""
+
+ weight = 8
+
+ def get_icon_class(self, item):
+ info = IAssociationInfo(item, None)
+ if info is not None:
+ return info.pictogram
+
+ def get_icon_hint(self, item):
+ return self.request.localizer.translate(item.icon_hint)
+
+
+@adapter_config(name='name', context=(IAssociationTarget, IPyAMSLayer, AssociationsTable), provides=IColumn)
+class AssociationsTablePublicNameColumn(NameColumn):
+ """Associations table name column"""
+
+ _header = _("Public title")
+
+ def getValue(self, obj):
+ info = IAssociationInfo(obj, None)
+ if info is not None:
+ return info.user_title
+ else:
+ return '--'
+
+
+@adapter_config(name='inner_name', context=(IAssociationTarget, IPyAMSLayer, AssociationsTable), provides=IColumn)
+class AssociationsTableInnerNameColumn(I18nColumn, GetAttrColumn):
+ """Associations table inner name column"""
+
+ _header = _("Inner title")
+ weight = 20
+
+ def getValue(self, obj):
+ info = IAssociationInfo(obj, None)
+ if info is not None:
+ return info.inner_title
+ else:
+ return '--'
+
+
+@adapter_config(name='size', context=(IAssociationTarget, IPyAMSLayer, AssociationsTable), provides=IColumn)
+class AssociationsTableSizeColumn(I18nColumn, GetAttrColumn):
+ """Associations table size column"""
+
+ _header = _("Size")
+ weight = 30
+
+ def getValue(self, obj):
+ info = IAssociationInfo(obj, None)
+ if info is not None:
+ return info.human_size
+ else:
+ return '--'
+
+
+@adapter_config(name='trash', context=(IAssociationTarget, IPyAMSLayer, AssociationsTable), provides=IColumn)
+class AssociationsTableTrashColumn(ProtectedFormObjectMixin, TrashColumn):
+ """Associations table trash column"""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/association/zmi/interfaces.py Mon Sep 11 14:54:30 2017 +0200
@@ -0,0 +1,29 @@
+#
+# 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
+
+# import packages
+from zope.interface import Interface
+
+
+class IAssociationsView(Interface):
+ """Associations view marker interface"""
+
+
+class IAssociationsParentForm(Interface):
+ """Associations view parent form marker interface"""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/association/zmi/paragraph.py Mon Sep 11 14:54:30 2017 +0200
@@ -0,0 +1,156 @@
+#
+# 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_content.component.association.interfaces import IAssociationParagraph, IAssociationContainer, \
+ IAssociationInfo
+from pyams_content.component.association.zmi.interfaces import IAssociationsParentForm
+from pyams_content.component.paragraph.interfaces import IParagraphContainerTarget, IParagraphContainer, \
+ IParagraphSummary
+from pyams_content.component.paragraph.zmi.interfaces import IParagraphInnerEditor
+from pyams_content.interfaces import MANAGE_CONTENT_PERMISSION
+from pyams_content.shared.common.interfaces import IWfSharedContent
+from pyams_form.interfaces.form import IInnerForm, IEditFormButtons
+from pyams_i18n.interfaces import II18n
+from pyams_skin.interfaces.viewlet import IToolbarAddingMenu
+from pyams_skin.layer import IPyAMSLayer
+from z3c.form.interfaces import INPUT_MODE
+
+# import packages
+from pyams_content.component.association.paragraph import AssociationParagraph
+from pyams_content.component.paragraph.zmi import BaseParagraphAJAXEditForm
+from pyams_content.component.paragraph.zmi.container import ParagraphContainerView
+from pyams_form.form import AJAXAddForm
+from pyams_form.security import ProtectedFormObjectMixin
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_skin.viewlet.toolbar import ToolbarMenuItem
+from pyams_template.template import template_config, get_view_template
+from pyams_utils.adapter import adapter_config, ContextRequestAdapter
+from pyams_utils.traversing import get_parent
+from pyams_viewlet.viewlet import viewlet_config
+from pyams_zmi.form import AdminDialogAddForm, AdminDialogEditForm
+from pyramid.view import view_config
+from z3c.form import field, button
+from zope.interface import implementer
+
+from pyams_content import _
+
+
+@viewlet_config(name='add-association-paragraph.menu', context=IParagraphContainerTarget, view=ParagraphContainerView,
+ layer=IPyAMSLayer, manager=IToolbarAddingMenu, weight=95)
+class AssociationParagraphAddMenu(ProtectedFormObjectMixin, ToolbarMenuItem):
+ """Associations paragraph add menu"""
+
+ label = _("Add associations paragraph...")
+ label_css_class = 'fa fa-fw fa-link'
+ url = 'add-association-paragraph.html'
+ modal_target = True
+
+
+@pagelet_config(name='add-association-paragraph.html', context=IParagraphContainerTarget, layer=IPyAMSLayer,
+ permission=MANAGE_CONTENT_PERMISSION)
+class AssociationParagraphAddForm(AdminDialogAddForm):
+ """Association paragraph add form"""
+
+ legend = _("Add new association paragraph")
+ icon_css_class = 'fa fa-fw fa-link'
+
+ fields = field.Fields(IAssociationParagraph).select('title')
+ ajax_handler = 'add-association-paragraph.json'
+ edit_permission = MANAGE_CONTENT_PERMISSION
+
+ def create(self, data):
+ return AssociationParagraph()
+
+ def add(self, object):
+ IParagraphContainer(self.context).append(object)
+
+
+@view_config(name='add-association-paragraph.json', context=IParagraphContainerTarget, request_type=IPyAMSLayer,
+ permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
+class AssociationParagraphAJAXAddForm(AJAXAddForm, AssociationParagraphAddForm):
+ """Association paragraph add form, JSON renderer"""
+
+ def get_ajax_output(self, changes):
+ return {'status': 'reload',
+ 'location': '#paragraphs.html'}
+
+
+@pagelet_config(name='properties.html', context=IAssociationParagraph, layer=IPyAMSLayer,
+ permission=MANAGE_CONTENT_PERMISSION)
+class AssociationParagraphPropertiesEditForm(AdminDialogEditForm):
+ """Association paragraph properties edit form"""
+
+ @property
+ def title(self):
+ content = get_parent(self.context, IWfSharedContent)
+ return II18n(content).query_attribute('title', request=self.request)
+
+ legend = _("Edit association paragraph properties")
+ icon_css_class = 'fa fa-fw fa-link'
+
+ fields = field.Fields(IAssociationParagraph).select('title')
+ ajax_handler = 'properties.json'
+ edit_permission = MANAGE_CONTENT_PERMISSION
+
+
+@view_config(name='properties.json', context=IAssociationParagraph, request_type=IPyAMSLayer,
+ permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
+class AssociationParagraphPropertiesAJAXEditForm(BaseParagraphAJAXEditForm, AssociationParagraphPropertiesEditForm):
+ """Association paragraph properties edit form, JSON renderer"""
+
+
+@adapter_config(context=(IAssociationParagraph, IPyAMSLayer), provides=IParagraphInnerEditor)
+@implementer(IInnerForm, IAssociationsParentForm)
+class AssociationParagraphInnerEditForm(AssociationParagraphPropertiesEditForm):
+ """Association paragraph inner edit form"""
+
+ legend = None
+
+ @property
+ def buttons(self):
+ if self.mode == INPUT_MODE:
+ return button.Buttons(IEditFormButtons)
+ else:
+ return button.Buttons()
+
+
+#
+# Association paragraph summary
+#
+
+@adapter_config(context=(IAssociationParagraph, IPyAMSLayer), provides=IParagraphSummary)
+@template_config(template='templates/paragraph-summary.pt', layer=IPyAMSLayer)
+class AssociationParagraphSummary(ContextRequestAdapter):
+ """Association paragraph renderer"""
+
+ language = None
+ associations = None
+
+ def update(self):
+ i18n = II18n(self.context)
+ if self.language:
+ for attr in ('title', ):
+ setattr(self, attr, i18n.get_attribute(attr, self.language, request=self.request))
+ else:
+ for attr in ('title', ):
+ setattr(self, attr, i18n.query_attribute(attr, request=self.request))
+ self.associations = [{'url': item.get_url(self.request),
+ 'title': IAssociationInfo(item).user_title}
+ for item in IAssociationContainer(self.context).values() if item.visible]
+
+ render = get_view_template()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/association/zmi/templates/associations-view.pt Mon Sep 11 14:54:30 2017 +0200
@@ -0,0 +1,13 @@
+<div class="ams-widget">
+ <header>
+ <span tal:condition="view.widget_icon_class | nothing"
+ class="widget-icon"><i tal:attributes="class view.widget_icon_class"></i>
+ </span>
+ <h2 tal:content="view.title"></h2>
+ <tal:var content="structure provider:pyams.widget_title" />
+ <tal:var content="structure provider:pyams.toolbar" />
+ </header>
+ <div class="widget-body no-widget-toolbar">
+ <tal:var content="structure view.table.render()" />
+ </div>
+</div>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/association/zmi/templates/associations.pt Mon Sep 11 14:54:30 2017 +0200
@@ -0,0 +1,14 @@
+<div class="form-group" i18n:domain="pyams_content">
+ <fieldset class="margin-top-10 padding-top-5 padding-bottom-0">
+ <legend
+ class="inner switcher margin-bottom-5 padding-right-10 no-y-padding pull-left width-auto"
+ tal:attributes="data-ams-switcher-state 'open' if view.table.values else None">
+ <i18n:var translate="">Associations</i18n:var>
+ </legend>
+ <div class="pull-left">
+ <tal:var content="structure provider:pyams.widget_title" />
+ </div>
+ <div class="clearfix"></div>
+ <tal:var content="structure view.table.render()" />
+ </fieldset>
+</div>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/association/zmi/templates/paragraph-summary.pt Mon Sep 11 14:54:30 2017 +0200
@@ -0,0 +1,9 @@
+<i18n:var domain="pyams_content">
+ <h3 tal:content="i18n:title">§ title</h3>
+ <ul>
+ <li tal:repeat="item view.associations">
+ <a tal:attributes="href item['url']"
+ tal:content="item['title']" target="_blank">Link</a>
+ </li>
+ </ul>
+</i18n:var>
--- a/src/pyams_content/component/extfile/__init__.py Mon Sep 11 14:53:15 2017 +0200
+++ b/src/pyams_content/component/extfile/__init__.py Mon Sep 11 14:54:30 2017 +0200
@@ -16,21 +16,27 @@
# import standard library
# import interfaces
-from pyams_content.component.extfile.interfaces import IBaseExtFile, IExtFile, IExtImage, IExtVideo, IExtAudio
+from pyams_content.component.association.interfaces import IAssociationInfo
+from pyams_content.component.extfile.interfaces import IBaseExtFile, IExtFile, IExtImage, IExtVideo, IExtAudio, \
+ IExtMedia
from pyams_content.shared.common.interfaces import IWfSharedContent
+from pyams_file.interfaces import IFileInfo, IResponsiveImage, DELETED_FILE
from pyams_form.interfaces.form import IFormContextPermissionChecker
+from pyams_i18n.interfaces import II18n, INegotiator
from zope.lifecycleevent.interfaces import IObjectAddedEvent, IObjectModifiedEvent, IObjectRemovedEvent
# import packages
-from persistent import Persistent
+from pyams_content.component.association import AssociationItem
from pyams_i18n.property import I18nFileProperty
from pyams_utils.adapter import adapter_config, ContextAdapter
+from pyams_utils.registry import query_utility
+from pyams_utils.request import check_request
+from pyams_utils.size import get_human_size
from pyams_utils.traversing import get_parent
from pyams_utils.vocabulary import vocabulary_config
from pyramid.events import subscriber
from pyramid.threadlocal import get_current_registry
-from zope.container.contained import Contained
-from zope.interface import implementer
+from zope.interface import implementer, alsoProvides
from zope.lifecycleevent import ObjectModifiedEvent
from zope.schema.fieldproperty import FieldProperty
from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm
@@ -58,12 +64,39 @@
@implementer(IBaseExtFile)
-class BaseExtFile(Persistent, Contained):
+class BaseExtFile(AssociationItem):
"""External file persistent class"""
title = FieldProperty(IExtFile['title'])
description = FieldProperty(IExtFile['description'])
author = FieldProperty(IExtFile['author'])
+ language = FieldProperty(IExtFile['language'])
+ filename = FieldProperty(IExtFile['filename'])
+
+
+@adapter_config(context=IBaseExtFile, provides=IAssociationInfo)
+class BaseExtFileAssociationInfoAdapter(ContextAdapter):
+ """Base external file association info adapter"""
+
+ @property
+ def pictogram(self):
+ return self.context.icon_class
+
+ @property
+ def user_title(self):
+ return II18n(self.context).query_attribute('title') or self.context.filename
+
+ @property
+ def inner_title(self):
+ return self.context.filename or '--'
+
+ @property
+ def human_size(self):
+ data = II18n(self.context).query_attribute('data')
+ if data and data.data:
+ return get_human_size(data.get_size())
+ else:
+ return '--'
@adapter_config(context=IBaseExtFile, provides=IFormContextPermissionChecker)
@@ -76,10 +109,34 @@
return IFormContextPermissionChecker(content).edit_permission
+def update_properties(extfile):
+ """Update missing file properties"""
+ request = check_request()
+ i18n = query_utility(INegotiator)
+ if i18n is not None:
+ lang = i18n.server_language
+ data = II18n(extfile).get_attribute('data', lang, request)
+ if data:
+ info = IFileInfo(data)
+ info.title = II18n(extfile).get_attribute('title', lang, request)
+ info.description = II18n(extfile).get_attribute('description', lang, request)
+ if not extfile.filename:
+ extfile.filename = info.filename
+ else:
+ info.filename = extfile.filename
+ for lang, data in (extfile.data or {}).items():
+ if data is not None:
+ IFileInfo(data).language = lang
+
+
@subscriber(IObjectAddedEvent, context_selector=IBaseExtFile)
def handle_added_extfile(event):
"""Handle added external file"""
- content = get_parent(event.object, IWfSharedContent)
+ # update inner file properties
+ extfile = event.object
+ update_properties(extfile)
+ # notify content modification
+ content = get_parent(extfile, IWfSharedContent)
if content is not None:
get_current_registry().notify(ObjectModifiedEvent(content))
@@ -87,7 +144,11 @@
@subscriber(IObjectModifiedEvent, context_selector=IBaseExtFile)
def handle_modified_extfile(event):
"""Handle modified external file"""
- content = get_parent(event.object, IWfSharedContent)
+ # update inner file properties
+ extfile = event.object
+ update_properties(extfile)
+ # notify content modification
+ content = get_parent(extfile, IWfSharedContent)
if content is not None:
get_current_registry().notify(ObjectModifiedEvent(content))
@@ -104,6 +165,9 @@
class ExtFile(BaseExtFile):
"""Generic external file persistent class"""
+ icon_class = 'fa-file-o'
+ icon_hint = _("Standard file")
+
data = I18nFileProperty(IExtFile['data'])
register_file_factory('file', ExtFile, _("Standard file"))
@@ -113,7 +177,23 @@
class ExtImage(BaseExtFile):
"""External image persistent class"""
- data = I18nFileProperty(IExtImage['data'])
+ icon_class = 'fa-file-image-o'
+ icon_hint = _("Image")
+
+ title = FieldProperty(IExtMedia['title'])
+ alt_title = FieldProperty(IExtImage['alt_title'])
+ _data = I18nFileProperty(IExtImage['data'])
+
+ @property
+ def data(self):
+ return self._data
+
+ @data.setter
+ def data(self, value):
+ self._data = value
+ for data in value.values():
+ if (data is not None) and (data is not DELETED_FILE):
+ alsoProvides(data, IResponsiveImage)
register_file_factory('image', ExtImage, _("Image"))
@@ -122,6 +202,10 @@
class ExtVideo(BaseExtFile):
"""External video file persistent class"""
+ icon_class = 'fa-file-video-o'
+ icon_hint = _("Video")
+
+ title = FieldProperty(IExtMedia['title'])
data = I18nFileProperty(IExtVideo['data'])
register_file_factory('video', ExtVideo, _("Video"))
@@ -131,6 +215,10 @@
class ExtAudio(BaseExtFile):
"""External audio file persistent class"""
+ icon_class = 'fa-file-audio-o'
+ icon_hint = _("Audio file")
+
+ title = FieldProperty(IExtMedia['title'])
data = I18nFileProperty(IExtAudio['data'])
register_file_factory('audio', ExtAudio, _("Audio file"))
--- a/src/pyams_content/component/extfile/container.py Mon Sep 11 14:53:15 2017 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,146 +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_content.component.extfile.interfaces import IExtFileContainer, IExtFileContainerTarget, \
- EXTFILE_CONTAINER_KEY, IExtFileLinksContainer, IExtFileLinksContainerTarget, EXTFILE_LINKS_CONTAINER_KEY
-from pyams_file.interfaces import IMediaFile, IImage, IVideo, IAudio
-from pyams_i18n.interfaces import II18n
-from zope.annotation.interfaces import IAnnotations
-from zope.location.interfaces import ISublocations
-from zope.traversing.interfaces import ITraversable
-
-# import packages
-from persistent import Persistent
-from persistent.list import PersistentList
-from pyams_utils.adapter import adapter_config, ContextAdapter
-from pyams_utils.traversing import get_parent
-from pyams_utils.vocabulary import vocabulary_config
-from pyramid.threadlocal import get_current_registry
-from zope.container.contained import Contained
-from zope.container.folder import Folder
-from zope.interface import implementer
-from zope.lifecycleevent import ObjectCreatedEvent
-from zope.location import locate
-from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm
-
-
-#
-# External files container
-#
-
-@implementer(IExtFileContainer)
-class ExtFileContainer(Folder):
- """External files container"""
-
- last_id = 1
-
- def __setitem__(self, key, value):
- key = str(self.last_id)
- super(ExtFileContainer, self).__setitem__(key, value)
- self.last_id += 1
-
- @property
- def files(self):
- return (file for file in self.values() if not IMediaFile.providedBy(II18n(file).query_attribute('data')))
-
- @property
- def medias(self):
- return (file for file in self.values() if IMediaFile.providedBy(II18n(file).query_attribute('data')))
-
- @property
- def images(self):
- return (file for file in self.values() if IImage.providedBy(II18n(file).query_attribute('data')))
-
- @property
- def videos(self):
- return (file for file in self.values() if IVideo.providedBy(II18n(file).query_attribute('data')))
-
- @property
- def audios(self):
- return (file for file in self.values() if IAudio.providedBy(II18n(file).query_attribute('data')))
-
-
-@adapter_config(context=IExtFileContainerTarget, provides=IExtFileContainer)
-def extfile_container_factory(target):
- """External files container factory"""
- annotations = IAnnotations(target)
- container = annotations.get(EXTFILE_CONTAINER_KEY)
- if container is None:
- container = annotations[EXTFILE_CONTAINER_KEY] = ExtFileContainer()
- get_current_registry().notify(ObjectCreatedEvent(container))
- locate(container, target, '++files++')
- return container
-
-
-@adapter_config(name='files', context=IExtFileContainerTarget, provides=ITraversable)
-class ExtFileContainerNamespace(ContextAdapter):
- """++files++ namespace adapter"""
-
- def traverse(self, name, furtherpath=None):
- return IExtFileContainer(self.context)
-
-
-@adapter_config(name='extfile', context=IExtFileContainerTarget, provides=ISublocations)
-class ExtFileContainerSublocations(ContextAdapter):
- """External files container sublocations"""
-
- def sublocations(self):
- return IExtFileContainer(self.context).values()
-
-
-@vocabulary_config(name='PyAMS content external files')
-class ExtFileContainerFilesVocabulary(SimpleVocabulary):
- """External files container files vocabulary"""
-
- def __init__(self, context):
- target = get_parent(context, IExtFileContainerTarget)
- terms = [SimpleTerm(file.__name__, title=II18n(file).query_attribute('title'))
- for file in IExtFileContainer(target).values()]
- super(ExtFileContainerFilesVocabulary, self).__init__(terms)
-
-
-#
-# External file links container
-#
-
-@implementer(IExtFileLinksContainer)
-class ExtFileLinksContainer(Persistent, Contained):
- """External files links container"""
-
- def __init__(self):
- self.files = PersistentList()
-
-
-@adapter_config(context=IExtFileLinksContainerTarget, provides=IExtFileLinksContainer)
-def extfile_links_container_factory(target):
- """External files links container factory"""
- annotations = IAnnotations(target)
- container = annotations.get(EXTFILE_LINKS_CONTAINER_KEY)
- if container is None:
- container = annotations[EXTFILE_LINKS_CONTAINER_KEY] = ExtFileLinksContainer()
- get_current_registry().notify(ObjectCreatedEvent(container))
- locate(container, target, '++files-links++')
- return container
-
-
-@adapter_config(name='files-links', context=IExtFileLinksContainerTarget, provides=ITraversable)
-class ExtFileLinksContainerNamespace(ContextAdapter):
- """++files-links++ namespace adapter"""
-
- def traverse(self, name, furtherpath=None):
- return IExtFileLinksContainer(self.context)
--- a/src/pyams_content/component/extfile/interfaces/__init__.py Mon Sep 11 14:53:15 2017 +0200
+++ b/src/pyams_content/component/extfile/interfaces/__init__.py Mon Sep 11 14:54:30 2017 +0200
@@ -16,14 +16,10 @@
# import standard library
# import interfaces
-from zope.annotation.interfaces import IAttributeAnnotatable
-from zope.container.interfaces import IContainer
+from pyams_content.component.association.interfaces import IAssociationItem, IAssociationTarget
# import packages
from pyams_i18n.schema import I18nTextLineField, I18nTextField, I18nFileField, I18nThumbnailImageField
-from pyams_utils.schema import PersistentList
-from zope.container.constraints import containers, contains
-from zope.interface import Interface, Attribute
from zope.schema import TextLine, Choice
from pyams_content import _
@@ -33,14 +29,12 @@
EXTFILE_LINKS_CONTAINER_KEY = 'pyams_content.extfile.links'
-class IBaseExtFile(IAttributeAnnotatable):
+class IBaseExtFile(IAssociationItem):
"""Base external file interface"""
- containers('.IExtFileContainer')
-
title = I18nTextLineField(title=_("Title"),
description=_("File title, as shown in front-office"),
- required=True)
+ required=False)
description = I18nTextField(title=_("Description"),
description=_("File description displayed by front-office template"),
@@ -50,6 +44,15 @@
description=_("Name of document's author"),
required=False)
+ language = Choice(title=_("Language"),
+ description=_("File's content language"),
+ vocabulary="PyAMS base languages",
+ required=False)
+
+ filename = TextLine(title=_("Save file as..."),
+ description=_("Name under which the file will be saved"),
+ required=False)
+
class IExtFile(IBaseExtFile):
"""Generic external file interface"""
@@ -62,10 +65,18 @@
class IExtMedia(IExtFile):
"""External media file interface"""
+ title = I18nTextLineField(title=_("Legend"),
+ description=_("File legend, as shown in front-office"),
+ required=False)
+
class IExtImage(IExtMedia):
"""External image file interface"""
+ alt_title = I18nTextLineField(title=_("Accessibility title"),
+ description=_("Alternate title used to describe image content"),
+ required=False)
+
data = I18nThumbnailImageField(title=_("Image data"),
description=_("Image content"),
required=True)
@@ -79,30 +90,5 @@
"""External audio file interface"""
-class IExtFileContainer(IContainer):
- """External files container"""
-
- contains(IBaseExtFile)
-
- files = Attribute("Files list iterator")
- medias = Attribute("Medias list iterator")
- images = Attribute("Images list iterator")
- videos = Attribute("Videos list iterator")
- audios = Attribute("Audios list iterator")
-
-
-class IExtFileContainerTarget(Interface):
+class IExtFileContainerTarget(IAssociationTarget):
"""External files container marker interface"""
-
-
-class IExtFileLinksContainer(Interface):
- """External files links container interface"""
-
- files = PersistentList(title=_("External files"),
- description=_("List of external files linked to this object"),
- value_type=Choice(vocabulary="PyAMS content external files"),
- required=False)
-
-
-class IExtFileLinksContainerTarget(Interface):
- """External files links container marker interface"""
--- a/src/pyams_content/component/extfile/zmi/__init__.py Mon Sep 11 14:53:15 2017 +0200
+++ b/src/pyams_content/component/extfile/zmi/__init__.py Mon Sep 11 14:54:30 2017 +0200
@@ -16,25 +16,21 @@
# import standard library
# import interfaces
-from pyams_content.component.extfile.interfaces import IExtFileContainer, IExtFileContainerTarget, IBaseExtFile, \
- IExtFile, IExtImage
+from pyams_content.component.association.interfaces import IAssociationContainer
+from pyams_content.component.association.zmi.interfaces import IAssociationsView
+from pyams_content.component.extfile.interfaces import IExtFileContainerTarget, IBaseExtFile, \
+ IExtFile, IExtImage, IExtVideo, IExtAudio
from pyams_content.interfaces import MANAGE_CONTENT_PERMISSION
-from pyams_file.interfaces import IFileInfo
-from pyams_i18n.interfaces import INegotiator, II18n
-from pyams_skin.interfaces.viewlet import IWidgetTitleViewletManager
+from pyams_skin.interfaces.viewlet import IToolbarAddingMenu
from pyams_skin.layer import IPyAMSLayer
from pyams_utils.interfaces import VIEW_SYSTEM_PERMISSION
-from z3c.form.interfaces import NOT_CHANGED
# import packages
+from pyams_content.component.association.zmi import AssociationItemAJAXAddForm, AssociationItemAJAXEditForm
from pyams_content.component.extfile import EXTERNAL_FILES_FACTORIES
-from pyams_content.component.extfile.zmi.container import ExtFileContainerView
-from pyams_form.form import AJAXAddForm, AJAXEditForm
from pyams_form.security import ProtectedFormObjectMixin
from pyams_pagelet.pagelet import pagelet_config
-from pyams_skin.viewlet.toolbar import ToolbarAction
-from pyams_utils.registry import query_utility
-from pyams_utils.traversing import get_parent
+from pyams_skin.viewlet.toolbar import ToolbarMenuItem, ToolbarMenuDivider
from pyams_viewlet.viewlet import viewlet_config
from pyams_zmi.form import AdminDialogAddForm, AdminDialogEditForm
from pyramid.view import view_config
@@ -58,12 +54,19 @@
required=True)
-@viewlet_config(name='add-extfile.menu', context=IExtFileContainerTarget, view=ExtFileContainerView,
- layer=IPyAMSLayer, manager=IWidgetTitleViewletManager, weight=50)
-class ExtFileAddMenu(ProtectedFormObjectMixin, ToolbarAction):
+@viewlet_config(name='add-extfile.divider', context=IExtFileContainerTarget, view=IAssociationsView,
+ layer=IPyAMSLayer, manager=IToolbarAddingMenu, weight=59)
+class ExtFileAddMenuDivider(ToolbarMenuDivider):
+ """External file add menu divider"""
+
+
+@viewlet_config(name='add-extfile.menu', context=IExtFileContainerTarget, view=IAssociationsView,
+ layer=IPyAMSLayer, manager=IToolbarAddingMenu, weight=60)
+class ExtFileAddMenu(ProtectedFormObjectMixin, ToolbarMenuItem):
"""External file add menu"""
label = _("Add external file")
+ label_css_class = 'fa fa-fw fa-file-o'
url = 'add-extfile.html'
modal_target = True
@@ -75,19 +78,10 @@
"""External file add form"""
legend = _("Add new external file")
- icon_css_class = 'fa fa-fw fa-file-text-o'
-
- fields = field.Fields(IExtFileFactoryChooser) + \
- field.Fields(IExtFile).omit('__parent__', '__name__')
+ icon_css_class = 'fa fa-fw fa-file-o'
- @property
- def ajax_handler(self):
- origin = self.request.params.get('origin')
- if origin == 'link':
- return 'add-extfile-link.json'
- else:
- return 'add-extfile.json'
-
+ fields = field.Fields(IExtFile).select('title', 'description', 'author', 'language', 'data', 'filename')
+ ajax_handler = 'add-extfile.json'
edit_permission = MANAGE_CONTENT_PERMISSION
def updateWidgets(self, prefix=None):
@@ -96,66 +90,29 @@
self.widgets['description'].widget_css_class = 'textarea'
def create(self, data):
- factory = EXTERNAL_FILES_FACTORIES.get(data.get('factory'))
+ factory = EXTERNAL_FILES_FACTORIES.get('file')
if factory is not None:
return factory[0]()
- def update_content(self, content, data):
- data['factory'] = NOT_CHANGED
- return super(ExtFileAddForm, self).update_content(content, data)
-
def add(self, object):
- IExtFileContainer(self.context)['none'] = object
- i18n = query_utility(INegotiator)
- if i18n is not None:
- lang = i18n.server_language
- data = II18n(object).get_attribute('data', lang, self.request)
- if data:
- info = IFileInfo(data)
- info.title = II18n(object).get_attribute('title', lang, self.request)
- info.description = II18n(object).get_attribute('description', lang, self.request)
- for lang, data in object.data.items():
- if data is not None:
- IFileInfo(data).language = lang
+ IAssociationContainer(self.context).append(object)
@view_config(name='add-extfile.json', context=IExtFileContainerTarget, request_type=IPyAMSLayer,
permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
-class ExtFileAJAXAddForm(AJAXAddForm, ExtFileAddForm):
+class ExtFileAJAXAddForm(AssociationItemAJAXAddForm, ExtFileAddForm):
"""External file add form, JSON renderer"""
- def get_ajax_output(self, changes):
- return {'status': 'reload',
- 'location': '#external-files.html'}
-
-
-@view_config(name='add-extfile-link.json', context=IExtFileContainerTarget, request_type=IPyAMSLayer,
- permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
-class ExtFileLinkAJAXAddForm(AJAXAddForm, ExtFileAddForm):
- """External file link add form, JSON renderer"""
-
- def get_ajax_output(self, changes):
- target = get_parent(self.context, IExtFileContainerTarget)
- container = IExtFileContainer(target)
- files = [{'id': file.__name__,
- 'text': II18n(file).query_attribute('title', request=self.request)}
- for file in container.values()]
- return {'status': 'callback',
- 'callback': 'PyAMS_content.extfiles.refresh',
- 'options': {'files': files,
- 'new_file': {'id': changes.__name__,
- 'text': II18n(changes).query_attribute('title', request=self.request)}}}
-
@pagelet_config(name='properties.html', context=IExtFile, layer=IPyAMSLayer, permission=VIEW_SYSTEM_PERMISSION)
class ExtFilePropertiesEditForm(AdminDialogEditForm):
"""External file properties edit form"""
legend = _("Update file properties")
- icon_css_class = 'fa fa-fw fa-file-text-o'
+ icon_css_class = 'fa fa-fw fa-file-o'
dialog_class = 'modal-large'
- fields = field.Fields(IExtFile).omit('__parent__', '__file__')
+ fields = field.Fields(IExtFile).select('title', 'description', 'author', 'language', 'data', 'filename')
ajax_handler = 'properties.json'
edit_permission = MANAGE_CONTENT_PERMISSION
@@ -164,21 +121,59 @@
if 'description' in self.widgets:
self.widgets['description'].widget_css_class = 'textarea'
- def update_content(self, content, data):
- changes = super(ExtFilePropertiesEditForm, self).update_content(content, data)
- if changes:
- i18n = query_utility(INegotiator)
- if i18n is not None:
- lang = i18n.server_language
- data = II18n(content).get_attribute('data', lang, self.request)
- if data:
- info = IFileInfo(data)
- info.title = II18n(content).get_attribute('title', lang, self.request)
- info.description = II18n(content).get_attribute('description', lang, self.request)
- for lang, data in content.data.items():
- if data and not IFileInfo(data).language:
- IFileInfo(data).language = lang
- return changes
+
+@view_config(name='properties.json', context=IExtFile, request_type=IPyAMSLayer,
+ permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
+class ExtFilePropertiesAJAXEditForm(AssociationItemAJAXEditForm, ExtFilePropertiesEditForm):
+ """External file properties edit form, JSON renderer"""
+
+ def get_ajax_output(self, changes):
+ if ('title' in changes.get(IBaseExtFile, ())) or \
+ ('filename' in changes.get(IBaseExtFile, ())) or \
+ ('data' in changes.get(IExtFile, ())):
+ return self.get_associations_table()
+ else:
+ return super(ExtFilePropertiesAJAXEditForm, self).get_ajax_output(changes)
+
+
+#
+# Images views
+#
+
+@viewlet_config(name='add-extimage.menu', context=IExtFileContainerTarget, view=IAssociationsView,
+ layer=IPyAMSLayer, manager=IToolbarAddingMenu, weight=61)
+class ExtImageAddMenu(ProtectedFormObjectMixin, ToolbarMenuItem):
+ """External image add menu"""
+
+ label = _("Add image")
+ label_css_class = 'fa fa-fw fa-file-image-o'
+
+ url = 'add-extimage.html'
+ modal_target = True
+
+
+@pagelet_config(name='add-extimage.html', context=IExtFileContainerTarget, layer=IPyAMSLayer,
+ permission=MANAGE_CONTENT_PERMISSION)
+class ExtImageAddForm(ExtFileAddForm):
+ """External image add form"""
+
+ legend = _("Add new image")
+ icon_css_class = 'fa fa-fw fa-file-image-o'
+
+ fields = field.Fields(IExtImage).select('title', 'alt_title', 'description', 'author',
+ 'language', 'data', 'filename')
+ ajax_handler = 'add-extimage.json'
+
+ def create(self, data):
+ factory = EXTERNAL_FILES_FACTORIES.get('image')
+ if factory is not None:
+ return factory[0]()
+
+
+@view_config(name='add-extimage.json', context=IExtFileContainerTarget, request_type=IPyAMSLayer,
+ permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
+class ExtImageAJAXAddForm(AssociationItemAJAXAddForm, ExtImageAddForm):
+ """External image add form, JSON renderer"""
@pagelet_config(name='properties.html', context=IExtImage, layer=IPyAMSLayer, permission=VIEW_SYSTEM_PERMISSION)
@@ -186,20 +181,147 @@
"""External image properties edit form"""
legend = _("Update image properties")
- icon_css_class = 'fa fa-fw fa-picture-o'
+ icon_css_class = 'fa fa-fw fa-file-image-o'
- fields = field.Fields(IExtImage).omit('__parent__', '__name__')
+ fields = field.Fields(IExtImage).select('title', 'alt_title', 'description', 'author',
+ 'language', 'data', 'filename')
-@view_config(name='properties.json', context=IExtFile, request_type=IPyAMSLayer,
+@view_config(name='properties.json', context=IExtImage, request_type=IPyAMSLayer,
permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
-class ExtFilePropertiesAJAXEditForm(AJAXEditForm, ExtFilePropertiesEditForm):
- """External file properties edit form, JSON renderer"""
+class ExtImagePropertiesAJAXEditForm(AssociationItemAJAXEditForm, ExtImagePropertiesEditForm):
+ """External image properties edit form, JSON renderer"""
def get_ajax_output(self, changes):
if ('title' in changes.get(IBaseExtFile, ())) or \
+ ('filename' in changes.get(IBaseExtFile, ())) or \
('data' in changes.get(IExtFile, ())):
- return {'status': 'reload',
- 'location': '#external-files.html'}
+ return self.get_associations_table()
+ else:
+ return super(ExtImagePropertiesAJAXEditForm, self).get_ajax_output(changes)
+
+
+#
+# Videos views
+#
+
+@viewlet_config(name='add-extvideo.menu', context=IExtFileContainerTarget, view=IAssociationsView,
+ layer=IPyAMSLayer, manager=IToolbarAddingMenu, weight=62)
+class ExtVideoAddMenu(ProtectedFormObjectMixin, ToolbarMenuItem):
+ """External video add menu"""
+
+ label = _("Add video")
+ label_css_class = 'fa fa-fw fa-file-video-o'
+
+ url = 'add-extvideo.html'
+ modal_target = True
+
+
+@pagelet_config(name='add-extvideo.html', context=IExtFileContainerTarget, layer=IPyAMSLayer,
+ permission=MANAGE_CONTENT_PERMISSION)
+class ExtVideoAddForm(ExtFileAddForm):
+ """External video add form"""
+
+ legend = _("Add new video")
+ icon_css_class = 'fa fa-fw fa-file-video-o'
+
+ fields = field.Fields(IExtVideo).select('title', 'description', 'author', 'language', 'data', 'filename')
+ ajax_handler = 'add-extvideo.json'
+
+ def create(self, data):
+ factory = EXTERNAL_FILES_FACTORIES.get('video')
+ if factory is not None:
+ return factory[0]()
+
+
+@view_config(name='add-extvideo.json', context=IExtFileContainerTarget, request_type=IPyAMSLayer,
+ permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
+class ExtVideoAJAXAddForm(AssociationItemAJAXAddForm, ExtVideoAddForm):
+ """External video add form, JSON renderer"""
+
+
+@pagelet_config(name='properties.html', context=IExtVideo, layer=IPyAMSLayer, permission=VIEW_SYSTEM_PERMISSION)
+class ExtVideoPropertiesEditForm(ExtFilePropertiesEditForm):
+ """External video properties edit form"""
+
+ legend = _("Update video properties")
+ icon_css_class = 'fa fa-fw fa-file-video-o'
+
+ fields = field.Fields(IExtVideo).select('title', 'description', 'author', 'language', 'data', 'filename')
+
+
+@view_config(name='properties.json', context=IExtVideo, request_type=IPyAMSLayer,
+ permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
+class ExtVideoPropertiesAJAXEditForm(AssociationItemAJAXEditForm, ExtVideoPropertiesEditForm):
+ """External video properties edit form, JSON renderer"""
+
+ def get_ajax_output(self, changes):
+ if ('title' in changes.get(IBaseExtFile, ())) or \
+ ('filename' in changes.get(IBaseExtFile, ())) or \
+ ('data' in changes.get(IExtFile, ())):
+ return self.get_associations_table()
else:
- return super(ExtFilePropertiesAJAXEditForm, self).get_ajax_output(changes)
+ return super(ExtVideoPropertiesAJAXEditForm, self).get_ajax_output(changes)
+
+
+#
+# Audio file views
+#
+
+@viewlet_config(name='add-extaudio.menu', context=IExtFileContainerTarget, view=IAssociationsView,
+ layer=IPyAMSLayer, manager=IToolbarAddingMenu, weight=63)
+class ExtAudioAddMenu(ProtectedFormObjectMixin, ToolbarMenuItem):
+ """External audio file add menu"""
+
+ label = _("Add audio file")
+ label_css_class = 'fa fa-fw fa-file-audio-o'
+
+ url = 'add-extaudio.html'
+ modal_target = True
+
+
+@pagelet_config(name='add-extaudio.html', context=IExtFileContainerTarget, layer=IPyAMSLayer,
+ permission=MANAGE_CONTENT_PERMISSION)
+class ExtAudioAddForm(ExtFileAddForm):
+ """External audio file add form"""
+
+ legend = _("Add new audio file")
+ icon_css_class = 'fa fa-fw fa-file-audio-o'
+
+ fields = field.Fields(IExtAudio).select('title', 'description', 'author', 'language', 'data', 'filename')
+ ajax_handler = 'add-extaudio.json'
+
+ def create(self, data):
+ factory = EXTERNAL_FILES_FACTORIES.get('audio')
+ if factory is not None:
+ return factory[0]()
+
+
+@view_config(name='add-extaudio.json', context=IExtFileContainerTarget, request_type=IPyAMSLayer,
+ permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
+class ExtAudioAJAXAddForm(AssociationItemAJAXAddForm, ExtAudioAddForm):
+ """External audio file add form, JSON renderer"""
+
+
+@pagelet_config(name='properties.html', context=IExtAudio, layer=IPyAMSLayer, permission=VIEW_SYSTEM_PERMISSION)
+class ExtAudioPropertiesEditForm(ExtFilePropertiesEditForm):
+ """External audio file properties edit form"""
+
+ legend = _("Update audio file properties")
+ icon_css_class = 'fa fa-fw fa-file-audio-o'
+
+ fields = field.Fields(IExtVideo).select('title', 'description', 'author', 'language', 'data', 'filename')
+
+
+@view_config(name='properties.json', context=IExtAudio, request_type=IPyAMSLayer,
+ permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
+class ExtAudioPropertiesAJAXEditForm(AssociationItemAJAXEditForm, ExtAudioPropertiesEditForm):
+ """External audio file properties edit form, JSON renderer"""
+
+ def get_ajax_output(self, changes):
+ if ('title' in changes.get(IBaseExtFile, ())) or \
+ ('filename' in changes.get(IBaseExtFile, ())) or \
+ ('data' in changes.get(IExtFile, ())):
+ return self.get_associations_table()
+ else:
+ return super(ExtAudioPropertiesAJAXEditForm, self).get_ajax_output(changes)
--- a/src/pyams_content/component/extfile/zmi/container.py Mon Sep 11 14:53:15 2017 +0200
+++ b/src/pyams_content/component/extfile/zmi/container.py Mon Sep 11 14:54:30 2017 +0200
@@ -16,53 +16,16 @@
# import standard library
# import interfaces
-from pyams_content.component.extfile.interfaces import IExtFileContainerTarget, IExtFileContainer, \
- IExtFileLinksContainerTarget, IExtFileLinksContainer
-from pyams_content.interfaces import MANAGE_CONTENT_PERMISSION
-from pyams_file.interfaces import IFileInfo, IFile, IImage
+from pyams_content.component.association.interfaces import IAssociationTarget, IAssociationContainer, IAssociationInfo
+from pyams_content.component.extfile.interfaces import IExtFile, IExtImage
from pyams_i18n.interfaces import II18n
-from pyams_skin.interfaces import IInnerPage, IPageHeader
from pyams_skin.layer import IPyAMSLayer
-from pyams_utils.interfaces import VIEW_SYSTEM_PERMISSION
-from pyams_utils.interfaces.data import IObjectData
-from pyams_zmi.interfaces.menu import IPropertiesMenu
-from pyams_zmi.layer import IAdminLayer
-from z3c.table.interfaces import IValues, IColumn
# import packages
-from pyams_content.component.extfile.zmi.widget import ExtFileLinkSelectFieldWidget
-from pyams_content.shared.common.zmi import WfModifiedContentColumnMixin
-from pyams_form.form import AJAXEditForm
-from pyams_form.security import ProtectedFormObjectMixin
-from pyams_pagelet.pagelet import pagelet_config
-from pyams_skin.page import DefaultPageHeaderAdapter
-from pyams_skin.table import BaseTable, I18nColumn, TrashColumn
-from pyams_skin.viewlet.menu import MenuItem, MenuDivider
-from pyams_template.template import template_config
-from pyams_utils.adapter import adapter_config, ContextRequestViewAdapter
-from pyams_utils.size import get_human_size
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 AdminDialogEditForm
-from pyams_zmi.view import AdminView
-from pyramid.decorator import reify
from pyramid.view import view_config
-from z3c.form import field
-from z3c.table.column import GetAttrColumn
-from zope.interface import implementer, alsoProvides, Interface
-
-from pyams_content import _
-
-
-@viewlet_config(name='external-files.menu', context=IExtFileContainerTarget, layer=IAdminLayer,
- manager=IPropertiesMenu, permission=VIEW_SYSTEM_PERMISSION, weight=200)
-class ExtFileContainerMenu(MenuItem):
- """External files container menu"""
-
- label = _("External files...")
- icon_class = 'fa-file-text-o'
- url = '#external-files.html'
+from zope.interface import Interface
#
@@ -73,191 +36,27 @@
renderer='json', xhr=True)
def get_files_list(request):
"""Get container files in JSON format for TinyMCE editor"""
- target = get_parent(request.context, IExtFileContainerTarget)
- if target is None:
- return []
- container = IExtFileContainer(target)
- return sorted([{'title': II18n(file).query_attribute('title', request=request),
- 'value': absolute_url(II18n(file).query_attribute('data', request=request), request)}
- for file in container.values()],
- key=lambda x: x['title'])
+ result = []
+ target = get_parent(request.context, IAssociationTarget)
+ if target is not None:
+ container = IAssociationContainer(target)
+ result.extend([{'title': IAssociationInfo(item).user_title,
+ 'value': absolute_url(II18n(item).query_attribute('data', request=request),
+ request=request)}
+ for item in container.values() if IExtFile.providedBy(item)])
+ return sorted(result, key=lambda x: x['title'])
@view_config(name='get-images-list.json', context=Interface, request_type=IPyAMSLayer,
renderer='json', xhr=True)
def get_images_list(request):
"""Get container images in JSON format for TinyMCE editor"""
- target = get_parent(request.context, IExtFileContainerTarget)
- if target is None:
- return []
- container = IExtFileContainer(target)
- return sorted([{'title': II18n(img).query_attribute('title', request=request),
- 'value': absolute_url(II18n(img).query_attribute('data', request=request), request)}
- for img in container.images],
- key=lambda x: x['title'])
-
-
-@pagelet_config(name='external-files.html', context=IExtFileContainerTarget, layer=IPyAMSLayer,
- permission=VIEW_SYSTEM_PERMISSION)
-@template_config(template='templates/container.pt', layer=IPyAMSLayer)
-@implementer(IInnerPage)
-class ExtFileContainerView(AdminView):
- """External files container view"""
-
- title = _("External files list")
-
- def __init__(self, context, request):
- super(ExtFileContainerView, self).__init__(context, request)
- self.files_table = ExtFileContainerTable(context, request, self, _("External files"), 'files')
- self.images_table = ExtFileContainerTable(context, request, self, _("Images"), 'images')
- self.videos_table = ExtFileContainerTable(context, request, self, _("Videos"), 'videos')
- self.audios_table = ExtFileContainerTable(context, request, self, _("Sounds"), 'audios')
-
- def update(self):
- super(ExtFileContainerView, self).update()
- self.files_table.update()
- self.images_table.update()
- self.videos_table.update()
- self.audios_table.update()
-
-
-class ExtFileContainerTable(BaseTable):
- """External files container table"""
-
- hide_toolbar = True
- cssClasses = {'table': 'table table-bordered table-striped table-hover table-tight'}
-
- def __init__(self, context, request, view, title, property):
- super(ExtFileContainerTable, self).__init__(context, request)
- self.view = view
- self.title = title
- self.files_property = property
- self.object_data = {'ams-widget-toggle-button': 'false'}
- alsoProvides(self, IObjectData)
-
- @property
- def data_attributes(self):
- attributes = super(ExtFileContainerTable, self).data_attributes
- attributes['table'] = {'data-ams-location': absolute_url(IExtFileContainer(self.context), self.request),
- 'data-ams-datatable-sort': 'false',
- 'data-ams-datatable-pagination': 'false'}
- return attributes
-
- @reify
- def values(self):
- return list(super(ExtFileContainerTable, self).values)
-
- def render(self):
- if not self.values:
- if self.files_property == 'files':
- for table in (self.view.images_table, self.view.videos_table, self.view.audios_table):
- if table.values:
- return ''
- translate = self.request.localizer.translate
- return translate(_("No currently stored external file."))
- else:
- return ''
- return super(ExtFileContainerTable, self).render()
-
-
-@adapter_config(name='name', context=(IExtFileContainerTarget, IPyAMSLayer, ExtFileContainerTable), provides=IColumn)
-class ExtFileContainerNameColumn(I18nColumn, WfModifiedContentColumnMixin, GetAttrColumn):
- """External files container name column"""
-
- _header = _("Title")
-
- weight = 10
-
- def getValue(self, obj):
- return II18n(obj).query_attribute('title', request=self.request)
-
-
-@adapter_config(name='filename', context=(IExtFileContainerTarget, IPyAMSLayer, ExtFileContainerTable), provides=IColumn)
-class ExtFileContainerFilenameColumn(I18nColumn, GetAttrColumn):
- """External file container filename column"""
-
- _header = _("Filename")
-
- weight = 15
-
- def getValue(self, obj):
- data = II18n(obj).query_attribute('data', request=self.request)
- if data is not None:
- return IFileInfo(data).filename
- else:
- return '--'
-
-
-@adapter_config(name='filesize', context=(IExtFileContainerTarget, IPyAMSLayer, ExtFileContainerTable), provides=IColumn)
-class ExtFileContainerFileSizeColumn(I18nColumn, GetAttrColumn):
- """External file container file size column"""
-
- _header = _("Size")
-
- weight = 20
-
- def getValue(self, obj):
- data = II18n(obj).query_attribute('data', request=self.request)
- if data is not None:
- result = get_human_size(IFile(data).get_size())
- if IImage.providedBy(data):
- result = '{0} ({1[0]}x{1[1]})'.format(result, IImage(data).get_image_size())
- return result
- else:
- return 'N/A'
-
-
-@adapter_config(name='trash', context=(IExtFileContainerTarget, IPyAMSLayer, ExtFileContainerTable), provides=IColumn)
-class ExtFileContainerTrashColumn(ProtectedFormObjectMixin, TrashColumn):
- """External files container trash column"""
-
-
-@adapter_config(context=(IExtFileContainerTarget, IPyAMSLayer, ExtFileContainerTable), provides=IValues)
-class ExtFileContainerValues(ContextRequestViewAdapter):
- """External files container values"""
-
- @property
- def values(self):
- return getattr(IExtFileContainer(self.context), self.view.files_property)
-
-
-@adapter_config(context=(IExtFileContainerTarget, IPyAMSLayer, ExtFileContainerView), provides=IPageHeader)
-class ExtFileHeaderAdapter(DefaultPageHeaderAdapter):
- """External files container header adapter"""
-
- back_url = '#properties.html'
- icon_class = 'fa fa-fw fa-file-text-o'
-
-
-#
-# External files links edit form
-#
-
-@pagelet_config(name='extfile-links.html', context=IExtFileLinksContainerTarget, layer=IPyAMSLayer,
- permission=VIEW_SYSTEM_PERMISSION)
-class ExtFileLinksContainerLinksEditForm(AdminDialogEditForm):
- """External file links container edit form"""
-
- legend = _("Edit external files links")
-
- fields = field.Fields(IExtFileLinksContainer)
- fields['files'].widgetFactory = ExtFileLinkSelectFieldWidget
-
- ajax_handler = 'extfile-links.json'
- edit_permission = MANAGE_CONTENT_PERMISSION
-
-
-@view_config(name='extfile-links.json', context=IExtFileLinksContainerTarget, request_type=IPyAMSLayer,
- permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
-class ExtFileLinksContainerAJAXEditForm(AJAXEditForm, ExtFileLinksContainerLinksEditForm):
- """External file links container edit form, JSON renderer"""
-
- def get_ajax_output(self, changes):
- if 'files' in changes.get(IExtFileLinksContainer, ()):
- return {'status': 'success',
- 'event': 'PyAMS_content.changed_item',
- 'event_options': {'object_type': 'extfiles_container',
- 'object_name': self.context.__name__,
- 'nb_files': len(IExtFileLinksContainer(self.context).files or ())}}
- else:
- return super(ExtFileLinksContainerAJAXEditForm, self).get_ajax_output(changes)
+ result = []
+ target = get_parent(request.context, IAssociationTarget)
+ if target is not None:
+ container = IAssociationContainer(target)
+ result.extend([{'title': IAssociationInfo(item).user_title,
+ 'value': absolute_url(II18n(item).query_attribute('data', request=request),
+ request=request)}
+ for item in container.values() if IExtImage.providedBy(item)])
+ return sorted(result, key=lambda x: x['title'])
--- a/src/pyams_content/component/extfile/zmi/templates/container.pt Mon Sep 11 14:53:15 2017 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,16 +0,0 @@
-<div class="ams-widget">
- <header>
- <span tal:condition="view.widget_icon_class | nothing"
- class="widget-icon"><i tal:attributes="class view.widget_icon_class"></i>
- </span>
- <h2 tal:content="view.title"></h2>
- <tal:var content="structure provider:pyams.widget_title" />
- <tal:var content="structure provider:pyams.toolbar" />
- </header>
- <div class="widget-body no-widget-toolbar">
- <tal:var content="structure view.files_table.render()" />
- <tal:var content="structure view.images_table.render()" />
- <tal:var content="structure view.videos_table.render()" />
- <tal:var content="structure view.audios_table.render()" />
- </div>
-</div>
--- a/src/pyams_content/component/extfile/zmi/templates/widget-display.pt Mon Sep 11 14:53:15 2017 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,22 +0,0 @@
-<input type="hidden" autocomplete="off" readonly
- data-ams-select2-multiple="true"
- tal:attributes="id view/id;
- name view/name;
- class string:select2 ${view/klass} ordered;
- style view/style;
- title view/title;
- value python:','.join(view.value);
- lang view/lang;
- onclick view/onclick;
- ondblclick view/ondblclick;
- onmousedown view/onmousedown;
- onmouseup view/onmouseup;
- onmouseover view/onmouseover;
- onmousemove view/onmousemove;
- onmouseout view/onmouseout;
- onkeypress view/onkeypress;
- onkeydown view/onkeydown;
- onkeyup view/onkeyup;
- disabled view/disabled;
- tabindex view/tabindex;
- data-ams-select2-values view/values_map;" />
--- a/src/pyams_content/component/extfile/zmi/templates/widget-input.pt Mon Sep 11 14:53:15 2017 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,44 +0,0 @@
-<label class="input bordered with-icon" i18n:domain="pyams_content"
- data-ams-plugins="pyams_content"
- tal:attributes="data-ams-plugin-pyams_content-src extension:resource_path('pyams_content.skin:pyams_content')">
- <i class="icon-append fa fa-plus-square txt-color-green hint opaque"
- title="Add external file" i18n:attributes="title"
- data-ams-url="add-extfile.html?origin=link" data-toggle="modal"
- tal:attributes="data-ams-select2-target string:${view/name}:list"></i>
- <div class="select2-parent">
- <select class="select2 ordered"
- data-ams-select2-allow-clear="true"
- tal:attributes="id view/id;
- name string:${view/name}:list;
- class string:${view/klass} select2 ordered;
- style view/style;
- title view/title;
- lang view/lang;
- onclick view/onclick;
- ondblclick view/ondblclick;
- onmousedown view/onmousedown;
- onmouseup view/onmouseup;
- onmouseover view/onmouseover;
- onmousemove view/onmousemove;
- onmouseout view/onmouseout;
- onkeypress view/onkeypress;
- onkeydown view/onkeydown;
- onkeyup view/onkeyup;
- disabled view/disabled;
- tabindex view/tabindex;
- onfocus view/onfocus;
- onblur view/onblur;
- onchange view/onchange;
- multiple view/multiple;
- size view/size">
- <option tal:repeat="entry view/selectedItems"
- tal:attributes="value entry/value;
- selected python:entry['value'] in view.value;"
- tal:content="entry/content"></option>
- <option tal:repeat="entry view/notselectedItems"
- tal:attributes="value entry/value;
- selected python:entry['value'] in view.value;"
- tal:content="entry/content"></option>
- </select>
- </div>
-</label>
--- a/src/pyams_content/component/extfile/zmi/widget.py Mon Sep 11 14:53:15 2017 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,42 +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 json
-
-# import interfaces
-from pyams_skin.layer import IPyAMSLayer
-
-# import packages
-from pyams_form.widget import widgettemplate_config
-from z3c.form.browser.orderedselect import OrderedSelectWidget
-from z3c.form.widget import FieldWidget
-
-
-@widgettemplate_config(mode='input', template='templates/widget-input.pt', layer=IPyAMSLayer)
-@widgettemplate_config(mode='display', template='templates/widget-display.pt', layer=IPyAMSLayer)
-class ExtFileLinksSelectWidget(OrderedSelectWidget):
- """External files links select widget"""
-
- @property
- def values_map(self):
- result = {}
- [result.update({entry['value']: entry['content']}) for entry in self.selectedItems]
- return json.dumps(result)
-
-
-def ExtFileLinkSelectFieldWidget(field, request):
- """External files links select widget factory"""
- return FieldWidget(field, ExtFileLinksSelectWidget(request))
--- a/src/pyams_content/component/gallery/__init__.py Mon Sep 11 14:53:15 2017 +0200
+++ b/src/pyams_content/component/gallery/__init__.py Mon Sep 11 14:54:30 2017 +0200
@@ -16,93 +16,33 @@
# import standard library
# import interfaces
-from pyams_content.component.gallery.interfaces import IGalleryFileInfo, GALLERY_FILE_INFO_KEY, IGallery, IGalleryFile
+from pyams_content.component.gallery.interfaces import IGallery, IGalleryTarget, \
+ GALLERY_CONTAINER_KEY, IGalleryRenderer
from pyams_content.shared.common.interfaces import IWfSharedContent
from pyams_form.interfaces.form import IFormContextPermissionChecker
-from pyams_i18n.interfaces import II18n
from zope.annotation.interfaces import IAnnotations
from zope.lifecycleevent.interfaces import IObjectAddedEvent, IObjectModifiedEvent, IObjectRemovedEvent
+from zope.location.interfaces import ISublocations
from zope.traversing.interfaces import ITraversable
# import packages
-from persistent import Persistent
-from pyams_file.property import FileProperty
+from pyams_catalog.utils import index_object
from pyams_utils.adapter import adapter_config, ContextAdapter
from pyams_utils.container import BTreeOrderedContainer
+from pyams_utils.request import check_request
from pyams_utils.traversing import get_parent
+from pyams_utils.vocabulary import vocabulary_config
from pyramid.events import subscriber
from pyramid.threadlocal import get_current_registry
+from zope.interface import implementer
from zope.lifecycleevent import ObjectCreatedEvent, ObjectModifiedEvent
-from zope.container.contained import Contained
-from zope.interface import implementer
from zope.location import locate
from zope.schema.fieldproperty import FieldProperty
+from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm
#
-# Gallery file
-#
-
-@implementer(IGalleryFileInfo)
-class GalleryFileInfo(Persistent, Contained):
- """Gallery file info"""
-
- title = FieldProperty(IGalleryFileInfo['title'])
- description = FieldProperty(IGalleryFileInfo['description'])
- author = FieldProperty(IGalleryFileInfo['author'])
- author_comments = FieldProperty(IGalleryFileInfo['author_comments'])
- sound = FileProperty(IGalleryFileInfo['sound'])
- sound_title = FieldProperty(IGalleryFileInfo['sound_title'])
- sound_description = FieldProperty(IGalleryFileInfo['sound_description'])
- pif_number = FieldProperty(IGalleryFileInfo['pif_number'])
- visible = FieldProperty(IGalleryFileInfo['visible'])
-
- def get_title(self, request=None):
- return II18n(self).query_attribute('title', request=request)
-
-
-@adapter_config(context=IGalleryFile, provides=IGalleryFileInfo)
-def media_gallery_info_factory(file):
- """Gallery file gallery info factory"""
- annotations = IAnnotations(file)
- info = annotations.get(GALLERY_FILE_INFO_KEY)
- if info is None:
- info = annotations[GALLERY_FILE_INFO_KEY] = GalleryFileInfo()
- get_current_registry().notify(ObjectCreatedEvent(info))
- locate(info, file, '++gallery-info++')
- return info
-
-
-@adapter_config(name='gallery-info', context=IGalleryFile, provides=ITraversable)
-class MediaGalleryInfoTraverser(ContextAdapter):
- """Gallery file gallery info adapter"""
-
- def traverse(self, name, furtherpath=None):
- return IGalleryFileInfo(self.context)
-
-
-@adapter_config(context=IGalleryFile, provides=IFormContextPermissionChecker)
-class GalleryFilePermissionChecker(ContextAdapter):
- """Gallery file permission checker"""
-
- @property
- def edit_permission(self):
- content = get_parent(self.context, IWfSharedContent)
- return IFormContextPermissionChecker(content).edit_permission
-
-
-@adapter_config(context=IGalleryFileInfo, provides=IFormContextPermissionChecker)
-class GalleryFileInfoPermissionChecker(ContextAdapter):
- """Gallery file info permission checker"""
-
- @property
- def edit_permission(self):
- content = get_parent(self.context, IWfSharedContent)
- return IFormContextPermissionChecker(content).edit_permission
-
-
-#
-# Gallery
+# Galleries container
#
@implementer(IGallery)
@@ -111,19 +51,53 @@
title = FieldProperty(IGallery['title'])
description = FieldProperty(IGallery['description'])
- visible = FieldProperty(IGallery['visible'])
+ renderer = FieldProperty(IGallery['renderer'])
last_id = 1
- def __setitem__(self, key, value):
+ def append(self, value, notify=True):
key = str(self.last_id)
- super(Gallery, self).__setitem__(key, value)
+ if not notify:
+ # pre-locate gallery item to avoid multiple notifications
+ locate(value, self.key)
+ self[key] = value
self.last_id += 1
+ if not notify:
+ # make sure that gallery item is correctly indexed
+ index_object(value)
def get_visible_images(self):
return [image for image in self.values() if image.visible]
+@adapter_config(context=IGalleryTarget, provides=IGallery)
+def gallery_factory(target):
+ """Galleries container factory"""
+ annotations = IAnnotations(target)
+ gallery = annotations.get(GALLERY_CONTAINER_KEY)
+ if gallery is None:
+ gallery = annotations[GALLERY_CONTAINER_KEY] = Gallery()
+ get_current_registry().notify(ObjectCreatedEvent(gallery))
+ locate(gallery, target, '++gallery++')
+ return gallery
+
+
+@adapter_config(name='gallery', context=IGalleryTarget, provides=ITraversable)
+class GalleryContainerNamespace(ContextAdapter):
+ """++gallery++ namespace traverser"""
+
+ def traverse(self, name, furtherpath=None):
+ return IGallery(self.context)
+
+
+@adapter_config(name='gallery', context=IGalleryTarget, provides=ISublocations)
+class GalleryContainerSublocations(ContextAdapter):
+ """Galleries container sublocations"""
+
+ def sublocations(self):
+ return IGallery(self.context).values()
+
+
@adapter_config(context=IGallery, provides=IFormContextPermissionChecker)
class GalleryPermissionChecker(ContextAdapter):
"""Gallery permission checker"""
@@ -156,3 +130,18 @@
content = get_parent(event.object, IWfSharedContent)
if content is not None:
get_current_registry().notify(ObjectModifiedEvent(content))
+
+
+@vocabulary_config(name='PyAMS gallery renderers')
+class GalleryRendererVocabulary(SimpleVocabulary):
+ """Gallery renderer utilities vocabulary"""
+
+ def __init__(self, context=None):
+ request = check_request()
+ translate = request.localizer.translate
+ registry = request.registry
+ context = Gallery()
+ terms = [SimpleTerm(name, title=translate(adapter.label))
+ for name, adapter in sorted(registry.getAdapters((context, request), IGalleryRenderer),
+ key=lambda x: x[1].weight)]
+ super(GalleryRendererVocabulary, self).__init__(terms)
--- a/src/pyams_content/component/gallery/container.py Mon Sep 11 14:53:15 2017 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,125 +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_content.component.gallery.interfaces import IGalleryContainer, IGalleryContainerTarget, \
- GALLERY_CONTAINER_KEY, IGalleryLinksContainer, IGalleryLinksContainerTarget, GALLERY_LINKS_CONTAINER_KEY
-from zope.annotation.interfaces import IAnnotations
-from zope.location.interfaces import ISublocations
-from zope.traversing.interfaces import ITraversable
-
-# import packages
-from persistent import Persistent
-from persistent.list import PersistentList
-from pyams_i18n.interfaces import II18n
-from pyams_utils.adapter import adapter_config, ContextAdapter
-from pyams_utils.traversing import get_parent
-from pyams_utils.vocabulary import vocabulary_config
-from pyramid.threadlocal import get_current_registry
-from zope.container.contained import Contained
-from zope.container.folder import Folder
-from zope.interface import implementer
-from zope.lifecycleevent import ObjectCreatedEvent
-from zope.location import locate
-from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm
-
-
-#
-# Galleries container
-#
-
-@implementer(IGalleryContainer)
-class GalleryContainer(Folder):
- """Galleries container"""
-
- last_id = 1
-
- def __setitem__(self, key, value):
- key = str(self.last_id)
- super(GalleryContainer, self).__setitem__(key, value)
- self.last_id += 1
-
-
-@adapter_config(context=IGalleryContainerTarget, provides=IGalleryContainer)
-def gallery_container_factory(target):
- """Galleries container factory"""
- annotations = IAnnotations(target)
- container = annotations.get(GALLERY_CONTAINER_KEY)
- if container is None:
- container = annotations[GALLERY_CONTAINER_KEY] = GalleryContainer()
- get_current_registry().notify(ObjectCreatedEvent(container))
- locate(container, target, '++gallery++')
- return container
-
-
-@adapter_config(name='gallery', context=IGalleryContainerTarget, provides=ITraversable)
-class GalleryContainerNamespace(ContextAdapter):
- """++gallery++ namespace traverser"""
-
- def traverse(self, name, furtherpath=None):
- return IGalleryContainer(self.context)
-
-
-@adapter_config(name='gallery', context=IGalleryContainerTarget, provides=ISublocations)
-class GalleryContainerSublocations(ContextAdapter):
- """Galleries container sublocations"""
-
- def sublocations(self):
- return IGalleryContainer(self.context).values()
-
-
-@vocabulary_config(name='PyAMS content galleries')
-class GalleryContainerGalleriesVocabulary(SimpleVocabulary):
- """Galleries container galleries vocabulary"""
-
- def __init__(self, context):
- target = get_parent(context, IGalleryContainerTarget)
- terms = [SimpleTerm(gallery.__name__, title=II18n(gallery).query_attribute('title'))
- for gallery in IGalleryContainer(target).values()]
- super(GalleryContainerGalleriesVocabulary, self).__init__(terms)
-
-
-#
-# Galleries links container
-#
-
-@implementer(IGalleryLinksContainer)
-class GalleryLinksContainer(Persistent, Contained):
- """Galleries links container"""
-
- def __init__(self):
- self.galleries = PersistentList()
-
-
-@adapter_config(context=IGalleryLinksContainerTarget, provides=IGalleryLinksContainer)
-def gallery_links_container_factory(target):
- """Galleries links container factory"""
- annotations = IAnnotations(target)
- container = annotations.get(GALLERY_LINKS_CONTAINER_KEY)
- if container is None:
- container = annotations[GALLERY_LINKS_CONTAINER_KEY] = GalleryLinksContainer()
- get_current_registry().notify(ObjectCreatedEvent(container))
- locate(container, target, '++gallery-links++')
- return container
-
-
-@adapter_config(name='gallery-links', context=IGalleryLinksContainerTarget, provides=ITraversable)
-class GalleryLinksContainerNamespace(ContextAdapter):
- """++gallery-links++ namespace adapter"""
-
- def traverse(self, name, furtherpath=None):
- return IGalleryLinksContainer(self.context)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/gallery/file.py Mon Sep 11 14:54:30 2017 +0200
@@ -0,0 +1,100 @@
+#
+# 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.
+#
+from pyams_file.interfaces import DELETED_FILE, IResponsiveImage
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.component.gallery.interfaces import IGalleryFile
+from pyams_content.shared.common.interfaces import IWfSharedContent
+from pyams_form.interfaces.form import IFormContextPermissionChecker
+from zope.lifecycleevent.interfaces import IObjectAddedEvent, IObjectModifiedEvent, IObjectRemovedEvent
+
+# import packages
+from persistent import Persistent
+from pyams_file.property import FileProperty
+from pyams_utils.adapter import adapter_config, ContextAdapter
+from pyams_utils.traversing import get_parent
+from pyramid.events import subscriber
+from pyramid.threadlocal import get_current_registry
+from zope.lifecycleevent import ObjectModifiedEvent
+from zope.container.contained import Contained
+from zope.interface import implementer, alsoProvides
+from zope.schema.fieldproperty import FieldProperty
+
+
+#
+# Gallery file
+#
+
+@implementer(IGalleryFile)
+class GalleryFile(Persistent, Contained):
+ """Gallery file info"""
+
+ title = FieldProperty(IGalleryFile['title'])
+ alt_title = FieldProperty(IGalleryFile['alt_title'])
+ _data = FileProperty(IGalleryFile['data'])
+ description = FieldProperty(IGalleryFile['description'])
+ author = FieldProperty(IGalleryFile['author'])
+ author_comments = FieldProperty(IGalleryFile['author_comments'])
+ sound = FileProperty(IGalleryFile['sound'])
+ sound_title = FieldProperty(IGalleryFile['sound_title'])
+ sound_description = FieldProperty(IGalleryFile['sound_description'])
+ pif_number = FieldProperty(IGalleryFile['pif_number'])
+ visible = FieldProperty(IGalleryFile['visible'])
+
+ @property
+ def data(self):
+ return self._data
+
+ @data.setter
+ def data(self, value):
+ self._data = value
+ if (value is not None) and (value is not DELETED_FILE):
+ alsoProvides(self._data, IResponsiveImage)
+
+
+@adapter_config(context=IGalleryFile, provides=IFormContextPermissionChecker)
+class GalleryFilePermissionChecker(ContextAdapter):
+ """Gallery file permission checker"""
+
+ @property
+ def edit_permission(self):
+ content = get_parent(self.context, IWfSharedContent)
+ return IFormContextPermissionChecker(content).edit_permission
+
+
+@subscriber(IObjectAddedEvent, context_selector=IGalleryFile)
+def handle_added_gallery_file(event):
+ """Handle added gallery file"""
+ content = get_parent(event.object, IWfSharedContent)
+ if content is not None:
+ get_current_registry().notify(ObjectModifiedEvent(content))
+
+
+@subscriber(IObjectModifiedEvent, context_selector=IGalleryFile)
+def handle_modified_gallery_file(event):
+ """Handle modified gallery file"""
+ content = get_parent(event.object, IWfSharedContent)
+ if content is not None:
+ get_current_registry().notify(ObjectModifiedEvent(content))
+
+
+@subscriber(IObjectRemovedEvent, context_selector=IGalleryFile)
+def handle_removed_gallery_file(event):
+ """Handle removed gallery file"""
+ content = get_parent(event.object, IWfSharedContent)
+ if content is not None:
+ get_current_registry().notify(ObjectModifiedEvent(content))
--- a/src/pyams_content/component/gallery/interfaces/__init__.py Mon Sep 11 14:53:15 2017 +0200
+++ b/src/pyams_content/component/gallery/interfaces/__init__.py Mon Sep 11 14:54:30 2017 +0200
@@ -16,35 +16,40 @@
# import standard library
# import interfaces
-from pyams_file.interfaces import IMediaFile
-from zope.container.interfaces import IContainer, IOrderedContainer
+from pyams_content.component.paragraph.interfaces import IBaseParagraph
+from zope.container.interfaces import IOrderedContainer
+from zope.contentprovider.interfaces import IContentProvider
# import packages
-from pyams_file.schema import FileField
+from pyams_file.schema import FileField, ImageField
from pyams_i18n.schema import I18nTextLineField, I18nTextField
-from pyams_utils.schema import PersistentList
from zope.annotation.interfaces import IAttributeAnnotatable
-from zope.container.constraints import containers, contains
-from zope.interface import Interface
-from zope.schema import Choice, Bool, TextLine
+from zope.container.constraints import contains, containers
+from zope.interface import Interface, Attribute
+from zope.schema import Bool, TextLine, Choice
from pyams_content import _
GALLERY_CONTAINER_KEY = 'pyams_content.gallery'
-GALLERY_FILE_INFO_KEY = 'pyams_content.gallery.info'
-GALLERY_LINKS_CONTAINER_KEY = 'pyams_content.gallery.links'
class IGalleryFile(Interface):
"""Gallery file marker interface"""
+ containers('.IGallery')
-class IGalleryFileInfo(Interface):
- """Gallery file info"""
+ title = I18nTextLineField(title=_("Legend"),
+ description=_("Image title"),
+ required=False)
- title = I18nTextLineField(title=_("Title"),
- required=False)
+ alt_title = I18nTextLineField(title=_("Accessibility title"),
+ description=_("Alternate title used to describe image content"),
+ required=False)
+
+ data = ImageField(title=_("Image data"),
+ description=_("Image content"),
+ required=True)
description = I18nTextField(title=_("Description"),
required=False)
@@ -81,8 +86,6 @@
class IBaseGallery(IOrderedContainer, IAttributeAnnotatable):
"""Base gallery interface"""
- containers('.IGalleryContainer')
-
title = I18nTextLineField(title=_("Title"),
description=_("Gallery title, as shown in front-office"),
required=True)
@@ -91,10 +94,11 @@
description=_("Gallery description displayed by front-office template"),
required=False)
- visible = Bool(title=_("Visible gallery?"),
- description=_("If 'no', this gallery won't be displayed in front office"),
- required=True,
- default=True)
+ renderer = Choice(title=_("Gallery style"),
+ vocabulary='PyAMS gallery renderers')
+
+ def append(self, value, notify=True):
+ """Append new file to gallery"""
def get_visible_images(self):
"""Get iterator over visible images"""
@@ -103,27 +107,18 @@
class IGallery(IBaseGallery):
"""Gallery interface"""
- contains(IMediaFile)
-
-
-class IGalleryContainer(IContainer):
- """Galleries container"""
-
- contains(IBaseGallery)
+ contains(IGalleryFile)
-class IGalleryContainerTarget(Interface):
- """Galleries container marker interface"""
+class IGalleryRenderer(IContentProvider):
+ """Gallery renderer utility interface"""
+
+ label = Attribute("Renderer label")
-class IGalleryLinksContainer(Interface):
- """Galleries links container interface"""
-
- galleries = PersistentList(title=_("Contained galleries"),
- description=_("List of images galleries linked to this object"),
- value_type=Choice(vocabulary="PyAMS content galleries"),
- required=False)
+class IGalleryTarget(IAttributeAnnotatable):
+ """Gallery container target marker interface"""
-class IGalleryLinksContainerTarget(Interface):
- """Galleries links container marker interface"""
+class IGalleryParagraph(IGallery, IBaseParagraph):
+ """Gallery paragraph"""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/gallery/paragraph.py Mon Sep 11 14:54:30 2017 +0200
@@ -0,0 +1,44 @@
+#
+# 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_content.component.gallery.interfaces import IGalleryParagraph
+from pyams_content.component.paragraph.interfaces import IParagraphFactory
+
+# import packages
+from pyams_content.component.gallery import Gallery as BaseGallery
+from pyams_content.component.paragraph import BaseParagraph
+from pyams_utils.registry import utility_config
+from zope.interface import implementer
+
+from pyams_content import _
+
+
+@implementer(IGalleryParagraph)
+class Gallery(BaseGallery, BaseParagraph):
+ """Gallery class"""
+
+ icon_class = 'fa-picture-o'
+ icon_hint = _("Images gallery")
+
+
+@utility_config(name='gallery', provides=IParagraphFactory)
+class GalleryFactory(object):
+ """Gallery paragraph factory"""
+
+ name = _("Images gallery")
+ content_type = Gallery
--- a/src/pyams_content/component/gallery/zmi/__init__.py Mon Sep 11 14:53:15 2017 +0200
+++ b/src/pyams_content/component/gallery/zmi/__init__.py Mon Sep 11 14:54:30 2017 +0200
@@ -14,145 +14,37 @@
# import standard library
+import json
# import interfaces
-from pyams_content.component.gallery.interfaces import IGalleryContainerTarget, IGallery, IGalleryContainer, \
- IGalleryFile, IGalleryFileInfo
-from pyams_content.component.gallery.zmi.interfaces import IGalleryImageAddFields
+from pyams_content.component.gallery.interfaces import IGallery, IGalleryRenderer
+from pyams_content.component.gallery.zmi.interfaces import IGalleryImageAddFields, IGalleryImagesView
from pyams_content.interfaces import MANAGE_CONTENT_PERMISSION
-from pyams_file.interfaces.archive import IArchiveExtractor
+from pyams_form.interfaces.form import IWidgetsPrefixViewletsManager
from pyams_i18n.interfaces import II18n
-from pyams_skin.interfaces.viewlet import IWidgetTitleViewletManager
from pyams_skin.layer import IPyAMSLayer
from pyams_utils.interfaces import VIEW_SYSTEM_PERMISSION
-from z3c.form.interfaces import NOT_CHANGED
# import packages
-from pyams_content.component.gallery import Gallery
-from pyams_content.component.gallery.zmi.container import GalleryContainerView
-from pyams_file.file import get_magic_content_type, FileFactory
-from pyams_form.form import AJAXAddForm, AJAXEditForm
-from pyams_form.security import ProtectedFormObjectMixin
+from pyams_content.shared.common.zmi import WfSharedContentPermissionMixin
+from pyams_form.form import AJAXEditForm
from pyams_pagelet.pagelet import pagelet_config
-from pyams_skin.viewlet.toolbar import ToolbarAction
-from pyams_utils.registry import query_utility
-from pyams_utils.traversing import get_parent
-from pyams_viewlet.viewlet import viewlet_config
-from pyams_zmi.form import AdminDialogAddForm, AdminDialogEditForm
+from pyams_template.template import template_config, get_view_template
+from pyams_utils.adapter import adapter_config, ContextRequestAdapter
+from pyams_viewlet.viewlet import viewlet_config, Viewlet
+from pyams_zmi.form import AdminDialogEditForm, AdminDialogDisplayForm
+from pyramid.exceptions import NotFound
+from pyramid.renderers import render_to_response
from pyramid.view import view_config
from z3c.form import field
-from zope.interface import alsoProvides
-from zope.lifecycleevent import ObjectCreatedEvent
+from zope.interface import implementer, Interface
from pyams_content import _
-@viewlet_config(name='add-gallery.menu', context=IGalleryContainerTarget, view=GalleryContainerView,
- layer=IPyAMSLayer, manager=IWidgetTitleViewletManager, weight=50)
-class GalleryAddMenu(ProtectedFormObjectMixin, ToolbarAction):
- """Gallery add menu"""
-
- label = _("Add gallery")
-
- url = 'add-gallery.html'
- modal_target = True
-
-
-@pagelet_config(name='add-gallery.html', context=IGalleryContainerTarget, layer=IPyAMSLayer,
- permission=MANAGE_CONTENT_PERMISSION)
-class GalleryAddForm(AdminDialogAddForm):
- """Gallery add form"""
-
- legend = _("Add new images gallery")
- icon_css_class = 'fa fa-fw fa-picture-o'
-
- fields = field.Fields(IGallery).omit('__parent__', '__name__') + \
- field.Fields(IGalleryImageAddFields)
-
- @property
- def ajax_handler(self):
- origin = self.request.params.get('origin')
- if origin == 'link':
- return 'add-gallery-link.json'
- else:
- return 'add-gallery.json'
-
- edit_permission = MANAGE_CONTENT_PERMISSION
-
- def updateWidgets(self, prefix=None):
- super(GalleryAddForm, self).updateWidgets(prefix)
- if 'description' in self.widgets:
- self.widgets['description'].widget_css_class = 'textarea'
- if 'author_comments' in self.widgets:
- self.widgets['author_comments'].widget_css_class = 'textarea'
-
- def create(self, data):
- gallery = Gallery()
- images = data['images_data']
- if images and (images is not NOT_CHANGED):
- medias = []
- if isinstance(images, (list, tuple)):
- images = images[1]
- if hasattr(images, 'seek'):
- images.seek(0)
- registry = self.request.registry
- content_type = get_magic_content_type(images)
- if hasattr(images, 'seek'):
- images.seek(0)
- extractor = query_utility(IArchiveExtractor, name=content_type.decode())
- if extractor is not None:
- extractor.initialize(images)
- for content, filename in extractor.get_contents():
- media = FileFactory(content)
- registry.notify(ObjectCreatedEvent(media))
- medias.append(media)
- else:
- media = FileFactory(images)
- registry.notify(ObjectCreatedEvent(media))
- medias.append(media)
- for media in medias:
- alsoProvides(media, IGalleryFile)
- IGalleryFileInfo(media).author = data.get('author')
- IGalleryFileInfo(media).author_comments = data.get('author_comments')
- gallery['none'] = media
- return gallery
-
- def update_content(self, content, data):
- content.title = data.get('title')
- content.description = data.get('description')
- content.visible = data.get('visible')
-
- def add(self, object):
- IGalleryContainer(self.context)['none'] = object
-
-
-@view_config(name='add-gallery.json', context=IGalleryContainerTarget, request_type=IPyAMSLayer,
- permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
-class GalleryAJAXAddForm(AJAXAddForm, GalleryAddForm):
- """Gallery add form, JSON renderer"""
-
- def get_ajax_output(self, changes):
- return {'status': 'reload',
- 'location': '#galleries.html'}
-
-
-@view_config(name='add-gallery-link.json', context=IGalleryContainerTarget, request_type=IPyAMSLayer,
- permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
-class GalleryLinkAJAXAddForm(AJAXAddForm, GalleryAddForm):
- """Gallery link add form, JSON renderer"""
-
- def get_ajax_output(self, changes):
- target = get_parent(self.context, IGalleryContainerTarget)
- container = IGalleryContainer(target)
- galleries = [{'id': gallery.__name__,
- 'text': II18n(gallery).query_attribute('title', request=self.request)}
- for gallery in container.values()]
- return {'status': 'callback',
- 'callback': 'PyAMS_content.galleries.refresh',
- 'options': {'galleries': galleries,
- 'new_gallery': {'id': changes.__name__,
- 'text': II18n(changes).query_attribute('title', request=self.request)}}}
-
+#
+# Gallery properties
+#
@pagelet_config(name='properties.html', context=IGallery, layer=IPyAMSLayer, permission=VIEW_SYSTEM_PERMISSION)
class GalleryPropertiesEditForm(AdminDialogEditForm):
@@ -167,7 +59,7 @@
def updateWidgets(self, prefix=None):
super(GalleryPropertiesEditForm, self).updateWidgets(prefix)
- if 'description' in self.comments:
+ if 'description' in self.widgets:
self.widgets['description'].widget_css_class = 'textarea'
@@ -182,3 +74,89 @@
'location': '#external-files.html'}
else:
return super(GalleryPropertiesAJAXEditForm, self).get_ajax_output(changes)
+
+
+#
+# Gallery contents
+#
+
+@pagelet_config(name='contents.html', context=IGallery, layer=IPyAMSLayer, permission=VIEW_SYSTEM_PERMISSION)
+@implementer(IGalleryImagesView)
+class GalleryContentForm(AdminDialogDisplayForm):
+ """Gallery contents form"""
+
+ legend = _("Update gallery contents")
+ dialog_class = 'modal-max'
+
+ fields = field.Fields(Interface)
+ show_widget_title = True
+
+
+@viewlet_config(name='gallery-images', context=IGallery, view=IGalleryImagesView, manager=IWidgetsPrefixViewletsManager)
+@template_config(template='templates/gallery-images.pt', layer=IPyAMSLayer)
+@implementer(IGalleryImagesView)
+class GalleryImagesViewlet(Viewlet):
+ """Gallery images viewlet"""
+
+ def get_title(self, image):
+ return II18n(image).query_attribute('title', request=self.request)
+
+
+@view_config(name='get-gallery-images.html', context=IGallery, request_type=IPyAMSLayer,
+ permission=VIEW_SYSTEM_PERMISSION)
+@implementer(IGalleryImagesView)
+class GalleryImagesView(WfSharedContentPermissionMixin):
+ """Gallery images view"""
+
+ def __init__(self, context, request):
+ self.context = context
+ self.request = request
+
+ def __call__(self):
+ return render_to_response('templates/gallery-images.pt', {'view': self}, request=self.request)
+
+ def get_title(self, image):
+ return II18n(image).query_attribute('title', request=self.request)
+
+
+@view_config(name='set-images-order.json', context=IGallery, request_type=IPyAMSLayer,
+ permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
+def set_images_order(request):
+ """Set gallery images order"""
+ images_names = json.loads(request.params.get('images'))
+ request.context.updateOrder(images_names)
+ return {'status': 'success'}
+
+
+@view_config(name='set-image-visibility.json', context=IGallery, request_type=IPyAMSLayer,
+ permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
+def set_image_visibility(request):
+ """Set gallery image visibility"""
+ gallery = IGallery(request.context)
+ image = gallery.get(str(request.params.get('object_name')))
+ if image is None:
+ raise NotFound()
+ image.visible = not image.visible
+ return {'visible': image.visible}
+
+
+#
+# Gallery renderers
+#
+
+class BaseGalleryRenderer(ContextRequestAdapter):
+ """Base gallery renderer"""
+
+ def update(self):
+ pass
+
+ render = get_view_template()
+
+
+@adapter_config(name='default', context=(IGallery, IPyAMSLayer), provides=IGalleryRenderer)
+@template_config(template='templates/renderer-default.pt', layer=IPyAMSLayer)
+class DefaultGalleryRenderer(BaseGalleryRenderer):
+ """Default gallery renderer"""
+
+ label = _("Default gallery renderer")
+ weight = 1
--- a/src/pyams_content/component/gallery/zmi/container.py Mon Sep 11 14:53:15 2017 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,208 +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_content.component.gallery.interfaces import IGalleryContainerTarget, IGalleryContainer, \
- IGalleryLinksContainerTarget, IGalleryLinksContainer
-from pyams_content.interfaces import MANAGE_CONTENT_PERMISSION
-from pyams_i18n.interfaces import II18n
-from pyams_skin.interfaces import IInnerPage, IPageHeader
-from pyams_utils.interfaces import VIEW_SYSTEM_PERMISSION
-from pyams_utils.interfaces.data import IObjectData
-from pyams_zmi.interfaces.menu import IPropertiesMenu
-from pyams_zmi.layer import IAdminLayer
-from z3c.table.interfaces import IColumn, IValues
-
-# import packages
-from pyams_content.component.gallery.zmi.widget import GalleryLinkSelectFieldWidget
-from pyams_content.shared.common.zmi import WfModifiedContentColumnMixin
-from pyams_form.form import AJAXEditForm
-from pyams_form.security import ProtectedFormObjectMixin
-from pyams_pagelet.pagelet import pagelet_config
-from pyams_skin.layer import IPyAMSLayer
-from pyams_skin.page import DefaultPageHeaderAdapter
-from pyams_skin.table import BaseTable, I18nColumn, TrashColumn, ActionColumn
-from pyams_skin.viewlet.menu import MenuItem
-from pyams_template.template import template_config
-from pyams_utils.adapter import adapter_config, ContextRequestViewAdapter
-from pyams_utils.url import absolute_url
-from pyams_viewlet.viewlet import viewlet_config
-from pyams_zmi.form import AdminDialogEditForm
-from pyams_zmi.view import AdminView
-from pyramid.decorator import reify
-from pyramid.view import view_config
-from z3c.table.column import GetAttrColumn
-from z3c.form import field
-from zope.interface import implementer, alsoProvides
-
-from pyams_content import _
-
-
-@viewlet_config(name='galleries.menu', context=IGalleryContainerTarget, layer=IAdminLayer,
- manager=IPropertiesMenu, permission=VIEW_SYSTEM_PERMISSION, weight=220)
-class GalleryContainerMenu(MenuItem):
- """Galleries container menu"""
-
- label = _("Images galleries...")
- icon_class = 'fa-picture-o'
- url = '#galleries.html'
-
-
-#
-# Galleries container views
-#
-
-@pagelet_config(name='galleries.html', context=IGalleryContainerTarget, layer=IPyAMSLayer,
- permission=VIEW_SYSTEM_PERMISSION)
-@template_config(template='templates/container.pt', layer=IPyAMSLayer)
-@implementer(IInnerPage)
-class GalleryContainerView(AdminView):
- """Galleries container view"""
-
- title = _("Galleries list")
-
- def __init__(self, context, request):
- super(GalleryContainerView, self).__init__(context, request)
- self.galleries_table = GalleryContainerTable(context, request)
-
- def update(self):
- super(GalleryContainerView, self).update()
- self.galleries_table.update()
-
-
-class GalleryContainerTable(BaseTable):
- """Galleries container table"""
-
- hide_header = True
- cssClasses = {'table': 'table table-bordered table-striped table-hover table-tight'}
-
- def __init__(self, context, request):
- super(GalleryContainerTable, self).__init__(context, request)
- self.object_data = {'ams-widget-toggle-button': 'false'}
- alsoProvides(self, IObjectData)
-
- @property
- def data_attributes(self):
- attributes = super(GalleryContainerTable, self).data_attributes
- attributes['table'] = {'data-ams-location': absolute_url(IGalleryContainer(self.context), self.request),
- 'data-ams-datatable-sort': 'false',
- 'data-ams-datatable-pagination': 'false'}
- return attributes
-
- @reify
- def values(self):
- return list(super(GalleryContainerTable, self).values)
-
- def render(self):
- if not self.values:
- translate = self.request.localizer.translate
- return translate(_("No currently defined gallery."))
- return super(GalleryContainerTable, self).render()
-
-
-@adapter_config(name='manage', context=(IGalleryContainerTarget, IPyAMSLayer, GalleryContainerTable), provides=IColumn)
-class GalleryContainerManageColumn(ActionColumn):
- """Gallery container manage column"""
-
- icon_class = 'fa fa-fw fa-camera'
- icon_hint = _("Display gallery contents")
-
- url = 'contents.html'
- target = None
- modal_target = True
-
- weight = 5
-
-
-@adapter_config(name='name', context=(IGalleryContainerTarget, IPyAMSLayer, GalleryContainerTable), provides=IColumn)
-class GalleryContainerNameColumn(I18nColumn, WfModifiedContentColumnMixin, GetAttrColumn):
- """Galleries container name column"""
-
- _header = _("Title")
-
- weight = 10
-
- def getValue(self, obj):
- return II18n(obj).query_attribute('title', request=self.request)
-
-
-@adapter_config(name='count', context=(IGalleryContainerTarget, IPyAMSLayer, GalleryContainerTable), provides=IColumn)
-class GalleryContainerCountColumn(I18nColumn, GetAttrColumn):
- """Gallery container images counter column"""
-
- _header = _("Images")
-
- weight = 20
-
- def getValue(self, obj):
- return len(obj)
-
-
-@adapter_config(name='trash', context=(IGalleryContainerTarget, IPyAMSLayer, GalleryContainerTable), provides=IColumn)
-class GalleryContainerTrashColumn(ProtectedFormObjectMixin, TrashColumn):
- """Galleries container trash column"""
-
-
-@adapter_config(context=(IGalleryContainerTarget, IPyAMSLayer, GalleryContainerTable), provides=IValues)
-class GalleryContainerValues(ContextRequestViewAdapter):
- """Galleries container values"""
-
- @property
- def values(self):
- return IGalleryContainer(self.context).values()
-
-
-@adapter_config(context=(IGalleryContainerTarget, IPyAMSLayer, GalleryContainerView), provides=IPageHeader)
-class GalleryHeaderAdapter(DefaultPageHeaderAdapter):
- """Galleries container header adapter"""
-
- back_url = '#properties.html'
- icon_class = 'fa fa-fw fa-picture-o'
-
-
-#
-# Galleries links edit form
-#
-
-@pagelet_config(name='gallery-links.html', context=IGalleryLinksContainerTarget, layer=IPyAMSLayer,
- permission=VIEW_SYSTEM_PERMISSION)
-class GalleryLinksContainerLinksEditForm(AdminDialogEditForm):
- """Galleries links container edit form"""
-
- legend = _("Edit galleries links")
-
- fields = field.Fields(IGalleryLinksContainer)
- fields['galleries'].widgetFactory = GalleryLinkSelectFieldWidget
-
- ajax_handler = 'gallery-links.json'
- edit_permission = MANAGE_CONTENT_PERMISSION
-
-
-@view_config(name='gallery-links.json', context=IGalleryLinksContainerTarget, request_type=IPyAMSLayer,
- permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
-class GalleryLinksContainerAJAXEditForm(AJAXEditForm, GalleryLinksContainerLinksEditForm):
- """Galleries links container edit form, JSON renderer"""
-
- def get_ajax_output(self, changes):
- if 'galleries' in changes.get(IGalleryLinksContainer, ()):
- return {'status': 'success',
- 'event': 'PyAMS_content.changed_item',
- 'event_options': {'object_type': 'galleries_container',
- 'object_name': self.context.__name__,
- 'nb_galleries': len(IGalleryLinksContainer(self.context).galleries or ())}}
- else:
- return super(GalleryLinksContainerAJAXEditForm, self).get_ajax_output(changes)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/gallery/zmi/file.py Mon Sep 11 14:54:30 2017 +0200
@@ -0,0 +1,231 @@
+#
+# 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.
+#
+from pyams_utils.traversing import get_parent
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.component.gallery.interfaces import IGallery, IGalleryFile
+from pyams_content.component.gallery.zmi.interfaces import IGalleryImageAddFields, IGalleryImagesView
+from pyams_content.interfaces import MANAGE_CONTENT_PERMISSION
+from pyams_file.interfaces.archive import IArchiveExtractor
+from pyams_i18n.interfaces import II18n
+from pyams_skin.interfaces.viewlet import IWidgetTitleViewletManager, IContextActions
+from pyams_skin.layer import IPyAMSLayer
+from pyams_utils.interfaces import VIEW_SYSTEM_PERMISSION, VIEW_PERMISSION
+from z3c.form.interfaces import NOT_CHANGED
+from zope.schema.interfaces import WrongType
+
+# import packages
+from pyams_content.component.gallery.file import GalleryFile
+from pyams_content.shared.common.zmi import WfSharedContentPermissionMixin
+from pyams_file.file import get_magic_content_type
+from pyams_file.zmi.file import FilePropertiesAction
+from pyams_form.form import AJAXAddForm, AJAXEditForm
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_skin.viewlet.toolbar import ToolbarAction, JsToolbarActionItem
+from pyams_utils.registry import query_utility
+from pyams_utils.url import absolute_url
+from pyams_viewlet.viewlet import viewlet_config
+from pyams_zmi.form import AdminDialogEditForm, AdminDialogAddForm
+from pyramid.view import view_config
+from z3c.form import field
+from zope.lifecycleevent import ObjectCreatedEvent
+
+from pyams_content import _
+
+
+@viewlet_config(name='add-image.menu', context=IGallery, view=IGalleryImagesView, manager=IWidgetTitleViewletManager)
+class GalleryImageAddMenu(WfSharedContentPermissionMixin, ToolbarAction):
+ """Gallery image add menu"""
+
+ label = _("Add image(s)")
+
+ url = 'add-image.html'
+ modal_target = True
+ stop_propagation = True
+
+
+@pagelet_config(name='add-image.html', context=IGallery, layer=IPyAMSLayer, permission=MANAGE_CONTENT_PERMISSION)
+class GalleryImageAddForm(AdminDialogAddForm):
+ """Gallery image add form"""
+
+ legend = _("Add image(s)")
+ icon_css_class = 'fa -fa-fw fa-picture-o'
+
+ fields = field.Fields(IGalleryImageAddFields)
+ ajax_handler = 'add-image.json'
+
+ def updateWidgets(self, prefix=None):
+ super(GalleryImageAddForm, self).updateWidgets(prefix)
+ if 'author_comments' in self.widgets:
+ self.widgets['author_comments'].widget_css_class = 'textarea'
+
+ def create(self, data):
+ medias = []
+ images = data['images_data']
+ if images and (images is not NOT_CHANGED):
+ filename = None
+ if isinstance(images, (list, tuple)):
+ filename, images = images
+ if hasattr(images, 'seek'):
+ images.seek(0)
+ registry = self.request.registry
+ content_type = get_magic_content_type(images)
+ if isinstance(content_type, bytes):
+ content_type = content_type.decode()
+ if hasattr(images, 'seek'):
+ images.seek(0)
+ extractor = query_utility(IArchiveExtractor, name=content_type)
+ if extractor is not None:
+ extractor.initialize(images)
+ for content, filename in extractor.get_contents():
+ try:
+ media = GalleryFile()
+ media.data = filename, content
+ except WrongType:
+ continue
+ else:
+ registry.notify(ObjectCreatedEvent(media))
+ medias.append(media)
+ else:
+ try:
+ media = GalleryFile()
+ media.data = filename, images if filename else images
+ except WrongType:
+ pass
+ else:
+ registry.notify(ObjectCreatedEvent(media))
+ medias.append(media)
+ for media in medias:
+ media.author = data.get('author')
+ media.author_comments = data.get('author_comments')
+ self.context.append(media)
+ return None
+
+
+@view_config(name='add-image.json', context=IGallery, request_type=IPyAMSLayer,
+ permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
+class GalleryImageAJAXAddForm(AJAXAddForm, GalleryImageAddForm):
+ """Gallery image add form, JSON renderer"""
+
+ def get_ajax_output(self, changes):
+ return {'status': 'reload',
+ 'location': absolute_url(self.context, self.request, 'get-gallery-images.html'),
+ 'target': '#gallery_images_{0}'.format(self.context.__name__)}
+
+
+@viewlet_config(name='file.showhide.action', context=IGalleryFile, layer=IPyAMSLayer, view=IGalleryImagesView,
+ manager=IContextActions, permission=VIEW_SYSTEM_PERMISSION, weight=1)
+class GalleryFileShowAddAction(JsToolbarActionItem):
+ """Gallery file show/hide action"""
+
+ label = _("Show/hide image")
+
+ @property
+ def label_css_class(self):
+ if self.context.visible:
+ return 'fa fa-fw fa-eye'
+ else:
+ return 'fa fa-fw fa-eye-slash text-danger'
+
+ hint_gravity = 'nw'
+
+ url = 'PyAMS_content.galleries.switchImageVisibility'
+
+
+@viewlet_config(name='file.properties.action', context=IGalleryFile, layer=IPyAMSLayer, view=IGalleryImagesView,
+ manager=IContextActions, permission=VIEW_SYSTEM_PERMISSION, weight=5)
+class GalleryFilePropertiesAction(FilePropertiesAction):
+ """Media properties action"""
+
+ url = 'gallery-file-properties.html'
+
+
+@pagelet_config(name='gallery-file-properties.html', context=IGalleryFile, layer=IPyAMSLayer,
+ permission=VIEW_SYSTEM_PERMISSION)
+class GalleryFilePropertiesEditForm(AdminDialogEditForm):
+ """Gallery file properties edit form"""
+
+ legend = _("Update image properties")
+ icon_css_class = 'fa fa-fw fa-picture-o'
+ dialog_class = 'modal-large'
+
+ fields = field.Fields(IGalleryFile).omit('__parent__', '__name__', 'visible')
+ ajax_handler = 'gallery-file-properties.json'
+
+ @property
+ def title(self):
+ gallery = get_parent(self.context, IGallery)
+ return II18n(gallery).query_attribute('title', request=self.request)
+
+ def updateWidgets(self, prefix=None):
+ super(GalleryFilePropertiesEditForm, self).updateWidgets(prefix)
+ if 'description' in self.widgets:
+ self.widgets['description'].widget_css_class = 'textarea'
+ if 'author_comments' in self.widgets:
+ self.widgets['author_comments'].widget_css_class = 'textarea'
+ if 'sound_description' in self.widgets:
+ self.widgets['sound_description'].widget_css_class = 'textarea'
+
+
+@view_config(name='gallery-file-properties.json', context=IGalleryFile, request_type=IPyAMSLayer,
+ permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
+class GalleryFileInfoPropertiesAJAXEditForm(AJAXEditForm, GalleryFilePropertiesEditForm):
+ """Gallery file properties edit form, JSON renderer"""
+
+
+@viewlet_config(name='gallery-file-download.action', context=IGalleryFile, layer=IPyAMSLayer, view=IGalleryImagesView,
+ manager=IContextActions, permission=VIEW_PERMISSION, weight=89)
+class GalleryFileDownloaderAction(JsToolbarActionItem):
+ """Gallery file downloader action"""
+
+ label = _("Download image...")
+ label_css_class = 'fa fa-fw fa-download'
+ hint_gravity = 'nw'
+
+ @property
+ def url(self):
+ return absolute_url(self.context.data, self.request, query={'download': '1'})
+
+
+@viewlet_config(name='gallery-file-remover.action', context=IGalleryFile, layer=IPyAMSLayer, view=IGalleryImagesView,
+ manager=IContextActions, weight=90)
+class GalleryFileRemoverAction(WfSharedContentPermissionMixin, JsToolbarActionItem):
+ """Gallery file remover action"""
+
+ label = _("Remove image...")
+ label_css_class = 'fa fa-fw fa-trash'
+ hint_gravity = 'nw'
+
+ url = 'PyAMS_content.galleries.removeFile'
+
+
+@view_config(name='delete-element.json', context=IGallery, request_type=IPyAMSLayer,
+ permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
+def delete_gallery_element(request):
+ """Delete gallery element"""
+ 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 image name doesn't exist!"))}}
+ del request.context[name]
+ return {'status': 'success'}
--- a/src/pyams_content/component/gallery/zmi/gallery.py Mon Sep 11 14:53:15 2017 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,245 +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 json
-
-# import interfaces
-from pyams_content.component.gallery.interfaces import IGallery, IGalleryFileInfo, IGalleryFile
-from pyams_content.component.gallery.zmi.interfaces import IGalleryImageAddFields
-from pyams_content.interfaces import MANAGE_CONTENT_PERMISSION
-from pyams_file.interfaces.archive import IArchiveExtractor
-from pyams_form.interfaces.form import IWidgetsPrefixViewletsManager
-from pyams_i18n.interfaces import II18n
-from pyams_skin.interfaces.viewlet import IWidgetTitleViewletManager, IContextActions
-from pyams_skin.layer import IPyAMSLayer
-from pyams_utils.interfaces import VIEW_SYSTEM_PERMISSION
-from z3c.form.interfaces import NOT_CHANGED
-
-# import packages
-from pyams_content.shared.common.zmi import WfSharedContentPermissionMixin
-from pyams_file.file import get_magic_content_type, FileFactory
-from pyams_file.zmi.file import FilePropertiesAction
-from pyams_form.form import AJAXAddForm, AJAXEditForm
-from pyams_pagelet.pagelet import pagelet_config
-from pyams_skin.viewlet.toolbar import ToolbarAction, ToolbarMenuDivider, JsToolbarMenuItem
-from pyams_template.template import template_config
-from pyams_utils.registry import query_utility
-from pyams_utils.url import absolute_url
-from pyams_viewlet.viewlet import viewlet_config, Viewlet
-from pyams_zmi.form import AdminDialogEditForm, AdminDialogAddForm, AdminDialogDisplayForm
-from pyramid.renderers import render_to_response
-from pyramid.view import view_config
-from z3c.form import field
-from zope.interface import alsoProvides, Interface
-from zope.lifecycleevent import ObjectCreatedEvent
-
-from pyams_content import _
-
-
-@pagelet_config(name='contents.html', context=IGallery, layer=IPyAMSLayer, permission=VIEW_SYSTEM_PERMISSION)
-class GalleryContentForm(AdminDialogDisplayForm):
- """Gallery contents form"""
-
- legend = _("Update gallery contents")
- dialog_class = 'modal-max'
-
- fields = field.Fields(Interface)
- show_widget_title = True
-
-
-@viewlet_config(name='add-image.menu', context=IGallery, view=GalleryContentForm, manager=IWidgetTitleViewletManager)
-class GalleryImageAddMenu(WfSharedContentPermissionMixin, ToolbarAction):
- """Gallery image add menu"""
-
- label = _("Add image(s)")
-
- url = 'add-image.html'
- modal_target = True
- stop_propagation = True
-
-
-@pagelet_config(name='add-image.html', context=IGallery, layer=IPyAMSLayer, permission=MANAGE_CONTENT_PERMISSION)
-class GalleryImageAddForm(AdminDialogAddForm):
- """Gallery image add form"""
-
- legend = _("Add image(s)")
- icon_css_class = 'fa -fa-fw fa-picture-o'
-
- fields = field.Fields(IGalleryImageAddFields)
- ajax_handler = 'add-image.json'
-
- def updateWidgets(self, prefix=None):
- super(GalleryImageAddForm, self).updateWidgets(prefix)
- if 'author_comments' in self.widgets:
- self.widgets['author_comments'].widget_css_class = 'textarea'
-
- def create(self, data):
- medias = []
- images = data['images_data']
- if images and (images is not NOT_CHANGED):
- if isinstance(images, (list, tuple)):
- images = images[1]
- if hasattr(images, 'seek'):
- images.seek(0)
- registry = self.request.registry
- content_type = get_magic_content_type(images)
- if hasattr(images, 'seek'):
- images.seek(0)
- extractor = query_utility(IArchiveExtractor, name=content_type.decode())
- if extractor is not None:
- extractor.initialize(images)
- for content, filename in extractor.get_contents():
- media = FileFactory(content)
- registry.notify(ObjectCreatedEvent(media))
- medias.append(media)
- else:
- media = FileFactory(images)
- registry.notify(ObjectCreatedEvent(media))
- medias.append(media)
- for media in medias:
- alsoProvides(media, IGalleryFile)
- IGalleryFileInfo(media).author = data.get('author')
- IGalleryFileInfo(media).author_comments = data.get('author_comments')
- self.context['none'] = media
- return None
-
-
-@view_config(name='add-image.json', context=IGallery, request_type=IPyAMSLayer,
- permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
-class GalleryImageAJAXAddForm(AJAXAddForm, GalleryImageAddForm):
- """Gallery image add form, JSON renderer"""
-
- def get_ajax_output(self, changes):
- return {'status': 'reload',
- 'location': absolute_url(self.context, self.request, 'get-gallery-images.html'),
- 'target': '#gallery-images'}
-
-
-@viewlet_config(name='gallery-images', context=IGallery, view=GalleryContentForm, manager=IWidgetsPrefixViewletsManager)
-@template_config(template='templates/gallery-images.pt', layer=IPyAMSLayer)
-class GalleryImagesViewlet(Viewlet):
- """Gallery images viewlet"""
-
- def get_info(self, image):
- return IGalleryFileInfo(image)
-
- def get_title(self, image):
- return II18n(IGalleryFileInfo(image)).query_attribute('title', request=self.request)
-
-
-@view_config(name='get-gallery-images.html', context=IGallery, request_type=IPyAMSLayer,
- permission=VIEW_SYSTEM_PERMISSION)
-class GalleryImagesView(WfSharedContentPermissionMixin):
- """Gallery images view"""
-
- def __init__(self, context, request):
- self.context = context
- self.request = request
-
- def __call__(self):
- return render_to_response('templates/gallery-images.pt', {'view': self}, request=self.request)
-
- def get_info(self, image):
- return IGalleryFileInfo(image)
-
- def get_title(self, image):
- return II18n(IGalleryFileInfo(image)).query_attribute('title', request=self.request)
-
-
-@view_config(name='set-images-order.json', context=IGallery, request_type=IPyAMSLayer,
- permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
-def set_images_order(request):
- """Set gallery images order"""
- images_names = json.loads(request.params.get('images'))
- request.context.updateOrder(images_names)
- return {'status': 'success'}
-
-
-@viewlet_config(name='file.properties.action', context=IGalleryFile, layer=IPyAMSLayer, view=Interface,
- manager=IContextActions, permission=VIEW_SYSTEM_PERMISSION, weight=1)
-class GalleryFilePropertiesAction(FilePropertiesAction):
- """Media properties action"""
-
- url = 'gallery-file-properties.html'
-
-
-@pagelet_config(name='gallery-file-properties.html', context=IGalleryFile, layer=IPyAMSLayer,
- permission=VIEW_SYSTEM_PERMISSION)
-class GalleryFilePropertiesEditForm(AdminDialogEditForm):
- """Gallery file properties edit form"""
-
- legend = _("Update image properties")
- icon_css_class = 'fa fa-fw fa-edit'
-
- fields = field.Fields(IGalleryFileInfo)
- ajax_handler = 'gallery-file-properties.json'
-
- def getContent(self):
- return IGalleryFileInfo(self.context)
-
- @property
- def title(self):
- return II18n(self.getContent()).query_attribute('title', request=self.request)
-
- def updateWidgets(self, prefix=None):
- super(GalleryFilePropertiesEditForm, self).updateWidgets(prefix)
- if 'description' in self.widgets:
- self.widgets['description'].widget_css_class = 'textarea'
- if 'author_comments' in self.widgets:
- self.widgets['author_comments'].widget_css_class = 'textarea'
- if 'sound_description' in self.widgets:
- self.widgets['sound_description'].widget_css_class = 'textarea'
-
-
-@view_config(name='gallery-file-properties.json', context=IGalleryFile, request_type=IPyAMSLayer,
- permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
-class GalleryFileInfoPropertiesAJAXEditForm(AJAXEditForm, GalleryFilePropertiesEditForm):
- """Gallery file properties edit form, JSON renderer"""
-
-
-@viewlet_config(name='gallery-file-remover.divider', context=IGalleryFile, layer=IPyAMSLayer, view=Interface,
- manager=IContextActions, weight=89)
-class GalleryFileRemoverDivider(WfSharedContentPermissionMixin, ToolbarMenuDivider):
- """Gallery file remover divider"""
-
-
-@viewlet_config(name='gallery-file-remover.action', context=IGalleryFile, layer=IPyAMSLayer, view=Interface,
- manager=IContextActions, weight=90)
-class GalleryFileRemoverAction(WfSharedContentPermissionMixin, JsToolbarMenuItem):
- """Gallery file remover action"""
-
- label = _("Remove image...")
- label_css_class = 'fa fa-fw fa-trash'
-
- url = 'PyAMS_content.galleries.removeFile'
-
-
-@view_config(name='delete-element.json', context=IGallery, request_type=IPyAMSLayer,
- permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
-def delete_gallery_element(request):
- """Delete gallery element"""
- 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 image name doesn't exist!"))}}
- del request.context[name]
- return {'status': 'success'}
--- a/src/pyams_content/component/gallery/zmi/interfaces.py Mon Sep 11 14:53:15 2017 +0200
+++ b/src/pyams_content/component/gallery/zmi/interfaces.py Mon Sep 11 14:54:30 2017 +0200
@@ -18,7 +18,7 @@
# import interfaces
# import packages
-from pyams_file.schema import FileField
+from pyams_file.schema import ImageField
from pyams_i18n.schema import I18nTextField
from zope.interface import Interface
from zope.schema import TextLine
@@ -26,6 +26,10 @@
from pyams_content import _
+class IGalleryImagesView(Interface):
+ """Marker interface for gallery contents view"""
+
+
class IGalleryImageAddFields(Interface):
"""Gallery image add fields"""
@@ -36,6 +40,6 @@
description=_("Comments relatives to author's rights management"),
required=False)
- images_data = FileField(title=_("Images data"),
- description=_("You can upload a single file or choose to upload a whole ZIP archive"),
- required=False)
+ images_data = ImageField(title=_("Images data"),
+ description=_("You can upload a single file or choose to upload a whole ZIP archive"),
+ required=False)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/gallery/zmi/paragraph.py Mon Sep 11 14:54:30 2017 +0200
@@ -0,0 +1,185 @@
+#
+# 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_content.component.gallery.interfaces import IGalleryParagraph, IBaseGallery
+from pyams_content.component.gallery.zmi.interfaces import IGalleryImagesView
+from pyams_content.component.paragraph.interfaces import IParagraphContainerTarget, IParagraphContainer
+from pyams_content.component.paragraph.zmi.interfaces import IParagraphInnerEditor
+from pyams_content.interfaces import MANAGE_CONTENT_PERMISSION
+from pyams_content.shared.common.interfaces import IWfSharedContent
+from pyams_form.interfaces.form import IInnerForm, IEditFormButtons, IInnerSubForm
+from pyams_i18n.interfaces import II18n
+from pyams_skin.interfaces.viewlet import IToolbarAddingMenu, IWidgetTitleViewletManager
+from pyams_skin.layer import IPyAMSLayer
+from z3c.form.interfaces import INPUT_MODE
+
+# import packages
+from pyams_content.component.gallery.paragraph import Gallery
+from pyams_content.component.paragraph.zmi import BaseParagraphAJAXEditForm
+from pyams_content.component.paragraph.zmi.container import ParagraphContainerView
+from pyams_content.shared.common.zmi import WfSharedContentPermissionMixin
+from pyams_form.form import AJAXAddForm
+from pyams_form.security import ProtectedFormObjectMixin
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_skin.viewlet.toolbar import ToolbarMenuItem, ToolbarAction
+from pyams_template.template import template_config
+from pyams_utils.adapter import adapter_config
+from pyams_utils.traversing import get_parent
+from pyams_viewlet.viewlet import viewlet_config
+from pyams_zmi.form import AdminDialogAddForm, AdminDialogEditForm, InnerAdminDisplayForm
+from pyramid.view import view_config
+from z3c.form import field, button
+from zope.interface import implementer, Interface
+
+from pyams_content import _
+
+
+@viewlet_config(name='add-gallery.menu', context=IParagraphContainerTarget, view=ParagraphContainerView,
+ layer=IPyAMSLayer, manager=IToolbarAddingMenu, weight=65)
+class GalleryAddMenu(ProtectedFormObjectMixin, ToolbarMenuItem):
+ """Gallery add menu"""
+
+ label = _("Add images gallery...")
+ label_css_class = 'fa fa-fw fa-picture-o'
+ url = 'add-gallery.html'
+ modal_target = True
+
+
+@pagelet_config(name='add-gallery.html', context=IParagraphContainerTarget, layer=IPyAMSLayer,
+ permission=MANAGE_CONTENT_PERMISSION)
+class GalleryAddForm(AdminDialogAddForm):
+ """Gallery add form"""
+
+ legend = _("Add new gallery")
+ icon_css_class = 'fa fa-fw fa-picture-o'
+
+ fields = field.Fields(IGalleryParagraph).omit('__parent__', '__name__', 'visible')
+ ajax_handler = 'add-gallery.json'
+ edit_permission = MANAGE_CONTENT_PERMISSION
+
+ def updateWidgets(self, prefix=None):
+ super(GalleryAddForm, self).updateWidgets(prefix)
+ if 'description' in self.widgets:
+ self.widgets['description'].widget_css_class = 'textarea'
+
+ def create(self, data):
+ return Gallery()
+
+ def add(self, object):
+ IParagraphContainer(self.context).append(object)
+
+
+@view_config(name='add-gallery.json', context=IParagraphContainerTarget, request_type=IPyAMSLayer,
+ permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
+class GalleryAJAXAddForm(AJAXAddForm, GalleryAddForm):
+ """Gallery paragraph add form, JSON renderer"""
+
+ def get_ajax_output(self, changes):
+ return {'status': 'reload',
+ 'location': '#paragraphs.html'}
+
+
+@pagelet_config(name='properties.html', context=IGalleryParagraph, layer=IPyAMSLayer,
+ permission=MANAGE_CONTENT_PERMISSION)
+class GalleryPropertiesEditForm(AdminDialogEditForm):
+ """Gallery properties edit form"""
+
+ @property
+ def title(self):
+ content = get_parent(self.context, IWfSharedContent)
+ return II18n(content).query_attribute('title', request=self.request)
+
+ legend = _("Edit gallery properties")
+ icon_css_class = 'fa fa-fw fa-picture-o'
+
+ fields = field.Fields(IGalleryParagraph).omit('__parent__', '__name__', 'visible')
+ ajax_handler = 'properties.json'
+ edit_permission = MANAGE_CONTENT_PERMISSION
+
+ def updateWidgets(self, prefix=None):
+ super(GalleryPropertiesEditForm, self).updateWidgets(prefix)
+ if 'description' in self.widgets:
+ self.widgets['description'].widget_css_class = 'textarea'
+
+
+@view_config(name='properties.json', context=IGalleryParagraph, request_type=IPyAMSLayer,
+ permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
+class GalleryPropertiesAJAXEditForm(BaseParagraphAJAXEditForm, GalleryPropertiesEditForm):
+ """Gallery paragraph properties edit form, JSON renderer"""
+
+ def get_ajax_output(self, changes):
+ updated = changes.get(IBaseGallery, ())
+ if 'title' in updated:
+ return {'status': 'success',
+ 'event': 'PyAMS_content.changed_item',
+ 'event_options': {'object_type': 'paragraph',
+ 'object_name': self.context.__name__,
+ 'title': II18n(self.context).query_attribute('title', request=self.request),
+ 'visible': self.context.visible}}
+ else:
+ return super(GalleryPropertiesAJAXEditForm, self).get_ajax_output(changes)
+
+
+@adapter_config(context=(IGalleryParagraph, IPyAMSLayer), provides=IParagraphInnerEditor)
+@implementer(IInnerForm)
+class GalleryInnerEditForm(GalleryPropertiesEditForm):
+ """Gallery properties inner edit form"""
+
+ legend = None
+
+ @property
+ def buttons(self):
+ if self.mode == INPUT_MODE:
+ return button.Buttons(IEditFormButtons)
+ else:
+ return button.Buttons()
+
+
+#
+# Gallery contents view
+#
+
+@adapter_config(name='gallery-images', context=(IGalleryParagraph, IPyAMSLayer, GalleryInnerEditForm),
+ provides=IInnerSubForm)
+@template_config(template='templates/gallery-images.pt', layer=IPyAMSLayer)
+@implementer(IGalleryImagesView)
+class GalleryContentsView(WfSharedContentPermissionMixin, InnerAdminDisplayForm):
+ """Gallery contents edit form"""
+
+ fields = field.Fields(Interface)
+ weight = 10
+
+ def get_title(self, image):
+ return II18n(image).query_attribute('title', request=self.request)
+
+
+@viewlet_config(name='add-image.menu', context=IGalleryParagraph, view=GalleryContentsView,
+ manager=IWidgetTitleViewletManager)
+class GalleryImageAddMenu(WfSharedContentPermissionMixin, ToolbarAction):
+ """Gallery image add menu"""
+
+ label = _("Add image(s)")
+
+ url = 'add-image.html'
+ modal_target = True
+ stop_propagation = True
+
+
+#
+# Gallery paragraph summary
+#
--- a/src/pyams_content/component/gallery/zmi/templates/gallery-images.pt Mon Sep 11 14:53:15 2017 +0200
+++ b/src/pyams_content/component/gallery/zmi/templates/gallery-images.pt Mon Sep 11 14:54:30 2017 +0200
@@ -1,51 +1,56 @@
-<div id="gallery-images" class="sortable gallery" i18n:domain="pyams_content"
+<div class="form-group" i18n:domain="pyams_content"
data-ams-plugins="pyams_content"
tal:attributes="data-ams-plugin-pyams_content-src extension:resource_path('pyams_content.skin:pyams_content');
- data-ams-location extension:absolute_url(context);
- class '{0} gallery'.format('sortable' if request.has_permission(view.permission) else '');"
- data-ams-plugin-pyams_content-async="false"
- data-ams-sortable-stop="PyAMS_content.galleries.setOrder">
- <div tal:repeat="image context.values()"
- class="image margin-5 margin-bottom-10 radius-4 padding-5 pull-left text-center"
- style="position: relative;"
- tal:attributes="data-ams-element-name image.__name__">
- <a class="fancybox" data-toggle
- data-ams-fancybox-type="image"
- tal:define="thumbnails extension:thumbnails(image);
- target thumbnails.get_thumbnail('800x600', 'jpeg');
- info view.get_info(image);"
- tal:attributes="href extension:absolute_url(target);">
- <i class="fa fa-fw fa-eye-slash txt-color-red pull-right opaque hint"
- style="position: absolute; right: 8px; top: 8px;"
- title="Hidden image" i18n:attributes="title"
- tal:condition="not:info.visible"></i>
- <img class="thumbnail hint"
- data-ams-hint-gravity="s"
- tal:define="thumbnail thumbnails.get_thumbnail('128x128', 'jpeg');
- image_size thumbnail.get_image_size();
- margin_left 64 - image_size[0] / 2;
- margin_top 64 - image_size[1] / 2;"
- tal:attributes="src extension:absolute_url(thumbnail);
- title info.get_title(request);
- style string:margin-left: ${margin_left}px;; margin-right: ${margin_left}px;; margin-top: ${margin_top}px;; margin-bottom: ${margin_top}px;;" />
- </a>
- <div class="btn-group dropup margin-top-10"
- tal:define="actions extension:context_actions(image);"
- tal:omit-tag="not:actions">
- <a class="btn btn-xs btn-default" target="download_window"
- tal:attributes="href extension:absolute_url(image)" i18n:translate="">
- Download
- </a>
- <tal:if condition="actions">
- <button class="btn btn-xs btn-primary dropdown-toggle" data-toggle="dropdown">
- <i class="fa fa-caret-up"></i>
- </button>
- <ul class="dropdown-menu">
+ id string:gallery_images_${context.__name__};"
+ data-ams-plugin-pyams_content-async="false">
+ <fieldset class="margin-top-10 padding-top-5 padding-bottom-0">
+ <legend
+ class="inner switcher margin-bottom-5 padding-right-10 no-y-padding pull-left width-auto"
+ tal:attributes="data-ams-switcher-state 'open' if context.values() else None">
+ <i18n:var translate="">Gallery images</i18n:var>
+ </legend>
+ <div class="pull-left">
+ <tal:var content="structure provider:pyams.widget_title" />
+ </div>
+ <div class="clearfix"></div>
+ <div class="sortable gallery"
+ tal:attributes="data-ams-location extension:absolute_url(context);
+ class '{0} gallery'.format('sortable' if request.has_permission(view.permission) else '');"
+ data-ams-sortable-stop="PyAMS_content.galleries.setOrder">
+ <div tal:repeat="image context.values()"
+ class="image margin-5 margin-bottom-10 radius-4 padding-5 pull-left text-center"
+ style="position: relative;"
+ tal:attributes="data-ams-element-name image.__name__">
+ <tal:var define="thumbnails extension:thumbnails(image.data);">
+ <tal:if condition="thumbnails">
+ <a class="fancybox" data-toggle
+ data-ams-fancybox-type="image"
+ tal:define="target thumbnails.get_thumbnail('800x600', 'jpeg')"
+ tal:attributes="href extension:absolute_url(target);">
+ <img class="thumbnail hint"
+ data-ams-hint-gravity="s"
+ tal:define="thumbnail thumbnails.get_thumbnail('128x128', 'jpeg');
+ image_size thumbnail.get_image_size();
+ margin_left 64 - image_size[0] / 2;
+ margin_top 64 - image_size[1] / 2;"
+ tal:attributes="src extension:absolute_url(thumbnail);
+ title i18n:image.title;
+ style string:margin-left: ${margin_left}px;; margin-right: ${margin_left}px;; margin-top: ${margin_top}px;; margin-bottom: ${margin_top}px;;" />
+ </a>
+ </tal:if>
+ <tal:if condition="not:thumbnails">
+ <img class="thumbnail hint" src="/--static--/pyams_skin/img/mimetypes/unknown.png"
+ tal:attributes="title i18n:image.title"
+ style="padding: 48px;" />
+ </tal:if>
+ </tal:var>
+ <div class="btn-group margin-top-10"
+ tal:define="actions extension:context_actions(image);">
<tal:loop repeat="viewlet actions.viewlets"
content="structure viewlet.render()" />
- </ul>
- </tal:if>
+ </div>
+ <span class="clearfix"></span>
+ </div>
</div>
- <span class="clearfix"></span>
- </div>
+ </fieldset>
</div>
--- a/src/pyams_content/component/gallery/zmi/templates/widget-display.pt Mon Sep 11 14:53:15 2017 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,22 +0,0 @@
-<input type="hidden" autocomplete="off" readonly
- data-ams-select2-multiple="true"
- tal:attributes="id view/id;
- name view/name;
- class string:select2 ${view/klass} ordered;
- style view/style;
- title view/title;
- value python:','.join(view.value);
- lang view/lang;
- onclick view/onclick;
- ondblclick view/ondblclick;
- onmousedown view/onmousedown;
- onmouseup view/onmouseup;
- onmouseover view/onmouseover;
- onmousemove view/onmousemove;
- onmouseout view/onmouseout;
- onkeypress view/onkeypress;
- onkeydown view/onkeydown;
- onkeyup view/onkeyup;
- disabled view/disabled;
- tabindex view/tabindex;
- data-ams-select2-values view/values_map;" />
--- a/src/pyams_content/component/gallery/zmi/templates/widget-input.pt Mon Sep 11 14:53:15 2017 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,44 +0,0 @@
-<label class="input bordered with-icon" i18n:domain="pyams_content"
- data-ams-plugins="pyams_content"
- tal:attributes="data-ams-plugin-pyams_content-src extension:resource_path('pyams_content.skin:pyams_content')">
- <i class="icon-append fa fa-plus-square txt-color-green hint opaque"
- title="Add gallery" i18n:attributes="title"
- data-ams-url="add-gallery.html?origin=link" data-toggle="modal"
- tal:attributes="data-ams-select2-target string:${view/name}:list"></i>
- <div class="select2-parent">
- <select class="select2 ordered"
- data-ams-select2-allow-clear="true"
- tal:attributes="id view/id;
- name string:${view/name}:list;
- class string:${view/klass} select2 ordered;
- style view/style;
- title view/title;
- lang view/lang;
- onclick view/onclick;
- ondblclick view/ondblclick;
- onmousedown view/onmousedown;
- onmouseup view/onmouseup;
- onmouseover view/onmouseover;
- onmousemove view/onmousemove;
- onmouseout view/onmouseout;
- onkeypress view/onkeypress;
- onkeydown view/onkeydown;
- onkeyup view/onkeyup;
- disabled view/disabled;
- tabindex view/tabindex;
- onfocus view/onfocus;
- onblur view/onblur;
- onchange view/onchange;
- multiple view/multiple;
- size view/size">
- <option tal:repeat="entry view/selectedItems"
- tal:attributes="value entry/value;
- selected python:entry['value'] in view.value;"
- tal:content="entry/content"></option>
- <option tal:repeat="entry view/notselectedItems"
- tal:attributes="value entry/value;
- selected python:entry['value'] in view.value;"
- tal:content="entry/content"></option>
- </select>
- </div>
-</label>
--- a/src/pyams_content/component/gallery/zmi/widget.py Mon Sep 11 14:53:15 2017 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,42 +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 json
-
-# import interfaces
-from pyams_skin.layer import IPyAMSLayer
-
-# import packages
-from pyams_form.widget import widgettemplate_config
-from z3c.form.browser.orderedselect import OrderedSelectWidget
-from z3c.form.widget import FieldWidget
-
-
-@widgettemplate_config(mode='input', template='templates/widget-input.pt', layer=IPyAMSLayer)
-@widgettemplate_config(mode='display', template='templates/widget-display.pt', layer=IPyAMSLayer)
-class GalleryLinksSelectWidget(OrderedSelectWidget):
- """Galleries links select widget"""
-
- @property
- def values_map(self):
- result = {}
- [result.update({entry['value']: entry['content']}) for entry in self.selectedItems]
- return json.dumps(result)
-
-
-def GalleryLinkSelectFieldWidget(field, request):
- """Galleries links select widget factory"""
- return FieldWidget(field, GalleryLinksSelectWidget(request))
--- a/src/pyams_content/component/illustration/__init__.py Mon Sep 11 14:53:15 2017 +0200
+++ b/src/pyams_content/component/illustration/__init__.py Mon Sep 11 14:54:30 2017 +0200
@@ -16,5 +16,125 @@
# import standard library
# import interfaces
+from pyams_content.component.illustration.interfaces import IIllustrationRenderer, IIllustration, IIllustrationTarget, \
+ ILLUSTRATION_KEY
+from pyams_file.interfaces import DELETED_FILE, IResponsiveImage, IFileInfo
+from pyams_i18n.interfaces import INegotiator, II18n
+from zope.annotation.interfaces import IAnnotations
+from zope.lifecycleevent.interfaces import IObjectAddedEvent, IObjectModifiedEvent
+from zope.location.interfaces import ISublocations
+from zope.traversing.interfaces import ITraversable
# import packages
+from persistent import Persistent
+from pyams_i18n.property import I18nFileProperty
+from pyams_utils.adapter import adapter_config, ContextAdapter
+from pyams_utils.registry import query_utility
+from pyams_utils.request import check_request
+from pyams_utils.vocabulary import vocabulary_config
+from pyramid.events import subscriber
+from pyramid.threadlocal import get_current_registry
+from zope.container.contained import Contained
+from zope.interface import implementer, alsoProvides
+from zope.lifecycleevent import ObjectCreatedEvent, ObjectAddedEvent
+from zope.location import locate
+from zope.schema.fieldproperty import FieldProperty
+from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm
+
+
+@implementer(IIllustration)
+class Illustration(Persistent, Contained):
+ """Illustration persistent class"""
+
+ title = FieldProperty(IIllustration['title'])
+ alt_title = FieldProperty(IIllustration['alt_title'])
+ description = FieldProperty(IIllustration['description'])
+ author = FieldProperty(IIllustration['author'])
+ _data = I18nFileProperty(IIllustration['data'])
+ renderer = FieldProperty(IIllustration['renderer'])
+
+ @property
+ def data(self):
+ return self._data
+
+ @data.setter
+ def data(self, value):
+ self._data = value
+ for data in value.values():
+ if (data is not None) and (data is not DELETED_FILE):
+ alsoProvides(data, IResponsiveImage)
+
+
+@adapter_config(context=IIllustrationTarget, provides=IIllustration)
+def illustration_factory(context):
+ """Illustration factory"""
+ annotations = IAnnotations(context)
+ illustration = annotations.get(ILLUSTRATION_KEY)
+ if illustration is None:
+ illustration = annotations[ILLUSTRATION_KEY] = Illustration()
+ registry = get_current_registry()
+ registry.notify(ObjectCreatedEvent(illustration))
+ locate(illustration, context, '++illustration++')
+ registry.notify(ObjectAddedEvent(illustration, context, '++illustration++'))
+ return illustration
+
+
+def update_illustration_properties(illustration):
+ """Update missing file properties"""
+ request = check_request()
+ i18n = query_utility(INegotiator)
+ if i18n is not None:
+ lang = i18n.server_language
+ data = II18n(illustration).get_attribute('data', lang, request)
+ if data:
+ info = IFileInfo(data)
+ info.title = II18n(illustration).get_attribute('title', lang, request)
+ info.description = II18n(illustration).get_attribute('alt_title', lang, request)
+ for lang, data in (illustration.data or {}).items():
+ if data is not None:
+ IFileInfo(data).language = lang
+
+
+@subscriber(IObjectAddedEvent, context_selector=IIllustration)
+def handle_added_illustration(event):
+ """Handle added illustration"""
+ illustration = event.object
+ update_illustration_properties(illustration)
+
+
+@subscriber(IObjectModifiedEvent, context_selector=IIllustration)
+def handle_modified_illustration(event):
+ """Handle modified illustration"""
+ illustration = event.object
+ update_illustration_properties(illustration)
+
+
+@adapter_config(name='illustration', context=IIllustrationTarget, provides=ITraversable)
+class IllustrationNamespace(ContextAdapter):
+ """++illustration++ namespace adapter"""
+
+ def traverse(self, name, furtherpath=None):
+ return IIllustration(self.context)
+
+
+@adapter_config(name='illustration', context=IIllustrationTarget, provides=ISublocations)
+class IllustrationSublocations(ContextAdapter):
+ """Illustration sub-locations adapter"""
+
+ def sublocations(self):
+ return IIllustration(self.context),
+
+
+@vocabulary_config(name='PyAMS illustration renderers')
+class IllustrationRendererVocabulary(SimpleVocabulary):
+ """Illustration renderer utilities vocabulary"""
+
+ def __init__(self, context=None):
+ request = check_request()
+ translate = request.localizer.translate
+ registry = request.registry
+ context = Illustration()
+ terms = [SimpleTerm(name, title=translate(adapter.label))
+ for name, adapter in sorted(registry.getAdapters((context, request), IIllustrationRenderer),
+ key=lambda x: x[1].weight)]
+ super(IllustrationRendererVocabulary, self).__init__(terms)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/illustration/interfaces/__init__.py Mon Sep 11 14:54:30 2017 +0200
@@ -0,0 +1,76 @@
+#
+# 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_content.component.paragraph.interfaces import IBaseParagraph
+from pyams_i18n.schema import I18nTextLineField, I18nThumbnailImageField, I18nTextField
+from zope.annotation.interfaces import IAttributeAnnotatable
+from zope.contentprovider.interfaces import IContentProvider
+
+# import packages
+from zope.interface import Interface, Attribute
+from zope.schema import Choice, TextLine
+
+from pyams_content import _
+
+
+#
+# Illustration
+#
+
+ILLUSTRATION_KEY = 'pyams_content.illustration'
+
+
+class IIllustration(Interface):
+ """Illustration paragraph"""
+
+ title = I18nTextLineField(title=_("Legend"),
+ description=_("Illustration title"),
+ required=False)
+
+ alt_title = I18nTextLineField(title=_("Accessibility title"),
+ description=_("Alternate title used to describe image content"),
+ required=False)
+
+ description = I18nTextField(title=_("Description"),
+ description=_(""),
+ required=False)
+
+ author = TextLine(title=_("Author"),
+ description=_("Name of picture's author"),
+ required=False)
+
+ data = I18nThumbnailImageField(title=_("Image data"),
+ description=_("Image content"),
+ required=True)
+
+ renderer = Choice(title=_("Image style"),
+ vocabulary='PyAMS illustration renderers')
+
+
+class IIllustrationTarget(IAttributeAnnotatable):
+ """Illustration target marker interface"""
+
+
+class IIllustrationRenderer(IContentProvider):
+ """Illustration renderer utility interface"""
+
+ label = Attribute("Renderer label")
+
+
+class IIllustrationParagraph(IIllustration, IBaseParagraph):
+ """Illustration paragraph"""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/illustration/paragraph.py Mon Sep 11 14:54:30 2017 +0200
@@ -0,0 +1,44 @@
+#
+# 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_content.component.illustration.interfaces import IIllustrationParagraph
+from pyams_content.component.paragraph.interfaces import IParagraphFactory
+
+# import packages
+from pyams_content.component.illustration import Illustration as BaseIllustration
+from pyams_content.component.paragraph import BaseParagraph
+from pyams_utils.registry import utility_config
+from zope.interface import implementer
+
+from pyams_content import _
+
+
+@implementer(IIllustrationParagraph)
+class Illustration(BaseIllustration, BaseParagraph):
+ """Illustration class"""
+
+ icon_class = 'fa-file-image-o'
+ icon_hint = _("Illustration")
+
+
+@utility_config(name='Illustration', provides=IParagraphFactory)
+class IllustrationFactory(object):
+ """Illustration paragraph factory"""
+
+ name = _("Illustration")
+ content_type = Illustration
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/illustration/zmi/__init__.py Mon Sep 11 14:54:30 2017 +0200
@@ -0,0 +1,131 @@
+#
+# 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_content.component.illustration.interfaces import IIllustration, IIllustrationRenderer, IIllustrationTarget
+from pyams_form.interfaces.form import IInnerSubForm
+from pyams_i18n.interfaces import II18n
+from pyams_skin.layer import IPyAMSLayer
+from pyams_zmi.interfaces import IPropertiesEditForm
+from transaction.interfaces import ITransactionManager
+
+# import packages
+from pyams_template.template import get_view_template, template_config
+from pyams_utils.adapter import ContextRequestAdapter, adapter_config
+from pyams_zmi.form import InnerAdminEditForm
+from z3c.form import field
+
+from pyams_content import _
+
+
+#
+# Illustration renderers
+#
+
+class BaseIllustrationRenderer(ContextRequestAdapter):
+ """Base illustration renderer"""
+
+ language = None
+
+ def update(self):
+ i18n = II18n(self.context)
+ if self.language:
+ self.legend = i18n.get_attribute('alt_title', self.language, request=self.request)
+ else:
+ self.legend = i18n.query_attribute('alt_title', request=self.request)
+
+ render = get_view_template()
+
+
+@adapter_config(name='default', context=(IIllustration, IPyAMSLayer), provides=IIllustrationRenderer)
+@template_config(template='templates/renderer-default.pt', layer=IPyAMSLayer)
+class DefaultIllustrationRenderer(BaseIllustrationRenderer):
+ """Default illustration renderer"""
+
+ label = _("Centered illustration")
+ weight = 1
+
+
+@adapter_config(name='left+zoom', context=(IIllustration, IPyAMSLayer), provides=IIllustrationRenderer)
+@template_config(template='templates/renderer-left.pt', layer=IPyAMSLayer)
+class LeftIllustrationWithZoomRenderer(BaseIllustrationRenderer):
+ """Illustrtaion renderer with small image and zoom"""
+
+ label = _("Small illustration on the left with zoom")
+ weight = 2
+
+
+@adapter_config(name='right+zoom', context=(IIllustration, IPyAMSLayer), provides=IIllustrationRenderer)
+@template_config(template='templates/renderer-right.pt', layer=IPyAMSLayer)
+class RightIllustrationWithZoomRenderer(BaseIllustrationRenderer):
+ """Illustrtaion renderer with small image and zoom"""
+
+ label = _("Small illustration on the right with zoom")
+ weight = 3
+
+
+#
+# Illustration properties inner edit form
+#
+
+@adapter_config(name='illustration', context=(IIllustrationTarget, IPyAMSLayer, IPropertiesEditForm),
+ provides=IInnerSubForm)
+class IllustrationPropertiesInnerEditForm(InnerAdminEditForm):
+ """Illustration properties inner edit form"""
+
+ prefix = 'illustration_form.'
+
+ css_class = 'form-group'
+ padding_class = ''
+ fieldset_class = 'margin-top-10 padding-top-5 padding-bottom-5'
+
+ legend = _("Illustration")
+ legend_class = 'inner switcher padding-right-10 no-y-padding pull-left'
+
+ fields = field.Fields(IIllustration).omit('__parent__', '__name__')
+ weight = 10
+
+ def getContent(self):
+ return IIllustration(self.context)
+
+ def updateWidgets(self, prefix=None):
+ super(IllustrationPropertiesInnerEditForm, self).updateWidgets(prefix)
+ if 'description' in self.widgets:
+ self.widgets['description'].widget_css_class = 'textarea'
+
+ @property
+ def switcher_state(self):
+ for lang, data in self.getContent().data:
+ if data:
+ return 'open'
+
+ def get_ajax_output(self, changes):
+ output = super(IllustrationPropertiesInnerEditForm, self).get_ajax_output(changes)
+ if 'data' in changes.get(IIllustration, ()):
+ # we have to commit transaction to be able to handle blobs...
+ ITransactionManager(self.context).get().commit()
+ context = IIllustration(self.context)
+ form = IllustrationPropertiesInnerEditForm(context, self.request)
+ form.update()
+ output.setdefault('callbacks', []).append({
+ 'callback': 'PyAMS_content.illustration.afterUpdateCallback',
+ 'options': {'parent': '{0}_{1}_{2}'.format(self.context.__class__.__name__,
+ getattr(form.getContent(), '__name__', 'noname').replace('++', ''),
+ form.id),
+ 'form': form.render()}
+ })
+ return output
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/illustration/zmi/paragraph.py Mon Sep 11 14:54:30 2017 +0200
@@ -0,0 +1,215 @@
+#
+# 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_content.component.paragraph.interfaces import IParagraphContainerTarget, \
+ IParagraphContainer, IParagraphSummary
+from pyams_content.component.illustration.interfaces import IIllustrationRenderer, IIllustration, IIllustrationParagraph
+from pyams_content.component.paragraph.zmi.interfaces import IParagraphInnerEditor
+from pyams_content.interfaces import MANAGE_CONTENT_PERMISSION
+from pyams_content.shared.common.interfaces import IWfSharedContent
+from pyams_form.interfaces.form import IInnerForm, IEditFormButtons
+from pyams_i18n.interfaces import II18n
+from pyams_skin.interfaces.viewlet import IToolbarAddingMenu
+from pyams_skin.layer import IPyAMSLayer
+from transaction.interfaces import ITransactionManager
+from z3c.form.interfaces import INPUT_MODE
+
+# import packages
+from pyams_content.component.illustration.paragraph import Illustration
+from pyams_content.component.paragraph.zmi import BaseParagraphAJAXEditForm
+from pyams_content.component.paragraph.zmi.container import ParagraphContainerView
+from pyams_form.form import AJAXAddForm
+from pyams_form.security import ProtectedFormObjectMixin
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_skin.viewlet.toolbar import ToolbarMenuItem
+from pyams_utils.adapter import ContextRequestAdapter, adapter_config
+from pyams_utils.traversing import get_parent
+from pyams_viewlet.viewlet import viewlet_config
+from pyams_zmi.form import AdminDialogAddForm, AdminDialogEditForm
+from pyramid.view import view_config
+from z3c.form import field, button
+from zope.interface import implementer
+
+from pyams_content import _
+
+
+#
+# Illustration
+#
+
+@viewlet_config(name='add-illustration.menu', context=IParagraphContainerTarget, view=ParagraphContainerView,
+ layer=IPyAMSLayer, manager=IToolbarAddingMenu, weight=60)
+class IllustrationAddMenu(ProtectedFormObjectMixin, ToolbarMenuItem):
+ """Illustration add menu"""
+
+ label = _("Add illustration...")
+ label_css_class = 'fa fa-fw fa-file-image-o'
+ url = 'add-illustration.html'
+ modal_target = True
+
+
+@pagelet_config(name='add-illustration.html', context=IParagraphContainerTarget, layer=IPyAMSLayer,
+ permission=MANAGE_CONTENT_PERMISSION)
+class IllustrationAddForm(AdminDialogAddForm):
+ """Illustration add form"""
+
+ legend = _("Add new illustration")
+ dialog_class = 'modal-large'
+ icon_css_class = 'fa fa-fw fa-file-image-o'
+
+ fields = field.Fields(IIllustrationParagraph).omit('__parent__', '__name__', 'visible')
+ ajax_handler = 'add-illustration.json'
+ edit_permission = MANAGE_CONTENT_PERMISSION
+
+ def updateWidgets(self, prefix=None):
+ super(IllustrationAddForm, self).updateWidgets(prefix)
+ if 'description' in self.widgets:
+ self.widgets['description'].widget_css_class = 'textarea'
+
+ def create(self, data):
+ return Illustration()
+
+ def add(self, object):
+ IParagraphContainer(self.context).append(object)
+
+
+@view_config(name='add-illustration.json', context=IParagraphContainerTarget, request_type=IPyAMSLayer,
+ permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
+class IllustrationAJAXAddForm(AJAXAddForm, IllustrationAddForm):
+ """HTML paragraph add form, JSON renderer"""
+
+ def get_ajax_output(self, changes):
+ return {'status': 'reload',
+ 'location': '#paragraphs.html'}
+
+
+@pagelet_config(name='properties.html', context=IIllustrationParagraph, layer=IPyAMSLayer,
+ permission=MANAGE_CONTENT_PERMISSION)
+class IllustrationPropertiesEditForm(AdminDialogEditForm):
+ """Illustration properties edit form"""
+
+ @property
+ def title(self):
+ content = get_parent(self.context, IWfSharedContent)
+ return II18n(content).query_attribute('title', request=self.request)
+
+ legend = _("Edit illustration properties")
+ dialog_class = 'modal-large'
+ icon_css_class = 'fa fa-fw fa-file-image-o'
+
+ fields = field.Fields(IIllustrationParagraph).omit('__parent__', '__name__', 'visible')
+ ajax_handler = 'properties.json'
+ edit_permission = MANAGE_CONTENT_PERMISSION
+
+ def updateWidgets(self, prefix=None):
+ super(IllustrationPropertiesEditForm, self).updateWidgets(prefix)
+ if 'description' in self.widgets:
+ self.widgets['description'].widget_css_class = 'textarea'
+
+
+@view_config(name='properties.json', context=IIllustrationParagraph, request_type=IPyAMSLayer,
+ permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
+class IllustrationPropertiesAJAXEditForm(BaseParagraphAJAXEditForm, IllustrationPropertiesEditForm):
+ """Illustration properties edit form, JSON renderer"""
+
+ def get_ajax_output(self, changes):
+ output = super(IllustrationPropertiesAJAXEditForm, self).get_ajax_output(changes)
+ if 'title' in changes.get(IIllustration, ()):
+ output.setdefault('events', []).append({
+ 'event': 'PyAMS_content.changed_item',
+ 'options': {'object_type': 'paragraph',
+ 'object_name': self.context.__name__,
+ 'title': II18n(self.context).query_attribute('title', request=self.request),
+ 'visible': self.context.visible}
+ })
+ return output
+
+
+@adapter_config(context=(IIllustrationParagraph, IPyAMSLayer), provides=IParagraphInnerEditor)
+@implementer(IInnerForm)
+class IllustrationInnerEditForm(IllustrationPropertiesEditForm):
+ """Illustration inner edit form"""
+
+ legend = None
+ ajax_handler = 'inner-properties.json'
+
+ @property
+ def buttons(self):
+ if self.mode == INPUT_MODE:
+ return button.Buttons(IEditFormButtons)
+ else:
+ return button.Buttons()
+
+
+@view_config(name='inner-properties.json', context=IIllustrationParagraph, request_type=IPyAMSLayer,
+ permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
+class IllustrationInnerAJAXEditForm(BaseParagraphAJAXEditForm, IllustrationInnerEditForm):
+ """Illustration paragraph inner edit form, JSON renderer"""
+
+ def get_ajax_output(self, changes):
+ output = super(IllustrationInnerAJAXEditForm, self).get_ajax_output(changes)
+ updated = changes.get(IIllustration, ())
+ if 'title' in updated:
+ output.setdefault('events', []).append({
+ 'event': 'PyAMS_content.changed_item',
+ 'options': {'object_type': 'paragraph',
+ 'object_name': self.context.__name__,
+ 'title': II18n(self.context).query_attribute('title', request=self.request),
+ 'visible': self.context.visible}
+ })
+ if 'data' in updated:
+ # we have to commit transaction to be able to handle blobs...
+ ITransactionManager(self.context).get().commit()
+ context = IIllustrationParagraph(self.context)
+ form = IllustrationInnerEditForm(context, self.request)
+ form.update()
+ output.setdefault('callbacks', []).append({
+ 'callback': 'PyAMS_content.illustration.afterUpdateCallback',
+ 'options': {'parent': '{0}_{1}_{2}'.format(self.context.__class__.__name__,
+ getattr(form.getContent(), '__name__', 'noname').replace('++', ''),
+ form.id),
+ 'form': form.render()}
+ })
+ return output
+
+
+#
+# Illustration summary
+#
+
+@adapter_config(context=(IIllustrationParagraph, IPyAMSLayer), provides=IParagraphSummary)
+class IllustrationSummary(ContextRequestAdapter):
+ """Illustration renderer"""
+
+ def __init__(self, context, request):
+ super(IllustrationSummary, self).__init__(context, request)
+ self.renderer = request.registry.queryMultiAdapter((context, request), IIllustrationRenderer,
+ name=self.context.renderer)
+
+ language = None
+
+ def update(self):
+ if self.renderer is not None:
+ self.renderer.language = self.language
+ self.renderer.update()
+
+ def render(self):
+ if self.renderer is not None:
+ return self.renderer.render()
+ else:
+ return ''
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/illustration/zmi/templates/renderer-default.pt Mon Sep 11 14:54:30 2017 +0200
@@ -0,0 +1,7 @@
+<div class="text-center margin-y-5">
+ <img tal:define="data i18n:context.data;
+ thumbnails extension:thumbnails(data);
+ target thumbnails.get_thumbnail('800x600', 'jpeg');"
+ tal:attributes="src extension:absolute_url(target)" /><br />
+ <span tal:content="view.legend">legend</span>
+</div>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/illustration/zmi/templates/renderer-left.pt Mon Sep 11 14:54:30 2017 +0200
@@ -0,0 +1,11 @@
+<div class="pull-left margin-10">
+ <a class="fancybox" data-toggle
+ data-ams-fancybox-type="image"
+ tal:define="thumbnails extension:thumbnails(context.data);
+ target thumbnails.get_thumbnail('800x600', 'png');
+ thumb thumbnails.get_thumbnail('300x200', 'png');"
+ tal:attributes="href extension:absolute_url(target)">
+ <img tal:attributes="src extension:absolute_url(thumb)" /><br />
+ <span tal:content="view.legend">legend</span>
+ </a><br />
+</div>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/illustration/zmi/templates/renderer-right.pt Mon Sep 11 14:54:30 2017 +0200
@@ -0,0 +1,11 @@
+<div class="pull-right margin-10">
+ <a class="fancybox" data-toggle
+ data-ams-fancybox-type="image"
+ tal:define="thumbnails extension:thumbnails(context.data);
+ target thumbnails.get_thumbnail('800x600', 'png');
+ thumb thumbnails.get_thumbnail('300x200', 'png');"
+ tal:attributes="href extension:absolute_url(target)">
+ <img tal:attributes="src extension:absolute_url(thumb)" /><br />
+ <span tal:content="view.legend">legend</span>
+ </a><br />
+</div>
--- a/src/pyams_content/component/links/__init__.py Mon Sep 11 14:53:15 2017 +0200
+++ b/src/pyams_content/component/links/__init__.py Mon Sep 11 14:54:30 2017 +0200
@@ -9,8 +9,6 @@
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
-from pyams_i18n.interfaces import II18n
-from pyams_utils.request import check_request
__docformat__ = 'restructuredtext'
@@ -19,76 +17,79 @@
# import interfaces
from hypatia.interfaces import ICatalog
+from pyams_content.component.association.interfaces import IAssociationInfo, IAssociationTarget, IAssociationContainer
from pyams_content.component.links.interfaces import IBaseLink, IInternalLink, IExternalLink, IMailtoLink
-from pyams_content.shared.common.interfaces import IWfSharedContent
-from pyams_form.interfaces.form import IFormContextPermissionChecker
-from zope.lifecycleevent.interfaces import IObjectAddedEvent, IObjectModifiedEvent, IObjectRemovedEvent
+from pyams_i18n.interfaces import II18n
+from pyams_sequence.interfaces import ISequentialIdInfo
# import packages
from hypatia.catalog import CatalogQuery
from hypatia.query import Eq, Any
-from persistent import Persistent
from pyams_catalog.query import CatalogResultSet
+from pyams_content.component.association import AssociationItem
from pyams_content.workflow import VISIBLE_STATES
from pyams_sequence.utility import get_last_version
from pyams_utils.adapter import adapter_config, ContextAdapter
from pyams_utils.registry import get_utility
+from pyams_utils.request import check_request
from pyams_utils.traversing import get_parent
from pyams_utils.url import absolute_url
-from pyramid.events import subscriber
-from pyramid.threadlocal import get_current_registry
-from zope.lifecycleevent import ObjectModifiedEvent
-from zope.container.contained import Contained
+from pyams_utils.vocabulary import vocabulary_config
from zope.interface import implementer
from zope.schema.fieldproperty import FieldProperty
+from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm
+
+from pyams_content import _
+#
+# Links vocabulary
+#
+
+@vocabulary_config(name='PyAMS content links')
+class ContentLinksVocabulary(SimpleVocabulary):
+ """Content links vocabulary"""
+
+ def __init__(self, context=None):
+ terms = []
+ target = get_parent(context, IAssociationTarget)
+ if target is not None:
+ terms = [SimpleTerm(link.__name__, title=IAssociationInfo(link).inner_title)
+ for link in IAssociationContainer(target).values() if IBaseLink.providedBy(link)]
+ super(ContentLinksVocabulary, self).__init__(terms)
+
+
+#
+# Base link persistent class
+#
+
@implementer(IBaseLink)
-class BaseLink(Persistent, Contained):
+class BaseLink(AssociationItem):
"""Base link persistent class"""
title = FieldProperty(IBaseLink['title'])
description = FieldProperty(IBaseLink['description'])
-@adapter_config(context=IBaseLink, provides=IFormContextPermissionChecker)
-class BaseLinkPermissionChecker(ContextAdapter):
- """Base link permission checker"""
+class BaseLinkInfoAdapter(ContextAdapter):
+ """Base link association info adapter"""
@property
- def edit_permission(self):
- content = get_parent(self.context, IWfSharedContent)
- return IFormContextPermissionChecker(content).edit_permission
+ def pictogram(self):
+ return self.context.icon_class
-@subscriber(IObjectAddedEvent, context_selector=IBaseLink)
-def handle_added_link(event):
- """Handle added link"""
- content = get_parent(event.object, IWfSharedContent)
- if content is not None:
- get_current_registry().notify(ObjectModifiedEvent(content))
-
-
-@subscriber(IObjectModifiedEvent, context_selector=IBaseLink)
-def handle_modified_link(event):
- """Handle modified link"""
- content = get_parent(event.object, IWfSharedContent)
- if content is not None:
- get_current_registry().notify(ObjectModifiedEvent(content))
-
-
-@subscriber(IObjectRemovedEvent, context_selector=IBaseLink)
-def handle_removed_link(event):
- """Handle removed link"""
- content = get_parent(event.object, IWfSharedContent)
- if content is not None:
- get_current_registry().notify(ObjectModifiedEvent(content))
-
+#
+# Internal links
+#
@implementer(IInternalLink)
class InternalLink(BaseLink):
"""Internal link persistent class"""
+ icon_class = 'fa-link'
+ icon_hint = _("Internal link")
+
reference = FieldProperty(IInternalLink['reference'])
def get_target(self, state=None):
@@ -109,40 +110,121 @@
def get_editor_url(self):
return 'oid://{0}'.format(self.reference)
- def get_url(self, request, view_name=None):
+ def get_url(self, request=None, view_name=None):
target = self.get_target(state=VISIBLE_STATES)
if target is not None:
+ if request is None:
+ request = check_request()
return absolute_url(target, request, view_name)
else:
return ''
+@adapter_config(context=IInternalLink, provides=IAssociationInfo)
+class InternalLinkAssociationInfoAdapter(BaseLinkInfoAdapter):
+ """Internal link association info adapter"""
+
+ @property
+ def user_title(self):
+ title = II18n(self.context).query_attribute('title')
+ if not title:
+ target = self.context.get_target()
+ if target is not None:
+ title = II18n(target).query_attribute('title')
+ return title or '--'
+
+ @property
+ def inner_title(self):
+ target = self.context.get_target()
+ if target is not None:
+ sequence = ISequentialIdInfo(target)
+ return '{0} ({1})'.format(II18n(target).query_attribute('title'),
+ sequence.get_short_oid())
+ else:
+ return '--'
+
+ @property
+ def human_size(self):
+ return '--'
+
+
+#
+# External links
+#
+
@implementer(IExternalLink)
class ExternalLink(BaseLink):
"""External link persistent class"""
+ icon_class = 'fa-external-link'
+ icon_hint = _("External link")
+
url = FieldProperty(IExternalLink['url'])
language = FieldProperty(IExternalLink['language'])
def get_editor_url(self):
return self.url
- def get_url(self, request, view_name=None):
+ def get_url(self, request=None, view_name=None):
return self.url
+@adapter_config(context=IExternalLink, provides=IAssociationInfo)
+class ExternalLinkAssociationInfoAdapter(BaseLinkInfoAdapter):
+ """External link association info adapter"""
+
+ @property
+ def user_title(self):
+ title = II18n(self.context).query_attribute('title')
+ if not title:
+ title = self.context.url
+ return title or '--'
+
+ @property
+ def inner_title(self):
+ return self.context.url
+
+ @property
+ def human_size(self):
+ return '--'
+
+
+#
+# Mailto links
+#
+
@implementer(IMailtoLink)
class MailtoLink(BaseLink):
"""Mailto link persistent class"""
+ icon_class = 'fa-envelope-o'
+ icon_hint = _("Mailto link")
+
address = FieldProperty(IMailtoLink['address'])
+ address_name = FieldProperty(IMailtoLink['address_name'])
def get_editor_url(self):
- request = check_request()
- return 'mailto:{0} <{1}>'.format(II18n(self).query_attribute('title', request=request),
- self.address)
+ return 'mailto:{0} <{1}>'.format(self.address_name, self.address)
+
+ def get_url(self, request=None, view_name=None):
+ return 'mailto:{0} <{1}>'.format(self.address_name, self.address)
+
+
+@adapter_config(context=IMailtoLink, provides=IAssociationInfo)
+class MailtoLinkAssociationInfoAdapter(BaseLinkInfoAdapter):
+ """Mailto link association info adapter"""
- def get_url(self, request, view_name=None):
- request = check_request()
- return 'mailto:{0} <{1}>'.format(II18n(self).query_attribute('title', request=request),
- self.address)
+ @property
+ def user_title(self):
+ title = II18n(self.context).query_attribute('title')
+ if not title:
+ title = self.context.address_name
+ return title or '--'
+
+ @property
+ def inner_title(self):
+ return self.context.get_url()
+
+ @property
+ def human_size(self):
+ return '--'
--- a/src/pyams_content/component/links/container.py Mon Sep 11 14:53:15 2017 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,121 +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_content.component.links.interfaces import ILinkContainer, ILinkContainerTarget, LINK_CONTAINER_KEY, \
- ILinkLinksContainer, LINK_LINKS_CONTAINER_KEY, ILinkLinksContainerTarget
-from pyams_i18n.interfaces import II18n
-from zope.annotation.interfaces import IAnnotations
-from zope.location.interfaces import ISublocations
-from zope.traversing.interfaces import ITraversable
-
-# import packages
-from persistent import Persistent
-from persistent.list import PersistentList
-from pyams_utils.adapter import adapter_config, ContextAdapter
-from pyams_utils.traversing import get_parent
-from pyams_utils.vocabulary import vocabulary_config
-from pyramid.threadlocal import get_current_registry
-from zope.container.contained import Contained
-from zope.container.folder import Folder
-from zope.interface import implementer
-from zope.lifecycleevent import ObjectCreatedEvent
-from zope.location import locate
-from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm
-
-
-@implementer(ILinkContainer)
-class LinkContainer(Folder):
- """Links container"""
-
- last_id = 1
-
- def __setitem__(self, key, value):
- key = str(self.last_id)
- super(LinkContainer, self).__setitem__(key, value)
- self.last_id += 1
-
-
-@adapter_config(context=ILinkContainerTarget, provides=ILinkContainer)
-def link_container_factory(target):
- """Links container factory"""
- annotations = IAnnotations(target)
- container = annotations.get(LINK_CONTAINER_KEY)
- if container is None:
- container = annotations[LINK_CONTAINER_KEY] = LinkContainer()
- get_current_registry().notify(ObjectCreatedEvent(container))
- locate(container, target, '++links++')
- return container
-
-
-@adapter_config(name='links', context=ILinkContainerTarget, provides=ITraversable)
-class LinkContainerNamespace(ContextAdapter):
- """++links++ namespace adapter"""
-
- def traverse(self, name, furtherpath=None):
- return ILinkContainer(self.context)
-
-
-@adapter_config(name='links', context=ILinkContainerTarget, provides=ISublocations)
-class LinkContainerSublocations(ContextAdapter):
- """Links container sublocations"""
-
- def sublocations(self):
- return ILinkContainer(self.context).values()
-
-
-@vocabulary_config(name='PyAMS content links')
-class LinkContainerLinksVocabulary(SimpleVocabulary):
- """Links container links vocabulary"""
-
- def __init__(self, context):
- target = get_parent(context, ILinkContainerTarget)
- terms = [SimpleTerm(link.__name__, title=II18n(link).query_attribute('title'))
- for link in ILinkContainer(target).values()]
- super(LinkContainerLinksVocabulary, self).__init__(terms)
-
-
-#
-# Link links container
-#
-
-@implementer(ILinkLinksContainer)
-class LinkLinksContainer(Persistent, Contained):
- """Links links container"""
-
- def __init__(self):
- self.links = PersistentList()
-
-
-@adapter_config(context=ILinkLinksContainerTarget, provides=ILinkLinksContainer)
-def link_links_container_factory(target):
- """Links links container factory"""
- annotations = IAnnotations(target)
- container = annotations.get(LINK_LINKS_CONTAINER_KEY)
- if container is None:
- container = annotations[LINK_LINKS_CONTAINER_KEY] = LinkLinksContainer()
- get_current_registry().notify(ObjectCreatedEvent(container))
- locate(container, target, '++links-links++')
- return container
-
-
-@adapter_config(name='links-links', context=ILinkLinksContainerTarget, provides=ITraversable)
-class LinkLinksContainerNamespace(ContextAdapter):
- """++links-links++ namespace adapter"""
-
- def traverse(self, name, furtherpath=None):
- return ILinkLinksContainer(self.context)
--- a/src/pyams_content/component/links/interfaces/__init__.py Mon Sep 11 14:53:15 2017 +0200
+++ b/src/pyams_content/component/links/interfaces/__init__.py Mon Sep 11 14:54:30 2017 +0200
@@ -17,32 +17,23 @@
import re
# import interfaces
-from zope.annotation.interfaces import IAttributeAnnotatable
-from zope.container.interfaces import IContainer
+from pyams_content.component.association.interfaces import IAssociationTarget, IAssociationItem
# import packages
from pyams_i18n.schema import I18nTextLineField, I18nTextField
from pyams_sequence.schema import InternalReference, InternalReferencesList
-from pyams_utils.schema import PersistentList
-from zope.container.constraints import containers, contains
from zope.interface import Interface
from zope.schema import Choice, TextLine
from pyams_content import _
-LINK_CONTAINER_KEY = 'pyams_content.link'
-LINK_LINKS_CONTAINER_KEY = 'pyams_content.link.links'
-
-
-class IBaseLink(IAttributeAnnotatable):
+class IBaseLink(IAssociationItem):
"""Base link interface"""
- containers('.ILinkContainer')
-
- title = I18nTextLineField(title=_("Title"),
+ title = I18nTextLineField(title=_("Alternate title"),
description=_("Link title, as shown in front-office"),
- required=True)
+ required=False)
description = I18nTextField(title=_("Description"),
description=_("Link description displayed by front-office template"),
@@ -51,9 +42,6 @@
def get_editor_url(self):
"""Get URL for use in HTML editor"""
- def get_url(self, request, view_name=None):
- """Get link URL"""
-
class IInternalLink(IBaseLink):
"""Internal link interface"""
@@ -92,28 +80,13 @@
constraint=EMAIL_REGEX.match,
required=True)
-
-class ILinkContainer(IContainer):
- """Links container"""
-
- contains(IBaseLink)
-
-
-class ILinkContainerTarget(Interface):
- """Links container marker interface"""
+ address_name = TextLine(title=_("Address name"),
+ description=_("Address as displayed in address book"),
+ required=True)
-class ILinkLinksContainer(Interface):
- """Links links container interface"""
-
- links = PersistentList(title=_("Contained links"),
- description=_("List of internal or external links linked to this object"),
- value_type=Choice(vocabulary="PyAMS content links"),
- required=False)
-
-
-class ILinkLinksContainerTarget(Interface):
- """Links links container marker interface"""
+class ILinkContainerTarget(IAssociationTarget):
+ """Links container marker interface"""
class IInternalReferencesList(Interface):
--- a/src/pyams_content/component/links/zmi/__init__.py Mon Sep 11 14:53:15 2017 +0200
+++ b/src/pyams_content/component/links/zmi/__init__.py Mon Sep 11 14:54:30 2017 +0200
@@ -16,22 +16,21 @@
# import standard library
# import interfaces
-from pyams_content.component.links.interfaces import ILinkContainerTarget, IInternalLink, ILinkContainer, IBaseLink, \
+from pyams_content.component.association.interfaces import IAssociationContainer
+from pyams_content.component.association.zmi.interfaces import IAssociationsView
+from pyams_content.component.links.interfaces import ILinkContainerTarget, IInternalLink, IBaseLink, \
IExternalLink, IMailtoLink
from pyams_content.interfaces import MANAGE_CONTENT_PERMISSION
-from pyams_i18n.interfaces import II18n
from pyams_skin.interfaces.viewlet import IToolbarAddingMenu
from pyams_skin.layer import IPyAMSLayer
from pyams_utils.interfaces import VIEW_SYSTEM_PERMISSION
# import packages
+from pyams_content.component.association.zmi import AssociationItemAJAXAddForm, AssociationItemAJAXEditForm
from pyams_content.component.links import InternalLink, ExternalLink, MailtoLink
-from pyams_content.component.links.zmi.container import LinkContainerView
-from pyams_form.form import AJAXAddForm, AJAXEditForm
from pyams_form.security import ProtectedFormObjectMixin
from pyams_pagelet.pagelet import pagelet_config
from pyams_skin.viewlet.toolbar import ToolbarMenuItem
-from pyams_utils.traversing import get_parent
from pyams_viewlet.viewlet import viewlet_config
from pyams_zmi.form import AdminDialogAddForm, AdminDialogEditForm
from pyramid.view import view_config
@@ -44,7 +43,7 @@
# Internal links views
#
-@viewlet_config(name='add-internal-link.menu', context=ILinkContainerTarget, view=LinkContainerView,
+@viewlet_config(name='add-internal-link.menu', context=ILinkContainerTarget, view=IAssociationsView,
layer=IPyAMSLayer, manager=IToolbarAddingMenu, weight=50)
class InternalLinkAddMenu(ProtectedFormObjectMixin, ToolbarMenuItem):
"""Internal link add menu"""
@@ -64,16 +63,8 @@
legend = _("Add new internal link")
icon_css_class = 'fa fa-fw fa-link'
- fields = field.Fields(IInternalLink).omit('__parent__', '__name__')
-
- @property
- def ajax_handler(self):
- origin = self.request.params.get('origin')
- if origin == 'link':
- return 'add-internal-link-link.json'
- else:
- return 'add-internal-link.json'
-
+ fields = field.Fields(IInternalLink).select('reference', 'title', 'description')
+ ajax_handler = 'add-internal-link.json'
edit_permission = MANAGE_CONTENT_PERMISSION
def updateWidgets(self, prefix=None):
@@ -85,36 +76,14 @@
return InternalLink()
def add(self, object):
- ILinkContainer(self.context)['none'] = object
+ IAssociationContainer(self.context).append(object)
@view_config(name='add-internal-link.json', context=ILinkContainerTarget, request_type=IPyAMSLayer,
permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
-class InternalLinkAJAXAddForm(AJAXAddForm, InternalLinkAddForm):
+class InternalLinkAJAXAddForm(AssociationItemAJAXAddForm, InternalLinkAddForm):
"""Internal link add form, JSON renderer"""
- def get_ajax_output(self, changes):
- return {'status': 'reload',
- 'location': '#links.html'}
-
-
-@view_config(name='add-internal-link-link.json', context=ILinkContainerTarget, request_type=IPyAMSLayer,
- permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
-class InternalLinkLinkAJAXAddForm(AJAXAddForm, InternalLinkAddForm):
- """Internal link link add form, JSON renderer"""
-
- def get_ajax_output(self, changes):
- target = get_parent(self.context, ILinkContainerTarget)
- container = ILinkContainer(target)
- links = [{'id': link.__name__,
- 'text': II18n(link).query_attribute('title', request=self.request)}
- for link in container.values()]
- return {'status': 'callback',
- 'callback': 'PyAMS_content.links.refresh',
- 'options': {'links': links,
- 'new_link': {'id': changes.__name__,
- 'text': II18n(changes).query_attribute('title', request=self.request)}}}
-
@pagelet_config(name='properties.html', context=IInternalLink, layer=IPyAMSLayer, permission=VIEW_SYSTEM_PERMISSION)
class InternalLinkPropertiesEditForm(AdminDialogEditForm):
@@ -123,7 +92,7 @@
legend = _("Edit internal link properties")
icon_css_class = 'fa fa-fw fa-link'
- fields = field.Fields(IInternalLink).omit('__parent__', '__name__')
+ fields = field.Fields(IInternalLink).select('reference', 'title', 'description')
ajax_handler = 'properties.json'
edit_permission = MANAGE_CONTENT_PERMISSION
@@ -135,14 +104,13 @@
@view_config(name='properties.json', context=IInternalLink, request_type=IPyAMSLayer,
permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
-class InternalLinkPropertiesAJAXEditForm(AJAXEditForm, InternalLinkPropertiesEditForm):
+class InternalLinkPropertiesAJAXEditForm(AssociationItemAJAXEditForm, InternalLinkPropertiesEditForm):
"""Internal link properties edit form, JSON renderer"""
def get_ajax_output(self, changes):
if ('title' in changes.get(IBaseLink, ())) or \
('reference' in changes.get(IInternalLink, ())):
- return {'status': 'reload',
- 'location': '#links.html'}
+ return self.get_associations_table()
else:
return super(InternalLinkPropertiesAJAXEditForm, self).get_ajax_output(changes)
@@ -151,7 +119,7 @@
# External links views
#
-@viewlet_config(name='add-external-link.menu', context=ILinkContainerTarget, view=LinkContainerView,
+@viewlet_config(name='add-external-link.menu', context=ILinkContainerTarget, view=IAssociationsView,
layer=IPyAMSLayer, manager=IToolbarAddingMenu, weight=51)
class ExternalLinkAddMenu(ProtectedFormObjectMixin, ToolbarMenuItem):
"""External link add menu"""
@@ -171,16 +139,8 @@
legend = _("Add new external link")
icon_css_class = 'fa fa-fw fa-external-link'
- fields = field.Fields(IExternalLink).omit('__parent__', '__name__')
-
- @property
- def ajax_handler(self):
- origin = self.request.params.get('origin')
- if origin == 'link':
- return 'add-external-link-link.json'
- else:
- return 'add-external-link.json'
-
+ fields = field.Fields(IExternalLink).select('url', 'title', 'description', 'language')
+ ajax_handler = 'add-external-link.json'
edit_permission = MANAGE_CONTENT_PERMISSION
def updateWidgets(self, prefix=None):
@@ -192,36 +152,14 @@
return ExternalLink()
def add(self, object):
- ILinkContainer(self.context)['none'] = object
+ IAssociationContainer(self.context).append(object)
@view_config(name='add-external-link.json', context=ILinkContainerTarget, request_type=IPyAMSLayer,
permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
-class ExternalLinkAJAXAddForm(AJAXAddForm, ExternalLinkAddForm):
+class ExternalLinkAJAXAddForm(AssociationItemAJAXAddForm, ExternalLinkAddForm):
"""External link add form, JSON renderer"""
- def get_ajax_output(self, changes):
- return {'status': 'reload',
- 'location': '#links.html'}
-
-
-@view_config(name='add-external-link-link.json', context=ILinkContainerTarget, request_type=IPyAMSLayer,
- permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
-class ExternalLinkLinkAJAXAddForm(AJAXAddForm, ExternalLinkAddForm):
- """External link link add form, JSON renderer"""
-
- def get_ajax_output(self, changes):
- target = get_parent(self.context, ILinkContainerTarget)
- container = ILinkContainer(target)
- links = [{'id': link.__name__,
- 'text': II18n(link).query_attribute('title', request=self.request)}
- for link in container.values()]
- return {'status': 'callback',
- 'callback': 'PyAMS_content.links.refresh',
- 'options': {'links': links,
- 'new_link': {'id': changes.__name__,
- 'text': II18n(changes).query_attribute('title', request=self.request)}}}
-
@pagelet_config(name='properties.html', context=IExternalLink, layer=IPyAMSLayer, permission=VIEW_SYSTEM_PERMISSION)
class ExternalLinkPropertiesEditForm(AdminDialogEditForm):
@@ -230,7 +168,7 @@
legend = _("Edit external link properties")
icon_css_class = 'fa fa-fw fa-external-link'
- fields = field.Fields(IExternalLink).omit('__parent__', '__name__')
+ fields = field.Fields(IExternalLink).select('url', 'title', 'description', 'language')
ajax_handler = 'properties.json'
edit_permission = MANAGE_CONTENT_PERMISSION
@@ -242,14 +180,13 @@
@view_config(name='properties.json', context=IExternalLink, request_type=IPyAMSLayer,
permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
-class ExternalLinkPropertiesAJAXEditForm(AJAXEditForm, ExternalLinkPropertiesEditForm):
+class ExternalLinkPropertiesAJAXEditForm(AssociationItemAJAXEditForm, ExternalLinkPropertiesEditForm):
"""External link properties edit form, JSON renderer"""
def get_ajax_output(self, changes):
if ('title' in changes.get(IBaseLink, ())) or \
- ('reference' in changes.get(IExternalLink, ())):
- return {'status': 'reload',
- 'location': '#links.html'}
+ ('url' in changes.get(IExternalLink, ())):
+ return self.get_associations_table()
else:
return super(ExternalLinkPropertiesAJAXEditForm, self).get_ajax_output(changes)
@@ -259,7 +196,7 @@
#
-@viewlet_config(name='add-mailto-link.menu', context=ILinkContainerTarget, view=LinkContainerView,
+@viewlet_config(name='add-mailto-link.menu', context=ILinkContainerTarget, view=IAssociationsView,
layer=IPyAMSLayer, manager=IToolbarAddingMenu, weight=52)
class MailtoLinkAddMenu(ProtectedFormObjectMixin, ToolbarMenuItem):
"""Mailto link add menu"""
@@ -279,16 +216,8 @@
legend = _("Add new mailto link")
icon_css_class = 'fa fa-fw fa-envelope-o'
- fields = field.Fields(IMailtoLink).omit('__parent__', '__name__')
-
- @property
- def ajax_handler(self):
- origin = self.request.params.get('origin')
- if origin == 'link':
- return 'add-mailto-link-link.json'
- else:
- return 'add-mailto-link.json'
-
+ fields = field.Fields(IMailtoLink).select('address', 'address_name', 'title', 'description')
+ ajax_handler = 'add-mailto-link.json'
edit_permission = MANAGE_CONTENT_PERMISSION
def updateWidgets(self, prefix=None):
@@ -300,36 +229,14 @@
return MailtoLink()
def add(self, object):
- ILinkContainer(self.context)['none'] = object
+ IAssociationContainer(self.context).append(object)
@view_config(name='add-mailto-link.json', context=ILinkContainerTarget, request_type=IPyAMSLayer,
permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
-class MailtoLinkAJAXAddForm(AJAXAddForm, MailtoLinkAddForm):
+class MailtoLinkAJAXAddForm(AssociationItemAJAXAddForm, MailtoLinkAddForm):
"""Mailto link add form, JSON renderer"""
- def get_ajax_output(self, changes):
- return {'status': 'reload',
- 'location': '#links.html'}
-
-
-@view_config(name='add-mailto-link-link.json', context=ILinkContainerTarget, request_type=IPyAMSLayer,
- permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
-class MailtoLinkLinkAJAXAddForm(AJAXAddForm, MailtoLinkAddForm):
- """Mailto link link add form, JSON renderer"""
-
- def get_ajax_output(self, changes):
- target = get_parent(self.context, ILinkContainerTarget)
- container = ILinkContainer(target)
- links = [{'id': link.__name__,
- 'text': II18n(link).query_attribute('title', request=self.request)}
- for link in container.values()]
- return {'status': 'callback',
- 'callback': 'PyAMS_content.links.refresh',
- 'options': {'links': links,
- 'new_link': {'id': changes.__name__,
- 'text': II18n(changes).query_attribute('title', request=self.request)}}}
-
@pagelet_config(name='properties.html', context=IMailtoLink, layer=IPyAMSLayer, permission=VIEW_SYSTEM_PERMISSION)
class MailtoLinkPropertiesEditForm(AdminDialogEditForm):
@@ -338,7 +245,7 @@
legend = _("Edit mailto link properties")
icon_css_class = 'fa fa-fw fa-envelope-o'
- fields = field.Fields(IMailtoLink).omit('__parent__', '__name__')
+ fields = field.Fields(IMailtoLink).select('address', 'address_name', 'title', 'description')
ajax_handler = 'properties.json'
edit_permission = MANAGE_CONTENT_PERMISSION
@@ -350,13 +257,12 @@
@view_config(name='properties.json', context=IMailtoLink, request_type=IPyAMSLayer,
permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
-class MailtoLinkPropertiesAJAXEditForm(AJAXEditForm, MailtoLinkPropertiesEditForm):
+class MailtoLinkPropertiesAJAXEditForm(AssociationItemAJAXEditForm, MailtoLinkPropertiesEditForm):
"""Mailto link properties edit form, JSON renderer"""
def get_ajax_output(self, changes):
if ('title' in changes.get(IBaseLink, ())) or \
('reference' in changes.get(IMailtoLink, ())):
- return {'status': 'reload',
- 'location': '#links.html'}
+ return self.get_associations_table()
else:
return super(MailtoLinkPropertiesAJAXEditForm, self).get_ajax_output(changes)
--- a/src/pyams_content/component/links/zmi/container.py Mon Sep 11 14:53:15 2017 +0200
+++ b/src/pyams_content/component/links/zmi/container.py Mon Sep 11 14:54:30 2017 +0200
@@ -14,56 +14,19 @@
# import standard library
-import html
# import interfaces
-from pyams_content.component.extfile.interfaces import IExtFileContainer, IExtFileContainerTarget
-from pyams_content.component.links.interfaces import ILinkContainerTarget, ILinkContainer, IInternalLink, \
- ILinkLinksContainerTarget, ILinkLinksContainer
-from pyams_content.interfaces import MANAGE_CONTENT_PERMISSION
+from pyams_content.component.association.interfaces import IAssociationContainer, IAssociationTarget, IAssociationInfo
+from pyams_content.component.extfile.interfaces import IBaseExtFile
+from pyams_content.component.links.interfaces import IBaseLink
from pyams_i18n.interfaces import II18n
-from pyams_skin.interfaces import IInnerPage, IPageHeader
from pyams_skin.layer import IPyAMSLayer
-from pyams_utils.interfaces import VIEW_SYSTEM_PERMISSION
-from pyams_utils.interfaces.data import IObjectData
-from pyams_zmi.interfaces.menu import IPropertiesMenu
-from pyams_zmi.layer import IAdminLayer
-from z3c.table.interfaces import IColumn, IValues
# import packages
-from pyams_content.component.links.zmi.widget import LinkLinksSelectFieldWidget
-from pyams_content.shared.common.zmi import WfModifiedContentColumnMixin
-from pyams_form.form import AJAXEditForm
-from pyams_form.security import ProtectedFormObjectMixin
-from pyams_pagelet.pagelet import pagelet_config
-from pyams_sequence.utility import get_sequence_dict
-from pyams_skin.page import DefaultPageHeaderAdapter
-from pyams_skin.table import BaseTable, I18nColumn, TrashColumn
-from pyams_skin.viewlet.menu import MenuItem
-from pyams_template.template import template_config
-from pyams_utils.adapter import adapter_config, ContextRequestViewAdapter
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 AdminDialogEditForm
-from pyams_zmi.view import AdminView
from pyramid.view import view_config
-from pyramid.decorator import reify
-from z3c.form import field
-from z3c.table.column import GetAttrColumn
-from zope.interface import implementer, alsoProvides, Interface
-
-from pyams_content import _
-
-
-@viewlet_config(name='links.menu', context=ILinkContainerTarget, layer=IAdminLayer,
- manager=IPropertiesMenu, permission=VIEW_SYSTEM_PERMISSION, weight=210)
-class LinkContainerMenu(MenuItem):
- """Links container menu"""
-
- label = _("Useful links...")
- icon_class = 'fa-link'
- url = '#links.html'
+from zope.interface import Interface
#
@@ -77,12 +40,12 @@
result = []
key_field_name = request.params.get('keyFieldName', 'title')
value_field_name = request.params.get('valueFieldName', 'value')
- target = get_parent(request.context, ILinkContainerTarget)
+ target = get_parent(request.context, IAssociationTarget)
if target is not None:
- container = ILinkContainer(target)
- result.extend([{key_field_name: link.__name__,
- value_field_name: II18n(link).query_attribute('title', request=request)}
- for link in container.values()])
+ container = IAssociationContainer(target)
+ result.extend([{key_field_name: item.__name__,
+ value_field_name: IAssociationInfo(item).user_title}
+ for item in container.values()])
return sorted(result, key=lambda x: x[value_field_name])
@@ -91,148 +54,15 @@
def get_links_list(request):
"""Get links list in JSON format for TinyMCE editor"""
result = []
- target = get_parent(request.context, IExtFileContainerTarget)
+ target = get_parent(request.context, IAssociationTarget)
if target is not None:
- container = IExtFileContainer(target)
- result.extend([{'title': II18n(file).query_attribute('title', request=request),
- 'value': absolute_url(II18n(file).query_attribute('data', request=request), request)}
- for file in container.values()])
- target = get_parent(request.context, ILinkContainerTarget)
- if target is not None:
- container = ILinkContainer(target)
- result.extend([{'title': II18n(link).query_attribute('title', request=request),
- 'value': link.get_editor_url()}
- for link in container.values()])
+ container = IAssociationContainer(target)
+ for item in container.values():
+ if IBaseLink.providedBy(item):
+ result.append({'title': IAssociationInfo(item).user_title,
+ 'value': item.get_editor_url()})
+ elif IBaseExtFile.providedBy(item):
+ result.append({'title': IAssociationInfo(item).user_title,
+ 'value': absolute_url(II18n(item).query_attribute('data', request=request),
+ request=request)})
return sorted(result, key=lambda x: x['title'])
-
-
-@pagelet_config(name='links.html', context=ILinkContainerTarget, layer=IPyAMSLayer,
- permission=VIEW_SYSTEM_PERMISSION)
-@template_config(template='templates/container.pt', layer=IPyAMSLayer)
-@implementer(IInnerPage)
-class LinkContainerView(AdminView):
- """Links container view"""
-
- title = _("Useful links list")
-
- def __init__(self, context, request):
- super(LinkContainerView, self).__init__(context, request)
- self.links_table = LinkContainerTable(context, request)
-
- def update(self):
- super(LinkContainerView, self).update()
- self.links_table.update()
-
-
-class LinkContainerTable(BaseTable):
- """Links container table"""
-
- hide_header = True
- cssClasses = {'table': 'table table-bordered table-striped table-hover table-tight'}
-
- def __init__(self, context, request):
- super(LinkContainerTable, self).__init__(context, request)
- self.object_data = {'ams-widget-toggle-button': 'false'}
- alsoProvides(self, IObjectData)
-
- @property
- def data_attributes(self):
- attributes = super(LinkContainerTable, self).data_attributes
- attributes['table'] = {'data-ams-location': absolute_url(ILinkContainer(self.context), self.request),
- 'data-ams-datatable-sort': 'false',
- 'data-ams-datatable-pagination': 'false'}
- return attributes
-
- @reify
- def values(self):
- return list(super(LinkContainerTable, self).values)
-
- def render(self):
- if not self.values:
- translate = self.request.localizer.translate
- return translate(_("No currently defined link."))
- return super(LinkContainerTable, self).render()
-
-
-@adapter_config(name='name', context=(ILinkContainerTarget, IPyAMSLayer, LinkContainerTable), provides=IColumn)
-class LinkContainerNameColumn(I18nColumn, WfModifiedContentColumnMixin, GetAttrColumn):
- """Links container name column"""
-
- _header = _("Title")
-
- weight = 10
-
- def getValue(self, obj):
- return II18n(obj).query_attribute('title', request=self.request)
-
-
-@adapter_config(name='target', context=(ILinkContainerTarget, IPyAMSLayer, LinkContainerTable), provides=IColumn)
-class LinkContainerTargetColumn(I18nColumn, GetAttrColumn):
- """Links container target column"""
-
- _header = _("Link target")
-
- weight = 20
-
- def getValue(self, obj):
- if IInternalLink.providedBy(obj):
- mapping = get_sequence_dict(obj.get_target())
- return mapping['text']
- else:
- return html.escape(obj.get_url(self.request))
-
-
-@adapter_config(name='trash', context=(ILinkContainerTarget, IPyAMSLayer, LinkContainerTable), provides=IColumn)
-class LinkContainerTrashColumn(ProtectedFormObjectMixin, TrashColumn):
- """Links container trash column"""
-
-
-@adapter_config(context=(ILinkContainerTarget, IPyAMSLayer, LinkContainerTable), provides=IValues)
-class LinkContainerValues(ContextRequestViewAdapter):
- """Links container values"""
-
- @property
- def values(self):
- return ILinkContainer(self.context).values()
-
-
-@adapter_config(context=(ILinkContainerTarget, IPyAMSLayer, LinkContainerView), provides=IPageHeader)
-class LinkHeaderAdapter(DefaultPageHeaderAdapter):
- """Links container header adapter"""
-
- back_url = '#properties.html'
- icon_class = 'fa fa-fw fa-link'
-
-
-#
-# Links links edit form
-#
-
-@pagelet_config(name='link-links.html', context=ILinkLinksContainerTarget, layer=IPyAMSLayer,
- permission=VIEW_SYSTEM_PERMISSION)
-class LinkLinksContainerLinksEditForm(AdminDialogEditForm):
- """Links links container edit form"""
-
- legend = _("Edit useful links links")
-
- fields = field.Fields(ILinkLinksContainer)
- fields['links'].widgetFactory = LinkLinksSelectFieldWidget
-
- ajax_handler = 'link-links.json'
- edit_permission = MANAGE_CONTENT_PERMISSION
-
-
-@view_config(name='link-links.json', context=ILinkLinksContainerTarget, request_type=IPyAMSLayer,
- permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
-class LinkLinksContainerAJAXEditForm(AJAXEditForm, LinkLinksContainerLinksEditForm):
- """Links links container edit form, JSON renderer"""
-
- def get_ajax_output(self, changes):
- if 'links' in changes.get(ILinkLinksContainer, ()):
- return {'status': 'success',
- 'event': 'PyAMS_content.changed_item',
- 'event_options': {'object_type': 'links_container',
- 'object_name': self.context.__name__,
- 'nb_links': len(ILinkLinksContainer(self.context).links or ())}}
- else:
- return super(LinkLinksContainerAJAXEditForm, self).get_ajax_output(changes)
--- a/src/pyams_content/component/links/zmi/templates/container.pt Mon Sep 11 14:53:15 2017 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,13 +0,0 @@
-<div class="ams-widget">
- <header>
- <span tal:condition="view.widget_icon_class | nothing"
- class="widget-icon"><i tal:attributes="class view.widget_icon_class"></i>
- </span>
- <h2 tal:content="view.title"></h2>
- <tal:var content="structure provider:pyams.widget_title" />
- <tal:var content="structure provider:pyams.toolbar" />
- </header>
- <div class="widget-body no-widget-toolbar">
- <tal:var content="structure view.links_table.render()" />
- </div>
-</div>
--- a/src/pyams_content/component/links/zmi/templates/widget-display.pt Mon Sep 11 14:53:15 2017 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,21 +0,0 @@
-<input type="hidden" autocomplete="off" readonly
- tal:attributes="id view/id;
- name view/name;
- class string:select2 ${view/klass} ordered;
- style view/style;
- title view/title;
- value python:','.join(view.value);
- lang view/lang;
- onclick view/onclick;
- ondblclick view/ondblclick;
- onmousedown view/onmousedown;
- onmouseup view/onmouseup;
- onmouseover view/onmouseover;
- onmousemove view/onmousemove;
- onmouseout view/onmouseout;
- onkeypress view/onkeypress;
- onkeydown view/onkeydown;
- onkeyup view/onkeyup;
- disabled view/disabled;
- tabindex view/tabindex;
- data-ams-select2-values view/values_map;" />
--- a/src/pyams_content/component/links/zmi/templates/widget-input.pt Mon Sep 11 14:53:15 2017 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,60 +0,0 @@
-<label class="input bordered with-icon" i18n:domain="pyams_content"
- data-ams-plugins="pyams_content"
- tal:attributes="data-ams-plugin-pyams_content-src extension:resource_path('pyams_content.skin:pyams_content')">
- <div class="btn-group icon-append">
- <i class="fa fa-fw fa-bars txt-color-green opaque" data-toggle="dropdown"
- tal:attributes="data-ams-select2-target view/name"></i>
- <ul class="dropdown-menu pull-right">
- <li class="small">
- <a data-ams-url="add-internal-link.html?origin=link"
- data-ams-stop-propagation="true" data-toggle="modal">
- <i class="fa fa-fw fa-link"></i>
- <span i18n:translate="">Add internal link...</span>
- </a>
- </li>
- <li class="small">
- <a data-ams-url="add-external-link.html?origin=link"
- data-ams-stop-propagation="true" data-toggle="modal">
- <i class="fa fa-fw fa-external-link"></i>
- <span i18n:translate="">Add external link...</span>
- </a>
- </li>
- <li class="small">
- <a data-ams-url="add-mailto-link.html?origin=link"
- data-ams-stop-propagation="true" data-toggle="modal">
- <i class="fa fa-fw fa-envelope-o"></i>
- <span i18n:translate="">Add mailto link...</span>
- </a>
- </li>
- </ul>
- </div>
- <div class="select2-parent">
- <input type="hidden" class="select2 ordered"
- data-ams-events-handlers='{"select2-open": "PyAMS_content.links.init"}'
- data-ams-select2-allow-clear="true"
- data-ams-select2-multiple="false"
- tal:attributes="id view/id;
- name view/name;
- class string:${view/klass} select2 ordered;
- style view/style;
- title view/title;
- lang view/lang;
- onclick view/onclick;
- ondblclick view/ondblclick;
- onmousedown view/onmousedown;
- onmouseup view/onmouseup;
- onmouseover view/onmouseover;
- onmousemove view/onmousemove;
- onmouseout view/onmouseout;
- onkeypress view/onkeypress;
- onkeydown view/onkeydown;
- onkeyup view/onkeyup;
- disabled view/disabled;
- tabindex view/tabindex;
- onfocus view/onfocus;
- onblur view/onblur;
- onchange view/onchange;
- value python:','.join(view.value);
- data-ams-select2-data view/values_data;" />
- </div>
-</label>
--- a/src/pyams_content/component/links/zmi/templates/widget-list-display.pt Mon Sep 11 14:53:15 2017 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,22 +0,0 @@
-<input type="hidden" autocomplete="off" readonly
- data-ams-select2-multiple="true"
- tal:attributes="id view/id;
- name view/name;
- class string:select2 ${view/klass} ordered;
- style view/style;
- title view/title;
- value python:','.join(view.value);
- lang view/lang;
- onclick view/onclick;
- ondblclick view/ondblclick;
- onmousedown view/onmousedown;
- onmouseup view/onmouseup;
- onmouseover view/onmouseover;
- onmousemove view/onmousemove;
- onmouseout view/onmouseout;
- onkeypress view/onkeypress;
- onkeydown view/onkeydown;
- onkeyup view/onkeyup;
- disabled view/disabled;
- tabindex view/tabindex;
- data-ams-select2-values view/values_map;" />
--- a/src/pyams_content/component/links/zmi/templates/widget-list-input.pt Mon Sep 11 14:53:15 2017 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,67 +0,0 @@
-<label class="input bordered with-icon" i18n:domain="pyams_content"
- data-ams-plugins="pyams_content"
- tal:attributes="data-ams-plugin-pyams_content-src extension:resource_path('pyams_content.skin:pyams_content')">
- <div class="btn-group icon-append">
- <i class="fa fa-fw fa-bars txt-color-green opaque" data-toggle="dropdown"
- tal:attributes="data-ams-select2-target string:${view/name}:list"></i>
- <ul class="dropdown-menu pull-right">
- <li class="small">
- <a data-ams-url="add-internal-link.html?origin=link"
- data-ams-stop-propagation="true" data-toggle="modal">
- <i class="fa fa-fw fa-link"></i>
- <span i18n:translate="">Add internal link...</span>
- </a>
- </li>
- <li class="small">
- <a data-ams-url="add-external-link.html?origin=link"
- data-ams-stop-propagation="true" data-toggle="modal">
- <i class="fa fa-fw fa-external-link"></i>
- <span i18n:translate="">Add external link...</span>
- </a>
- </li>
- <li class="small">
- <a data-ams-url="add-mailto-link.html?origin=link"
- data-ams-stop-propagation="true" data-toggle="modal">
- <i class="fa fa-fw fa-envelope-o"></i>
- <span i18n:translate="">Add mailto link...</span>
- </a>
- </li>
- </ul>
- </div>
- <div class="select2-parent">
- <select class="select2 ordered"
- data-ams-select2-allow-clear="true"
- tal:attributes="id view/id;
- name string:${view/name}:list;
- class string:${view/klass} select2 ordered;
- style view/style;
- title view/title;
- lang view/lang;
- onclick view/onclick;
- ondblclick view/ondblclick;
- onmousedown view/onmousedown;
- onmouseup view/onmouseup;
- onmouseover view/onmouseover;
- onmousemove view/onmousemove;
- onmouseout view/onmouseout;
- onkeypress view/onkeypress;
- onkeydown view/onkeydown;
- onkeyup view/onkeyup;
- disabled view/disabled;
- tabindex view/tabindex;
- onfocus view/onfocus;
- onblur view/onblur;
- onchange view/onchange;
- multiple view/multiple;
- size view/size">
- <option tal:repeat="entry view/selectedItems"
- tal:attributes="value entry/value;
- selected python:entry['value'] in view.value;"
- tal:content="entry/content"></option>
- <option tal:repeat="entry view/notselectedItems"
- tal:attributes="value entry/value;
- selected python:entry['value'] in view.value;"
- tal:content="entry/content"></option>
- </select>
- </div>
-</label>
--- a/src/pyams_content/component/links/zmi/widget.py Mon Sep 11 14:53:15 2017 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,65 +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 json
-
-# import interfaces
-from pyams_skin.layer import IPyAMSLayer
-
-# import packages
-from pyams_form.widget import widgettemplate_config
-from z3c.form.browser.orderedselect import OrderedSelectWidget
-from z3c.form.widget import FieldWidget
-
-
-@widgettemplate_config(mode='input', template='templates/widget-input.pt', layer=IPyAMSLayer)
-@widgettemplate_config(mode='display', template='templates/widget-display.pt', layer=IPyAMSLayer)
-class SingleLinkLinkSelectWidget(OrderedSelectWidget):
- """Single Link link select widget"""
-
- @property
- def values_data(self):
- result = sorted([{'id': entry['value'], 'text': entry['content']} for entry in self.items],
- key=lambda x: x['text'])
- return json.dumps(result)
-
- @property
- def values_map(self):
- result = {}
- [result.update({entry['value']: entry['content']}) for entry in self.selectedItems]
- return json.dumps(result)
-
-
-def SingleLinkLinkSelectFieldWidget(field, request):
- """Single link link select widget factory"""
- return FieldWidget(field, SingleLinkLinkSelectWidget(request))
-
-
-@widgettemplate_config(mode='input', template='templates/widget-list-input.pt', layer=IPyAMSLayer)
-@widgettemplate_config(mode='display', template='templates/widget-list-display.pt', layer=IPyAMSLayer)
-class LinkLinksSelectWidget(OrderedSelectWidget):
- """Multiple inks links select widget"""
-
- @property
- def values_map(self):
- result = {}
- [result.update({entry['value']: entry['content']}) for entry in self.selectedItems]
- return json.dumps(result)
-
-
-def LinkLinksSelectFieldWidget(field, request):
- """Links links select widget factory"""
- return FieldWidget(field, LinkLinksSelectWidget(request))
--- a/src/pyams_content/component/paragraph/__init__.py Mon Sep 11 14:53:15 2017 +0200
+++ b/src/pyams_content/component/paragraph/__init__.py Mon Sep 11 14:54:30 2017 +0200
@@ -9,6 +9,7 @@
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
+from pyams_utils.request import check_request
__docformat__ = 'restructuredtext'
@@ -16,31 +17,78 @@
# import standard library
# import interfaces
-from pyams_content.component.paragraph.interfaces import IBaseParagraph, IHTMLParagraph
+from pyams_content.component.paragraph.interfaces import IBaseParagraph, IParagraphFactory, IParagraphContainerTarget, \
+ IParagraphContainer, IParagraphFactorySettings
from pyams_content.shared.common.interfaces import IWfSharedContent
from pyams_form.interfaces.form import IFormContextPermissionChecker
+from pyams_workflow.interfaces import IWorkflowState
from zope.lifecycleevent.interfaces import IObjectAddedEvent, IObjectModifiedEvent, IObjectRemovedEvent
# import packages
from persistent import Persistent
from pyams_utils.adapter import adapter_config, ContextAdapter
+from pyams_utils.registry import query_utility
from pyams_utils.traversing import get_parent
+from pyams_utils.vocabulary import vocabulary_config
from pyramid.events import subscriber
from pyramid.threadlocal import get_current_registry
+from zope.component.globalregistry import getGlobalSiteManager
from zope.container.contained import Contained
from zope.interface import implementer
from zope.lifecycleevent import ObjectModifiedEvent
from zope.schema.fieldproperty import FieldProperty
+from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm
+#
+# Auto-creation of default paragraphs
+#
+
+@subscriber(IObjectAddedEvent, context_selector=IParagraphContainerTarget)
+def handle_new_paragraphs_container(event):
+ """Handle new paragraphs container"""
+ container = IParagraphContainer(event.object)
+ content = get_parent(container, IWfSharedContent)
+ version_state = IWorkflowState(content, None) if content is not None else None
+ if (version_state is None) or (version_state.version_id == 1):
+ # only apply to first version
+ settings = get_parent(container, IParagraphFactorySettings)
+ if settings is not None:
+ for factory_name in settings.auto_created_paragraphs or ():
+ factory = query_utility(IParagraphFactory, name=factory_name)
+ if factory is not None:
+ container.append(factory.content_type())
+
+
+#
+# Base paragraph classes and subscribers
+#
+
@implementer(IBaseParagraph)
class BaseParagraph(Persistent, Contained):
"""Base paragraph persistent class"""
+ icon_class = ''
+ icon_hint = ''
+
visible = FieldProperty(IBaseParagraph['visible'])
title = FieldProperty(IBaseParagraph['title'])
+@vocabulary_config(name='PyAMS paragraph factories')
+class ParagraphFactoriesVocabulary(SimpleVocabulary):
+ """Paragraph factories vocabulary"""
+
+ def __init__(self, context=None):
+ request = check_request()
+ registry = request.registry
+ translate = request.localizer.translate
+ terms = sorted([SimpleTerm(name, title=translate(util.name))
+ for name, util in registry.getUtilitiesFor(IParagraphFactory)],
+ key=lambda x: x.title)
+ super(ParagraphFactoriesVocabulary, self).__init__(terms)
+
+
@adapter_config(context=IBaseParagraph, provides=IFormContextPermissionChecker)
class BaseParagraphPermissionChecker(ContextAdapter):
"""Paragraph permission checker"""
--- a/src/pyams_content/component/paragraph/container.py Mon Sep 11 14:53:15 2017 +0200
+++ b/src/pyams_content/component/paragraph/container.py Mon Sep 11 14:54:30 2017 +0200
@@ -37,9 +37,9 @@
last_id = 1
- def __setitem__(self, key, value):
+ def append(self, value):
key = str(self.last_id)
- super(ParagraphContainer, self).__setitem__(key, value)
+ self[key] = value
self.last_id += 1
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/paragraph/header.py Mon Sep 11 14:54:30 2017 +0200
@@ -0,0 +1,53 @@
+#
+# 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_content.component.paragraph.interfaces import IParagraphFactory
+from pyams_content.component.paragraph.interfaces.header import IHeaderParagraph
+from pyams_i18n.interfaces import II18n
+
+# import packages
+from pyams_content.component.paragraph import BaseParagraph
+from pyams_utils.registry import utility_config
+from pyams_utils.text import get_text_start
+from zope.interface import implementer
+from zope.schema.fieldproperty import FieldProperty
+
+from pyams_content import _
+
+
+@implementer(IHeaderParagraph)
+class HeaderParagraph(BaseParagraph):
+ """Header paragraph"""
+
+ icon_class = 'fa-header'
+ icon_hint = _("Header")
+
+ @property
+ def title(self):
+ header = II18n(self).query_attribute('header')
+ return get_text_start(header, 50, 10)
+
+ header = FieldProperty(IHeaderParagraph['header'])
+
+
+@utility_config(name='Header paragraph', provides=IParagraphFactory)
+class HTMLParagraphFactory(object):
+ """HTML paragraph factory"""
+
+ name = _("Header paragraph")
+ content_type = HeaderParagraph
--- a/src/pyams_content/component/paragraph/html.py Mon Sep 11 14:53:15 2017 +0200
+++ b/src/pyams_content/component/paragraph/html.py Mon Sep 11 14:54:30 2017 +0200
@@ -14,25 +14,134 @@
# import standard library
+import re
# import interfaces
-from pyams_content.component.extfile.interfaces import IExtFileLinksContainerTarget
-from pyams_content.component.gallery.interfaces import IGalleryLinksContainerTarget
-from pyams_content.component.links.interfaces import ILinkLinksContainerTarget
-from pyams_content.component.paragraph.interfaces import IHTMLParagraph
+from pyams_content.component.association.interfaces import IAssociationContainer
+from pyams_content.component.extfile.interfaces import IExtFileContainerTarget, IBaseExtFile
+from pyams_content.component.illustration.interfaces import IIllustrationTarget
+from pyams_content.component.links.interfaces import ILinkContainerTarget, IInternalLink, IExternalLink, IMailtoLink
+from pyams_content.component.paragraph.interfaces import IParagraphFactory
+from pyams_content.component.paragraph.interfaces.html import IHTMLParagraph
+from pyams_i18n.interfaces import II18n
+from zope.lifecycleevent.interfaces import IObjectAddedEvent, IObjectModifiedEvent
# import packages
+from pyams_content.component.links import InternalLink, ExternalLink, MailtoLink
from pyams_content.component.paragraph import BaseParagraph
+from pyams_utils.registry import utility_config
+from pyams_utils.request import check_request
+from pyams_utils.url import absolute_url
+from pyquery import PyQuery
+from pyramid.events import subscriber
+from pyramid.threadlocal import get_current_registry
from zope.interface import implementer
+from zope.lifecycleevent import ObjectCreatedEvent
from zope.schema.fieldproperty import FieldProperty
+from pyams_content import _
+
#
# HTML paragraph
#
-@implementer(IHTMLParagraph, IExtFileLinksContainerTarget, ILinkLinksContainerTarget, IGalleryLinksContainerTarget)
+@implementer(IHTMLParagraph, IIllustrationTarget, IExtFileContainerTarget, ILinkContainerTarget)
class HTMLParagraph(BaseParagraph):
"""HTML paragraph"""
+ icon_class = 'fa-html5'
+ icon_hint = _("HTML paragraph")
+
body = FieldProperty(IHTMLParagraph['body'])
+
+
+@utility_config(name='HTML paragraph', provides=IParagraphFactory)
+class HTMLParagraphFactory(object):
+ """HTML paragraph factory"""
+
+ name = _("HTML paragraph")
+ content_type = HTMLParagraph
+
+
+FULL_EMAIL = re.compile('(.*) \<(.*)\>')
+
+
+def check_associations(context, body, lang, notify=True):
+ """Check for link associations from HTML content"""
+ registry = get_current_registry()
+ associations = IAssociationContainer(context)
+ html = PyQuery('<html>{0}</html>'.format(body))
+ for link in html('a[href]'):
+ link_info = None
+ has_link = False
+ href = link.attrib['href']
+ if href.startswith('oid://'):
+ oid = href.split('//', 1)[1]
+ for association in associations.values():
+ internal_info = IInternalLink(association, None)
+ if (internal_info is not None) and (internal_info.reference == oid):
+ has_link = True
+ break
+ if not has_link:
+ link_info = InternalLink()
+ link_info.visible = False
+ link_info.reference = oid
+ link_info.title = {lang: link.attrib.get('title') or link.text}
+ elif href.startswith('mailto:'):
+ name = None
+ email = href[7:]
+ if '<' in email:
+ groups = FULL_EMAIL.findall(email)
+ if groups:
+ name, email = groups[0]
+ for association in associations.values():
+ mailto_info = IMailtoLink(association, None)
+ if (mailto_info is not None) and (mailto_info.address == email):
+ has_link = True
+ break
+ if not has_link:
+ link_info = MailtoLink()
+ link_info.visible = False
+ link_info.address = email
+ link_info.address_name = name or email
+ link_info.title = {lang: link.attrib.get('title') or link.text}
+ else:
+ for association in associations.values():
+ external_info = IExternalLink(association, None)
+ if (external_info is not None) and (external_info.url == href):
+ has_link = True
+ break
+ else:
+ extfile_info = IBaseExtFile(association, None)
+ if extfile_info is not None:
+ request = check_request()
+ extfile_url = absolute_url(II18n(extfile_info).query_attribute('data', request=request),
+ request=request)
+ if extfile_url.endswith(href):
+ has_link = True
+ break
+ if not has_link:
+ link_info = ExternalLink()
+ link_info.visible = False
+ link_info.url = href
+ link_info.title = {lang: link.attrib.get('title') or link.text}
+ if link_info is not None:
+ registry.notify(ObjectCreatedEvent(link_info))
+ associations.append(link_info, notify=notify)
+
+
+@subscriber(IObjectAddedEvent, context_selector=IHTMLParagraph)
+def handle_added_html_paragraph(event):
+ """Check for new associations from added paragraph"""
+ paragraph = event.object
+ for lang, body in (paragraph.body or {}).items():
+ check_associations(paragraph, body, lang, notify=False)
+
+
+@subscriber(IObjectModifiedEvent, context_selector=IHTMLParagraph)
+def handle_modified_html_paragraph(event):
+ """Check for new associations from modified paragraph"""
+ paragraph = event.object
+ for lang, body in (paragraph.body or {}).items():
+ check_associations(paragraph, body, lang, notify=False)
--- a/src/pyams_content/component/paragraph/illustration.py Mon Sep 11 14:53:15 2017 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,67 +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_content.component.paragraph.interfaces import IIllustrationParagraph, IIllustrationRenderer
-from pyams_file.interfaces import DELETED_FILE, IResponsiveImage
-
-# import packages
-from pyams_content.component.paragraph import BaseParagraph
-from pyams_file.property import FileProperty
-from pyams_utils.request import check_request
-from pyams_utils.vocabulary import vocabulary_config
-from zope.interface import implementer, alsoProvides
-from zope.schema.fieldproperty import FieldProperty
-from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm
-
-
-#
-# Illustration
-#
-
-@implementer(IIllustrationParagraph)
-class Illustration(BaseParagraph):
- """Illustration class"""
-
- _data = FileProperty(IIllustrationParagraph['data'])
- legend = FieldProperty(IIllustrationParagraph['legend'])
- renderer = FieldProperty(IIllustrationParagraph['renderer'])
-
- @property
- def data(self):
- return self._data
-
- @data.setter
- def data(self, value):
- self._data = value
- if (value is not None) and (value is not DELETED_FILE):
- alsoProvides(self._data, IResponsiveImage)
-
-
-@vocabulary_config(name='PyAMS illustration renderers')
-class IllustrationRendererVocabulary(SimpleVocabulary):
- """Illustration renderer utilities vocabulary"""
-
- def __init__(self, context=None):
- request = check_request()
- translate = request.localizer.translate
- registry = request.registry
- context = Illustration()
- terms = [SimpleTerm(name, title=translate(adapter.label))
- for name, adapter in sorted(registry.getAdapters((context, request), IIllustrationRenderer),
- key=lambda x: x[1].weight)]
- super(IllustrationRendererVocabulary, self).__init__(terms)
--- a/src/pyams_content/component/paragraph/interfaces/__init__.py Mon Sep 11 14:53:15 2017 +0200
+++ b/src/pyams_content/component/paragraph/interfaces/__init__.py Mon Sep 11 14:54:30 2017 +0200
@@ -21,11 +21,10 @@
from zope.contentprovider.interfaces import IContentProvider
# import packages
-from pyams_file.schema import ImageField
-from pyams_i18n.schema import I18nTextLineField, I18nHTMLField
+from pyams_i18n.schema import I18nTextLineField
from zope.container.constraints import containers, contains
from zope.interface import Interface, Attribute
-from zope.schema import Bool, Choice
+from zope.schema import Bool, List, Choice
from pyams_content import _
@@ -38,12 +37,15 @@
containers('.IParagraphContainer')
+ icon_class = Attribute("Icon class in paragraphs list")
+ icon_hint = Attribute("Icon hint in paragraphs list")
+
visible = Bool(title=_("Visible?"),
description=_("Is this paragraph visible in front-office?"),
required=True,
default=True)
- title = I18nTextLineField(title=_("Title"),
+ title = I18nTextLineField(title=_("§ Title"),
description=_("Paragraph title"),
required=False)
@@ -53,46 +55,34 @@
contains(IBaseParagraph)
+ def append(self, value):
+ """Add given value to container"""
+
class IParagraphContainerTarget(Interface):
"""Paragraphs container marker interface"""
+class IParagraphFactory(Interface):
+ """Paragraph factory utility interface"""
+
+ name = Attribute("Factory name")
+ content_type = Attribute("Factory content type")
+
+
+class IParagraphFactorySettings(Interface):
+ """Paragraph factory settings interface
+
+ This interface is used to defined default auto-created paragraphs
+ for a given shared tool."""
+
+ auto_created_paragraphs = List(title=_("Default paragraphs"),
+ description=_("List of paragraphs automatically added to a new content"),
+ required=False,
+ value_type=Choice(vocabulary='PyAMS paragraph factories'))
+
+
class IParagraphSummary(IContentProvider):
"""Paragraph summary renderer"""
language = Attribute("Summary language")
-
-
-#
-# HTML paragraph
-#
-
-class IHTMLParagraph(IBaseParagraph):
- """HTML body paragraph"""
-
- body = I18nHTMLField(title=_("Body"),
- required=False)
-
-
-#
-# Illustration
-#
-
-class IIllustrationRenderer(IContentProvider):
- """Illustration renderer utility interface"""
-
- label = Attribute("Renderer label")
-
-
-class IIllustrationParagraph(IBaseParagraph):
- """Illustration paragraph"""
-
- data = ImageField(title=_("Image data"),
- required=True)
-
- legend = I18nTextLineField(title=_("Legend"),
- required=False)
-
- renderer = Choice(title=_("Image style"),
- vocabulary='PyAMS illustration renderers')
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/paragraph/interfaces/header.py Mon Sep 11 14:54:30 2017 +0200
@@ -0,0 +1,35 @@
+#
+# 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_content.component.paragraph.interfaces import IBaseParagraph
+
+# import packages
+from pyams_i18n.schema import I18nHTMLField, I18nTextField
+
+from pyams_content import _
+
+
+#
+# HTML paragraph
+#
+
+class IHeaderParagraph(IBaseParagraph):
+ """Header paragraph"""
+
+ header = I18nTextField(title=_("Header"),
+ required=False)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/paragraph/interfaces/html.py Mon Sep 11 14:54:30 2017 +0200
@@ -0,0 +1,35 @@
+#
+# 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_content.component.paragraph.interfaces import IBaseParagraph
+
+# import packages
+from pyams_i18n.schema import I18nHTMLField
+
+from pyams_content import _
+
+
+#
+# HTML paragraph
+#
+
+class IHTMLParagraph(IBaseParagraph):
+ """HTML body paragraph"""
+
+ body = I18nHTMLField(title=_("Body"),
+ required=False)
--- a/src/pyams_content/component/paragraph/zmi/__init__.py Mon Sep 11 14:53:15 2017 +0200
+++ b/src/pyams_content/component/paragraph/zmi/__init__.py Mon Sep 11 14:54:30 2017 +0200
@@ -16,27 +16,84 @@
# import standard library
# import interfaces
-from pyams_content.component.paragraph.interfaces import IBaseParagraph
+from pyams_content.component.paragraph.interfaces import IBaseParagraph, IParagraphFactorySettings
+from pyams_content.interfaces import MANAGE_TOOL_PERMISSION
+from pyams_form.interfaces.form import IFormHelp
from pyams_i18n.interfaces import II18n
+from pyams_skin.layer import IPyAMSLayer
+from pyams_zmi.interfaces.menu import IPropertiesMenu
+from pyams_zmi.layer import IAdminLayer
# import packages
from pyams_form.form import AJAXEditForm
+from pyams_form.help import FormHelp
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_skin.viewlet.menu import MenuItem
+from pyams_utils.adapter import adapter_config
+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_content import _
+
+
+#
+# Default paragraphs settings
+#
+
+@viewlet_config(name='default-paragraphs.menu', context=IParagraphFactorySettings, layer=IAdminLayer,
+ manager=IPropertiesMenu, permission=MANAGE_TOOL_PERMISSION, weight=5)
+class DefaultParagraphsSettingsMenu(MenuItem):
+ """Default paragraphs settings menu"""
+
+ label = _("Default paragraphs...")
+ icon_class = 'fa-paragraph'
+ url = 'default-paragraphs.html'
+ modal_target = True
+
+
+@pagelet_config(name='default-paragraphs.html', context=IParagraphFactorySettings, layer=IPyAMSLayer,
+ permission=MANAGE_TOOL_PERMISSION)
+class DefaultParagraphsEditForm(AdminDialogEditForm):
+ """Default paragraphs edit form"""
+
+ legend = _("Default paragraphs")
+
+ fields = field.Fields(IParagraphFactorySettings)
+ ajax_handler = 'default-paragraphs.json'
+ edit_permission = MANAGE_TOOL_PERMISSION
+
+
+@view_config(name='default-paragraphs.json', context=IParagraphFactorySettings, request_type=IPyAMSLayer,
+ permission=MANAGE_TOOL_PERMISSION, renderer='json', xhr=True)
+class DefaultParagraphAJAXEditForm(AJAXEditForm, DefaultParagraphsEditForm):
+ """Default paragraphs edit form, JSON renderer"""
+
+
+@adapter_config(context=(IParagraphFactorySettings, IPyAMSLayer, DefaultParagraphsEditForm), provides=IFormHelp)
+class DefaultParagraphsEditFormHelp(FormHelp):
+ """Default paragraphs edit form help"""
+
+ message = _("These paragraphs will be added automatically to any new created content.")
+ message_format = 'rest'
+
+
+#
+# Base paragraph add form
+#
class BaseParagraphAJAXEditForm(AJAXEditForm):
"""Base paragraph AJAX edit form"""
def get_ajax_output(self, changes):
- updated = changes.get(IBaseParagraph, ())
- if ('title' in updated) or ('visible' in updated):
- return {'status': 'success',
- 'event': 'PyAMS_content.changed_item',
- 'event_options': {'object_type': 'paragraph',
- 'object_name': self.context.__name__,
- 'title': II18n(self.context).query_attribute('title', request=self.request),
- 'visible': self.context.visible}}
+ output = super(BaseParagraphAJAXEditForm, self).get_ajax_output(changes)
if 'title' in changes.get(IBaseParagraph, ()):
- return {'status': 'reload',
- 'location': '#paragraphs.html'}
- else:
- return super(BaseParagraphAJAXEditForm, self).get_ajax_output(changes)
+ output.setdefault('events', []).append({
+ 'event': 'PyAMS_content.changed_item',
+ 'options': {'object_type': 'paragraph',
+ 'object_name': self.context.__name__,
+ 'title': II18n(self.context).query_attribute('title', request=self.request),
+ 'visible': self.context.visible}
+ })
+ return output
--- a/src/pyams_content/component/paragraph/zmi/container.py Mon Sep 11 14:53:15 2017 +0200
+++ b/src/pyams_content/component/paragraph/zmi/container.py Mon Sep 11 14:54:30 2017 +0200
@@ -9,6 +9,7 @@
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
+from pyams_content.component.association.zmi import AssociationsContainerView
__docformat__ = 'restructuredtext'
@@ -18,9 +19,9 @@
# import interfaces
from pyams_content.interfaces import MANAGE_CONTENT_PERMISSION
-from pyams_content.component.extfile.interfaces import IExtFileLinksContainerTarget, IExtFileLinksContainer
-from pyams_content.component.gallery.interfaces import IGalleryLinksContainerTarget, IGalleryLinksContainer
-from pyams_content.component.links.interfaces import ILinkLinksContainerTarget, ILinkLinksContainer
+from pyams_content.component.association.interfaces import IAssociationContainer
+from pyams_content.component.extfile.interfaces import IExtFileContainerTarget, IExtFile
+from pyams_content.component.links.interfaces import ILinkContainerTarget, IBaseLink
from pyams_content.component.paragraph.interfaces import IParagraphContainerTarget, IParagraphContainer, IBaseParagraph
from pyams_content.component.paragraph.zmi.interfaces import IParagraphInnerEditor, IParagraphTitleToolbar
from pyams_form.interfaces.form import IFormSecurityContext
@@ -38,7 +39,7 @@
from pyams_form.security import ProtectedFormObjectMixin
from pyams_pagelet.pagelet import pagelet_config
from pyams_skin.page import DefaultPageHeaderAdapter
-from pyams_skin.table import BaseTable, I18nColumn, TrashColumn, ActionColumn, JsActionColumn
+from pyams_skin.table import BaseTable, I18nColumn, TrashColumn, JsActionColumn, SorterColumn, ImageColumn
from pyams_skin.viewlet.menu import MenuItem
from pyams_template.template import template_config
from pyams_utils.adapter import adapter_config, ContextRequestViewAdapter
@@ -131,21 +132,9 @@
@adapter_config(name='sorter', context=(IParagraphContainerTarget, IPyAMSLayer, ParagraphContainerTable),
provides=IColumn)
-class ParagraphContainerSorterColumn(ProtectedFormObjectMixin, ActionColumn):
+class ParagraphContainerSorterColumn(ProtectedFormObjectMixin, SorterColumn):
"""Paragraphs container sorter column"""
- cssClasses = {'th': 'action',
- 'td': 'action sorter'}
-
- icon_class = 'fa fa-fw fa-sort'
- icon_hint = _("Click and drag to sort paragraphs...")
-
- url = '#'
- weight = 1
-
- def get_url(self, item):
- return '#'
-
@adapter_config(name='show-hide', context=(IParagraphContainerTarget, IPyAMSLayer, ParagraphContainerTable),
provides=IColumn)
@@ -176,6 +165,20 @@
return super(ParagraphContainerShowHideColumn, self).renderCell(item)
+@adapter_config(name='pictogram', context=(IParagraphContainerTarget, IPyAMSLayer, ParagraphContainerTable),
+ provides=IColumn)
+class ParagraphContainerPictogramColumn(ImageColumn):
+ """Paragraph container pictogram column"""
+
+ weight = 6
+
+ def get_icon_class(self, item):
+ return item.icon_class
+
+ def get_icon_hint(self, item):
+ return self.request.localizer.translate(item.icon_hint)
+
+
@adapter_config(context=ParagraphContainerShowHideColumn, provides=IFormSecurityContext)
def ShowHideColumnSecurityContextFactory(column):
"""Show/hide column security context factory"""
@@ -190,58 +193,14 @@
"""Paragraph title toolbar viewlet manager"""
-@viewlet_config(name='files', context=IExtFileLinksContainerTarget, layer=IPyAMSLayer, view=ParagraphContainerTable,
- manager=IParagraphTitleToolbar, permission=VIEW_SYSTEM_PERMISSION, weight=10)
-@template_config(template='templates/paragraph-title-icon.pt', layer=IPyAMSLayer)
-class ParagraphContainerExtFileLinksAction(Viewlet):
- """Paragraph container external files links column"""
-
- action_class = 'action extfiles nowrap width-40'
- icon_class = 'fa fa-fw fa-file-text-o'
- icon_hint = _("External files")
-
- url = 'extfile-links.html'
- modal_target = True
-
- @property
- def count(self):
- return len(IExtFileLinksContainer(self.context).files or ())
-
-
-@viewlet_config(name='links', context=ILinkLinksContainerTarget, layer=IPyAMSLayer, view=ParagraphContainerTable,
- manager=IParagraphTitleToolbar, permission=VIEW_SYSTEM_PERMISSION, weight=20)
-@template_config(template='templates/paragraph-title-icon.pt', layer=IPyAMSLayer)
-class ParagraphContainerLinkLinksAction(Viewlet):
- """Paragraph container links links column"""
-
- action_class = 'action links nowrap width-40'
- icon_class = 'fa fa-fw fa-link'
- icon_hint = _("Useful links")
-
- url = 'link-links.html'
- modal_target = True
-
- @property
- def count(self):
- return len(ILinkLinksContainer(self.context).links or ())
-
-
-@viewlet_config(name='gallery', context=IGalleryLinksContainerTarget, layer=IPyAMSLayer, view=ParagraphContainerTable,
- manager=IParagraphTitleToolbar, permission=VIEW_SYSTEM_PERMISSION, weight=30)
-@template_config(template='templates/paragraph-title-icon.pt', layer=IPyAMSLayer)
-class ParagraphContainerGalleryLinksAction(Viewlet):
- """Paragraph container gallery links column"""
-
- action_class = 'action galleries nowrap width-40'
- icon_class = 'fa fa-fw fa-picture-o'
- icon_hint = _("Images galleries")
-
- url = 'gallery-links.html'
- modal_target = True
-
- @property
- def count(self):
- return len(IGalleryLinksContainer(self.context).galleries or ())
+def getParagraphTitleHints(item, request, table):
+ """Get paragraphs column title hints"""
+ registry = request.registry
+ provider = registry.queryMultiAdapter((item, request, table), IContentProvider,
+ name='pyams_paragraph.title_toolbar')
+ if provider is not None:
+ provider.update()
+ return provider.render()
@adapter_config(name='name', context=(IParagraphContainerTarget, IPyAMSLayer, ParagraphContainerTable),
@@ -249,7 +208,7 @@
class ParagraphContainerTitleColumn(I18nColumn, WfModifiedContentColumnMixin, GetAttrColumn):
"""Paragraph container title column"""
- _header = _("Title")
+ _header = _("Show/hide all paragraphs")
weight = 50
@@ -258,30 +217,24 @@
' data-ams-stop-propagation="true"' \
' data-ams-click-handler="PyAMS_content.paragraphs.switchAllEditors">' \
' <i class="fa fa-plus-square-o"></i>' \
- '</span> '.format(
+ '</span> '.format(
title=self.request.localizer.translate(_("Click to open/close all paragraphs editors"))) + \
super(ParagraphContainerTitleColumn, self).renderHeadCell()
def renderCell(self, item):
- registry = self.request.registry
- provider = registry.queryMultiAdapter((item, self.request, self.table), IContentProvider,
- name='pyams_paragraph.title_toolbar')
- if provider is not None:
- provider.update()
- provider = provider.render()
- else:
- provider = ''
+ provider = getParagraphTitleHints(item, self.request, self.table) or ''
return '<div>{provider}<span class="small hint" title="{title}" data-ams-hint-gravity="e"' \
' data-ams-stop-propagation="true" ' \
' data-ams-click-handler="PyAMS_content.paragraphs.switchEditor">' \
' <i class="fa fa-plus-square-o"></i>' \
- '</span> '.format(provider=provider,
- title=self.request.localizer.translate(_("Click to open/close paragraph editor"))) + \
+ '</span> '.format(
+ provider=provider,
+ title=self.request.localizer.translate(_("Click to open/close paragraph editor"))) + \
'<span class="title">{0}</span>'.format(super(ParagraphContainerTitleColumn, self).renderCell(item)) + \
'</div><div class="inner-table-form editor margin-x-10 margin-bottom-0"></div>'
def getValue(self, obj):
- return II18n(obj).query_attribute('title', request=self.request) or '--'
+ return II18n(obj).query_attribute('title', request=self.request) or ' - - - - - - - -'
@adapter_config(name='trash', context=(IParagraphContainerTarget, IPyAMSLayer, ParagraphContainerTable),
@@ -304,17 +257,47 @@
"""Paragraphs container header adapter"""
back_url = '#properties.html'
-
icon_class = 'fa fa-fw fa-paragraph'
-@view_config(name='set-paragraphs-order.json', context=IParagraphContainerTarget, request_type=IPyAMSLayer,
+@viewlet_config(name='links', context=ILinkContainerTarget, layer=IPyAMSLayer, view=ParagraphContainerTable,
+ manager=IParagraphTitleToolbar, permission=VIEW_SYSTEM_PERMISSION, weight=10)
+@template_config(template='templates/paragraph-title-icon.pt', layer=IPyAMSLayer)
+class ParagraphContainerLinksCounter(Viewlet):
+ """Paragraph container external links count column"""
+
+ action_class = 'action links nowrap width-40'
+ icon_class = 'fa fa-fw fa-link'
+ icon_hint = _("Links")
+
+ @property
+ def count(self):
+ return len([file for file in IAssociationContainer(self.context).values()
+ if IBaseLink.providedBy(file)])
+
+
+@viewlet_config(name='files', context=IExtFileContainerTarget, layer=IPyAMSLayer, view=ParagraphContainerTable,
+ manager=IParagraphTitleToolbar, permission=VIEW_SYSTEM_PERMISSION, weight=10)
+@template_config(template='templates/paragraph-title-icon.pt', layer=IPyAMSLayer)
+class ParagraphContainerExtFileCounter(Viewlet):
+ """Paragraph container external files count column"""
+
+ action_class = 'action extfiles nowrap width-40'
+ icon_class = 'fa fa-fw fa-file-text-o'
+ icon_hint = _("External files")
+
+ @property
+ def count(self):
+ return len([file for file in IAssociationContainer(self.context).values()
+ if IExtFile.providedBy(file)])
+
+
+@view_config(name='set-paragraphs-order.json', context=IParagraphContainer, request_type=IPyAMSLayer,
permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
def set_paragraphs_order(request):
"""Update paragraphs order"""
- container = IParagraphContainer(request.context)
order = list(map(str, json.loads(request.params.get('names'))))
- container.updateOrder(order)
+ request.context.updateOrder(order)
return {'status': 'success'}
@@ -363,3 +346,44 @@
editor.update()
result[key] = editor.render()
return result
+
+
+#
+# Paragraphs associations view
+#
+
+@viewlet_config(name='paragraphs-associations.menu', context=IParagraphContainerTarget, layer=IAdminLayer,
+ manager=IPropertiesMenu, permission=VIEW_SYSTEM_PERMISSION, weight=101)
+class ParagraphsAssociationsMenu(MenuItem):
+ """Paragraphs associations container menu"""
+
+ label = _("Associations...")
+ icon_class = 'fa-link'
+ url = '#paragraphs-associations.html'
+
+
+@pagelet_config(name='paragraphs-associations.html', context=IParagraphContainerTarget, layer=IPyAMSLayer,
+ permission=VIEW_SYSTEM_PERMISSION)
+@template_config(template='templates/associations.pt', layer=IPyAMSLayer)
+@implementer(IInnerPage)
+class ParagraphsAssociationsView(AdminView):
+ """Paragraphs associations view"""
+
+ title = _("Paragraphs associations")
+
+ @reify
+ def associations(self):
+ result = []
+ for paragraph in IParagraphContainer(self.context).values():
+ associations = IAssociationContainer(paragraph, None)
+ if associations is not None:
+ view = AssociationsContainerView(paragraph, self.request)
+ view.widget_icon_class = 'fa fa-fw {0}'.format(paragraph.icon_class)
+ view.title = II18n(paragraph).query_attribute('title', request=self.request) or ' - - - - - - - -'
+ result.append(view)
+ return result
+
+ def update(self):
+ super(ParagraphsAssociationsView, self).update()
+ for association in self.associations:
+ association.update()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/paragraph/zmi/header.py Mon Sep 11 14:54:30 2017 +0200
@@ -0,0 +1,174 @@
+#
+# 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_content.component.paragraph.interfaces import IParagraphContainerTarget, IParagraphContainer, \
+ IParagraphSummary
+from pyams_content.component.paragraph.interfaces.header import IHeaderParagraph
+from pyams_content.component.paragraph.zmi.interfaces import IParagraphInnerEditor
+from pyams_content.interfaces import MANAGE_CONTENT_PERMISSION
+from pyams_content.shared.common.interfaces import IWfSharedContent
+from pyams_form.interfaces.form import IInnerForm, IEditFormButtons
+from pyams_i18n.interfaces import II18n
+from pyams_skin.interfaces.viewlet import IToolbarAddingMenu
+from pyams_skin.layer import IPyAMSLayer
+from z3c.form.interfaces import INPUT_MODE
+
+# import packages
+from pyams_content.component.paragraph.header import HeaderParagraph
+from pyams_content.component.paragraph.zmi import BaseParagraphAJAXEditForm
+from pyams_content.component.paragraph.zmi.container import ParagraphContainerView
+from pyams_form.form import AJAXAddForm
+from pyams_form.security import ProtectedFormObjectMixin
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_skin.viewlet.toolbar import ToolbarMenuItem
+from pyams_template.template import template_config, get_view_template
+from pyams_utils.adapter import adapter_config, ContextRequestAdapter
+from pyams_utils.traversing import get_parent
+from pyams_viewlet.viewlet import viewlet_config
+from pyams_zmi.form import AdminDialogAddForm, AdminDialogEditForm
+from pyramid.view import view_config
+from z3c.form import field, button
+from zope.interface import implementer
+
+from pyams_content import _
+
+
+@viewlet_config(name='add-header-paragraph.menu', context=IParagraphContainerTarget, view=ParagraphContainerView,
+ layer=IPyAMSLayer, manager=IToolbarAddingMenu, weight=40)
+class HeaderParagraphAddMenu(ProtectedFormObjectMixin, ToolbarMenuItem):
+ """Header paragraph add menu"""
+
+ label = _("Add header paragraph...")
+ label_css_class = 'fa fa-fw fa-header'
+ url = 'add-header-paragraph.html'
+ modal_target = True
+
+
+@pagelet_config(name='add-header-paragraph.html', context=IParagraphContainerTarget, layer=IPyAMSLayer,
+ permission=MANAGE_CONTENT_PERMISSION)
+class HeaderParagraphAddForm(AdminDialogAddForm):
+ """Header paragraph add form"""
+
+ legend = _("Add new header paragraph")
+ icon_css_class = 'fa fa-fw fa-header'
+
+ fields = field.Fields(IHeaderParagraph).select('header')
+ ajax_handler = 'add-header-paragraph.json'
+ edit_permission = MANAGE_CONTENT_PERMISSION
+
+ def updateWidgets(self, prefix=None):
+ super(HeaderParagraphAddForm, self).updateWidgets(prefix)
+ if 'header' in self.widgets:
+ self.widgets['header'].widget_css_class = 'textarea height-100'
+
+ def create(self, data):
+ return HeaderParagraph()
+
+ def add(self, object):
+ IParagraphContainer(self.context).append(object)
+
+
+@view_config(name='add-header-paragraph.json', context=IParagraphContainerTarget, request_type=IPyAMSLayer,
+ permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
+class HeaderParagraphAJAXAddForm(AJAXAddForm, HeaderParagraphAddForm):
+ """Header paragraph add form, JSON renderer"""
+
+ def get_ajax_output(self, changes):
+ return {'status': 'reload',
+ 'location': '#paragraphs.html'}
+
+
+@pagelet_config(name='properties.html', context=IHeaderParagraph, layer=IPyAMSLayer,
+ permission=MANAGE_CONTENT_PERMISSION)
+class HeaderParagraphPropertiesEditForm(AdminDialogEditForm):
+ """Header paragraph properties edit form"""
+
+ @property
+ def title(self):
+ content = get_parent(self.context, IWfSharedContent)
+ return II18n(content).query_attribute('title', request=self.request)
+
+ legend = _("Edit header paragraph properties")
+ icon_css_class = 'fa fa-fw fa-header'
+
+ fields = field.Fields(IHeaderParagraph).select('header')
+ ajax_handler = 'properties.json'
+ edit_permission = MANAGE_CONTENT_PERMISSION
+
+ def updateWidgets(self, prefix=None):
+ super(HeaderParagraphPropertiesEditForm, self).updateWidgets(prefix)
+ if 'header' in self.widgets:
+ self.widgets['header'].widget_css_class = 'textarea height-100'
+
+
+@view_config(name='properties.json', context=IHeaderParagraph, request_type=IPyAMSLayer,
+ permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
+class HeaderParagraphPropertiesAJAXEditForm(BaseParagraphAJAXEditForm, HeaderParagraphPropertiesEditForm):
+ """Header paragraph properties edit form, JSON renderer"""
+
+ def get_ajax_output(self, changes):
+ output = super(HeaderParagraphPropertiesAJAXEditForm, self).get_ajax_output(changes)
+ if 'header' in changes.get(IHeaderParagraph, ()):
+ output.setdefault('events', []).append({
+ 'event': 'PyAMS_content.changed_item',
+ 'options': {'object_type': 'paragraph',
+ 'object_name': self.context.__name__,
+ 'title': II18n(self.context).query_attribute('title', request=self.request),
+ 'visible': self.context.visible}
+ })
+ return output
+
+
+@adapter_config(context=(IHeaderParagraph, IPyAMSLayer), provides=IParagraphInnerEditor)
+@implementer(IInnerForm)
+class HeaderParagraphInnerEditForm(HeaderParagraphPropertiesEditForm):
+ """Header paragraph inner edit form"""
+
+ legend = None
+ label_css_class = 'control-label col-md-2'
+ input_css_class = 'col-md-10'
+
+ @property
+ def buttons(self):
+ if self.mode == INPUT_MODE:
+ return button.Buttons(IEditFormButtons)
+ else:
+ return button.Buttons()
+
+
+#
+# HTML paragraph summary
+#
+
+@adapter_config(context=(IHeaderParagraph, IPyAMSLayer), provides=IParagraphSummary)
+@template_config(template='templates/header-summary.pt', layer=IPyAMSLayer)
+class HeaderParagraphSummary(ContextRequestAdapter):
+ """Header paragraph renderer"""
+
+ language = None
+
+ def update(self):
+ i18n = II18n(self.context)
+ if self.language:
+ for attr in ('header', ):
+ setattr(self, attr, i18n.get_attribute(attr, self.language, request=self.request))
+ else:
+ for attr in ('header', ):
+ setattr(self, attr, i18n.query_attribute(attr, request=self.request))
+
+ render = get_view_template()
--- a/src/pyams_content/component/paragraph/zmi/html.py Mon Sep 11 14:53:15 2017 +0200
+++ b/src/pyams_content/component/paragraph/zmi/html.py Mon Sep 11 14:54:30 2017 +0200
@@ -16,8 +16,11 @@
# import standard library
# import interfaces
-from pyams_content.component.paragraph.interfaces import IParagraphContainerTarget, IHTMLParagraph, \
- IParagraphContainer, IParagraphSummary
+from pyams_content.component.association.zmi.interfaces import IAssociationsParentForm
+from pyams_content.component.illustration.interfaces import IIllustration, IIllustrationRenderer
+from pyams_content.component.paragraph.interfaces import IParagraphContainerTarget, IParagraphContainer, \
+ IParagraphSummary
+from pyams_content.component.paragraph.interfaces.html import IHTMLParagraph
from pyams_content.component.paragraph.zmi.interfaces import IParagraphInnerEditor
from pyams_content.interfaces import MANAGE_CONTENT_PERMISSION
from pyams_content.shared.common.interfaces import IWfSharedContent
@@ -25,9 +28,11 @@
from pyams_i18n.interfaces import II18n
from pyams_skin.interfaces.viewlet import IToolbarAddingMenu
from pyams_skin.layer import IPyAMSLayer
+from pyams_zmi.interfaces import IPropertiesEditForm
from z3c.form.interfaces import INPUT_MODE
# import packages
+from pyams_content.component.association.zmi import AssociationsTable
from pyams_content.component.paragraph.html import HTMLParagraph
from pyams_content.component.paragraph.zmi import BaseParagraphAJAXEditForm
from pyams_content.component.paragraph.zmi.container import ParagraphContainerView
@@ -40,9 +45,10 @@
from pyams_utils.traversing import get_parent
from pyams_viewlet.viewlet import viewlet_config
from pyams_zmi.form import AdminDialogAddForm, AdminDialogEditForm
+from pyramid.threadlocal import get_current_registry
from pyramid.view import view_config
from z3c.form import field, button
-from zope.interface import implementer
+from zope.interface import implementer, Interface
from pyams_content import _
@@ -73,7 +79,7 @@
label_css_class = 'control-label col-md-2'
input_css_class = 'col-md-10'
- fields = field.Fields(IHTMLParagraph).omit('__parent__', '__name__')
+ fields = field.Fields(IHTMLParagraph).omit('__parent__', '__name__', 'visible')
ajax_handler = 'add-html-paragraph.json'
edit_permission = MANAGE_CONTENT_PERMISSION
@@ -86,7 +92,7 @@
return HTMLParagraph()
def add(self, object):
- IParagraphContainer(self.context)['none'] = object
+ IParagraphContainer(self.context).append(object)
@view_config(name='add-html-paragraph.json', context=IParagraphContainerTarget, request_type=IPyAMSLayer,
@@ -115,27 +121,48 @@
label_css_class = 'control-label col-md-2'
input_css_class = 'col-md-10'
- fields = field.Fields(IHTMLParagraph).omit('__parent__', '__name__')
+ fields = field.Fields(IHTMLParagraph).omit('__parent__', '__name__', 'visible')
ajax_handler = 'properties.json'
edit_permission = MANAGE_CONTENT_PERMISSION
def updateWidgets(self, prefix=None):
super(HTMLParagraphPropertiesEditForm, self).updateWidgets(prefix)
if 'body' in self.widgets:
- for lang in self.widgets['body'].langs:
- widget = self.widgets['body'].widgets[lang]
+ body_widget = self.widgets['body']
+ for lang in body_widget.langs:
+ widget = body_widget.widgets[lang]
widget.id = '{id}_{name}'.format(id=widget.id, name=self.context.__name__)
- self.widgets['body'].widget_css_class = 'textarea'
+ body_widget.widget_css_class = 'textarea'
+
+
+class IHTMLParagraphInnerEditForm(Interface):
+ """Marker interface for HTML paragraph inner form"""
+
+
+@view_config(name='properties.json', context=IHTMLParagraph, request_type=IPyAMSLayer,
+ permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
+class HTMLParagraphPropertiesAJAXEditForm(BaseParagraphAJAXEditForm, HTMLParagraphPropertiesEditForm):
+ """HTML paragraph properties edit form, JSON renderer"""
+
+ def get_ajax_output(self, changes):
+ output = super(HTMLParagraphPropertiesAJAXEditForm, self).get_ajax_output(changes)
+ if 'body' in changes.get(IHTMLParagraph, ()):
+ associations_table = AssociationsTable(self.context, self.request, None)
+ associations_table.update()
+ output.setdefault('callbacks', []).append({
+ 'callback': 'PyAMS_content.associations.afterUpdateCallback',
+ 'options': {'parent': associations_table.id,
+ 'table': associations_table.render()}})
+ return output
@adapter_config(context=(IHTMLParagraph, IPyAMSLayer), provides=IParagraphInnerEditor)
-@implementer(IInnerForm)
+@implementer(IInnerForm, IPropertiesEditForm, IAssociationsParentForm, IHTMLParagraphInnerEditForm)
class HTMLParagraphInnerEditForm(HTMLParagraphPropertiesEditForm):
"""HTML paragraph inner edit form"""
legend = None
- main_group_legend = _("HTML paragraph")
- main_group_class = 'inner'
+ ajax_handler = 'inner-properties.json'
@property
def buttons(self):
@@ -145,10 +172,22 @@
return button.Buttons()
-@view_config(name='properties.json', context=IHTMLParagraph, request_type=IPyAMSLayer,
+@view_config(name='inner-properties.json', context=IHTMLParagraph, request_type=IPyAMSLayer,
permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
-class HTMLParagraphPropertiesAJAXEditForm(BaseParagraphAJAXEditForm, HTMLParagraphPropertiesEditForm):
- """HTML paragraph properties edit form, JSON renderer"""
+class HTMLParagraphInnerAJAXEditForm(BaseParagraphAJAXEditForm, HTMLParagraphInnerEditForm):
+ """HTML paragraph inner edit form, JSON renderer"""
+
+ def get_ajax_output(self, changes):
+ output = super(HTMLParagraphInnerAJAXEditForm, self).get_ajax_output(changes)
+ if 'body' in changes.get(IHTMLParagraph, ()):
+ associations_table = AssociationsTable(self.context, self.request, None)
+ associations_table.update()
+ output.setdefault('callbacks', []).append({
+ 'callback': 'PyAMS_content.associations.afterUpdateCallback',
+ 'options': {'parent': associations_table.id,
+ 'table': associations_table.render()}
+ })
+ return output
#
@@ -160,6 +199,8 @@
class HTMLParagraphSummary(ContextRequestAdapter):
"""HTML paragraph renderer"""
+ illustration = None
+ illustration_renderer = None
language = None
def update(self):
@@ -170,5 +211,18 @@
else:
for attr in ('title', 'body'):
setattr(self, attr, i18n.query_attribute(attr, request=self.request))
+ self.illustration = IIllustration(self.context)
+ if self.illustration.data:
+ registry = get_current_registry()
+ renderer = self.illustration_renderer = registry.queryMultiAdapter((self.illustration, self.request),
+ IIllustrationRenderer,
+ name=self.illustration.renderer)
+ if renderer is not None:
+ renderer.update()
render = get_view_template()
+
+ def render_illustration(self):
+ if not self.illustration_renderer:
+ return ''
+ return self.illustration_renderer.render()
--- a/src/pyams_content/component/paragraph/zmi/illustration.py Mon Sep 11 14:53:15 2017 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,207 +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_content.component.paragraph.interfaces import IParagraphContainerTarget, IIllustrationParagraph, \
- IParagraphContainer, IParagraphSummary, IIllustrationRenderer
-from pyams_content.component.paragraph.zmi.interfaces import IParagraphInnerEditor
-from pyams_content.interfaces import MANAGE_CONTENT_PERMISSION
-from pyams_content.shared.common.interfaces import IWfSharedContent
-from pyams_form.interfaces.form import IInnerForm, IEditFormButtons
-from pyams_i18n.interfaces import II18n
-from pyams_skin.interfaces.viewlet import IToolbarAddingMenu
-from pyams_skin.layer import IPyAMSLayer
-from z3c.form.interfaces import INPUT_MODE
-
-# import packages
-from pyams_content.component.paragraph.illustration import Illustration
-from pyams_content.component.paragraph.zmi import BaseParagraphAJAXEditForm
-from pyams_content.component.paragraph.zmi.container import ParagraphContainerView
-from pyams_form.form import AJAXAddForm
-from pyams_form.security import ProtectedFormObjectMixin
-from pyams_pagelet.pagelet import pagelet_config
-from pyams_skin.viewlet.toolbar import ToolbarMenuItem
-from pyams_template.template import template_config, get_view_template
-from pyams_utils.adapter import ContextRequestAdapter, adapter_config
-from pyams_utils.traversing import get_parent
-from pyams_viewlet.viewlet import viewlet_config
-from pyams_zmi.form import AdminDialogAddForm, AdminDialogEditForm
-from pyramid.view import view_config
-from z3c.form import field, button
-from zope.interface import implementer
-
-from pyams_content import _
-
-
-#
-# Illustration
-#
-
-@viewlet_config(name='add-illustration.menu', context=IParagraphContainerTarget, view=ParagraphContainerView,
- layer=IPyAMSLayer, manager=IToolbarAddingMenu, weight=60)
-class IllustrationAddMenu(ProtectedFormObjectMixin, ToolbarMenuItem):
- """Illustration add menu"""
-
- label = _("Add illustration...")
- label_css_class = 'fa fa-fw fa-file-image-o'
- url = 'add-illustration.html'
- modal_target = True
-
-
-@pagelet_config(name='add-illustration.html', context=IParagraphContainerTarget, layer=IPyAMSLayer,
- permission=MANAGE_CONTENT_PERMISSION)
-class IllustrationAddForm(AdminDialogAddForm):
- """Illustration add form"""
-
- legend = _("Add new illustration")
- dialog_class = 'modal-large'
- icon_css_class = 'fa fa-fw fa-file-image-o'
-
- fields = field.Fields(IIllustrationParagraph).omit('__parent__', '__name__')
- ajax_handler = 'add-illustration.json'
- edit_permission = MANAGE_CONTENT_PERMISSION
-
- def create(self, data):
- return Illustration()
-
- def add(self, object):
- IParagraphContainer(self.context)['none'] = object
-
-
-@view_config(name='add-illustration.json', context=IParagraphContainerTarget, request_type=IPyAMSLayer,
- permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
-class IllustrationAJAXAddForm(AJAXAddForm, IllustrationAddForm):
- """HTML paragraph add form, JSON renderer"""
-
- def get_ajax_output(self, changes):
- return {'status': 'reload',
- 'location': '#paragraphs.html'}
-
-
-@pagelet_config(name='properties.html', context=IIllustrationParagraph, layer=IPyAMSLayer,
- permission=MANAGE_CONTENT_PERMISSION)
-class IllustrationPropertiesEditForm(AdminDialogEditForm):
- """Illustration properties edit form"""
-
- @property
- def title(self):
- content = get_parent(self.context, IWfSharedContent)
- return II18n(content).query_attribute('title', request=self.request)
-
- legend = _("Edit illustration properties")
- dialog_class = 'modal-large'
- icon_css_class = 'fa fa-fw fa-file-image-o'
-
- fields = field.Fields(IIllustrationParagraph).omit('__parent__', '__name__')
- ajax_handler = 'properties.json'
- edit_permission = MANAGE_CONTENT_PERMISSION
-
-
-@adapter_config(context=(IIllustrationParagraph, IPyAMSLayer), provides=IParagraphInnerEditor)
-@implementer(IInnerForm)
-class IllustrationInnerEditForm(IllustrationPropertiesEditForm):
- """Illustration inner edit form"""
-
- legend = None
- main_group_legend = _("Illustration")
- main_group_class = 'inner'
-
- @property
- def buttons(self):
- if self.mode == INPUT_MODE:
- return button.Buttons(IEditFormButtons)
- else:
- return button.Buttons()
-
-
-@view_config(name='properties.json', context=IIllustrationParagraph, request_type=IPyAMSLayer,
- permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
-class IllustrationPropertiesAJAXEditForm(BaseParagraphAJAXEditForm, IllustrationPropertiesEditForm):
- """HTML paragraph properties edit form, JSON renderer"""
-
-
-#
-# Illustration summary
-#
-
-@adapter_config(context=(IIllustrationParagraph, IPyAMSLayer), provides=IParagraphSummary)
-class IllustrationSummary(ContextRequestAdapter):
- """Illustration renderer"""
-
- def __init__(self, context, request):
- super(IllustrationSummary, self).__init__(context, request)
- self.renderer = request.registry.queryMultiAdapter((context, request), IIllustrationRenderer,
- name=self.context.renderer)
-
- language = None
-
- def update(self):
- if self.renderer is not None:
- self.renderer.language = self.language
- self.renderer.update()
-
- def render(self):
- if self.renderer is not None:
- return self.renderer.render()
- else:
- return ''
-
-
-#
-# Illustration renderers
-#
-
-class BaseIllustrationRenderer(ContextRequestAdapter):
- """Base illustration renderer"""
-
- language = None
-
- def update(self):
- i18n = II18n(self.context)
- if self.language:
- self.legend = i18n.get_attribute('legend', self.language, request=self.request)
- else:
- self.legend = i18n.query_attribute('legend', request=self.request)
-
- render = get_view_template()
-
-
-@adapter_config(name='default', context=(IIllustrationParagraph, IPyAMSLayer), provides=IIllustrationRenderer)
-@template_config(template='templates/illustration.pt', layer=IPyAMSLayer)
-class DefaultIllustrationRenderer(BaseIllustrationRenderer):
- """Default illustration renderer"""
-
- label = _("Centered illustration")
- weight = 1
-
-
-@adapter_config(name='left+zoom', context=(IIllustrationParagraph, IPyAMSLayer), provides=IIllustrationRenderer)
-@template_config(template='templates/illustration-left.pt', layer=IPyAMSLayer)
-class LeftIllustrationWithZoomRenderer(BaseIllustrationRenderer):
- """Illustrtaion renderer with small image and zoom"""
-
- label = _("Small illustration on the left with zoom")
- weight = 2
-
-
-@adapter_config(name='right+zoom', context=(IIllustrationParagraph, IPyAMSLayer), provides=IIllustrationRenderer)
-@template_config(template='templates/illustration-right.pt', layer=IPyAMSLayer)
-class RightIllustrationWithZoomRenderer(BaseIllustrationRenderer):
- """Illustrtaion renderer with small image and zoom"""
-
- label = _("Small illustration on the right with zoom")
- weight = 3
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/paragraph/zmi/templates/associations.pt Mon Sep 11 14:54:30 2017 +0200
@@ -0,0 +1,15 @@
+<div class="ams-widget">
+ <header>
+ <span tal:condition="view.widget_icon_class | nothing"
+ class="widget-icon"><i tal:attributes="class view.widget_icon_class"></i>
+ </span>
+ <h2 tal:content="view.title"></h2>
+ <tal:var content="structure provider:pyams.widget_title" />
+ <tal:var content="structure provider:pyams.toolbar" />
+ </header>
+ <div class="widget-body no-widget-toolbar">
+ <tal:loop repeat="table view.associations">
+ <tal:var content="structure table.render()" />
+ </tal:loop>
+ </div>
+</div>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/paragraph/zmi/templates/header-summary.pt Mon Sep 11 14:54:30 2017 +0200
@@ -0,0 +1,1 @@
+<div class="margin-bottom-10" tal:content="structure extension:html(view.header)">header</div>
--- a/src/pyams_content/component/paragraph/zmi/templates/html-summary.pt Mon Sep 11 14:53:15 2017 +0200
+++ b/src/pyams_content/component/paragraph/zmi/templates/html-summary.pt Mon Sep 11 14:54:30 2017 +0200
@@ -1,2 +1,3 @@
<h3 tal:content="view.title">title</h3>
<div tal:content="structure view.body">body</div>
+<tal:var content="structure view.render_illustration()">Illustration</tal:var>
--- a/src/pyams_content/component/paragraph/zmi/templates/illustration-left.pt Mon Sep 11 14:53:15 2017 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,11 +0,0 @@
-<div class="pull-left margin-10">
- <a class="fancybox" data-toggle
- data-ams-fancybox-type="image"
- tal:define="thumbnails extension:thumbnails(context.data);
- target thumbnails.get_thumbnail('800x600', 'png');
- thumb thumbnails.get_thumbnail('300x200', 'png');"
- tal:attributes="href extension:absolute_url(target)">
- <img tal:attributes="src extension:absolute_url(thumb)" /><br />
- <span tal:content="view.legend">legend</span>
- </a><br />
-</div>
--- a/src/pyams_content/component/paragraph/zmi/templates/illustration-right.pt Mon Sep 11 14:53:15 2017 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,11 +0,0 @@
-<div class="pull-right margin-10">
- <a class="fancybox" data-toggle
- data-ams-fancybox-type="image"
- tal:define="thumbnails extension:thumbnails(context.data);
- target thumbnails.get_thumbnail('800x600', 'png');
- thumb thumbnails.get_thumbnail('300x200', 'png');"
- tal:attributes="href extension:absolute_url(target)">
- <img tal:attributes="src extension:absolute_url(thumb)" /><br />
- <span tal:content="view.legend">legend</span>
- </a><br />
-</div>
--- a/src/pyams_content/component/paragraph/zmi/templates/illustration.pt Mon Sep 11 14:53:15 2017 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,6 +0,0 @@
-<div class="text-center margin-y-5">
- <img tal:define="thumbnails extension:thumbnails(context.data);
- target thumbnails.get_thumbnail('800x600', 'jpeg');"
- tal:attributes="src extension:absolute_url(target)" /><br />
- <span tal:content="view.legend">legend</span>
-</div>
--- a/src/pyams_content/component/paragraph/zmi/templates/paragraph-title-icon.pt Mon Sep 11 14:53:15 2017 +0200
+++ b/src/pyams_content/component/paragraph/zmi/templates/paragraph-title-icon.pt Mon Sep 11 14:54:30 2017 +0200
@@ -1,14 +1,10 @@
-<div tal:attributes="class string:${view.action_class} pull-left">
- <a class="hint"
- tal:attributes="title view.icon_hint;
- href extension:absolute_url(context, view.url);
- data-toggle 'modal' if view.modal_target else None;"
- data-ams-stop-propagation="true" data-ams-hint-gravity="s" data-ams-hint-offset="2">
- <i tal:attributes="class view.icon_class"></i>
- </a>
+<div tal:attributes="class string:${view.action_class} pull-left"
+ tal:define="count view.count" tal:condition="count">
<span class="count">
- <tal:if define="count view.count" condition="count">
- (<span tal:content="count"></span>)
- </tal:if>
+ <span tal:content="count"></span>
</span>
+ <i tal:attributes="class string:${view.icon_class} hint opaque align-base;
+ title view.icon_hint;"
+ data-ams-hint-offset="3"
+ i18n:attributes="title"></i>
</div>