# HG changeset patch # User Thierry Florac # Date 1519229884 -3600 # Node ID 0ba2bb1a692e0e7dc8bc0a6584f752feec2d1fed # Parent 992892d242a79f3f910a977c5f6b3ebb9186b19d Integration of external videos as paragraphs diff -r 992892d242a7 -r 0ba2bb1a692e src/pyams_content/component/video/__init__.py --- /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 +# 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('
') + output.append('- {0} :'.format(translate(checker.label))) + output.append([checker.sep.join(checker_output)]) + output.append('
') + else: + output.append('- {0} : {1}'.format(translate(checker.label), translate(VALUE_OK))) + return output diff -r 992892d242a7 -r 0ba2bb1a692e src/pyams_content/component/video/interfaces/__init__.py --- /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 +# 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""" diff -r 992892d242a7 -r 0ba2bb1a692e src/pyams_content/component/video/paragraph.py --- /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 +# 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 diff -r 992892d242a7 -r 0ba2bb1a692e src/pyams_content/component/video/provider/__init__.py --- /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 +# 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 diff -r 992892d242a7 -r 0ba2bb1a692e src/pyams_content/component/video/provider/dailymotion.py --- /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 +# 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 [] diff -r 992892d242a7 -r 0ba2bb1a692e src/pyams_content/component/video/provider/interfaces.py --- /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 +# 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) diff -r 992892d242a7 -r 0ba2bb1a692e src/pyams_content/component/video/provider/vimeo.py --- /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 +# 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 [] diff -r 992892d242a7 -r 0ba2bb1a692e src/pyams_content/component/video/provider/youtube.py --- /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 +# 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 [] diff -r 992892d242a7 -r 0ba2bb1a692e src/pyams_content/component/video/provider/zmi/__init__.py --- /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 +# 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 diff -r 992892d242a7 -r 0ba2bb1a692e src/pyams_content/component/video/provider/zmi/templates/dailymotion-preview.pt --- /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 @@ +
+ +
diff -r 992892d242a7 -r 0ba2bb1a692e src/pyams_content/component/video/provider/zmi/templates/vimeo-preview.pt --- /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 @@ +
+ +
diff -r 992892d242a7 -r 0ba2bb1a692e src/pyams_content/component/video/provider/zmi/templates/youtube-preview.pt --- /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 @@ +
+ +
diff -r 992892d242a7 -r 0ba2bb1a692e src/pyams_content/component/video/zmi/__init__.py --- /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 +# 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 + + diff -r 992892d242a7 -r 0ba2bb1a692e src/pyams_content/component/video/zmi/paragraph.py --- /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 +# 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() diff -r 992892d242a7 -r 0ba2bb1a692e src/pyams_content/component/video/zmi/templates/video-settings.pt --- /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 @@ +
diff -r 992892d242a7 -r 0ba2bb1a692e 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/zmi/templates/video-summary.pt Wed Feb 21 17:18:04 2018 +0100 @@ -0,0 +1,7 @@ +

title

+
body
+
Description
+