# HG changeset patch # User Thierry Florac # Date 1505134470 -7200 # Node ID 67bad9f880ee7917e6a172adb890742e879fb417 # Parent 99a481dc4c89dc5310b9f401150cd8323ad13b46 Use 'associations' to handle links and external files diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/association/__init__.py --- /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 +# 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)) diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/association/container.py --- /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 +# 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) diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/association/interfaces/__init__.py --- /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 +# 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""" diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/association/paragraph.py --- /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 +# 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 diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/association/zmi/__init__.py --- /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 +# 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 ''.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""" diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/association/zmi/interfaces.py --- /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 +# 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""" diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/association/zmi/paragraph.py --- /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 +# 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() diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/association/zmi/templates/associations-view.pt --- /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 @@ +
+
+ + +

+ + +
+
+ +
+
diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/association/zmi/templates/associations.pt --- /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 @@ +
+
+ + Associations + +
+ +
+
+ +
+
diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/association/zmi/templates/paragraph-summary.pt --- /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 @@ + +

§ title

+ +
diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/extfile/__init__.py --- 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")) diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/extfile/container.py --- 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 -# 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) diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/extfile/interfaces/__init__.py --- 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""" diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/extfile/zmi/__init__.py --- 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) diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/extfile/zmi/container.py --- 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']) diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/extfile/zmi/templates/container.pt --- 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 @@ -
-
- - -

- - -
-
- - - - -
-
diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/extfile/zmi/templates/widget-display.pt --- 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 @@ - diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/extfile/zmi/templates/widget-input.pt --- 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 @@ - diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/extfile/zmi/widget.py --- 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 -# 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)) diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/gallery/__init__.py --- 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) diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/gallery/container.py --- 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 -# 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) diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/gallery/file.py --- /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 +# 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)) diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/gallery/interfaces/__init__.py --- 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""" diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/gallery/paragraph.py --- /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 +# 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 diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/gallery/zmi/__init__.py --- 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 diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/gallery/zmi/container.py --- 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 -# 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) diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/gallery/zmi/file.py --- /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 +# 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'} diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/gallery/zmi/gallery.py --- 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 -# 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'} diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/gallery/zmi/interfaces.py --- 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) diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/gallery/zmi/paragraph.py --- /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 +# 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 +# diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/gallery/zmi/templates/gallery-images.pt --- 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 @@ - - - + diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/gallery/zmi/templates/renderer-default.pt diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/gallery/zmi/templates/widget-display.pt --- 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 @@ - diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/gallery/zmi/templates/widget-input.pt --- 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 @@ - diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/gallery/zmi/widget.py --- 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 -# 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)) diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/illustration/__init__.py --- 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) diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/illustration/interfaces/__init__.py --- /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 +# 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""" diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/illustration/paragraph.py --- /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 +# 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 diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/illustration/zmi/__init__.py --- /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 +# 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 diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/illustration/zmi/paragraph.py --- /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 +# 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 '' diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/illustration/zmi/templates/renderer-default.pt --- /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 @@ +
+
+ legend +
diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/illustration/zmi/templates/renderer-left.pt --- /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 @@ + diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/illustration/zmi/templates/renderer-right.pt --- /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 @@ + diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/links/__init__.py --- 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 '--' diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/links/container.py --- 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 -# 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) diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/links/interfaces/__init__.py --- 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): diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/links/zmi/__init__.py --- 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) diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/links/zmi/container.py --- 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) diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/links/zmi/templates/container.pt --- 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 @@ -
-
- - -

- - -
-
- -
-
diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/links/zmi/templates/widget-display.pt --- 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 @@ - diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/links/zmi/templates/widget-input.pt --- 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 @@ - diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/links/zmi/templates/widget-list-display.pt --- 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 @@ - diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/links/zmi/templates/widget-list-input.pt --- 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 @@ - diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/links/zmi/widget.py --- 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 -# 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)) diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/paragraph/__init__.py --- 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""" diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/paragraph/container.py --- 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 diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/paragraph/header.py --- /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 +# 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 diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/paragraph/html.py --- 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('{0}'.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) diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/paragraph/illustration.py --- 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 -# 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) diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/paragraph/interfaces/__init__.py --- 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') diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/paragraph/interfaces/header.py --- /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 +# 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) diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/paragraph/interfaces/html.py --- /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 +# 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) diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/paragraph/zmi/__init__.py --- 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 diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/paragraph/zmi/container.py --- 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">' \ ' ' \ - ' '.format( + '   '.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 '
{provider}' \ ' ' \ - ' '.format(provider=provider, - title=self.request.localizer.translate(_("Click to open/close paragraph editor"))) + \ + '   '.format( + provider=provider, + title=self.request.localizer.translate(_("Click to open/close paragraph editor"))) + \ '{0}'.format(super(ParagraphContainerTitleColumn, self).renderCell(item)) + \ '
' 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() diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/paragraph/zmi/header.py --- /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 +# 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() diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/paragraph/zmi/html.py --- 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() diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/paragraph/zmi/illustration.py --- 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 -# 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 diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/paragraph/zmi/templates/associations.pt --- /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 @@ +
+
+ + +

+ + +
+
+ + + +
+
diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/paragraph/zmi/templates/header-summary.pt --- /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 @@ +
header
diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/paragraph/zmi/templates/html-summary.pt --- 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 @@

title

body
+Illustration diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/paragraph/zmi/templates/illustration-left.pt --- 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 @@ - diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/paragraph/zmi/templates/illustration-right.pt --- 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 @@ - diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/paragraph/zmi/templates/illustration.pt --- 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 @@ -
-
- legend -
diff -r 99a481dc4c89 -r 67bad9f880ee src/pyams_content/component/paragraph/zmi/templates/paragraph-title-icon.pt --- 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 @@ -
- - - +
- - () - + +