Add interfaces and subscribers to be able to provide additional information to an internal link based on link's target class
authorThierry Florac <tflorac@ulthar.net>
Wed, 25 Sep 2019 09:50:05 +0200 (2019-09-25)
changeset 1351 045be80a5645
parent 1350 1bbc829453f9
child 1352 8242968d86b1
Add interfaces and subscribers to be able to provide additional information to an internal link based on link's target class
src/pyams_content/component/links/__init__.py
src/pyams_content/component/links/interfaces.py
src/pyams_content/component/links/zmi/__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
 
 
--- 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"""
 
--- 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