Added links to framed paragraphs
authorThierry Florac <tflorac@ulthar.net>
Fri, 03 Jul 2020 18:43:46 +0200
changeset 1398 fc32ec8a8f53
parent 1397 d05fc6aa4217
child 1399 a01a3c9612ff
Added links to framed paragraphs
src/pyams_content/component/paragraph/__init__.py
src/pyams_content/component/paragraph/association.py
src/pyams_content/component/paragraph/frame.py
src/pyams_content/component/paragraph/html.py
src/pyams_content/component/paragraph/zmi/frame.py
--- a/src/pyams_content/component/paragraph/__init__.py	Fri Jul 03 14:42:15 2020 +0200
+++ b/src/pyams_content/component/paragraph/__init__.py	Fri Jul 03 18:43:46 2020 +0200
@@ -10,21 +10,20 @@
 # FOR A PARTICULAR PURPOSE.
 #
 
-__docformat__ = 'restructuredtext'
-
 from persistent import Persistent
 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.lifecycleevent.interfaces import IObjectAddedEvent, IObjectModifiedEvent, IObjectRemovedEvent
+from zope.lifecycleevent.interfaces import IObjectAddedEvent, IObjectModifiedEvent, \
+    IObjectRemovedEvent
 from zope.schema.fieldproperty import FieldProperty
 from zope.schema.vocabulary import SimpleTerm, SimpleVocabulary
 
-from pyams_content.component.paragraph.interfaces import CONTENT_PARAGRAPHS_VOCABULARY, IBaseParagraph, \
-    IParagraphContainer, IParagraphContainerTarget, IParagraphFactory, IParagraphFactorySettings, IParagraphTitle, \
-    PARAGRAPH_FACTORIES_VOCABULARY
+from pyams_content.component.paragraph.interfaces import CONTENT_PARAGRAPHS_VOCABULARY, \
+    IBaseParagraph, IParagraphContainer, IParagraphContainerTarget, IParagraphFactory, \
+    IParagraphFactorySettings, IParagraphTitle, PARAGRAPH_FACTORIES_VOCABULARY
 from pyams_content.features.checker import BaseContentChecker
 from pyams_content.features.preview.interfaces import IPreviewTarget
 from pyams_content.features.renderer import RenderedContentMixin
@@ -40,6 +39,9 @@
 from pyams_workflow.interfaces import IWorkflowState
 
 
+__docformat__ = 'restructuredtext'
+
+
 #
 # Auto-creation of default paragraphs
 #
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/paragraph/association.py	Fri Jul 03 18:43:46 2020 +0200
@@ -0,0 +1,104 @@
+#
+# Copyright (c) 2015-2020 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+"""PyAMS_*** module
+
+"""
+
+__docformat__ = 'restructuredtext'
+
+import re
+
+from pyquery import PyQuery
+from pyramid.threadlocal import get_current_registry
+from zope.lifecycleevent import ObjectCreatedEvent
+
+from pyams_content.component.association import IAssociationContainer
+from pyams_content.component.extfile import IBaseExtFile
+from pyams_content.component.links import ExternalLink, IExternalLink, IInternalLink, IMailtoLink, \
+    InternalLink, MailtoLink
+from pyams_i18n.interfaces import II18n
+from pyams_sequence.interfaces import ISequentialIntIds
+from pyams_utils.registry import get_utility
+from pyams_utils.request import check_request
+from pyams_utils.url import absolute_url
+
+
+FULL_EMAIL = re.compile('(.*) \<(.*)\>')
+
+
+def check_associations(context, body, lang, notify=True):
+    """Check for link associations from HTML content"""
+    associations = IAssociationContainer(context, None)
+    if associations is None:
+        return
+    registry = get_current_registry()
+    html = PyQuery('<html>{0}</html>'.format(body))
+    for link in html('a[href]'):
+        link_info = None
+        has_link = False
+        href = link.attrib['href']
+        if href.startswith('oid://'):
+            sequence = get_utility(ISequentialIntIds)
+            oid = sequence.get_full_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}
+        elif href.startswith('http://') or href.startswith('https://'):
+            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)
\ No newline at end of file
--- a/src/pyams_content/component/paragraph/frame.py	Fri Jul 03 14:42:15 2020 +0200
+++ b/src/pyams_content/component/paragraph/frame.py	Fri Jul 03 18:43:46 2020 +0200
@@ -12,17 +12,22 @@
 
 __docformat__ = 'restructuredtext'
 
+from pyramid.events import subscriber
 from zope.interface import implementer
+from zope.lifecycleevent import IObjectAddedEvent, IObjectModifiedEvent
 from zope.schema.fieldproperty import FieldProperty
 
 from pyams_content.component.extfile.interfaces import IExtFileContainerTarget
 from pyams_content.component.illustration.interfaces import IIllustrationTarget
 from pyams_content.component.links.interfaces import ILinkContainerTarget
-from pyams_content.component.paragraph import BaseParagraph, BaseParagraphContentChecker, BaseParagraphFactory
+from pyams_content.component.paragraph import BaseParagraph, BaseParagraphContentChecker, \
+    BaseParagraphFactory
+from pyams_content.component.paragraph.association import check_associations
 from pyams_content.component.paragraph.interfaces import IParagraphFactory
-from pyams_content.component.paragraph.interfaces.frame import FRAME_PARAGRAPH_NAME, FRAME_PARAGRAPH_RENDERERS, \
-    FRAME_PARAGRAPH_TYPE, IFrameParagraph
-from pyams_content.features.checker.interfaces import IContentChecker, MISSING_LANG_VALUE, MISSING_VALUE
+from pyams_content.component.paragraph.interfaces.frame import FRAME_PARAGRAPH_NAME, \
+    FRAME_PARAGRAPH_RENDERERS, FRAME_PARAGRAPH_TYPE, IFrameParagraph
+from pyams_content.features.checker.interfaces import IContentChecker, MISSING_LANG_VALUE, \
+    MISSING_VALUE
 from pyams_content.features.renderer import RenderersVocabulary
 from pyams_i18n.interfaces import II18n, II18nManager, INegotiator
 from pyams_utils.adapter import adapter_config
@@ -88,3 +93,20 @@
     """Framed text paragraph renderers vocabulary"""
 
     content_interface = IFrameParagraph
+
+
+@subscriber(IObjectAddedEvent, context_selector=IFrameParagraph)
+def handle_added_frame_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=IFrameParagraph)
+def handle_modified_frame_paragraph(event):
+    """Check for new associations from modified paragraph"""
+    paragraph = event.object
+    for lang, body in (paragraph.body or {}).items():
+        check_associations(paragraph, body, lang, notify=False)
+
--- a/src/pyams_content/component/paragraph/html.py	Fri Jul 03 14:42:15 2020 +0200
+++ b/src/pyams_content/component/paragraph/html.py	Fri Jul 03 18:43:46 2020 +0200
@@ -10,24 +10,17 @@
 # FOR A PARTICULAR PURPOSE.
 #
 
-import re
-
-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.lifecycleevent.interfaces import IObjectAddedEvent, IObjectModifiedEvent
 from zope.schema.fieldproperty import FieldProperty
 
-from pyams_content.component.association.interfaces import IAssociationContainer
-from pyams_content.component.extfile.interfaces import IBaseExtFile, IExtFileContainerTarget
+from pyams_content.component.extfile.interfaces import IExtFileContainerTarget
 from pyams_content.component.illustration.interfaces import IIllustrationTarget
-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.interfaces import ILinkContainerTarget
 from pyams_content.component.paragraph import BaseParagraph, BaseParagraphContentChecker, \
     BaseParagraphFactory
+from pyams_content.component.paragraph.association import check_associations
 from pyams_content.component.paragraph.interfaces import IParagraphFactory
 from pyams_content.component.paragraph.interfaces.html import HTML_PARAGRAPH_NAME, \
     HTML_PARAGRAPH_RENDERERS, HTML_PARAGRAPH_TYPE, IHTMLParagraph, IRawParagraph, \
@@ -36,13 +29,10 @@
     MISSING_VALUE
 from pyams_content.features.renderer import RenderersVocabulary
 from pyams_i18n.interfaces import II18n, II18nManager, INegotiator
-from pyams_sequence.interfaces import ISequentialIntIds
 from pyams_utils.adapter import adapter_config
 from pyams_utils.factory import factory_config
 from pyams_utils.registry import get_utility, utility_config
-from pyams_utils.request import check_request
 from pyams_utils.traversing import get_parent
-from pyams_utils.url import absolute_url
 from pyams_utils.vocabulary import vocabulary_config
 
 
@@ -129,75 +119,6 @@
     content_type = HTMLParagraph
 
 
-FULL_EMAIL = re.compile('(.*) \<(.*)\>')
-
-
-def check_associations(context, body, lang, notify=True):
-    """Check for link associations from HTML content"""
-    registry = get_current_registry()
-    associations = IAssociationContainer(context)
-    html = PyQuery('<html>{0}</html>'.format(body))
-    for link in html('a[href]'):
-        link_info = None
-        has_link = False
-        href = link.attrib['href']
-        if href.startswith('oid://'):
-            sequence = get_utility(ISequentialIntIds)
-            oid = sequence.get_full_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}
-        elif href.startswith('http://') or href.startswith('https://'):
-            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"""
--- a/src/pyams_content/component/paragraph/zmi/frame.py	Fri Jul 03 14:42:15 2020 +0200
+++ b/src/pyams_content/component/paragraph/zmi/frame.py	Fri Jul 03 18:43:46 2020 +0200
@@ -10,8 +10,6 @@
 # FOR A PARTICULAR PURPOSE.
 #
 
-__docformat__ = 'restructuredtext'
-
 from z3c.form import button
 from z3c.form.interfaces import INPUT_MODE
 from zope.interface import Interface, implementer
@@ -20,15 +18,20 @@
 from pyams_content.component.association.zmi import AssociationsTable
 from pyams_content.component.association.zmi.interfaces import IAssociationsParentForm
 from pyams_content.component.paragraph.frame import FrameParagraph
-from pyams_content.component.paragraph.interfaces import IBaseParagraph, IParagraphContainerTarget, IParagraphTitle
-from pyams_content.component.paragraph.interfaces.frame import FRAME_PARAGRAPH_TYPE, IFrameParagraph
-from pyams_content.component.paragraph.zmi import BaseParagraphAJAXAddForm, BaseParagraphAJAXEditForm, \
-    BaseParagraphAddForm, BaseParagraphAddMenu, BaseParagraphPropertiesEditForm, IParagraphInnerEditFormButtons, \
+from pyams_content.component.paragraph.interfaces import IBaseParagraph, \
+    IParagraphContainerTarget, IParagraphTitle
+from pyams_content.component.paragraph.interfaces.frame import FRAME_PARAGRAPH_TYPE, \
+    IFrameParagraph
+from pyams_content.component.paragraph.zmi import BaseParagraphAJAXAddForm, \
+    BaseParagraphAJAXEditForm, BaseParagraphAddForm, BaseParagraphAddMenu, \
+    BaseParagraphPropertiesEditForm, IParagraphInnerEditFormButtons, \
     get_json_paragraph_refresh_event, get_json_paragraph_toolbar_refresh_event
 from pyams_content.component.paragraph.zmi.container import ParagraphContainerTable, \
     ParagraphTitleToolbarViewletManager
-from pyams_content.component.paragraph.zmi.interfaces import IParagraphContainerView, IParagraphInnerEditor
+from pyams_content.component.paragraph.zmi.interfaces import IParagraphContainerView, \
+    IParagraphInnerEditor
 from pyams_content.interfaces import MANAGE_CONTENT_PERMISSION
+from pyams_content.zmi import pyams_content
 from pyams_form.form import ajax_config
 from pyams_form.interfaces.form import IInnerForm
 from pyams_i18n.interfaces import II18n
@@ -38,12 +41,16 @@
 from pyams_skin.interfaces.viewlet import IToolbarAddingMenu
 from pyams_skin.layer import IPyAMSLayer
 from pyams_utils.adapter import ContextRequestAdapter, adapter_config
+from pyams_utils.fanstatic import get_resource_path
 from pyams_utils.html import html_to_text
 from pyams_utils.text import get_text_start
 from pyams_utils.traversing import get_parent
 from pyams_viewlet.viewlet import viewlet_config
 from pyams_zmi.interfaces import IPropertiesEditForm
 
+
+__docformat__ = 'restructuredtext'
+
 from pyams_content import _
 
 
@@ -60,9 +67,14 @@
     """Custom configuration for 'body' widget editor"""
 
     configuration = {
+        'ams-plugins': 'pyams_content',
+        'ams-plugin-pyams_content-src': get_resource_path(pyams_content),
+        'ams-plugin-pyams_content-async': 'false',
+        'ams-tinymce-init-callback': 'PyAMS_content.TinyMCE.initEditor',
         'ams-tinymce-menubar': False,
-        'ams-tinymce-plugins': ['paste', 'lists'],
-        'ams-tinymce-toolbar': 'undo redo | pastetext | bold italic superscript | bullist numlist',
+        'ams-tinymce-plugins': ['paste', 'lists', 'charmap', 'internal_links', 'link'],
+        'ams-tinymce-toolbar': 'undo redo | pastetext | bold italic superscript | '
+                               'bullist numlist | charmap internal_links link',
         'ams-tinymce-toolbar1': False,
         'ams-tinymce-toolbar2': False,
         'ams-tinymce-height': 150
@@ -149,10 +161,12 @@
             parent = get_parent(self.context, IAssociationContainerTarget)
             output.setdefault('events', []).append(
                 get_json_paragraph_toolbar_refresh_event(parent, self.request,
-                                                         ParagraphContainerTable, ParagraphTitleToolbarViewletManager))
+                                                         ParagraphContainerTable,
+                                                         ParagraphTitleToolbarViewletManager))
             # refresh associations table
             output.setdefault('events', []).append(
-                get_json_switched_table_refresh_event(self.context, self.request, AssociationsTable))
+                get_json_switched_table_refresh_event(self.context, self.request,
+                                                      AssociationsTable))
         return output