# HG changeset patch # User Thierry Florac # Date 1569397805 -7200 # Node ID 045be80a56450fc69a91eac111d926905ea92701 # Parent 1bbc829453f9ce90de78c1fd91b3f23866004c18 Add interfaces and subscribers to be able to provide additional information to an internal link based on link's target class diff -r 1bbc829453f9 -r 045be80a5645 src/pyams_content/component/links/__init__.py --- a/src/pyams_content/component/links/__init__.py Wed Sep 25 09:47:48 2019 +0200 +++ b/src/pyams_content/component/links/__init__.py Wed Sep 25 09:50:05 2019 +0200 @@ -10,20 +10,20 @@ # FOR A PARTICULAR PURPOSE. # -__docformat__ = 'restructuredtext' - from html import escape -from pyramid.encode import url_quote -from zope.interface import implementer +from pyramid.encode import url_quote, urlencode +from pyramid.events import subscriber +from zope.interface import alsoProvides, implementer, directlyProvidedBy, noLongerProvides +from zope.lifecycleevent import IObjectAddedEvent, IObjectModifiedEvent from zope.schema.fieldproperty import FieldProperty from zope.schema.vocabulary import SimpleTerm, SimpleVocabulary -from pyams_content import _ from pyams_content.component.association import AssociationItem -from pyams_content.component.association.interfaces import IAssociationContainer, IAssociationContainerTarget, \ - IAssociationInfo -from pyams_content.component.links.interfaces import IBaseLink, IExternalLink, IInternalLink, IMailtoLink +from pyams_content.component.association.interfaces import IAssociationContainer, \ + IAssociationContainerTarget, IAssociationInfo +from pyams_content.component.links.interfaces import IBaseLink, IExternalLink, IInternalLink, \ + IInternalLinkCustomInfoTarget, IMailtoLink, IInternalLinkCustomInfo, ICustomInternalLinkTarget from pyams_content.features.checker import BaseContentChecker from pyams_content.features.checker.interfaces import ERROR_VALUE, IContentChecker from pyams_content.interfaces import IBaseContent @@ -42,6 +42,11 @@ from pyams_workflow.interfaces import IWorkflow, IWorkflowPublicationInfo +__docformat__ = 'restructuredtext' + +from pyams_content import _ + + # # Links vocabulary # @@ -55,7 +60,8 @@ target = get_parent(context, IAssociationContainerTarget) if target is not None: terms = [SimpleTerm(link.__name__, title=IAssociationInfo(link).inner_title) - for link in IAssociationContainer(target).values() if IBaseLink.providedBy(link)] + for link in IAssociationContainer(target).values() if + IBaseLink.providedBy(link)] super(ContentLinksVocabulary, self).__init__(terms) @@ -106,7 +112,7 @@ request = check_request() translate = request.localizer.translate return II18n(self.context).query_attribute('title', request) or \ - '({0})'.format(translate(self.context.icon_hint).lower()) + '({0})'.format(translate(self.context.icon_hint).lower()) # @@ -157,14 +163,48 @@ if target is not None: if request is None: request = check_request() + params = None + if IInternalLinkCustomInfoTarget.providedBy(target): + custom_info = IInternalLinkCustomInfo(self, None) + if custom_info is not None: + params = custom_info.get_url_params() + if params: + params = urlencode(params) if self.force_canonical_url: - return canonical_url(target, request, view_name) + return canonical_url(target, request, view_name, query=params) else: - return relative_url(target, request, view_name=view_name) + return relative_url(target, request, view_name=view_name, query=params) else: return '' +@subscriber(IObjectAddedEvent, context_selector=IInternalLink) +def handle_new_internal_link(event): + """Check if link target is providing custom info""" + link = event.object + target = link.target + if target is not None: + info = IInternalLinkCustomInfoTarget(target, None) + if info is not None: + alsoProvides(link, info.internal_link_marker_interface) + + +@subscriber(IObjectModifiedEvent, context_selector=IInternalLink) +def handle_updated_internal_link(event): + """Check when modified if new link target is providing custom info""" + link = event.object + # remove previous provided interfaces + ifaces = tuple([iface for iface in directlyProvidedBy(link) + if issubclass(iface, IInternalLinkCustomInfo)]) + for iface in ifaces: + noLongerProvides(link, iface) + target = link.target + if target is not None: + info = IInternalLinkCustomInfoTarget(target, None) + if info is not None: + alsoProvides(link, info.internal_link_marker_interface) + + @adapter_config(context=IInternalLink, provides=IAssociationInfo) class InternalLinkAssociationInfoAdapter(BaseLinkInfoAdapter): """Internal link association info adapter""" @@ -206,8 +246,10 @@ if workflow is not None: target = self.context.get_target(state=workflow.published_states) if target is None: - output.append(translate(ERROR_VALUE).format(field=IInternalLink['reference'].title, - message=translate(_("target is not published")))) + output.append( + translate(ERROR_VALUE).format(field=IInternalLink['reference'].title, + message=translate( + _("target is not published")))) return output diff -r 1bbc829453f9 -r 045be80a5645 src/pyams_content/component/links/interfaces.py --- a/src/pyams_content/component/links/interfaces.py Wed Sep 25 09:47:48 2019 +0200 +++ b/src/pyams_content/component/links/interfaces.py Wed Sep 25 09:50:05 2019 +0200 @@ -10,21 +10,18 @@ # FOR A PARTICULAR PURPOSE. # -__docformat__ = 'restructuredtext' +from zope.interface import Attribute, Interface +from zope.schema import Bool, Choice, InterfaceField, TextLine, URI + +from pyams_content.component.association.interfaces import IAssociationContainerTarget, \ + IAssociationItem +from pyams_content.reference.pictograms.interfaces import SELECTED_PICTOGRAM_VOCABULARY +from pyams_i18n.schema import I18nTextField, I18nTextLineField +from pyams_sequence.interfaces import IInternalReference +from pyams_utils.schema import MailAddressField -# import standard library - -# import interfaces -from pyams_content.component.association.interfaces import IAssociationContainerTarget, IAssociationItem -from pyams_content.reference.pictograms.interfaces import SELECTED_PICTOGRAM_VOCABULARY -from pyams_sequence.interfaces import IInternalReference - -# import packages -from pyams_i18n.schema import I18nTextLineField, I18nTextField -from pyams_utils.schema import MailAddressField -from zope.interface import Attribute -from zope.schema import Choice, TextLine, URI, Bool +__docformat__ = 'restructuredtext' from pyams_content import _ @@ -37,7 +34,8 @@ required=False) description = I18nTextField(title=_("Description"), - description=_("Link description displayed by front-office template"), + description=_( + "Link description displayed by front-office template"), required=False) pictogram_name = Choice(title=_("Pictogram"), @@ -55,13 +53,45 @@ """Internal link interface""" force_canonical_url = Bool(title=_("Force canonical URL?"), - description=_("By default, internal links use a \"relative\" URL, which tries to " - "display link target in the current context; by using a canonical URL, " - "you can display target in it's attachment context (if defined)"), + description=_("By default, internal links use a \"relative\" URL, " + "which tries to display link target in the current " + "context; by using a canonical URL, you can display " + "target in it's attachment context (if defined)"), required=False, default=False) +# +# Custom internal link properties support +# These interfaces are used to be able to add custom properties to an internal link +# when it's target is of a given content type +# + +class IInternalLinkCustomInfoTarget(Interface): + """Internal link target info + + This optional interface can be supported be any content to be able to provide any + additional information to link properties + """ + + internal_link_marker_interface = InterfaceField(title=_("Marker interface provided by links " + "directed to contents supporting this " + "interface")) + + +class ICustomInternalLinkTarget(Interface): + """Base interface for custom internal link target""" + + +class IInternalLinkCustomInfo(Interface): + """Base interface for custom link properties""" + + properties_interface = InterfaceField(title=_("Info properties interface")) + + def get_url_params(self): + """Get custom params to generate link URL""" + + class IExternalLink(IBaseLink): """External link interface""" diff -r 1bbc829453f9 -r 045be80a5645 src/pyams_content/component/links/zmi/__init__.py --- a/src/pyams_content/component/links/zmi/__init__.py Wed Sep 25 09:47:48 2019 +0200 +++ b/src/pyams_content/component/links/zmi/__init__.py Wed Sep 25 09:50:05 2019 +0200 @@ -10,41 +10,50 @@ # FOR A PARTICULAR PURPOSE. # -__docformat__ = 'restructuredtext' - from z3c.form import field from z3c.form.browser.checkbox import SingleCheckBoxFieldWidget -from zope.interface import implementer +from zope.interface import Interface, implementer -from pyams_content import _ from pyams_content.component.association.interfaces import IAssociationContainer -from pyams_content.component.association.zmi import AssociationItemAJAXAddForm, AssociationItemAJAXEditForm +from pyams_content.component.association.zmi import AssociationItemAJAXAddForm, \ + AssociationItemAJAXEditForm from pyams_content.component.association.zmi.interfaces import IAssociationsView -from pyams_content.component.links import ExternalLink, InternalLink, MailtoLink -from pyams_content.component.links.interfaces import IExternalLink, IInternalLink, ILinkContainerTarget, IMailtoLink +from pyams_content.component.links import ExternalLink, IInternalLinkCustomInfo, InternalLink, \ + MailtoLink +from pyams_content.component.links.interfaces import ICustomInternalLinkTarget, IExternalLink, \ + IInternalLink, ILinkContainerTarget, IMailtoLink from pyams_content.component.paragraph.zmi import get_json_paragraph_markers_refresh_event from pyams_content.component.paragraph.zmi.container import ParagraphContainerCounterBase -from pyams_content.component.paragraph.zmi.interfaces import IParagraphContainerTable, IParagraphTitleToolbar +from pyams_content.component.paragraph.zmi.interfaces import IParagraphContainerTable, \ + IParagraphTitleToolbar from pyams_content.interfaces import MANAGE_CONTENT_PERMISSION from pyams_content.reference.pictograms.zmi.widget import PictogramSelectFieldWidget from pyams_form.form import ajax_config +from pyams_form.interfaces.form import IInnerSubForm from pyams_form.security import ProtectedFormObjectMixin from pyams_pagelet.pagelet import pagelet_config from pyams_skin.interfaces.viewlet import IToolbarAddingMenu from pyams_skin.layer import IPyAMSLayer from pyams_skin.viewlet.toolbar import ToolbarMenuItem +from pyams_utils.adapter import adapter_config from pyams_utils.interfaces import VIEW_SYSTEM_PERMISSION from pyams_viewlet.viewlet import viewlet_config -from pyams_zmi.form import AdminDialogAddForm, AdminDialogEditForm +from pyams_zmi.form import AdminDialogAddForm, AdminDialogEditForm, InnerAdminEditForm from pyams_zmi.interfaces import IPropertiesEditForm +__docformat__ = 'restructuredtext' + +from pyams_content import _ + + # # Internal links views # -@viewlet_config(name='internal-links', context=ILinkContainerTarget, layer=IPyAMSLayer, view=IParagraphContainerTable, - manager=IParagraphTitleToolbar, permission=VIEW_SYSTEM_PERMISSION, weight=10) +@viewlet_config(name='internal-links', context=ILinkContainerTarget, layer=IPyAMSLayer, + view=IParagraphContainerTable, manager=IParagraphTitleToolbar, + permission=VIEW_SYSTEM_PERMISSION, weight=10) class InternalLinksCounter(ParagraphContainerCounterBase): """Internal links count column""" @@ -61,8 +70,8 @@ if IInternalLink.providedBy(file)]) -@viewlet_config(name='add-internal-link.menu', context=ILinkContainerTarget, view=IAssociationsView, - layer=IPyAMSLayer, manager=IToolbarAddingMenu, weight=50) +@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""" @@ -83,7 +92,8 @@ legend = _("Add new internal link") icon_css_class = 'fa fa-fw fa-external-link-square fa-rotate-90' - fields = field.Fields(IInternalLink).select('reference', 'force_canonical_url', 'title', 'description', + fields = field.Fields(IInternalLink).select('reference', 'force_canonical_url', 'title', + 'description', 'pictogram_name') fields['force_canonical_url'].widgetFactory = SingleCheckBoxFieldWidget fields['pictogram_name'].widgetFactory = PictogramSelectFieldWidget @@ -99,12 +109,14 @@ def get_ajax_output(self, changes): output = super(self.__class__, self).get_ajax_output(changes) if output: - output.setdefault('events', []).append(get_json_paragraph_markers_refresh_event(self.context, self.request, - self, InternalLinksCounter)) + output.setdefault('events', []).append( + get_json_paragraph_markers_refresh_event(self.context, self.request, + self, InternalLinksCounter)) return output -@pagelet_config(name='properties.html', context=IInternalLink, layer=IPyAMSLayer, permission=VIEW_SYSTEM_PERMISSION) +@pagelet_config(name='properties.html', context=IInternalLink, layer=IPyAMSLayer, + permission=VIEW_SYSTEM_PERMISSION) @ajax_config(name='properties.json', context=IInternalLink, layer=IPyAMSLayer, permission=MANAGE_CONTENT_PERMISSION, base=AssociationItemAJAXEditForm) @implementer(IPropertiesEditForm) @@ -117,7 +129,8 @@ icon_css_class = 'fa fa-fw fa-external-link-square fa-rotate-90' dialog_class = 'modal-large' - fields = field.Fields(IInternalLink).select('reference', 'force_canonical_url', 'title', 'description', + fields = field.Fields(IInternalLink).select('reference', 'force_canonical_url', 'title', + 'description', 'pictogram_name') fields['force_canonical_url'].widgetFactory = SingleCheckBoxFieldWidget fields['pictogram_name'].widgetFactory = PictogramSelectFieldWidget @@ -131,12 +144,42 @@ return super(self.__class__, self).get_ajax_output(changes) +@adapter_config(name='custom', + context=(ICustomInternalLinkTarget, IPyAMSLayer, InternalLinkPropertiesEditForm), + provides=IInnerSubForm) +class CustomInternalLinkPropertiesEditForm(InnerAdminEditForm): + """Custom internal link properties edit form""" + + prefix = 'custom_properties.' + + css_class = 'form-group' + padding_class = '' + + legend = _("Custom target properties") + fieldset_class = 'bordered' + + @property + def fields(self): + info = IInternalLinkCustomInfo(self.context, None) + if info is not None: + return field.Fields(info.properties_interface).omit('properties_interface') + return field.Fields(Interface) + + weight = 1 + + def render(self): + if not self.fields: + return '' + return super(CustomInternalLinkPropertiesEditForm, self).render() + + # # External links views # -@viewlet_config(name='external-links', context=ILinkContainerTarget, layer=IPyAMSLayer, view=IParagraphContainerTable, - manager=IParagraphTitleToolbar, permission=VIEW_SYSTEM_PERMISSION, weight=11) +@viewlet_config(name='external-links', context=ILinkContainerTarget, layer=IPyAMSLayer, + view=IParagraphContainerTable, manager=IParagraphTitleToolbar, + permission=VIEW_SYSTEM_PERMISSION, weight=11) class ExternalLinksCounter(ParagraphContainerCounterBase): """External links count column""" @@ -153,8 +196,8 @@ if IExternalLink.providedBy(file)]) -@viewlet_config(name='add-external-link.menu', context=ILinkContainerTarget, view=IAssociationsView, - layer=IPyAMSLayer, manager=IToolbarAddingMenu, weight=51) +@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""" @@ -175,7 +218,8 @@ legend = _("Add new external link") icon_css_class = 'fa fa-fw fa-external-link' - fields = field.Fields(IExternalLink).select('url', 'title', 'description', 'pictogram_name', 'language') + fields = field.Fields(IExternalLink).select('url', 'title', 'description', 'pictogram_name', + 'language') fields['pictogram_name'].widgetFactory = PictogramSelectFieldWidget edit_permission = MANAGE_CONTENT_PERMISSION @@ -189,12 +233,14 @@ def get_ajax_output(self, changes): output = super(self.__class__, self).get_ajax_output(changes) if output: - output.setdefault('events', []).append(get_json_paragraph_markers_refresh_event(self.context, self.request, - self, ExternalLinksCounter)) + output.setdefault('events', []).append( + get_json_paragraph_markers_refresh_event(self.context, self.request, + self, ExternalLinksCounter)) return output -@pagelet_config(name='properties.html', context=IExternalLink, layer=IPyAMSLayer, permission=VIEW_SYSTEM_PERMISSION) +@pagelet_config(name='properties.html', context=IExternalLink, layer=IPyAMSLayer, + permission=VIEW_SYSTEM_PERMISSION) @ajax_config(name='properties.json', context=IExternalLink, layer=IPyAMSLayer, permission=MANAGE_CONTENT_PERMISSION, base=AssociationItemAJAXEditForm) @implementer(IPropertiesEditForm) @@ -207,7 +253,8 @@ icon_css_class = 'fa fa-fw fa-external-link' dialog_class = 'modal-large' - fields = field.Fields(IExternalLink).select('url', 'title', 'description', 'pictogram_name', 'language') + fields = field.Fields(IExternalLink).select('url', 'title', 'description', 'pictogram_name', + 'language') fields['pictogram_name'].widgetFactory = PictogramSelectFieldWidget edit_permission = None # defined by IFormContextPermissionChecker adapter @@ -223,8 +270,9 @@ # Mailto links views # -@viewlet_config(name='mailto-links', context=ILinkContainerTarget, layer=IPyAMSLayer, view=IParagraphContainerTable, - manager=IParagraphTitleToolbar, permission=VIEW_SYSTEM_PERMISSION, weight=12) +@viewlet_config(name='mailto-links', context=ILinkContainerTarget, layer=IPyAMSLayer, + view=IParagraphContainerTable, manager=IParagraphTitleToolbar, + permission=VIEW_SYSTEM_PERMISSION, weight=12) class MailtoLinksCounter(ParagraphContainerCounterBase): """Mailto links count column""" @@ -263,7 +311,8 @@ legend = _("Add new mailto link") icon_css_class = 'fa fa-fw fa-envelope-o' - fields = field.Fields(IMailtoLink).select('address', 'address_name', 'title', 'description', 'pictogram_name') + fields = field.Fields(IMailtoLink).select('address', 'address_name', 'title', 'description', + 'pictogram_name') fields['pictogram_name'].widgetFactory = PictogramSelectFieldWidget edit_permission = MANAGE_CONTENT_PERMISSION @@ -277,12 +326,14 @@ def get_ajax_output(self, changes): output = super(self.__class__, self).get_ajax_output(changes) if output: - output.setdefault('events', []).append(get_json_paragraph_markers_refresh_event(self.context, self.request, - self, MailtoLinksCounter)) + output.setdefault('events', []).append( + get_json_paragraph_markers_refresh_event(self.context, self.request, + self, MailtoLinksCounter)) return output -@pagelet_config(name='properties.html', context=IMailtoLink, layer=IPyAMSLayer, permission=VIEW_SYSTEM_PERMISSION) +@pagelet_config(name='properties.html', context=IMailtoLink, layer=IPyAMSLayer, + permission=VIEW_SYSTEM_PERMISSION) @ajax_config(name='properties.json', context=IMailtoLink, layer=IPyAMSLayer, permission=MANAGE_CONTENT_PERMISSION, base=AssociationItemAJAXEditForm) @implementer(IPropertiesEditForm) @@ -294,7 +345,8 @@ legend = _("Edit mailto link properties") icon_css_class = 'fa fa-fw fa-envelope-o' - fields = field.Fields(IMailtoLink).select('address', 'address_name', 'title', 'description', 'pictogram_name') + fields = field.Fields(IMailtoLink).select('address', 'address_name', 'title', 'description', + 'pictogram_name') fields['pictogram_name'].widgetFactory = PictogramSelectFieldWidget edit_permission = None # defined by IFormContextPermissionChecker adapter