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
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