Integration of external videos as paragraphs
authorThierry Florac <thierry.florac@onf.fr>
Wed, 21 Feb 2018 17:18:04 +0100
changeset 404 0ba2bb1a692e
parent 403 992892d242a7
child 405 9c147733c02e
Integration of external videos as paragraphs
src/pyams_content/component/video/__init__.py
src/pyams_content/component/video/interfaces/__init__.py
src/pyams_content/component/video/paragraph.py
src/pyams_content/component/video/provider/__init__.py
src/pyams_content/component/video/provider/dailymotion.py
src/pyams_content/component/video/provider/interfaces.py
src/pyams_content/component/video/provider/vimeo.py
src/pyams_content/component/video/provider/youtube.py
src/pyams_content/component/video/provider/zmi/__init__.py
src/pyams_content/component/video/provider/zmi/templates/dailymotion-preview.pt
src/pyams_content/component/video/provider/zmi/templates/vimeo-preview.pt
src/pyams_content/component/video/provider/zmi/templates/youtube-preview.pt
src/pyams_content/component/video/zmi/__init__.py
src/pyams_content/component/video/zmi/paragraph.py
src/pyams_content/component/video/zmi/templates/video-settings.pt
src/pyams_content/component/video/zmi/templates/video-summary.pt
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/video/__init__.py	Wed Feb 21 17:18:04 2018 +0100
@@ -0,0 +1,122 @@
+#
+# Copyright (c) 2008-2015 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.
+#
+from pyams_i18n.interfaces import II18nManager, INegotiator, II18n
+from pyams_utils.traversing import get_parent
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+from persistent import Persistent
+
+# import interfaces
+from pyams_content.component.video.interfaces import IExternalVideo, IExternalVideoProvider, IExternalVideoSettings
+from pyams_content.features.checker.interfaces import IContentChecker, MISSING_VALUE, MISSING_LANG_VALUE
+from zope.annotation import IAnnotations
+
+# import packages
+from pyams_content.features.checker import BaseContentChecker, VALUE_OK
+from pyams_utils.adapter import adapter_config
+from pyams_utils.registry import query_utility, get_utility
+from zope.container.contained import Contained
+from zope.interface import implementer
+from zope.location import locate
+from zope.schema.fieldproperty import FieldProperty
+
+from pyams_content import _
+
+
+@implementer(IExternalVideo)
+class ExternalVideo(Persistent, Contained):
+    """External video persistent class"""
+
+    description = FieldProperty(IExternalVideo['description'])
+    author = FieldProperty(IExternalVideo['author'])
+    provider_name = FieldProperty(IExternalVideo['provider_name'])
+
+    def get_provider(self):
+        return query_utility(IExternalVideoProvider, name=self.provider_name)
+
+    @property
+    def settings(self):
+        provider = self.get_provider()
+        if provider is None:
+            return None
+        return provider.settings_interface(self)
+
+
+EXTERNAL_VIDEO_SETTINGS_KEY = 'pyams_content.video::{0}'
+
+
+@adapter_config(context=IExternalVideo, provides=IExternalVideoSettings)
+def ExternalVideoSettingsFactory(context):
+    """External video settings factory"""
+    if not context.provider_name:
+        return None
+    annotations = IAnnotations(context)
+    settings_key = EXTERNAL_VIDEO_SETTINGS_KEY.format(context.provider_name.lower())
+    settings = annotations.get(settings_key)
+    if settings is None:
+        provider = context.get_provider()
+        if provider is not None:
+            settings = annotations[settings_key] = IExternalVideoSettings(provider)
+            locate(settings, context)
+    return settings
+
+
+@adapter_config(context=IExternalVideo, provides=IContentChecker)
+class ExternalVideoContentChecker(BaseContentChecker):
+    """External video content checker"""
+
+    label = _("External video")
+    weight = 50
+
+    def inner_check(self, request):
+        output = []
+        translate = request.localizer.translate
+        manager = get_parent(self.context, II18nManager)
+        if manager is not None:
+            langs = manager.get_languages()
+        else:
+            negotiator = get_utility(INegotiator)
+            langs = (negotiator.server_language, )
+        missing_value = translate(MISSING_VALUE)
+        missing_lang_value = translate(MISSING_LANG_VALUE)
+        i18n = II18n(self.context)
+        for attr in ('description', ):
+            for lang in langs:
+                value = i18n.get_attribute(attr, lang, request)
+                if not value:
+                    if len(langs) == 1:
+                        output.append(missing_value.format(field=translate(IExternalVideo[attr].title)))
+                    else:
+                        output.append(missing_lang_value.format(field=translate(IExternalVideo[attr].title),
+                                                                lang=lang))
+        for attr in ('author', 'provider_name'):
+            value = getattr(self.context, attr)
+            if not value:
+                output.append(missing_value.format(field=translate(IExternalVideo[attr].title)))
+        settings = self.context.settings
+        if settings is None:
+            pass
+        else:
+            checker = IContentChecker(settings, None)
+            if checker is not None:
+                checker_output = checker.inner_check(request)
+                if checker_output:
+                    output.append('<div class="padding-left-20">')
+                    output.append('- {0} :'.format(translate(checker.label)))
+                    output.append([checker.sep.join(checker_output)])
+                    output.append('</div>')
+                else:
+                    output.append('- {0} : {1}'.format(translate(checker.label), translate(VALUE_OK)))
+        return output
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/video/interfaces/__init__.py	Wed Feb 21 17:18:04 2018 +0100
@@ -0,0 +1,71 @@
+#
+# Copyright (c) 2008-2015 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.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.component.paragraph.interfaces import IBaseParagraph
+from zope.annotation import IAttributeAnnotatable
+from zope.contentprovider.interfaces import IContentProvider
+
+# import packages
+from pyams_i18n.schema import I18nHTMLField, I18nTextField
+from zope.interface import Interface, Attribute
+from zope.schema import Choice, TextLine
+
+from pyams_content import _
+
+
+class IExternalVideoSettings(Interface):
+    """External video settings"""
+
+
+class IExternalVideoProvider(Interface):
+    """External video provider"""
+
+    settings_interface = Attribute("Video provider settings interface")
+
+
+class IExternalVideo(IAttributeAnnotatable):
+    """Base interface for external video integration"""
+
+    description = I18nTextField(title=_("Description"),
+                                description=_("File description displayed by front-office template"),
+                                required=False)
+
+    author = TextLine(title=_("Author"),
+                      description=_("Name of document's author"),
+                      required=False)
+
+    provider_name = Choice(title=_("Video provider"),
+                           description=_("Name of external platform providing selected video"),
+                           required=False,
+                           vocabulary="PyAMS video providers")
+
+    def get_provider(self):
+        """Get external video provider utility"""
+
+    settings = Attribute("Video settings")
+
+
+class IExternalVideoParagraph(IExternalVideo, IBaseParagraph):
+    """External video paragraph"""
+
+    body = I18nHTMLField(title=_("Body"),
+                         required=False)
+
+
+class IExternalVideoRenderer(IContentProvider):
+    """External video renderer"""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/video/paragraph.py	Wed Feb 21 17:18:04 2018 +0100
@@ -0,0 +1,87 @@
+#
+# Copyright (c) 2008-2015 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.
+#
+from pyams_utils.traversing import get_parent
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.component.paragraph.interfaces import IParagraphFactory
+from pyams_content.component.video.interfaces import IExternalVideoParagraph
+from pyams_content.features.checker.interfaces import IContentChecker, MISSING_VALUE, MISSING_LANG_VALUE
+from pyams_i18n.interfaces import II18n, II18nManager, INegotiator
+
+# import packages
+from pyams_content.component.paragraph import BaseParagraph, BaseParagraphFactory
+from pyams_content.component.video import ExternalVideo, ExternalVideoContentChecker
+from pyams_utils.adapter import adapter_config
+from pyams_utils.registry import utility_config, get_utility
+from pyams_utils.request import check_request
+from zope.interface import implementer
+from zope.schema.fieldproperty import FieldProperty
+
+from pyams_content import _
+
+
+@implementer(IExternalVideoParagraph)
+class ExternalVideoParagraph(ExternalVideo, BaseParagraph):
+    """External video paragraph"""
+
+    icon_class = 'fa-youtube-play'
+    icon_hint = _("External video")
+
+    body = FieldProperty(IExternalVideoParagraph['body'])
+
+
+@utility_config(name='External video', provides=IParagraphFactory)
+class ExternalVideoParagraphFactory(BaseParagraphFactory):
+    """External video paragraph factory"""
+
+    name = _("External video")
+    content_type = ExternalVideoParagraph
+
+
+@adapter_config(context=IExternalVideoParagraph, provides=IContentChecker)
+class ExternalVideoParagraphContentChecker(ExternalVideoContentChecker):
+    """External video paragraph content checker"""
+
+    @property
+    def label(self):
+        request = check_request()
+        translate = request.localizer.translate
+        return II18n(self.context).query_attribute('title', request) or \
+            '({0})'.format(translate(self.context.icon_hint).lower())
+
+    def inner_check(self, request):
+        output = super(ExternalVideoParagraphContentChecker, self).inner_check(request)
+        translate = request.localizer.translate
+        manager = get_parent(self.context, II18nManager)
+        if manager is not None:
+            langs = manager.get_languages()
+        else:
+            negotiator = get_utility(INegotiator)
+            langs = (negotiator.server_language, )
+        missing_value = translate(MISSING_VALUE)
+        missing_lang_value = translate(MISSING_LANG_VALUE)
+        i18n = II18n(self.context)
+        for attr in ('title', ):
+            for lang in langs:
+                value = i18n.get_attribute(attr, lang, request)
+                if not value:
+                    if len(langs) == 1:
+                        output.insert(0, missing_value.format(field=translate(IExternalVideoParagraph[attr].title)))
+                    else:
+                        output.insert(0, missing_lang_value.format(field=translate(IExternalVideoParagraph[attr].title),
+                                                                   lang=lang))
+        return output
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/video/provider/__init__.py	Wed Feb 21 17:18:04 2018 +0100
@@ -0,0 +1,31 @@
+#
+# Copyright (c) 2008-2015 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.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.component.video.interfaces import IExternalVideoProvider
+
+# import packages
+from pyams_utils.vocabulary import vocabulary_config
+from zope.componentvocabulary.vocabulary import UtilityVocabulary
+
+
+@vocabulary_config(name='PyAMS video providers')
+class VideoProvidersVocabulary(UtilityVocabulary):
+    """Video providers vocabulary"""
+
+    interface = IExternalVideoProvider
+    nameOnly = True
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/video/provider/dailymotion.py	Wed Feb 21 17:18:04 2018 +0100
@@ -0,0 +1,97 @@
+#
+# Copyright (c) 2008-2015 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.
+#
+
+__docformat__ = 'restructuredtext'
+
+# import standard library
+import re
+
+from persistent import Persistent
+
+# import interfaces
+from pyams_content.component.video.interfaces import IExternalVideo, IExternalVideoProvider, IExternalVideoSettings
+from pyams_content.component.video.provider.interfaces import IDailymotionVideoSettings
+from pyams_content.features.checker.interfaces import IContentChecker
+
+# import packages
+from pyams_content.component.video import ExternalVideoSettingsFactory
+from pyams_content.features.checker import BaseContentChecker
+from pyams_utils.adapter import adapter_config
+from pyams_utils.registry import utility_config
+from zope.interface import implementer
+from zope.schema.fieldproperty import FieldProperty
+
+from pyams_content import _
+
+
+DAILYMOTION_BASE_URL = re.compile('http://dai.ly/(.*)')
+
+
+@implementer(IDailymotionVideoSettings)
+class DailymotionVideoSettings(Persistent):
+    """Dailymotion video settings"""
+
+    _video_id = FieldProperty(IDailymotionVideoSettings['video_id'])
+    width = FieldProperty(IDailymotionVideoSettings['width'])
+    height = FieldProperty(IDailymotionVideoSettings['height'])
+    start_at = FieldProperty(IDailymotionVideoSettings['start_at'])
+    autoplay = FieldProperty(IDailymotionVideoSettings['autoplay'])
+    show_info = FieldProperty(IDailymotionVideoSettings['show_info'])
+    show_commands = FieldProperty(IDailymotionVideoSettings['show_commands'])
+    ui_theme = FieldProperty(IDailymotionVideoSettings['ui_theme'])
+    show_branding = FieldProperty(IDailymotionVideoSettings['show_branding'])
+    show_endscreen = FieldProperty(IDailymotionVideoSettings['show_endscreen'])
+    allow_fullscreen = FieldProperty(IDailymotionVideoSettings['allow_fullscreen'])
+    allow_sharing = FieldProperty(IDailymotionVideoSettings['allow_sharing'])
+
+    @property
+    def video_id(self):
+        return self._video_id
+
+    @video_id.setter
+    def video_id(self, value):
+        if value:
+            match = DAILYMOTION_BASE_URL.match(value)
+            if match:
+                value = match.groups()[0]
+        self._video_id = value
+
+
+@utility_config(name='Dailymotion', provides=IExternalVideoProvider)
+class DailymotionVideoProvider(object):
+    """Dailymotion video provider"""
+
+    settings_interface = IDailymotionVideoSettings
+
+
+@adapter_config(context=IExternalVideo, provides=IDailymotionVideoSettings)
+def DailymotionVideoSettingsFactory(context):
+    """Dailymotion video settings factory"""
+    if context.provider_name != 'Dailymotion':
+        return None
+    return ExternalVideoSettingsFactory(context)
+
+
+@adapter_config(context=DailymotionVideoProvider, provides=IExternalVideoSettings)
+def DailymotionVideoProviderSettingsFactory(context):
+    """Dailymotion video provider settings factory"""
+    return DailymotionVideoSettings()
+
+
+@adapter_config(context=IDailymotionVideoSettings, provides=IContentChecker)
+class DailymotionVideoSettingsContentChecker(BaseContentChecker):
+    """Dailymotion video settings content checker"""
+
+    label = _("Dailymotion settings")
+
+    def inner_check(self, request):
+        return []
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/video/provider/interfaces.py	Wed Feb 21 17:18:04 2018 +0100
@@ -0,0 +1,218 @@
+#
+# Copyright (c) 2008-2015 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.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.component.video import IExternalVideoSettings
+
+# import packages
+from pyams_utils.schema import ColorField
+from zope.schema import TextLine, Bool, Int, Choice
+
+from pyams_content import _
+
+
+class IYoutubeVideoSettings(IExternalVideoSettings):
+    """Youtube video provider settings"""
+
+    video_id = TextLine(title=_("Video ID"),
+                        description=_("To get video ID, just use the 'Share' button in Youtube platform and "
+                                      "copy/paste the given URL here"),
+                        required=True)
+
+    width = Int(title=_("Video width"),
+                description=_("Initial video frame width; mandatory for old browsers but may be overridden by "
+                              "presentation skin"),
+                required=True,
+                min=200,
+                default=720)
+
+    height = Int(title=_("Video height"),
+                 description=_("Initial video frame height; mandatory for old browsers but may be overridden by "
+                               "presentation skin"),
+                 required=True,
+                 min=200,
+                 default=405)
+
+    start_at = TextLine(title=_("Start at"),
+                        description=_("Position at which to start video, in 'seconds' or 'minutes:seconds' format"),
+                        required=False,
+                        default='0:00')
+
+    stop_at = TextLine(title=_("Stop at"),
+                       description=_("Position at which to stop video, in 'seconds' or 'minutes:seconds' format"),
+                       required=False)
+
+    autoplay = Bool(title=_("Auto play?"),
+                    description=_("If 'yes', video is started automatically on page load"),
+                    required=True,
+                    default=False)
+
+    loop = Bool(title=_("Loop playback?"),
+                description=_("If 'yes', video is played indefinitely"),
+                required=True,
+                default=False)
+
+    show_info = Bool(title=_("Show video info?"),
+                     description=_("If 'no', video title and information won't be displayed"),
+                     required=True,
+                     default=True)
+
+    show_commands = Bool(title=_("Show commands?"),
+                         description=_("Show video player commands"),
+                         required=True,
+                         default=True)
+
+    hide_branding = Bool(title=_("Hide branding?"),
+                         description=_("If 'yes', Youtube branding won't be displayed"),
+                         required=True,
+                         default=False)
+
+    show_related = Bool(title=_("Show related videos?"),
+                        description=_("Show related videos when video end"),
+                        required=True,
+                        default=True)
+
+    allow_fullscreen = Bool(title=_("Allow full screen?"),
+                            description=_("If 'yes', video can be displayed in full screen"),
+                            required=True,
+                            default=True)
+
+    disable_keyboard = Bool(title=_("Disable keyboard?"),
+                            description=_("If 'yes', video player can't be controlled via keyboard shortcuts"),
+                            required=True,
+                            default=False)
+
+
+class IDailymotionVideoSettings(IExternalVideoSettings):
+    """Dailymotion video provider settings"""
+
+    video_id = TextLine(title=_("Video ID"),
+                        description=_("To get video ID, just use the 'Share' button in Dailymotion platform, "
+                                      "click on \"Copy link\" and paste the given URL here"),
+                        required=True)
+
+    width = Int(title=_("Video width"),
+                description=_("Initial video frame width; mandatory for old browsers but may be overridden by "
+                              "presentation skin"),
+                required=True,
+                min=200,
+                default=720)
+
+    height = Int(title=_("Video height"),
+                 description=_("Initial video frame height; mandatory for old browsers but may be overridden by "
+                               "presentation skin"),
+                 required=True,
+                 min=200,
+                 default=405)
+
+    start_at = TextLine(title=_("Start at"),
+                        description=_("Position at which to start video, in 'seconds' or 'minutes:seconds' format"),
+                        required=False,
+                        default='0:00')
+
+    autoplay = Bool(title=_("Auto play?"),
+                    description=_("If 'yes', video is started automatically on page load"),
+                    required=True,
+                    default=False)
+
+    show_info = Bool(title=_("Show video info?"),
+                     description=_("If 'no', video title and information won't be displayed"),
+                     required=True,
+                     default=True)
+
+    show_commands = Bool(title=_("Show commands?"),
+                         description=_("Show video player commands"),
+                         required=True,
+                         default=True)
+
+    ui_theme = Choice(title=_("UI theme"),
+                      description=_("Default base color theme"),
+                      values=('dark', 'light'),
+                      default='dark')
+
+    show_branding = Bool(title=_("Show branding?"),
+                         description=_("If 'no', Dailymotion branding won't be displayed"),
+                         required=True,
+                         default=True)
+
+    show_endscreen = Bool(title=_("Show end screen?"),
+                          description=_("Show end screen when video end"),
+                          required=True,
+                          default=True)
+
+    allow_fullscreen = Bool(title=_("Allow full screen?"),
+                            description=_("If 'yes', video can be displayed in full screen"),
+                            required=True,
+                            default=True)
+
+    allow_sharing = Bool(title=_("Allow sharing?"),
+                         description=_("If 'no', video sharing will be disabled"),
+                         required=True,
+                         default=True)
+
+
+class IVimeoVideoSettings(IExternalVideoSettings):
+    """Vimeo video provider settings"""
+
+    video_id = TextLine(title=_("Video ID"),
+                        description=_("To get video ID, just use the 'Share' button in Vimeo platform, "
+                                      "click on \"Link\" entry and copy/paste the given URL here"),
+                        required=True)
+
+    width = Int(title=_("Video width"),
+                description=_("Initial video frame width; mandatory for old browsers but may be overridden by "
+                              "presentation skin"),
+                required=True,
+                min=200,
+                default=720)
+
+    height = Int(title=_("Video height"),
+                 description=_("Initial video frame height; mandatory for old browsers but may be overridden by "
+                               "presentation skin"),
+                 required=True,
+                 min=200,
+                 default=405)
+
+    show_title = Bool(title=_("Show title?"),
+                      description=_("If 'no', video title won't be displayed"),
+                      required=True,
+                      default=True)
+
+    show_signature = Bool(title=_("Show signature?"),
+                          description=_("If 'no', video signature won't be displayed"),
+                          required=True,
+                          default=True)
+
+    color = ColorField(title=_("Infos color"),
+                       description=_("Color used for title and signature"),
+                       required=True,
+                       default='ffffff')
+
+    autoplay = Bool(title=_("Auto play?"),
+                    description=_("If 'yes', video is started automatically on page load"),
+                    required=True,
+                    default=False)
+
+    loop = Bool(title=_("Loop playback?"),
+                description=_("If 'yes', video is played indefinitely"),
+                required=True,
+                default=False)
+
+    allow_fullscreen = Bool(title=_("Allow full screen?"),
+                            description=_("If 'yes', video can be displayed in full screen"),
+                            required=True,
+                            default=True)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/video/provider/vimeo.py	Wed Feb 21 17:18:04 2018 +0100
@@ -0,0 +1,95 @@
+#
+# Copyright (c) 2008-2015 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.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+import re
+
+from persistent import Persistent
+
+# import interfaces
+from pyams_content.component.video.interfaces import IExternalVideo, IExternalVideoProvider, IExternalVideoSettings
+from pyams_content.component.video.provider.interfaces import IVimeoVideoSettings
+from pyams_content.features.checker.interfaces import IContentChecker
+
+# import packages
+from pyams_content.component.video import ExternalVideoSettingsFactory
+from pyams_content.features.checker import BaseContentChecker
+from pyams_utils.adapter import adapter_config
+from pyams_utils.registry import utility_config
+from zope.interface import implementer
+from zope.schema.fieldproperty import FieldProperty
+
+from pyams_content import _
+
+
+VIMEO_BASE_URL = re.compile('https://vimeo.com/([0-9]+)')
+
+
+@implementer(IVimeoVideoSettings)
+class VimeoVideoSettings(Persistent):
+    """Vimeo video settings"""
+
+    _video_id = FieldProperty(IVimeoVideoSettings['video_id'])
+    width = FieldProperty(IVimeoVideoSettings['width'])
+    height = FieldProperty(IVimeoVideoSettings['height'])
+    show_title = FieldProperty(IVimeoVideoSettings['show_title'])
+    show_signature = FieldProperty(IVimeoVideoSettings['show_signature'])
+    color = FieldProperty(IVimeoVideoSettings['color'])
+    autoplay = FieldProperty(IVimeoVideoSettings['autoplay'])
+    loop = FieldProperty(IVimeoVideoSettings['loop'])
+    allow_fullscreen = FieldProperty(IVimeoVideoSettings['allow_fullscreen'])
+
+    @property
+    def video_id(self):
+        return self._video_id
+
+    @video_id.setter
+    def video_id(self, value):
+        if value:
+            match = VIMEO_BASE_URL.match(value)
+            if match:
+                value = match.groups()[0]
+        self._video_id = value
+
+
+@utility_config(name='Vimeo', provides=IExternalVideoProvider)
+class VimeoVideoProvider(object):
+    """Vimeo video provider"""
+
+    settings_interface = IVimeoVideoSettings
+
+
+@adapter_config(context=IExternalVideo, provides=IVimeoVideoSettings)
+def VimeoVideoSettingsFactory(context):
+    """Vimeo video settings factory"""
+    if context.provider_name != 'Vimeo':
+        return None
+    return ExternalVideoSettingsFactory(context)
+
+
+@adapter_config(context=VimeoVideoProvider, provides=IExternalVideoSettings)
+def VimeoVideoProviderSettingsFactory(context):
+    """Vimeo video provider settings factory"""
+    return VimeoVideoSettings()
+
+
+@adapter_config(context=IVimeoVideoSettings, provides=IContentChecker)
+class VimeoVideoSettingsContentChecker(BaseContentChecker):
+    """Vimeo video settings content checker"""
+
+    label = _("Vimeo settings")
+
+    def inner_check(self, request):
+        return []
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/video/provider/youtube.py	Wed Feb 21 17:18:04 2018 +0100
@@ -0,0 +1,99 @@
+#
+# Copyright (c) 2008-2015 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.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+import re
+
+from persistent import Persistent
+
+# import interfaces
+from pyams_content.component.video.interfaces import IExternalVideo, IExternalVideoProvider, IExternalVideoSettings
+from pyams_content.component.video.provider.interfaces import IYoutubeVideoSettings
+from pyams_content.features.checker.interfaces import IContentChecker
+
+# import packages
+from pyams_content.component.video import ExternalVideoSettingsFactory
+from pyams_content.features.checker import BaseContentChecker
+from pyams_utils.adapter import adapter_config
+from pyams_utils.registry import utility_config
+from zope.interface import implementer
+from zope.schema.fieldproperty import FieldProperty
+
+from pyams_content import _
+
+
+YOUTUBE_BASE_URL = re.compile('https://youtu.be/(.*)')
+
+
+@implementer(IYoutubeVideoSettings)
+class YoutubeVideoSettings(Persistent):
+    """Youtube video settings"""
+
+    _video_id = FieldProperty(IYoutubeVideoSettings['video_id'])
+    width = FieldProperty(IYoutubeVideoSettings['width'])
+    height = FieldProperty(IYoutubeVideoSettings['height'])
+    start_at = FieldProperty(IYoutubeVideoSettings['start_at'])
+    stop_at = FieldProperty(IYoutubeVideoSettings['stop_at'])
+    autoplay = FieldProperty(IYoutubeVideoSettings['autoplay'])
+    loop = FieldProperty(IYoutubeVideoSettings['loop'])
+    show_info = FieldProperty(IYoutubeVideoSettings['show_info'])
+    show_commands = FieldProperty(IYoutubeVideoSettings['show_commands'])
+    hide_branding = FieldProperty(IYoutubeVideoSettings['hide_branding'])
+    show_related = FieldProperty(IYoutubeVideoSettings['show_related'])
+    allow_fullscreen = FieldProperty(IYoutubeVideoSettings['allow_fullscreen'])
+    disable_keyboard = FieldProperty(IYoutubeVideoSettings['disable_keyboard'])
+
+    @property
+    def video_id(self):
+        return self._video_id
+
+    @video_id.setter
+    def video_id(self, value):
+        if value:
+            match = YOUTUBE_BASE_URL.match(value)
+            if match:
+                value = match.groups()[0]
+        self._video_id = value
+
+
+@utility_config(name='Youtube', provides=IExternalVideoProvider)
+class YoutubeVideoProvider(object):
+    """Youtube video provider"""
+
+    settings_interface = IYoutubeVideoSettings
+
+
+@adapter_config(context=IExternalVideo, provides=IYoutubeVideoSettings)
+def YoutubeVideoSettingsFactory(context):
+    """Youtube video settings factory"""
+    if context.provider_name != 'Youtube':
+        return None
+    return ExternalVideoSettingsFactory(context)
+
+
+@adapter_config(context=YoutubeVideoProvider, provides=IExternalVideoSettings)
+def YoutubeVideoProviderSettingsFactory(context):
+    """Youtubr video provider settings factory"""
+    return YoutubeVideoSettings()
+
+
+@adapter_config(context=IYoutubeVideoSettings, provides=IContentChecker)
+class YoutubeVideoSettingsContentChecker(BaseContentChecker):
+    """Youtube video settings content checker"""
+
+    label = _("Youtube settings")
+
+    def inner_check(self, request):
+        return []
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/video/provider/zmi/__init__.py	Wed Feb 21 17:18:04 2018 +0100
@@ -0,0 +1,110 @@
+#
+# Copyright (c) 2008-2015 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.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.component.video.interfaces import IExternalVideoRenderer
+from pyams_skin.layer import IPyAMSLayer
+
+# import packages
+from pyams_content.component.video.provider.dailymotion import DailymotionVideoSettings
+from pyams_content.component.video.provider.vimeo import VimeoVideoSettings
+from pyams_content.component.video.provider.youtube import YoutubeVideoSettings
+from pyams_template.template import template_config
+from pyams_utils.adapter import adapter_config
+from pyams_viewlet.viewlet import BaseContentProvider
+from pyramid.encode import urlencode
+
+
+def time_to_seconds(value):
+    """Convert min:sec value to seconds"""
+    if value and (':' in value):
+        min, sec = value.split(':', 1)
+        return str(int(min)*60 + int(sec))
+    else:
+        return value or ''
+
+
+YOUTUBE_PARAMS = (
+    ('start_at', 'start', time_to_seconds),
+    ('stop_at', 'end', time_to_seconds),
+    ('autoplay', 'autoplay', int),
+    ('loop', 'loop', int),
+    ('show_info', 'showinfo', int),
+    ('show_commands', 'controls', int),
+    ('hide_branding', 'modestbranding', int),
+    ('show_related', 'rel', int),
+    ('allow_fullscreen', 'fs', int),
+    ('disable_keyboard', 'disablekb', int)
+)
+
+
+DAILYMOTION_PARAMS = (
+    ('start_at', 'start', time_to_seconds),
+    ('autoplay', 'autoplay', int),
+    ('show_info', 'ui-start-screen-info', int),
+    ('show_commands', 'controls', int),
+    ('ui_theme', 'ui-theme', str),
+    ('show_branding', 'ui-logo', int),
+    ('show_endscreen', 'endscreen-enable', int),
+    ('allow_sharing', 'sharing-enable', int)
+)
+
+
+VIMEO_PARAMS = (
+    ('show_title', 'title', int),
+    ('show_signature', 'byline', int),
+    ('color', 'color', str),
+    ('autoplay', 'autoplay', int),
+    ('loop', 'loop', int)
+)
+
+
+class BaseExternalVideoRenderer(BaseContentProvider):
+    """Base external video renderer"""
+
+    params = ()
+
+    def get_url_params(self):
+        settings = self.context
+        params = {}
+        for attr, param, handler in self.params:
+            params[param] = handler(getattr(settings, attr))
+        return urlencode(params)
+
+
+@adapter_config(context=(YoutubeVideoSettings, IPyAMSLayer), provides=IExternalVideoRenderer)
+@template_config(template='templates/youtube-preview.pt', layer=IPyAMSLayer)
+class YoutubeVideoRenderer(BaseExternalVideoRenderer):
+    """Youtube video renderer"""
+
+    params = YOUTUBE_PARAMS
+
+
+@adapter_config(context=(DailymotionVideoSettings, IPyAMSLayer), provides=IExternalVideoRenderer)
+@template_config(template='templates/dailymotion-preview.pt', layer=IPyAMSLayer)
+class DailymotionVideoRenderer(BaseExternalVideoRenderer):
+    """Dailymotion video renderer"""
+
+    params = DAILYMOTION_PARAMS
+
+
+@adapter_config(context=(VimeoVideoSettings, IPyAMSLayer), provides=IExternalVideoRenderer)
+@template_config(template='templates/vimeo-preview.pt', layer=IPyAMSLayer)
+class VimeoVideoRenderer(BaseExternalVideoRenderer):
+    """Vimeo video renderer"""
+
+    params = VIMEO_PARAMS
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/video/provider/zmi/templates/dailymotion-preview.pt	Wed Feb 21 17:18:04 2018 +0100
@@ -0,0 +1,8 @@
+<div class="video-wrapper">
+	<iframe type="text/html" class="video" frameborder="0"
+			tal:attributes="src string:https://www.dailymotion.com/embed/video/${context.video_id}?${view.get_url_params()};
+							width context.width;
+							height context.height;
+							allowfullscreen 'true' if context.allow_fullscreen else None;
+							allow 'autoplay' if context.autoplay else None;"></iframe>
+</div>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/video/provider/zmi/templates/vimeo-preview.pt	Wed Feb 21 17:18:04 2018 +0100
@@ -0,0 +1,9 @@
+<div class="video-wrapper">
+	<iframe type="text/html" class="video" frameborder="0"
+			tal:attributes="src string:https://player.vimeo.com/video/${context.video_id}?${view.get_url_params()};
+							width context.width;
+							height context.height;
+							allowfullscreen 'true' if context.allow_fullscreen else None;
+							webkitallowfullscreen 'true' if context.allow_fullscreen else None;
+							mozallowfullscreen 'true' if context.allow_fullscreen else None;"></iframe>
+</div>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/video/provider/zmi/templates/youtube-preview.pt	Wed Feb 21 17:18:04 2018 +0100
@@ -0,0 +1,7 @@
+<div class="video-wrapper">
+	<iframe type="text/html" class="video" frameborder="0"
+			tal:attributes="src string:https://www.youtube.com/embed/${context.video_id}?${view.get_url_params()};
+							width context.width;
+							height context.height;
+							allowfullscreen 'allowfullscreen' if context.allow_fullscreen else None;"></iframe>
+</div>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/video/zmi/__init__.py	Wed Feb 21 17:18:04 2018 +0100
@@ -0,0 +1,21 @@
+#
+# Copyright (c) 2008-2015 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.
+#
+
+__docformat__ = 'restructuredtext'
+
+# import standard library
+
+# import interfaces
+
+# import packages
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/video/zmi/paragraph.py	Wed Feb 21 17:18:04 2018 +0100
@@ -0,0 +1,292 @@
+#
+# Copyright (c) 2008-2015 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.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.component.paragraph.interfaces import IParagraphContainerTarget, IParagraphContainer, IBaseParagraph, \
+    IParagraphSummary
+from pyams_content.component.paragraph.zmi.interfaces import IParagraphContainerView, IParagraphInnerEditor
+from pyams_content.component.video.interfaces import IExternalVideoProvider, IExternalVideoSettings, \
+    IExternalVideoParagraph, IExternalVideoRenderer
+from pyams_content.interfaces import MANAGE_CONTENT_PERMISSION
+from pyams_form.interfaces.form import IWidgetsSuffixViewletsManager, IInnerForm, IEditFormButtons
+from pyams_i18n.interfaces import II18n
+from pyams_skin.interfaces.viewlet import IToolbarAddingMenu
+from pyams_skin.layer import IPyAMSLayer
+from pyams_utils.interfaces.data import IObjectData
+from z3c.form.interfaces import DISPLAY_MODE, INPUT_MODE, IDataExtractedEvent
+
+# import packages
+from pyams_content.component.paragraph.zmi import BaseParagraphAddMenu, BaseParagraphAJAXAddForm, \
+    BaseParagraphPropertiesEditForm, BaseParagraphAJAXEditForm
+from pyams_content.component.video.paragraph import ExternalVideoParagraph
+from pyams_form.group import NamedWidgetsGroup
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_template.template import template_config
+from pyams_utils.adapter import adapter_config
+from pyams_utils.registry import get_utility, get_current_registry
+from pyams_utils.url import absolute_url
+from pyams_viewlet.viewlet import viewlet_config, Viewlet, BaseContentProvider
+from pyams_zmi.form import AdminDialogAddForm, InnerAdminAddForm, InnerAdminEditForm
+from pyramid.events import subscriber
+from pyramid.exceptions import NotFound
+from pyramid.response import Response
+from pyramid.view import view_config
+from z3c.form import field, button
+from zope.interface import implementer, alsoProvides, Interface, Invalid
+from zope.schema import getFieldNamesInOrder
+
+from pyams_content import _
+
+
+@viewlet_config(name='add-external-video.menu', context=IParagraphContainerTarget, view=IParagraphContainerView,
+                layer=IPyAMSLayer, manager=IToolbarAddingMenu, weight=75)
+class ExternalVideoParagraphAddMenu(BaseParagraphAddMenu):
+    """External video paragraph add menu"""
+
+    label = _("External video...")
+    label_css_class = 'fa fa-fw fa-youtube-play'
+    url = 'add-external-video.html'
+    paragraph_type = 'External video'
+
+
+@pagelet_config(name='add-external-video.html', context=IParagraphContainerTarget, layer=IPyAMSLayer,
+                permission=MANAGE_CONTENT_PERMISSION)
+class ExternalVideoParagraphAddForm(AdminDialogAddForm):
+    """External video paragraph add form"""
+
+    legend = _("Add new external video...")
+    dialog_class = 'modal-large'
+    icon_css_class = 'fa fa-fw fa-youtube-play'
+
+    fields = field.Fields(IExternalVideoParagraph).omit('__parent__', '__name__', 'visible')
+    ajax_handler = 'add-external-video.json'
+    edit_permission = MANAGE_CONTENT_PERMISSION
+
+    def updateWidgets(self, prefix=None):
+        super(ExternalVideoParagraphAddForm, self).updateWidgets(prefix)
+        if 'description' in self.widgets:
+            self.widgets['description'].widget_css_class = 'textarea'
+        if 'body' in self.widgets:
+            self.widgets['body'].label = ''
+            self.add_group(NamedWidgetsGroup(self, 'body_group', self.widgets, ('body',),
+                                             bordered=False,
+                                             legend=_("HTML content"),
+                                             css_class='inner switcher padding-right-10 no-y-padding pull-left',
+                                             switch=True,
+                                             hide_if_empty=True))
+            self.add_group(NamedWidgetsGroup(self, 'data_group', self.widgets,
+                                             ('description', 'author', 'provider_name'),
+                                             bordered=False))
+        if 'provider_name' in self.widgets:
+            widget = self.widgets['provider_name']
+            widget.object_data = {
+                'ams-change-handler': 'MyAMS.helpers.select2ChangeHelper',
+                'ams-change-stop-propagation': 'true',
+                'ams-select2-helper-type': 'html',
+                'ams-select2-helper-url': absolute_url(self.context, self.request,
+                                                       'get-video-provider-settings-add-form.html'),
+                'ams-select2-helper-argument': 'provider_name',
+                'ams-select2-helper-target': '#video-settings-helper'
+            }
+            alsoProvides(widget, IObjectData)
+
+    def create(self, data):
+        return ExternalVideoParagraph()
+
+    def update_content(self, content, data):
+        changes = super(ExternalVideoParagraphAddForm, self).update_content(content, data)
+        settings = IExternalVideoSettings(content, None)
+        if settings is not None:
+            provider = content.get_provider()
+            form = InnerAdminEditForm(settings, self.request)
+            form.edit_permission = MANAGE_CONTENT_PERMISSION
+            form.fields = field.Fields(provider.settings_interface)
+            form.update()
+            settings_data, errors = form.extractData()
+            if not errors:
+                changes.update(form.update_content(settings, settings_data))
+        return changes
+
+    def add(self, object):
+        IParagraphContainer(self.context).append(object)
+
+
+@view_config(name='add-external-video.json', context=IParagraphContainerTarget, request_type=IPyAMSLayer,
+             permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
+class ExternalVideoParagraphAJAXAddForm(BaseParagraphAJAXAddForm, ExternalVideoParagraphAddForm):
+    """External video paragraph add form, JSON renderer"""
+
+
+@subscriber(IDataExtractedEvent, form_selector=ExternalVideoParagraphAddForm)
+def handle_video_paragraph_add_form_data_extraction(event):
+    """Handle provider name data extraction"""
+    data = event.data
+    if not data.get('provider_name'):
+        event.form.widgets.errors += (Invalid(_("Video provider is required")), )
+
+
+@viewlet_config(name='external-video-settings', context=Interface, layer=IPyAMSLayer,
+                view=ExternalVideoParagraphAddForm, manager=IWidgetsSuffixViewletsManager)
+@template_config(template='templates/video-settings.pt', layer=IPyAMSLayer)
+class VideoSettingsWidgetsSuffix(Viewlet):
+    """External video settings edit form widgets suffix"""
+
+
+@view_config(name='get-video-provider-settings-add-form.html', context=IParagraphContainerTarget,
+             request_type=IPyAMSLayer, permission=MANAGE_CONTENT_PERMISSION, xhr=True)
+def ExternalVideoProviderSettingsAddForm(request):
+    """External video provider settings form"""
+    provider_name = request.params.get('provider_name')
+    if provider_name is None:
+        raise NotFound("No provided provider_name argument")
+    elif (not provider_name) or (provider_name == '--NOVALUE--'):
+        return Response('')
+    else:
+        provider = get_utility(IExternalVideoProvider, name=provider_name)
+        form = InnerAdminAddForm(request.context, request)
+        form.legend = request.localizer.translate(_("Video provider settings"))
+        form.label_css_class = 'control-label col-md-4'
+        form.input_css_class = 'col-md-8'
+        form.fields = field.Fields(provider.settings_interface)
+        form.update()
+        return Response(form.render())
+
+
+@pagelet_config(name='properties.html', context=IExternalVideoParagraph, layer=IPyAMSLayer,
+                permission=MANAGE_CONTENT_PERMISSION)
+class ExternalVideoParagraphPropertiesEditForm(BaseParagraphPropertiesEditForm):
+    """External video paragraph properties edit form"""
+
+    legend = _("Edit video properties")
+    icon_css_class = 'fa fa-fw fa-youtube-play'
+
+    ajax_handler = 'properties.json'
+    edit_permission = MANAGE_CONTENT_PERMISSION
+
+    @property
+    def fields(self):
+        fields = field.Fields(IExternalVideoParagraph).omit('__parent__', '__name__', 'visible')
+        provider = self.context.get_provider()
+        if provider is not None:
+            fields += field.Fields(provider.settings_interface)
+        return fields
+
+    def updateWidgets(self, prefix=None):
+        super(ExternalVideoParagraphPropertiesEditForm, self).updateWidgets(prefix)
+        if 'description' in self.widgets:
+            self.widgets['description'].widget_css_class = 'textarea'
+        if 'body' in self.widgets:
+            self.widgets['body'].label = ''
+            self.add_group(NamedWidgetsGroup(self, 'body_group', self.widgets, ('body',),
+                                             bordered=False,
+                                             fieldset_class='margin-top-10 padding-y-5',
+                                             legend=_("HTML content"),
+                                             css_class='inner switcher padding-right-10 no-y-padding pull-left',
+                                             switch=True,
+                                             hide_if_empty=True))
+            self.add_group(NamedWidgetsGroup(self, 'data_group', self.widgets,
+                                             ('description', 'author', 'provider_name'),
+                                             bordered=False))
+        if 'provider_name' in self.widgets:
+            self.widgets['provider_name'].mode = DISPLAY_MODE
+            provider = self.context.get_provider()
+            if provider is not None:
+                self.add_group(NamedWidgetsGroup(self, 'provider_group', self.widgets,
+                                                 getFieldNamesInOrder(provider.settings_interface),
+                                                 legend=_("Video provider settings"),
+                                                 fieldset_class='margin-top-10 padding-y-5',
+                                                 css_class='inner padding-right-10 no-y-padding pull-left',
+                                                 bordered=False))
+
+
+@view_config(name='properties.json', context=IExternalVideoParagraph, request_type=IPyAMSLayer,
+             permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
+class ExternalVideoParagraphPropertiesAJAXEditForm(BaseParagraphAJAXEditForm, ExternalVideoParagraphPropertiesEditForm):
+    """External video paragraph properties edit form, JSON renderer"""
+
+    def get_ajax_output(self, changes):
+        output = super(ExternalVideoParagraphPropertiesAJAXEditForm, self).get_ajax_output(changes)
+        if 'title' in changes.get(IBaseParagraph, ()):
+            output.setdefault('events', []).append({
+                'event': 'myams.refresh',
+                'options': {
+                    'handler': 'PyAMS_content.paragraphs.refreshParagraph',
+                    'object_name': self.context.__name__,
+                    'title': II18n(self.context).query_attribute('title', request=self.request),
+                    'visible': self.context.visible
+                }
+            })
+        return output
+
+
+@adapter_config(context=(IExternalVideoParagraph, IPyAMSLayer), provides=IParagraphInnerEditor)
+@implementer(IInnerForm)
+class ExternalVideoParagraphInnerEditForm(ExternalVideoParagraphPropertiesEditForm):
+    """External video paragraph properties inner deit 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=IExternalVideoParagraph, request_type=IPyAMSLayer,
+             permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
+class ExternalVideoParagraphInnerAJAXEditForm(BaseParagraphAJAXEditForm, ExternalVideoParagraphInnerEditForm):
+    """External video paragraph inner edit form, JSON renderer"""
+
+
+#
+# Video paragraph summary
+#
+
+@adapter_config(context=(IExternalVideoParagraph, IPyAMSLayer), provides=IParagraphSummary)
+@template_config(template='templates/video-summary.pt', layer=IPyAMSLayer)
+class ExternalVideoParagraphSummary(BaseContentProvider):
+    """External video paragraph summary"""
+
+    video_renderer = None
+
+    def __init__(self, context, request):
+        super(ExternalVideoParagraphSummary, self).__init__(context, request)
+        provider = context.get_provider()
+        if provider is not None:
+            registry = get_current_registry()
+            self.video_renderer = registry.queryMultiAdapter((context.settings, request), IExternalVideoRenderer)
+
+    def update(self):
+        i18n = II18n(self.context)
+        if self.language:
+            for attr in ('title', 'body', 'description'):
+                setattr(self, attr, i18n.get_attribute(attr, self.language, request=self.request))
+        else:
+            for attr in ('title', 'body', 'description'):
+                setattr(self, attr, i18n.query_attribute(attr, request=self.request))
+        renderer = self.video_renderer
+        if renderer is not None:
+            renderer.update()
+
+    def render_video(self):
+        renderer = self.video_renderer
+        if not renderer:
+            return ''
+        return renderer.render()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/video/zmi/templates/video-settings.pt	Wed Feb 21 17:18:04 2018 +0100
@@ -0,0 +1,1 @@
+<div id="video-settings-helper"></div>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/video/zmi/templates/video-summary.pt	Wed Feb 21 17:18:04 2018 +0100
@@ -0,0 +1,7 @@
+<h3 tal:condition="view.title"
+	tal:content="view.title">title</h3>
+<div tal:condition="view.body"
+	 tal:content="structure view.body">body</div>
+<div tal:condition="view.description"
+	 tal:content="structure extension:html(view.description)">Description</div>
+<tal:var replace="structure view.render_video()" />