# HG changeset patch # User Damien Correia # Date 1536226075 -7200 # Node ID a2f3b82f93c3cc9fd4597bbb51ee7b492d8a6adb # Parent 1afd36ed6947d2cb4f2ac5e4bda584f1a66cb890# Parent 9f9ffb0be25dc65ce311eeed3fcc8a8f6b2b4de0 Merge default diff -r 1afd36ed6947 -r a2f3b82f93c3 .hgtags --- a/.hgtags Tue Jul 17 15:12:43 2018 +0200 +++ b/.hgtags Thu Sep 06 11:27:55 2018 +0200 @@ -20,3 +20,4 @@ 1978e4dad1d8f950411807ed2df23fd030a39b60 0.1.14 fc8fe2dede6309db4a8cfc5b53eb894cca2f6970 0.1.15 9cc7207c1399658ef821a1ecdde86df0b5a1298d 0.1.15.1 +f687b90488819f2dba88ed44a254d7cba1f8c652 0.1.16 diff -r 1afd36ed6947 -r a2f3b82f93c3 buildout.cfg --- a/buildout.cfg Tue Jul 17 15:12:43 2018 +0200 +++ b/buildout.cfg Thu Sep 06 11:27:55 2018 +0200 @@ -86,4 +86,4 @@ eggs = pyams_content [test] [versions] -pyams_content = 0.1.16 +pyams_content = 0.1.17 diff -r 1afd36ed6947 -r a2f3b82f93c3 docs/HISTORY.txt --- a/docs/HISTORY.txt Tue Jul 17 15:12:43 2018 +0200 +++ b/docs/HISTORY.txt Thu Sep 06 11:27:55 2018 +0200 @@ -1,6 +1,24 @@ History ======= +0.1.16 +------ + - use iterators in all dashboards + - updated view content portlet to select and merge several views + - handle cache for header and footer + - added custom file view to check publication status before giving access + - added arguments to avoid using cache when getting view results (for example in preview mode) + - added "oid_to_urls" HTML renderer to convert internal "oid://" links to URLs + - added Opengraph metas for shared contents + - added support for illustration to thesaurus terms + - added support for tags and collections, which are specific kinds of themes + - added location map paragraph + - added redirections manager + - added renderer resources management + - use locale name in header and footer cache key + - updated system manager permissions + - updated internal links management + 0.1.15.1 -------- - added request argument when rendering pictogram image diff -r 1afd36ed6947 -r a2f3b82f93c3 setup.py --- a/setup.py Tue Jul 17 15:12:43 2018 +0200 +++ b/setup.py Thu Sep 06 11:27:55 2018 +0200 @@ -22,7 +22,7 @@ README = os.path.join(DOCS, 'README.txt') HISTORY = os.path.join(DOCS, 'HISTORY.txt') -version = '0.1.16' +version = '0.1.17' long_description = open(README).read() + '\n\n' + open(HISTORY).read() tests_require = [] diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content.egg-info/PKG-INFO --- a/src/pyams_content.egg-info/PKG-INFO Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content.egg-info/PKG-INFO Thu Sep 06 11:27:55 2018 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 1.1 Name: pyams-content -Version: 0.1.15.1 +Version: 0.1.16 Summary: PyAMS base content interfaces and classes Home-page: http://hg.ztfy.org/pyams/pyams_content Author: Thierry Florac @@ -73,6 +73,24 @@ History ======= + 0.1.16 + ------ + - use iterators in all dashboards + - updated view content portlet to select and merge several views + - handle cache for header and footer + - added custom file view to check publication status before giving access + - added arguments to avoid using cache when getting view results (for example in preview mode) + - added "oid_to_urls" HTML renderer to convert internal "oid://" links to URLs + - added Opengraph metas for shared contents + - added support for illustration to thesaurus terms + - added support for tags and collections, which are specific kinds of themes + - added location map paragraph + - added redirections manager + - added renderer resources management + - use locale name in header and footer cache key + - updated system manager permissions + - updated internal links management + 0.1.15.1 -------- - added request argument when rendering pictogram image diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content.egg-info/SOURCES.txt --- a/src/pyams_content.egg-info/SOURCES.txt Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content.egg-info/SOURCES.txt Thu Sep 06 11:27:55 2018 +0200 @@ -27,6 +27,7 @@ src/pyams_content/component/extfile/interfaces/__init__.py src/pyams_content/component/extfile/zmi/__init__.py src/pyams_content/component/extfile/zmi/container.py +src/pyams_content/component/file/__init__.py src/pyams_content/component/gallery/__init__.py src/pyams_content/component/gallery/file.py src/pyams_content/component/gallery/paragraph.py @@ -35,12 +36,15 @@ src/pyams_content/component/gallery/zmi/file.py src/pyams_content/component/gallery/zmi/interfaces.py src/pyams_content/component/gallery/zmi/paragraph.py +src/pyams_content/component/gallery/zmi/templates/gallery-media-thumbnail.pt src/pyams_content/component/gallery/zmi/templates/gallery-medias.pt src/pyams_content/component/illustration/__init__.py src/pyams_content/component/illustration/paragraph.py +src/pyams_content/component/illustration/thesaurus.py src/pyams_content/component/illustration/interfaces/__init__.py src/pyams_content/component/illustration/zmi/__init__.py src/pyams_content/component/illustration/zmi/paragraph.py +src/pyams_content/component/illustration/zmi/thesaurus.py src/pyams_content/component/illustration/zmi/templates/illustration-thumbnail.pt src/pyams_content/component/illustration/zmi/templates/paragraph-illustration-icon.pt src/pyams_content/component/keynumber/__init__.py @@ -54,7 +58,6 @@ src/pyams_content/component/links/interfaces/__init__.py src/pyams_content/component/links/zmi/__init__.py src/pyams_content/component/links/zmi/container.py -src/pyams_content/component/links/zmi/reverse.py src/pyams_content/component/media/__init__.py src/pyams_content/component/paragraph/__init__.py src/pyams_content/component/paragraph/audio.py @@ -65,6 +68,7 @@ src/pyams_content/component/paragraph/html.py src/pyams_content/component/paragraph/keynumber.py src/pyams_content/component/paragraph/keypoint.py +src/pyams_content/component/paragraph/map.py src/pyams_content/component/paragraph/milestone.py src/pyams_content/component/paragraph/pictogram.py src/pyams_content/component/paragraph/verbatim.py @@ -77,6 +81,7 @@ src/pyams_content/component/paragraph/interfaces/html.py src/pyams_content/component/paragraph/interfaces/keynumber.py src/pyams_content/component/paragraph/interfaces/keypoint.py +src/pyams_content/component/paragraph/interfaces/map.py src/pyams_content/component/paragraph/interfaces/milestone.py src/pyams_content/component/paragraph/interfaces/pictogram.py src/pyams_content/component/paragraph/interfaces/verbatim.py @@ -91,6 +96,7 @@ src/pyams_content/component/paragraph/zmi/interfaces.py src/pyams_content/component/paragraph/zmi/keynumber.py src/pyams_content/component/paragraph/zmi/keypoint.py +src/pyams_content/component/paragraph/zmi/map.py src/pyams_content/component/paragraph/zmi/milestone.py src/pyams_content/component/paragraph/zmi/pictogram.py src/pyams_content/component/paragraph/zmi/preview.py @@ -105,7 +111,6 @@ src/pyams_content/component/theme/zmi/__init__.py src/pyams_content/component/theme/zmi/manager.py src/pyams_content/component/theme/zmi/portlet.py -src/pyams_content/component/theme/zmi/templates/themes-info.pt src/pyams_content/component/video/__init__.py src/pyams_content/component/video/paragraph.py src/pyams_content/component/video/interfaces/__init__.py @@ -141,8 +146,10 @@ src/pyams_content/features/header/__init__.py src/pyams_content/features/header/interfaces/__init__.py src/pyams_content/features/header/skin/__init__.py +src/pyams_content/features/header/skin/interfaces.py src/pyams_content/features/header/zmi/__init__.py src/pyams_content/features/header/zmi/templates/renderer-settings.pt +src/pyams_content/features/html/__init__.py src/pyams_content/features/menu/__init__.py src/pyams_content/features/menu/interfaces/__init__.py src/pyams_content/features/menu/portlet/__init__.py @@ -164,6 +171,13 @@ src/pyams_content/features/preview/zmi/__init__.py src/pyams_content/features/preview/zmi/interfaces.py src/pyams_content/features/preview/zmi/templates/preview.pt +src/pyams_content/features/redirect/__init__.py +src/pyams_content/features/redirect/container.py +src/pyams_content/features/redirect/tween.py +src/pyams_content/features/redirect/interfaces/__init__.py +src/pyams_content/features/redirect/zmi/__init__.py +src/pyams_content/features/redirect/zmi/container.py +src/pyams_content/features/redirect/zmi/templates/manager-test.pt src/pyams_content/features/renderer/__init__.py src/pyams_content/features/renderer/interfaces/__init__.py src/pyams_content/features/renderer/skin/__init__.py @@ -233,6 +247,7 @@ src/pyams_content/shared/common/portlet/content/zmi/preview.pt src/pyams_content/shared/common/skin/__init__.py src/pyams_content/shared/common/skin/oid.py +src/pyams_content/shared/common/skin/opengraph.py src/pyams_content/shared/common/skin/url.py src/pyams_content/shared/common/zmi/__init__.py src/pyams_content/shared/common/zmi/dashboard.py @@ -243,6 +258,7 @@ src/pyams_content/shared/common/zmi/portal.py src/pyams_content/shared/common/zmi/properties.py src/pyams_content/shared/common/zmi/rename.py +src/pyams_content/shared/common/zmi/reverse.py src/pyams_content/shared/common/zmi/search.py src/pyams_content/shared/common/zmi/security.py src/pyams_content/shared/common/zmi/site.py @@ -321,6 +337,7 @@ src/pyams_content/shared/site/zmi/widget/templates/folders-input.pt src/pyams_content/shared/view/__init__.py src/pyams_content/shared/view/manager.py +src/pyams_content/shared/view/merge.py src/pyams_content/shared/view/reference.py src/pyams_content/shared/view/theme.py src/pyams_content/shared/view/interfaces/__init__.py diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/component/association/zmi/__init__.py --- a/src/pyams_content/component/association/zmi/__init__.py Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/component/association/zmi/__init__.py Thu Sep 06 11:27:55 2018 +0200 @@ -124,11 +124,18 @@ attributes.setdefault('table', {}).update({ 'data-ams-location': absolute_url(container, self.request), 'data-ams-tablednd-drag-handle': 'td.sorter', - 'data-ams-tablednd-drop-target': 'set-associations-order.json', - 'data-ams-visibility-switcher': 'switch-association-visibility.json' + 'data-ams-tablednd-drop-target': 'set-associations-order.json' + }) + attributes.setdefault('td', {}).update({ + 'data-ams-attribute-switcher': self.get_switcher_target }) return attributes + @staticmethod + def get_switcher_target(element, column): + if column.__name__ == 'show-hide': + return 'switch-association-visibility.json' + @reify def values(self): return list(super(AssociationsTable, self).values) diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/component/gallery/zmi/interfaces.py --- a/src/pyams_content/component/gallery/zmi/interfaces.py Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/component/gallery/zmi/interfaces.py Thu Sep 06 11:27:55 2018 +0200 @@ -39,7 +39,7 @@ author = TextLine(title=_("Author"), description=_("Name of document's author"), - required=False) + required=True) author_comments = I18nTextField(title=_("Author comments"), description=_("Comments relatives to author's rights management"), diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/component/gallery/zmi/templates/gallery-medias.pt --- a/src/pyams_content/component/gallery/zmi/templates/gallery-medias.pt Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/component/gallery/zmi/templates/gallery-medias.pt Thu Sep 06 11:27:55 2018 +0200 @@ -2,6 +2,7 @@ data-ams-plugins="pyams_content" tal:define="gallery_images context.values()" tal:attributes="data-ams-plugin-pyams_content-src tales:resource_path('pyams_content.skin:pyams_content'); + data-ams-plugin-pyams_content-css tales:resource_path('pyams_content.skin:pyams_content_css'); id string:gallery_medias_${context.__name__};" data-ams-plugin-pyams_content-async="false">
diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/component/illustration/interfaces/__init__.py --- a/src/pyams_content/component/illustration/interfaces/__init__.py Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/component/illustration/interfaces/__init__.py Thu Sep 06 11:27:55 2018 +0200 @@ -61,8 +61,8 @@ class IIllustration(IBasicIllustration, IRenderedContent): """Illustration paragraph""" - description = I18nTextField(title=_("Description"), - description=_(""), + description = I18nTextField(title=_("Associated text"), + description=_("Illustration description displayed in front-office templates"), required=False) author = TextLine(title=_("Author"), diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/component/illustration/zmi/__init__.py --- a/src/pyams_content/component/illustration/zmi/__init__.py Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/component/illustration/zmi/__init__.py Thu Sep 06 11:27:55 2018 +0200 @@ -51,15 +51,31 @@ css_class = 'form-group' padding_class = '' - fieldset_class = 'bordered margin-top-10 padding-y-5' + # fieldset_class = 'bordered margin-top-10 padding-y-5' legend = _("Illustration") - legend_class = 'illustration switcher no-y-padding padding-right-10' + # legend_class = 'illustration switcher no-y-padding padding-right-10' fields = field.Fields(IBasicIllustration).omit('__parent__', '__name__') weight = 10 + @property + def legend_class(self): + if IBaseParagraph.providedBy(self.context): + return 'illustration switcher no-y-padding padding-right-10 pull-left width-auto' + else: + return 'illustration switcher no-y-padding' + + @property + def fieldset_class(self): + result = 'margin-top-10 padding-y-5' + if not IBaseParagraph.providedBy(self.context): + result += ' bordered' + return result + + hide_widgets_prefix_div = True + def getContent(self): return IIllustration(self.context) @@ -69,9 +85,10 @@ @property def switcher_state(self): - content = self.getContent() - if content.has_data(): - return 'open' + if not IBaseParagraph.providedBy(self.context): + content = self.getContent() + if content.has_data(): + return 'open' def get_ajax_output(self, changes): output = super(BasicIllustrationPropertiesInnerEditForm, self).get_ajax_output(changes) @@ -102,22 +119,6 @@ return _("Header illustration") @property - def legend_class(self): - if IBaseParagraph.providedBy(self.context): - return 'illustration switcher no-y-padding padding-right-10 pull-left width-auto' - else: - return 'illustration switcher no-y-padding' - - @property - def fieldset_class(self): - result = 'margin-top-10 padding-y-5' - if not IBaseParagraph.providedBy(self.context): - result += ' bordered' - return result - - hide_widgets_prefix_div = True - - @property def switcher_state(self): if not IBaseParagraph.providedBy(self.context): return 'open' diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/component/keynumber/interfaces/__init__.py --- a/src/pyams_content/component/keynumber/interfaces/__init__.py Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/component/keynumber/interfaces/__init__.py Thu Sep 06 11:27:55 2018 +0200 @@ -16,6 +16,7 @@ # import standard library # import interfaces +from pyams_content.component.paragraph.interfaces import IBaseParagraph from pyams_content.interfaces.container import IOrderedContainer from zope.annotation.interfaces import IAttributeAnnotatable @@ -23,7 +24,7 @@ from pyams_i18n.schema import I18nTextLineField from zope.container.constraints import containers, contains from zope.interface import Interface -from zope.schema import Bool, TextLine +from zope.schema import Bool, TextLine, Choice from pyams_content import _ @@ -76,3 +77,21 @@ class IKeyNumberContainerTarget(Interface): """Key numbers container target interface""" + + +KEYNUMBER_PARAGRAPH_TYPE = 'KeyNumbers' +KEYNUMBER_PARAGRAPH_NAME = _("Key numbers") +KEYNUMBER_PARAGRAPH_RENDERERS = 'PyAMS.keynumbers.renderers' + + +# +# KeyNumber paragraph +# + +class IKeyNumberParagraph(IKeyNumberContainerTarget, IBaseParagraph): + """Key numbers paragraph interface""" + + renderer = Choice(title=_("Key numbers template"), + description=_("Presentation template used for key numbers"), + vocabulary=KEYNUMBER_PARAGRAPH_RENDERERS, + default='default') diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/component/keynumber/paragraph.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_content/component/keynumber/paragraph.py Thu Sep 06 11:27:55 2018 +0200 @@ -0,0 +1,93 @@ +# +# 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 IParagraphFactory +from pyams_content.component.keynumber.interfaces import KEYNUMBER_PARAGRAPH_TYPE, KEYNUMBER_PARAGRAPH_NAME, \ + KEYNUMBER_PARAGRAPH_RENDERERS, IKeyNumberParagraph +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, BaseParagraphContentChecker +from pyams_content.features.renderer import RenderersVocabulary +from pyams_utils.adapter import adapter_config +from pyams_utils.factory import factory_config +from pyams_utils.registry import get_utility, utility_config +from pyams_utils.request import check_request +from pyams_utils.traversing import get_parent +from pyams_utils.vocabulary import vocabulary_config +from zope.interface import implementer +from zope.schema.fieldproperty import FieldProperty + + +@implementer(IKeyNumberParagraph) +@factory_config(provided=IKeyNumberParagraph) +class KeyNumberParagraph(BaseParagraph): + """Key numbers paragraph""" + + icon_class = 'fa-dashboard' + icon_hint = KEYNUMBER_PARAGRAPH_NAME + + renderer = FieldProperty(IKeyNumberParagraph['renderer']) + + +@utility_config(name=KEYNUMBER_PARAGRAPH_TYPE, provides=IParagraphFactory) +class KeyNumberParagraphFactory(BaseParagraphFactory): + """Key numbers paragraph factory""" + + name = KEYNUMBER_PARAGRAPH_NAME + content_type = KeyNumberParagraph + + +@adapter_config(context=IKeyNumberParagraph, provides=IContentChecker) +class KeyNumberParagraphContentChecker(BaseParagraphContentChecker): + """Key numbers 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 = [] + 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, ) + i18n = II18n(self.context) + for lang in langs: + value = i18n.get_attribute('title', lang, request) + if not value: + field_title = translate(IKeyNumberParagraph['title'].title) + if len(langs) == 1: + output.append(translate(MISSING_VALUE).format(field=field_title)) + else: + output.append(translate(MISSING_LANG_VALUE).format(field=field_title, lang=lang)) + return output + + +@vocabulary_config(name=KEYNUMBER_PARAGRAPH_RENDERERS) +class KeyNumberParagraphRendererVocabulary(RenderersVocabulary): + """Key numbers paragraph renderers vocabulary""" + + content_interface = IKeyNumberParagraph diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/component/keynumber/zmi/__init__.py --- a/src/pyams_content/component/keynumber/zmi/__init__.py Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/component/keynumber/zmi/__init__.py Thu Sep 06 11:27:55 2018 +0200 @@ -94,11 +94,24 @@ attributes.setdefault('table', {}).update({ 'data-ams-location': absolute_url(container, self.request), 'data-ams-tablednd-drag-handle': 'td.sorter', - 'data-ams-tablednd-drop-target': 'set-keynumbers-order.json', - 'data-ams-visibility-switcher': 'switch-keynumber-visibility.json' + 'data-ams-tablednd-drop-target': 'set-keynumbers-order.json' + }) + attributes.setdefault('td', {}).update({ + 'data-ams-attribute-switcher': self.get_switcher_target, + 'data-ams-switcher-attribute-name': self.get_switcher_attribute }) return attributes + @staticmethod + def get_switcher_target(element, column): + if column.__name__ == 'show-hide': + return 'switch-keynumber-visibility.json' + + @staticmethod + def get_switcher_attribute(element, column): + if column.__name__ == 'show-hide': + return 'visible' + @reify def values(self): return list(super(KeyNumbersTable, self).values) diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/component/keynumber/zmi/paragraph.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_content/component/keynumber/zmi/paragraph.py Thu Sep 06 11:27:55 2018 +0200 @@ -0,0 +1,125 @@ +# +# 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 +from pyams_content.component.keynumber.interfaces import KEYNUMBER_PARAGRAPH_TYPE, IKeyNumberParagraph +from pyams_content.component.paragraph.zmi.interfaces import IParagraphInnerEditor, IParagraphContainerView +from pyams_content.interfaces import MANAGE_CONTENT_PERMISSION +from pyams_content.shared.common import IWfSharedContent +from pyams_form.interfaces.form import IInnerForm +from pyams_i18n.interfaces import II18n +from pyams_skin.interfaces.viewlet import IToolbarAddingMenu +from pyams_skin.layer import IPyAMSLayer +from z3c.form.interfaces import INPUT_MODE + +# import packages +from pyams_content.component.keynumber.zmi import IKeyNumbersParentForm +from pyams_content.component.keynumber.paragraph import KeyNumberParagraph +from pyams_content.component.paragraph.zmi import BaseParagraphAddMenu, BaseParagraphAJAXAddForm, \ + BaseParagraphPropertiesEditForm, BaseParagraphAJAXEditForm, IParagraphEditFormButtons +from pyams_content.features.renderer.zmi.widget import RendererFieldWidget +from pyams_form.form import ajax_config +from pyams_pagelet.pagelet import pagelet_config +from pyams_skin.event import get_json_widget_refresh_event +from pyams_utils.adapter import adapter_config +from pyams_utils.traversing import get_parent +from pyams_viewlet.viewlet import viewlet_config +from pyams_zmi.form import AdminDialogAddForm +from z3c.form import field, button +from zope.interface import implementer, Interface + +from pyams_content import _ + + +@viewlet_config(name='add-keynumber-paragraph.menu', context=IParagraphContainerTarget, view=IParagraphContainerView, + layer=IPyAMSLayer, manager=IToolbarAddingMenu, weight=600) +class KeyNumberParagraphAddMenu(BaseParagraphAddMenu): + """Key number paragraph add menu""" + + label = _("Key numbers...") + label_css_class = 'fa fa-fw fa-dashboard' + url = 'add-keynumber-paragraph.html' + paragraph_type = KEYNUMBER_PARAGRAPH_TYPE + + +@pagelet_config(name='add-keynumber-paragraph.html', context=IParagraphContainerTarget, layer=IPyAMSLayer, + permission=MANAGE_CONTENT_PERMISSION) +@ajax_config(name='add-keynumber-paragraph.json', context=IParagraphContainerTarget, layer=IPyAMSLayer, + base=BaseParagraphAJAXAddForm) +class KeyNumberParagraphAddForm(AdminDialogAddForm): + """Key number paragraph add form""" + + legend = _("Add new key number paragraph") + icon_css_class = 'fa fa-fw fa-dashboard' + + fields = field.Fields(IKeyNumberParagraph).select('title', 'renderer') + edit_permission = MANAGE_CONTENT_PERMISSION + + def create(self, data): + return KeyNumberParagraph() + + def add(self, object): + IParagraphContainer(self.context).append(object) + + +@pagelet_config(name='properties.html', context=IKeyNumberParagraph, layer=IPyAMSLayer, + permission=MANAGE_CONTENT_PERMISSION) +@ajax_config(name='properties.json', context=IKeyNumberParagraph, layer=IPyAMSLayer, + base=BaseParagraphAJAXEditForm) +@implementer(IKeyNumbersParentForm) +class KeyNumberParagraphPropertiesEditForm(BaseParagraphPropertiesEditForm): + """Key number paragraph properties edit form""" + + prefix = 'keynumbers_properties.' + + @property + def title(self): + content = get_parent(self.context, IWfSharedContent) + return II18n(content).query_attribute('title', request=self.request) + + legend = _("Edit key number paragraph properties") + icon_css_class = 'fa fa-fw fa-dashboard' + + fields = field.Fields(IKeyNumberParagraph).select('title', 'renderer') + fields['renderer'].widgetFactory = RendererFieldWidget + + edit_permission = MANAGE_CONTENT_PERMISSION + + +@adapter_config(context=(IKeyNumberParagraph, IPyAMSLayer), provides=IParagraphInnerEditor) +@ajax_config(name='inner-properties.json', context=IKeyNumberParagraph, layer=IPyAMSLayer, + base=BaseParagraphAJAXEditForm) +@implementer(IInnerForm, IKeyNumbersParentForm) +class KeyNumberParagraphInnerEditForm(KeyNumberParagraphPropertiesEditForm): + """Key number paragraph inner edit form""" + + legend = None + + @property + def buttons(self): + if self.mode == INPUT_MODE: + return button.Buttons(IParagraphEditFormButtons) + else: + return button.Buttons() + + def get_ajax_output(self, changes): + output = super(self.__class__, self).get_ajax_output(changes) + updated = changes.get(IKeyNumberParagraph, ()) + if 'renderer' in updated: + output.setdefault('events', []).append( + get_json_widget_refresh_event(self.context, self.request, KeyNumberParagraphInnerEditForm, 'renderer')) + return output diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/component/links/__init__.py --- a/src/pyams_content/component/links/__init__.py Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/component/links/__init__.py Thu Sep 06 11:27:55 2018 +0200 @@ -16,14 +16,16 @@ # import standard library # import interfaces -from pyams_content.component.association.interfaces import IAssociationInfo, IAssociationContainerTarget, IAssociationContainer +from pyams_content.component.association.interfaces import IAssociationInfo, IAssociationContainerTarget, \ + IAssociationContainer from pyams_content.component.links.interfaces import IBaseLink, IInternalLink, IExternalLink, IMailtoLink from pyams_content.features.checker.interfaces import IContentChecker, ERROR_VALUE from pyams_content.interfaces import IBaseContent, MANAGE_CONTENT_PERMISSION from pyams_content.reference.pictograms.interfaces import IPictogramTable from pyams_form.interfaces.form import IFormContextPermissionChecker from pyams_i18n.interfaces import II18n -from pyams_sequence.interfaces import ISequentialIdInfo +from pyams_sequence.interfaces import ISequentialIdInfo, IInternalReference +from pyams_skin.layer import IPyAMSUserLayer from pyams_workflow.interfaces import IWorkflow, IWorkflowPublicationInfo # import packages @@ -115,8 +117,27 @@ # Internal links # +@implementer(IInternalReference) +class InternalReferenceMixin(object): + """Internal reference mixin class""" + + reference = None + + @volatile_property + def target(self): + return get_reference_target(self.reference) + + def get_target(self, state=None, request=None): + if request is None: + request = check_request() + if (not state) and not IPyAMSUserLayer.providedBy(request): + return self.target + else: + return get_reference_target(self.reference, state, request) + + @implementer(IInternalLink) -class InternalLink(BaseLink): +class InternalLink(BaseLink, InternalReferenceMixin): """Internal link persistent class""" icon_class = 'fa-external-link-square fa-rotate-90' @@ -124,16 +145,6 @@ reference = FieldProperty(IInternalLink['reference']) - @volatile_property - def target(self): - return get_reference_target(self.reference) - - def get_target(self, state=None): - if not state: - return self.target - else: - return get_reference_target(self.reference, state) - def is_visible(self, request=None): target = self.get_target() if target is not None: diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/component/links/zmi/reverse.py --- a/src/pyams_content/component/links/zmi/reverse.py Tue Jul 17 15:12:43 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,109 +0,0 @@ -# -# 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 hypatia.interfaces import ICatalog -from pyams_content.shared.common.interfaces import IWfSharedContent -from pyams_content.shared.common.interfaces.zmi import ISiteRootDashboardTable -from pyams_content.shared.site.interfaces import ISiteContainer -from pyams_portal.interfaces import IPortalTemplate -from pyams_sequence.interfaces import ISequentialIdInfo -from pyams_skin.interfaces import IInnerPage -from pyams_skin.layer import IPyAMSLayer -from pyams_utils.interfaces import VIEW_SYSTEM_PERMISSION -from pyams_workflow.interfaces import IWorkflowVersions -from pyams_zmi.interfaces.menu import IContentManagementMenu -from pyams_zmi.layer import IAdminLayer -from z3c.table.interfaces import IValues, IColumn - -# import packages -from hypatia.catalog import CatalogQuery -from hypatia.query import Eq, Or -from pyams_catalog.query import CatalogResultSet -from pyams_pagelet.pagelet import pagelet_config -from pyams_skin.container import ContainerView -from pyams_skin.table import BaseTable, NameColumn -from pyams_skin.viewlet.menu import MenuItem -from pyams_utils.adapter import adapter_config, ContextRequestViewAdapter -from pyams_utils.list import unique -from pyams_utils.registry import get_utility -from pyams_utils.traversing import get_parent -from pyams_viewlet.viewlet import viewlet_config -from pyams_zmi.view import AdminView -from zope.interface import implementer, Interface - -from pyams_content import _ - - -@viewlet_config(name='reverse-links.menu', context=IWfSharedContent, layer=IAdminLayer, - manager=IContentManagementMenu, permission=VIEW_SYSTEM_PERMISSION, weight=40) -class SequentialITargetReverseLinksMenu(MenuItem): - """Sequential ID target reverse links menu""" - - label = _("Reverse links") - icon_class = 'fa-anchor' - url = '#reverse-links.html' - - -@implementer(ISiteRootDashboardTable) -class SequentialIdTargetReverseLinkTable(BaseTable): - """Sequential ID target reverse links table""" - - title = _("Content's internal links") - - -@adapter_config(name='name', context=(Interface, IPyAMSLayer, SequentialIdTargetReverseLinkTable), provides=IColumn) -class ReverseLinkNameColumn(NameColumn): - """Reverse link name column""" - - _header = _("Title") - - -@adapter_config(context=(IWfSharedContent, IPyAMSLayer, SequentialIdTargetReverseLinkTable), provides=IValues) -class SequentialIdTargetReverseLinkValues(ContextRequestViewAdapter): - """Sequential ID target reverse links values""" - - @property - def values(self): - - def get_item(result): - parent = get_parent(result, IWfSharedContent) - if parent is not None: - return IWorkflowVersions(parent).get_last_versions(count=1)[0] - parent = get_parent(result, IPortalTemplate) - if parent is None: - parent = get_parent(result, ISiteContainer) - if parent is None: - parent = self.request.root - return parent - - catalog = get_utility(ICatalog) - oid = ISequentialIdInfo(self.context).hex_oid - params = Or(Eq(catalog['link_reference'], oid), - Eq(catalog['link_references'], oid)) - return unique(map(get_item, - CatalogResultSet(CatalogQuery(catalog).query(params, - sort_index='modified_date')))) - - -@pagelet_config(name='reverse-links.html', context=IWfSharedContent, layer=IPyAMSLayer, - permission=VIEW_SYSTEM_PERMISSION) -@implementer(IInnerPage) -class SequentialIdTargetReverseLinkView(AdminView, ContainerView): - """Sequential ID target reverse links view""" - - table_class = SequentialIdTargetReverseLinkTable diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/component/paragraph/audio.py --- a/src/pyams_content/component/paragraph/audio.py Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/component/paragraph/audio.py Thu Sep 06 11:27:55 2018 +0200 @@ -16,7 +16,7 @@ # import standard library # import interfaces -from pyams_content.component.illustration.interfaces import IIllustrationTarget +from pyams_content.component.illustration.interfaces import IBasicIllustrationTarget from pyams_content.component.paragraph.interfaces import IParagraphFactory from pyams_content.component.paragraph.interfaces.audio import IAudioParagraph, AUDIO_PARAGRAPH_TYPE, \ AUDIO_PARAGRAPH_RENDERERS, AUDIO_PARAGRAPH_NAME @@ -36,7 +36,7 @@ from zope.schema.fieldproperty import FieldProperty -@implementer(IAudioParagraph, IIllustrationTarget) +@implementer(IAudioParagraph, IBasicIllustrationTarget) @factory_config(provided=IAudioParagraph) class AudioParagraph(BaseParagraph): """Audio paragraph class""" diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/component/paragraph/container.py --- a/src/pyams_content/component/paragraph/container.py Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/component/paragraph/container.py Thu Sep 06 11:27:55 2018 +0200 @@ -16,8 +16,8 @@ # import standard library # import interfaces -from pyams_content.component.paragraph.interfaces import IParagraphContainer, IParagraphContainerTarget, \ - PARAGRAPH_CONTAINER_KEY +from pyams_content.component.paragraph.interfaces import IParagraphFactory, IParagraphContainer, \ + IParagraphContainerTarget, PARAGRAPH_CONTAINER_KEY from pyams_content.features.checker.interfaces import IContentChecker from zope.location.interfaces import ISublocations from zope.traversing.interfaces import ITraversable @@ -26,6 +26,7 @@ from pyams_content.features.checker import BaseContentChecker from pyams_utils.adapter import adapter_config, ContextAdapter, get_annotation_adapter from pyams_utils.container import BTreeOrderedContainer +from pyams_utils.registry import get_global_registry from zope.interface import implementer from pyams_content import _ @@ -42,6 +43,22 @@ self[key] = value self.last_id += 1 + def get_visible_paragraphs(self, anchors_only=False, factories=None): + for paragraph in self.values(): + if not paragraph.visible: + continue + if anchors_only and not paragraph.anchor: + continue + if factories: + registry = get_global_registry() + has_factory = False + for factory_name in factories: + factory = registry.queryUtility(IParagraphFactory, name=factory_name) + has_factory = (factory is not None) and (factory.content_type == paragraph.__class__) + if not has_factory: + continue + yield paragraph + @adapter_config(context=IParagraphContainerTarget, provides=IParagraphContainer) def paragraph_container_factory(target): diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/component/paragraph/frame.py --- a/src/pyams_content/component/paragraph/frame.py Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/component/paragraph/frame.py Thu Sep 06 11:27:55 2018 +0200 @@ -46,7 +46,7 @@ class FrameParagraph(BaseParagraph): """Framed text paragraph""" - icon_class = 'fa-list-alt' + icon_class = 'fa-window-maximize' icon_hint = FRAME_PARAGRAPH_NAME body = FieldProperty(IFrameParagraph['body']) diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/component/paragraph/header.py --- a/src/pyams_content/component/paragraph/header.py Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/component/paragraph/header.py Thu Sep 06 11:27:55 2018 +0200 @@ -16,24 +16,23 @@ # import standard library # import interfaces -from pyams_content.component.paragraph.interfaces import IParagraphFactory -from pyams_content.component.paragraph.interfaces.header import IHeaderParagraph, HEADER_PARAGRAPH_TYPE, \ - HEADER_PARAGRAPH_RENDERERS, HEADER_PARAGRAPH_NAME -from pyams_content.features.checker.interfaces import IContentChecker, MISSING_VALUE, MISSING_LANG_VALUE -from pyams_i18n.interfaces import II18n, II18nManager, INegotiator +from pyams_content.component.paragraph.interfaces.header import IHeaderParagraph, HEADER_PARAGRAPH_RENDERERS, \ + HEADER_PARAGRAPH_NAME +from pyams_content.features.checker.interfaces import IContentChecker, ERROR_VALUE +from pyams_i18n.interfaces import II18n # import packages -from pyams_content.component.paragraph import BaseParagraph, BaseParagraphContentChecker, BaseParagraphFactory +from pyams_content.component.paragraph import BaseParagraph, BaseParagraphContentChecker from pyams_content.features.renderer import RenderersVocabulary from pyams_utils.adapter import adapter_config from pyams_utils.factory import factory_config -from pyams_utils.registry import utility_config, get_utility from pyams_utils.text import get_text_start -from pyams_utils.traversing import get_parent from pyams_utils.vocabulary import vocabulary_config from zope.interface import implementer from zope.schema.fieldproperty import FieldProperty +from pyams_content import _ + @implementer(IHeaderParagraph) @factory_config(provided=IHeaderParagraph) @@ -52,37 +51,15 @@ renderer = FieldProperty(IHeaderParagraph['renderer']) -@utility_config(name=HEADER_PARAGRAPH_TYPE, provides=IParagraphFactory) -class HeaderParagraphFactory(BaseParagraphFactory): - """Header paragraph factory""" - - name = HEADER_PARAGRAPH_NAME - content_type = HeaderParagraph - - @adapter_config(context=IHeaderParagraph, provides=IContentChecker) class HeaderParagraphContentChecker(BaseParagraphContentChecker): """Header paragraph content checker""" 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, ) - i18n = II18n(self.context) - for lang in langs: - value = i18n.get_attribute('header', lang, request) - if not value: - field_title = translate(IHeaderParagraph['header'].title) - if len(langs) == 1: - output.append(translate(MISSING_VALUE).format(field=field_title)) - else: - output.append(translate(MISSING_LANG_VALUE).format(field=field_title, lang=lang)) - return output + field_title = translate(IHeaderParagraph['header'].title) + return [translate(ERROR_VALUE).format(field=field_title, + message=_("This paragraph type is deprecated and should be removed!")), ] @vocabulary_config(name=HEADER_PARAGRAPH_RENDERERS) diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/component/paragraph/interfaces/__init__.py --- a/src/pyams_content/component/paragraph/interfaces/__init__.py Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/component/paragraph/interfaces/__init__.py Thu Sep 06 11:27:55 2018 +0200 @@ -66,11 +66,17 @@ def append(self, value): """Add given value to container""" + def get_visible_paragraphs(self, anchors_only=False, factories=None): + """Get visible paragraphs matching given arguments""" + class IParagraphContainerTarget(IAttributeAnnotatable): """Paragraphs container marker interface""" +PARAGRAPH_FACTORIES_VOCABULARY = 'PyAMS paragraph factories' + + class IParagraphFactory(Interface): """Paragraph factory utility interface""" @@ -93,7 +99,7 @@ auto_created_paragraphs = List(title=_("Default paragraphs"), description=_("List of paragraphs automatically added to a new content"), required=False, - value_type=Choice(vocabulary='PyAMS paragraph factories')) + value_type=Choice(vocabulary=PARAGRAPH_FACTORIES_VOCABULARY)) class IParagraphRenderer(IContentProvider): diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/component/paragraph/interfaces/audio.py --- a/src/pyams_content/component/paragraph/interfaces/audio.py Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/component/paragraph/interfaces/audio.py Thu Sep 06 11:27:55 2018 +0200 @@ -51,7 +51,7 @@ author = TextLine(title=_("Author"), description=_("Name of document's author"), - required=True) + required=False) renderer = Choice(title=_("Audio template"), description=_("Presentation template used for this audio file"), diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/component/paragraph/interfaces/keynumber.py --- a/src/pyams_content/component/paragraph/interfaces/keynumber.py Tue Jul 17 15:12:43 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,39 +0,0 @@ -# -# 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.keynumber.interfaces import IKeyNumberContainerTarget -from pyams_content.component.paragraph import IBaseParagraph - -# import packages -from zope.schema import Bool, Choice, TextLine - -from pyams_content import _ - - -KEYNUMBER_PARAGRAPH_TYPE = 'KeyNumbers' -KEYNUMBER_PARAGRAPH_NAME = _("Key numbers") -KEYNUMBER_PARAGRAPH_RENDERERS = 'PyAMS.keynumbers.renderers' - - -class IKeyNumberParagraph(IKeyNumberContainerTarget, IBaseParagraph): - """Key numbers paragraph interface""" - - renderer = Choice(title=_("Key numbers template"), - description=_("Presentation template used for key numbers"), - vocabulary=KEYNUMBER_PARAGRAPH_RENDERERS, - default='default') diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/component/paragraph/interfaces/map.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_content/component/paragraph/interfaces/map.py Thu Sep 06 11:27:55 2018 +0200 @@ -0,0 +1,60 @@ +# +# Copyright (c) 2008-2018 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' + + +try: + from pyams_gis.schema import GeoPointField +except ImportError: + have_gis = False +else: + have_gis = True + +if have_gis: + + # import standard library + + # import interfaces + from pyams_content.component.paragraph.interfaces import IBaseParagraph + + # import packages + from zope.schema import Choice, Bool + + from pyams_content import _ + + + # + # Map paragraph + # + + MAP_PARAGRAPH_TYPE = 'Map' + MAP_PARAGRAPH_NAME = _("Location map") + MAP_PARAGRAPH_RENDERERS = 'PyAMS.paragraph.map.renderers' + + + class IMapParagraph(IBaseParagraph): + """Map paragraph interface""" + + gps_location = GeoPointField(title=_("GPS location"), + description=_("GPS coordinates used to locate map"), + required=False) + + display_marker = Bool(title=_("Display location mark?"), + description=_("If 'yes', a location marker will be displayed on map"), + required=True, + default=True) + + renderer = Choice(title=_("Map template"), + description=_("Presentation template used for this map"), + vocabulary=MAP_PARAGRAPH_RENDERERS, + default='default') diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/component/paragraph/interfaces/video.py --- a/src/pyams_content/component/paragraph/interfaces/video.py Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/component/paragraph/interfaces/video.py Thu Sep 06 11:27:55 2018 +0200 @@ -45,8 +45,8 @@ title = I18nTextLineField(title=_("Legend"), required=False) - description = I18nTextField(title=_("Description"), - description=_("File description displayed by front-office template"), + description = I18nTextField(title=_("Associated text"), + description=_("Video description displayed by front-office template"), required=False) author = TextLine(title=_("Author"), diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/component/paragraph/keynumber.py --- a/src/pyams_content/component/paragraph/keynumber.py Tue Jul 17 15:12:43 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,93 +0,0 @@ -# -# 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 IParagraphFactory -from pyams_content.component.paragraph.interfaces.keynumber import IKeyNumberParagraph, KEYNUMBER_PARAGRAPH_TYPE, \ - KEYNUMBER_PARAGRAPH_RENDERERS, KEYNUMBER_PARAGRAPH_NAME -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, BaseParagraphContentChecker -from pyams_content.features.renderer import RenderersVocabulary -from pyams_utils.adapter import adapter_config -from pyams_utils.factory import factory_config -from pyams_utils.registry import get_utility, utility_config -from pyams_utils.request import check_request -from pyams_utils.traversing import get_parent -from pyams_utils.vocabulary import vocabulary_config -from zope.interface import implementer -from zope.schema.fieldproperty import FieldProperty - - -@implementer(IKeyNumberParagraph) -@factory_config(provided=IKeyNumberParagraph) -class KeyNumberParagraph(BaseParagraph): - """Key numbers paragraph""" - - icon_class = 'fa-dashboard' - icon_hint = KEYNUMBER_PARAGRAPH_NAME - - renderer = FieldProperty(IKeyNumberParagraph['renderer']) - - -@utility_config(name=KEYNUMBER_PARAGRAPH_TYPE, provides=IParagraphFactory) -class KeyNumberParagraphFactory(BaseParagraphFactory): - """Key numbers paragraph factory""" - - name = KEYNUMBER_PARAGRAPH_NAME - content_type = KeyNumberParagraph - - -@adapter_config(context=IKeyNumberParagraph, provides=IContentChecker) -class KeyNumberParagraphContentChecker(BaseParagraphContentChecker): - """Key numbers 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 = [] - 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, ) - i18n = II18n(self.context) - for lang in langs: - value = i18n.get_attribute('title', lang, request) - if not value: - field_title = translate(IKeyNumberParagraph['title'].title) - if len(langs) == 1: - output.append(translate(MISSING_VALUE).format(field=field_title)) - else: - output.append(translate(MISSING_LANG_VALUE).format(field=field_title, lang=lang)) - return output - - -@vocabulary_config(name=KEYNUMBER_PARAGRAPH_RENDERERS) -class KeyNumberParagraphRendererVocabulary(RenderersVocabulary): - """Key numbers paragraph renderers vocabulary""" - - content_interface = IKeyNumberParagraph diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/component/paragraph/map.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_content/component/paragraph/map.py Thu Sep 06 11:27:55 2018 +0200 @@ -0,0 +1,64 @@ +# +# Copyright (c) 2008-2018 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' + + +from pyams_content.component.paragraph.interfaces.map import have_gis +if have_gis: + + # import standard library + + # import interfaces + from pyams_content.component.paragraph.interfaces import IParagraphFactory + from pyams_content.component.paragraph.interfaces.map import IMapParagraph, MAP_PARAGRAPH_NAME, have_gis, \ + MAP_PARAGRAPH_TYPE, MAP_PARAGRAPH_RENDERERS + + # import packages + from pyams_content.component.paragraph import BaseParagraph, BaseParagraphFactory + from pyams_content.features.renderer import RenderersVocabulary + from pyams_utils.factory import factory_config + from pyams_utils.registry import utility_config + from pyams_utils.vocabulary import vocabulary_config + from zope.interface import implementer + from zope.schema.fieldproperty import FieldProperty + + + @implementer(IMapParagraph) + @factory_config(provided=IMapParagraph) + class MapParagraph(BaseParagraph): + """Map paragraph""" + + icon_class = 'fa-map-marker' + icon_hint = MAP_PARAGRAPH_NAME + + if have_gis: + gps_location = FieldProperty(IMapParagraph['gps_location']) + display_marker = FieldProperty(IMapParagraph['display_marker']) + + renderer = FieldProperty(IMapParagraph['renderer']) + + + @utility_config(name=MAP_PARAGRAPH_TYPE, provides=IParagraphFactory) + class MapParagraphFactory(BaseParagraphFactory): + """Map paragraph factory""" + + name = MAP_PARAGRAPH_NAME + content_type = MapParagraph + secondary_menu = True + + + @vocabulary_config(name=MAP_PARAGRAPH_RENDERERS) + class MapParagraphRenderersVocabulary(RenderersVocabulary): + """Map paragraph renderers vocabulary""" + + content_interface = IMapParagraph diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/component/paragraph/zmi/__init__.py --- a/src/pyams_content/component/paragraph/zmi/__init__.py Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/component/paragraph/zmi/__init__.py Thu Sep 06 11:27:55 2018 +0200 @@ -37,7 +37,7 @@ from pyams_form.schema import ActionButton, CloseButton from pyams_form.security import ProtectedFormObjectMixin from pyams_pagelet.pagelet import pagelet_config -from pyams_skin.event import get_json_switched_table_refresh_event +from pyams_skin.event import get_json_switched_table_refresh_event, get_json_widget_refresh_event from pyams_skin.table import get_element_id from pyams_skin.viewlet.menu import MenuItem, MenuDivider from pyams_skin.viewlet.toolbar import ToolbarMenuItem @@ -175,7 +175,7 @@ return MenuDivider.__new__(cls) for factory_name in settings.allowed_paragraphs or (): factory = query_utility(IParagraphFactory, name=factory_name) - if factory.secondary_menu: + if (factory is not None) and factory.secondary_menu: return MenuDivider.__new__(cls) return None @@ -241,6 +241,20 @@ if 'title' in changes.get(IBaseParagraph, ()): output.setdefault('events', []).append( get_json_paragraph_refresh_event(self.context, self.request)) + elif 'renderer' in self.widgets: + renderer_interface = self.widgets['renderer'].field.interface + if 'renderer' in changes.get(renderer_interface): + output.setdefault('events', []).append( + get_json_widget_refresh_event(self.context, self.request, self.__class__, 'renderer')) + renderer = self.context.get_renderer() + if (renderer is not None) and \ + (renderer.settings_interface is not None): + output['smallbox'] = { + 'status': 'info', + 'message': self.request.localizer.translate(_("You changed renderer selection. Don't omit to " + "update new renderer properties...")), + 'timeout': 5000 + } return output diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/component/paragraph/zmi/container.py --- a/src/pyams_content/component/paragraph/zmi/container.py Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/component/paragraph/zmi/container.py Thu Sep 06 11:27:55 2018 +0200 @@ -44,7 +44,7 @@ from pyams_skin.container import switch_element_visibility, switch_element_attribute from pyams_skin.page import DefaultPageHeaderAdapter from pyams_skin.table import BaseTable, I18nColumn, TrashColumn, SorterColumn, ImageColumn, \ - VisibilitySwitcherColumn + VisibilitySwitcherColumn, AttributeSwitcherColumn from pyams_skin.viewlet.menu import MenuItem from pyams_template.template import template_config from pyams_utils.adapter import adapter_config, ContextRequestViewAdapter, NullAdapter @@ -108,11 +108,29 @@ 'data-ams-post-reload': 'PyAMS_content.paragraphs.postReload', 'data-ams-tablednd-drag-handle': 'td.sorter', 'data-ams-tablednd-drop-target': 'set-paragraphs-order.json', - 'data-ams-visibility-switcher': 'switch-paragraph-visibility.json', - 'data-ams-anchor-switcher': 'switch-paragraph-anchor.json' + 'data-ams-anchor-icon-on': 'fa fa-fw fa-anchor', + 'data-ams-anchor-icon-off': 'fa fa-fw fa-anchor txt-color-silver opacity-50' + }) + attributes.setdefault('td', {}).update({ + 'data-ams-attribute-switcher': self.get_switcher_target, + 'data-ams-switcher-attribute-name': self.get_switcher_attribute }) return attributes + @staticmethod + def get_switcher_target(element, column): + if column.__name__ == 'show-hide': + return 'switch-paragraph-visibility.json' + elif column.__name__ == 'anchor': + return 'switch-paragraph-anchor.json' + + @staticmethod + def get_switcher_attribute(element, column): + if column.__name__ == 'show-hide': + return 'visible' + elif column.__name__ == 'anchor': + return 'anchor' + class ParagraphContainerTable(ParagraphContainerBaseTable): """Paragraph container base table""" @@ -186,16 +204,16 @@ @adapter_config(name='anchor', context=(IParagraphContainerTarget, IPyAMSLayer, ParagraphContainerBaseTable), provides=IColumn) -class ParagraphContainerAnchorColumn(ProtectedFormObjectMixin, VisibilitySwitcherColumn): +class ParagraphContainerAnchorColumn(ProtectedFormObjectMixin, AttributeSwitcherColumn): """Paragraphs container anchor switcher column""" switch_attribute = 'anchor' - visible_icon_class = 'fa fa-fw fa-anchor' - hidden_icon_class = 'fa fa-fw fa-anchor txt-color-silver opacity-50' + + on_icon_class = 'fa fa-fw fa-anchor' + off_icon_class = 'fa fa-fw fa-anchor txt-color-silver opacity-50' icon_hint = _("Set navigation anchor") - url = 'PyAMS_content.paragraphs.switchAnchor' weight = 6 diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/component/paragraph/zmi/frame.py --- a/src/pyams_content/component/paragraph/zmi/frame.py Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/component/paragraph/zmi/frame.py Thu Sep 06 11:27:55 2018 +0200 @@ -69,7 +69,8 @@ 'ams-tinymce-plugins': ['lists', ], 'ams-tinymce-toolbar': 'undo redo | bold italic | bullist numlist', 'ams-tinymce-toolbar1': False, - 'ams-tinymce-toolbar2': False + 'ams-tinymce-toolbar2': False, + 'ams-tinymce-height': 150 } @@ -83,7 +84,7 @@ """Framed text paragraph add menu""" label = _("Framed text...") - label_css_class = 'fa fa-fw fa-list-alt' + label_css_class = 'fa fa-fw fa-window-maximize' url = 'add-frame-paragraph.html' paragraph_type = FRAME_PARAGRAPH_TYPE @@ -98,7 +99,7 @@ legend = _("Add new framed text paragraph") dialog_class = 'modal-large' - icon_css_class = 'fa fa-fw fa-list-alt' + icon_css_class = 'fa fa-fw fa-window-maximize' label_css_class = 'control-label col-md-2' input_css_class = 'col-md-10' @@ -124,7 +125,7 @@ legend = _("Edit framed text paragraph properties") dialog_class = 'modal-large' - icon_css_class = 'fa fa-fw fa-list-alt' + icon_css_class = 'fa fa-fw fa-window-maximize' label_css_class = 'control-label col-md-2' input_css_class = 'col-md-10' diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/component/paragraph/zmi/header.py --- a/src/pyams_content/component/paragraph/zmi/header.py Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/component/paragraph/zmi/header.py Thu Sep 06 11:27:55 2018 +0200 @@ -16,68 +16,27 @@ # import standard library # import interfaces -from pyams_content.component.paragraph.interfaces import IParagraphContainerTarget, IParagraphContainer -from pyams_content.component.paragraph.interfaces.header import IHeaderParagraph, HEADER_PARAGRAPH_TYPE -from pyams_content.component.paragraph.zmi.interfaces import IParagraphInnerEditor, IParagraphContainerView +from pyams_content.component.paragraph.interfaces.header import IHeaderParagraph +from pyams_content.component.paragraph.zmi.interfaces import IParagraphInnerEditor from pyams_content.interfaces import MANAGE_CONTENT_PERMISSION from pyams_form.interfaces.form import IInnerForm -from pyams_skin.interfaces.viewlet import IToolbarAddingMenu from pyams_skin.layer import IPyAMSLayer from z3c.form.interfaces import INPUT_MODE # import packages -from pyams_content.component.paragraph.header import HeaderParagraph -from pyams_content.component.paragraph.zmi import BaseParagraphAJAXAddForm, BaseParagraphAJAXEditForm, \ - BaseParagraphAddMenu, BaseParagraphPropertiesEditForm, get_json_paragraph_refresh_event, IParagraphEditFormButtons +from pyams_content.component.paragraph.zmi import BaseParagraphAJAXEditForm, \ + BaseParagraphPropertiesEditForm, get_json_paragraph_refresh_event, IParagraphEditFormButtons from pyams_content.features.renderer.zmi.widget import RendererFieldWidget from pyams_form.form import ajax_config from pyams_pagelet.pagelet import pagelet_config from pyams_skin.event import get_json_widget_refresh_event from pyams_utils.adapter import adapter_config -from pyams_viewlet.viewlet import viewlet_config -from pyams_zmi.form import AdminDialogAddForm from z3c.form import field, button from zope.interface import implementer from pyams_content import _ -@viewlet_config(name='add-header-paragraph.menu', context=IParagraphContainerTarget, view=IParagraphContainerView, - layer=IPyAMSLayer, manager=IToolbarAddingMenu, weight=40) -class HeaderParagraphAddMenu(BaseParagraphAddMenu): - """Header paragraph add menu""" - - label = _("Header...") - label_css_class = 'fa fa-fw fa-download fa-rotate-180' - url = 'add-header-paragraph.html' - paragraph_type = HEADER_PARAGRAPH_TYPE - - -@pagelet_config(name='add-header-paragraph.html', context=IParagraphContainerTarget, layer=IPyAMSLayer, - permission=MANAGE_CONTENT_PERMISSION) -@ajax_config(name='add-header-paragraph.json', context=IParagraphContainerTarget, layer=IPyAMSLayer, - base=BaseParagraphAJAXAddForm) -class HeaderParagraphAddForm(AdminDialogAddForm): - """Header paragraph add form""" - - legend = _("Add new header paragraph") - icon_css_class = 'fa fa-fw fa-download fa-rotate-180' - - fields = field.Fields(IHeaderParagraph).select('header', 'renderer') - edit_permission = MANAGE_CONTENT_PERMISSION - - def updateWidgets(self, prefix=None): - super(HeaderParagraphAddForm, self).updateWidgets(prefix) - if 'header' in self.widgets: - self.widgets['header'].widget_css_class = 'input height-100' - - def create(self, data): - return HeaderParagraph() - - def add(self, object): - IParagraphContainer(self.context).append(object) - - @pagelet_config(name='properties.html', context=IHeaderParagraph, layer=IPyAMSLayer, permission=MANAGE_CONTENT_PERMISSION) @ajax_config(name='properties.json', context=IHeaderParagraph, layer=IPyAMSLayer, diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/component/paragraph/zmi/keynumber.py --- a/src/pyams_content/component/paragraph/zmi/keynumber.py Tue Jul 17 15:12:43 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,127 +0,0 @@ -# -# 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 -from pyams_content.component.paragraph.interfaces.keynumber import KEYNUMBER_PARAGRAPH_TYPE, IKeyNumberParagraph -from pyams_content.component.paragraph.zmi import IParagraphContainerView, IParagraphEditFormButtons -from pyams_content.component.paragraph.zmi.interfaces import IParagraphInnerEditor -from pyams_content.interfaces import MANAGE_CONTENT_PERMISSION -from pyams_content.shared.common import IWfSharedContent -from pyams_form.interfaces.form import IInnerForm -from pyams_i18n.interfaces import II18n -from pyams_skin.interfaces.viewlet import IToolbarAddingMenu -from pyams_skin.layer import IPyAMSLayer -from z3c.form.interfaces import INPUT_MODE - -# import packages -from pyams_content.component.keynumber.zmi import IKeyNumbersParentForm -from pyams_content.component.paragraph.keynumber import KeyNumberParagraph -from pyams_content.component.paragraph.zmi import BaseParagraphAddMenu, BaseParagraphAJAXAddForm, \ - BaseParagraphPropertiesEditForm, BaseParagraphAJAXEditForm -from pyams_content.features.renderer.zmi.widget import RendererFieldWidget -from pyams_form.form import ajax_config -from pyams_pagelet.pagelet import pagelet_config -from pyams_skin.event import get_json_widget_refresh_event -from pyams_utils.adapter import adapter_config -from pyams_utils.traversing import get_parent -from pyams_viewlet.viewlet import viewlet_config -from pyams_zmi.form import AdminDialogAddForm -from z3c.form import field, button -from zope.interface import implementer, Interface - -from pyams_content import _ - - -@viewlet_config(name='add-keynumber-paragraph.menu', context=IParagraphContainerTarget, view=IParagraphContainerView, - layer=IPyAMSLayer, manager=IToolbarAddingMenu, weight=600) -class KeyNumberParagraphAddMenu(BaseParagraphAddMenu): - """Key number paragraph add menu""" - - label = _("Key numbers...") - label_css_class = 'fa fa-fw fa-dashboard' - url = 'add-keynumber-paragraph.html' - paragraph_type = KEYNUMBER_PARAGRAPH_TYPE - - -@pagelet_config(name='add-keynumber-paragraph.html', context=IParagraphContainerTarget, layer=IPyAMSLayer, - permission=MANAGE_CONTENT_PERMISSION) -@ajax_config(name='add-keynumber-paragraph.json', context=IParagraphContainerTarget, layer=IPyAMSLayer, - base=BaseParagraphAJAXAddForm) -class KeyNumberParagraphAddForm(AdminDialogAddForm): - """Key number paragraph add form""" - - legend = _("Add new key number paragraph") - icon_css_class = 'fa fa-fw fa-dashboard' - - fields = field.Fields(IKeyNumberParagraph).select('title', 'renderer') - edit_permission = MANAGE_CONTENT_PERMISSION - - def create(self, data): - return KeyNumberParagraph() - - def add(self, object): - IParagraphContainer(self.context).append(object) - - -@pagelet_config(name='properties.html', context=IKeyNumberParagraph, layer=IPyAMSLayer, - permission=MANAGE_CONTENT_PERMISSION) -@ajax_config(name='properties.json', context=IKeyNumberParagraph, layer=IPyAMSLayer, - base=BaseParagraphAJAXEditForm) -@implementer(IKeyNumbersParentForm) -class KeyNumberParagraphPropertiesEditForm(BaseParagraphPropertiesEditForm): - """Key number paragraph properties edit form""" - - prefix = 'keynumbers_properties.' - - @property - def title(self): - content = get_parent(self.context, IWfSharedContent) - return II18n(content).query_attribute('title', request=self.request) - - legend = _("Edit key number paragraph properties") - icon_css_class = 'fa fa-fw fa-dashboard' - - fields = field.Fields(IKeyNumberParagraph).select('title', 'renderer') - fields['renderer'].widgetFactory = RendererFieldWidget - - edit_permission = MANAGE_CONTENT_PERMISSION - - -@adapter_config(context=(IKeyNumberParagraph, IPyAMSLayer), provides=IParagraphInnerEditor) -@ajax_config(name='inner-properties.json', context=IKeyNumberParagraph, layer=IPyAMSLayer, - base=BaseParagraphAJAXEditForm) -@implementer(IInnerForm, IKeyNumbersParentForm) -class KeyNumberParagraphInnerEditForm(KeyNumberParagraphPropertiesEditForm): - """Key number paragraph inner edit form""" - - legend = None - - @property - def buttons(self): - if self.mode == INPUT_MODE: - return button.Buttons(IParagraphEditFormButtons) - else: - return button.Buttons() - - def get_ajax_output(self, changes): - output = super(self.__class__, self).get_ajax_output(changes) - updated = changes.get(IKeyNumberParagraph, ()) - if 'renderer' in updated: - output.setdefault('events', []).append( - get_json_widget_refresh_event(self.context, self.request, KeyNumberParagraphInnerEditForm, 'renderer')) - return output diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/component/paragraph/zmi/map.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_content/component/paragraph/zmi/map.py Thu Sep 06 11:27:55 2018 +0200 @@ -0,0 +1,125 @@ +# +# Copyright (c) 2008-2018 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' + + +from pyams_content.component.paragraph.interfaces.map import have_gis +if have_gis: + + # import standard library + + # import interfaces + from pyams_content.component.paragraph.interfaces import IParagraphContainerTarget, IParagraphContainer, \ + IBaseParagraph, PARAGRAPH_HIDDEN_FIELDS + from pyams_content.component.paragraph.interfaces.map import MAP_PARAGRAPH_TYPE, IMapParagraph + from pyams_content.component.paragraph.zmi.interfaces import IParagraphInnerEditor + from pyams_content.interfaces import MANAGE_CONTENT_PERMISSION + from pyams_form.interfaces.form import IInnerForm + from pyams_skin.interfaces.viewlet import IToolbarAddingMenu + from pyams_skin.layer import IPyAMSLayer + from z3c.form.interfaces import INPUT_MODE + + # import packages + from pyams_content.component.paragraph.map import MapParagraph + from pyams_content.component.paragraph.zmi import IParagraphContainerView, BaseParagraphAddMenu, \ + BaseParagraphAJAXAddForm, BaseParagraphAJAXEditForm, BaseParagraphPropertiesEditForm, IParagraphEditFormButtons, \ + get_json_paragraph_refresh_event + from pyams_content.features.renderer.zmi.widget import RendererFieldWidget + from pyams_form.form import ajax_config + from pyams_pagelet.pagelet import pagelet_config + from pyams_skin.event import get_json_form_refresh_event + from pyams_utils.adapter import adapter_config + from pyams_viewlet.viewlet import viewlet_config + from pyams_zmi.form import AdminDialogAddForm + from z3c.form import field, button + from zope.interface import implementer + + from pyams_content import _ + + + @viewlet_config(name='add-map-paragraph.menu', context=IParagraphContainerTarget, view=IParagraphContainerView, + layer=IPyAMSLayer, manager=IToolbarAddingMenu, weight=600) + class MapParagraphAddMenu(BaseParagraphAddMenu): + """Map paragraph add menu""" + + label = _("Location map...") + label_css_class = 'fa fa-fw fa-map-marker' + url = 'add-map-paragraph.html' + paragraph_type = MAP_PARAGRAPH_TYPE + + + @pagelet_config(name='add-map-paragraph.html', context=IParagraphContainerTarget, layer=IPyAMSLayer, + permission=MANAGE_CONTENT_PERMISSION) + @ajax_config(name='add-map-paragraph.json', context=IParagraphContainerTarget, layer=IPyAMSLayer, + base=BaseParagraphAJAXAddForm) + class MapParagraphAddForm(AdminDialogAddForm): + """Map paragraph add form""" + + legend = _("Add new location map") + dialog_class = 'modal-large' + icon_css_class = 'fa fa-fw fa-map-marker' + + fields = field.Fields(IMapParagraph).omit(*PARAGRAPH_HIDDEN_FIELDS) + edit_permission = MANAGE_CONTENT_PERMISSION + + def create(self, data): + return MapParagraph() + + def add(self, object): + IParagraphContainer(self.context).append(object) + + + @pagelet_config(name='properties.html', context=IMapParagraph, layer=IPyAMSLayer, + permission=MANAGE_CONTENT_PERMISSION) + @ajax_config(name='properties.json', context=IMapParagraph, request_type=IPyAMSLayer, + base=BaseParagraphAJAXEditForm) + class MapParagraphPropertiesEditForm(BaseParagraphPropertiesEditForm): + """Map paragraph properties edit form""" + + prefix = 'map_properties.' + + legend = _("Edit location map properties") + icon_css_class = 'fa fa-fw fa-map-marker' + + fields = field.Fields(IMapParagraph).omit(*PARAGRAPH_HIDDEN_FIELDS) + fields['renderer'].widgetFactory = RendererFieldWidget + + edit_permission = MANAGE_CONTENT_PERMISSION + + + @adapter_config(context=(IMapParagraph, IPyAMSLayer), provides=IParagraphInnerEditor) + @ajax_config(name='inner-properties.json', context=IMapParagraph, layer=IPyAMSLayer, + base=BaseParagraphAJAXEditForm) + @implementer(IInnerForm) + class MapParagraphInnerEditForm(MapParagraphPropertiesEditForm): + """Map paragraph inner edit form""" + + legend = None + + @property + def buttons(self): + if self.mode == INPUT_MODE: + return button.Buttons(IParagraphEditFormButtons) + else: + return button.Buttons() + + def get_ajax_output(self, changes): + output = super(self.__class__, self).get_ajax_output(changes) + updated = changes.get(IBaseParagraph, ()) + if 'title' in updated: + output.setdefault('events', []).append(get_json_paragraph_refresh_event(self.context, self.request)) + updated = changes.get(IMapParagraph, ()) + if 'renderer' in updated: + output.setdefault('events', []).append(get_json_form_refresh_event(self.context, self.request, + MapParagraphInnerEditForm)) + return output diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/component/paragraph/zmi/milestone.py --- a/src/pyams_content/component/paragraph/zmi/milestone.py Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/component/paragraph/zmi/milestone.py Thu Sep 06 11:27:55 2018 +0200 @@ -179,11 +179,18 @@ 'id': self.id, 'data-ams-location': absolute_url(IMilestoneContainer(self.context), self.request), 'data-ams-tablednd-drag-handle': 'td.sorter', - 'data-ams-tablednd-drop-target': 'set-milestones-order.json', - 'data-ams-visibility-switcher': 'switch-milestone-visibility.json' + 'data-ams-tablednd-drop-target': 'set-milestones-order.json' } + attributes.setdefault('td', {}).update({ + 'data-ams-attribute-switcher': self.get_switcher_target + }) return attributes + @staticmethod + def get_switcher_target(element, column): + if column.__name__ == 'show-hide': + return 'switch-milestone-visibility.json' + @reify def values(self): return list(super(MilestonesTable, self).values) diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/component/paragraph/zmi/pictogram.py --- a/src/pyams_content/component/paragraph/zmi/pictogram.py Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/component/paragraph/zmi/pictogram.py Thu Sep 06 11:27:55 2018 +0200 @@ -181,11 +181,18 @@ 'id': self.id, 'data-ams-location': absolute_url(IPictogramContainer(self.context), self.request), 'data-ams-tablednd-drag-handle': 'td.sorter', - 'data-ams-tablednd-drop-target': 'set-pictograms-order.json', - 'data-ams-visibility-switcher': 'switch-pictogram-visibility.json' + 'data-ams-tablednd-drop-target': 'set-pictograms-order.json' } + attributes.setdefault('td', {}).update({ + 'data-ams-attribute-switcher': self.get_switcher_target + }) return attributes + @staticmethod + def get_switcher_target(element, column): + if column.__name__ == 'show-hide': + return 'switch-pictogram-visibility.json' + @reify def values(self): return list(super(PictogramsTable, self).values) @@ -214,6 +221,19 @@ return {'status': 'success'} +@adapter_config(name='show-hide', context=(IPictogramContainerTarget, IPyAMSLayer, PictogramsTable), + provides=IColumn) +class PictogramsTableShowHideColumn(ProtectedFormObjectMixin, VisibilitySwitcherColumn): + """Pictograms container visibility switcher column""" + + +@view_config(name='switch-pictogram-visibility.json', context=IPictogramContainer, request_type=IPyAMSLayer, + permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True) +def switch_pictogram_visibility(request): + """Set pictogram visibility""" + return switch_element_visibility(request, IPictogramContainer) + + @adapter_config(name='image', context=(IPictogramContainerTarget, IPyAMSLayer, PictogramsTable), provides=IColumn) class PictogramsTableImageColumn(GetAttrColumn): """Pictogram image column""" @@ -233,19 +253,6 @@ return '--' -@adapter_config(name='show-hide', context=(IPictogramContainerTarget, IPyAMSLayer, PictogramsTable), - provides=IColumn) -class PictogramsTableShowHideColumn(ProtectedFormObjectMixin, VisibilitySwitcherColumn): - """Pictograms container visibility switcher column""" - - -@view_config(name='switch-pictogram-visibility.json', context=IPictogramContainer, request_type=IPyAMSLayer, - permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True) -def switch_pictogram_visibility(request): - """Set pictogram visibility""" - return switch_element_visibility(request, IPictogramContainer) - - @adapter_config(name='name', context=(IPictogramContainerTarget, IPyAMSLayer, PictogramsTable), provides=IColumn) class PictogramsTableNameColumn(I18nColumn, I18nAttrColumn): """Pictograms table name column""" @@ -257,7 +264,9 @@ def getValue(self, obj): value = super(PictogramsTableNameColumn, self).getValue(obj) if not value: - value = II18n(obj.pictogram).query_attribute('header', request=self.request) + pictogram = obj.pictogram + if pictogram is not None: + value = II18n(pictogram).query_attribute('header', request=self.request) return value or BaseParagraph.empty_title diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/component/theme/zmi/manager.py --- a/src/pyams_content/component/theme/zmi/manager.py Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/component/theme/zmi/manager.py Thu Sep 06 11:27:55 2018 +0200 @@ -9,7 +9,6 @@ # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # -from pyams_content.component.theme import ICollectionsManagerTarget, ICollectionsManager __docformat__ = 'restructuredtext' @@ -17,8 +16,10 @@ # import standard library # import interfaces -from pyams_content.component.theme.interfaces import ITagsManagerTarget, ITagsManager, IThemesManagerTarget, \ - IThemesManager +from pyams_content.component.theme.interfaces import \ + ITagsManagerTarget, ITagsManager, \ + IThemesManagerTarget, IThemesManager, \ + ICollectionsManagerTarget, ICollectionsManager from pyams_content.interfaces import MANAGE_TOOL_PERMISSION, MANAGE_SITE_ROOT_PERMISSION from pyams_skin.layer import IPyAMSLayer from pyams_utils.interfaces.data import IObjectData @@ -29,7 +30,7 @@ from pyams_content.skin import pyams_content from pyams_form.form import ajax_config from pyams_pagelet.pagelet import pagelet_config -from pyams_skin.viewlet.menu import MenuItem +from pyams_skin.viewlet.menu import MenuItem, MenuDivider from pyams_utils.fanstatic import get_resource_path from pyams_viewlet.viewlet import viewlet_config from pyams_zmi.form import AdminDialogEditForm @@ -43,6 +44,12 @@ # Tags management view # +@viewlet_config(name='tags.divider', context=ITagsManagerTarget, layer=IAdminLayer, + manager=IPropertiesMenu, permission=MANAGE_SITE_ROOT_PERMISSION, weight=39) +class TagsManagerMenuDivider(MenuDivider): + """Tags manager menu divider""" + + @viewlet_config(name='tags-manager.menu', context=ITagsManagerTarget, layer=IAdminLayer, manager=IPropertiesMenu, permission=MANAGE_SITE_ROOT_PERMISSION, weight=40) class TagsManagerMenu(MenuItem): diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/component/video/interfaces/__init__.py --- a/src/pyams_content/component/video/interfaces/__init__.py Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/component/video/interfaces/__init__.py Thu Sep 06 11:27:55 2018 +0200 @@ -45,8 +45,8 @@ class IExternalVideo(IAttributeAnnotatable): """Base interface for external video integration""" - description = I18nTextField(title=_("Description"), - description=_("File description displayed by front-office template"), + description = I18nTextField(title=_("Associated text"), + description=_("Video description displayed by front-office template"), required=False) author = TextLine(title=_("Author"), diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/features/alert/__init__.py --- a/src/pyams_content/features/alert/__init__.py Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/features/alert/__init__.py Thu Sep 06 11:27:55 2018 +0200 @@ -19,58 +19,28 @@ # import interfaces from pyams_content.features.alert.interfaces import IAlertItem from pyams_content.interfaces import MANAGE_SITE_ROOT_PERMISSION -from pyams_content.reference.pictograms import IPictogramTable from pyams_form.interfaces.form import IFormContextPermissionChecker # import packages -from pyams_sequence.reference import get_reference_target +from pyams_content.component.links import InternalReferenceMixin from pyams_utils.adapter import adapter_config, ContextAdapter -from pyams_utils.registry import query_utility -from pyams_utils.zodb import volatile_property from zope.container.contained import Contained from zope.interface import implementer from zope.schema.fieldproperty import FieldProperty @implementer(IAlertItem) -class AlertItem(Persistent, Contained): +class AlertItem(Persistent, Contained, InternalReferenceMixin): """Alert item persistent class""" visible = FieldProperty(IAlertItem['visible']) gravity = FieldProperty(IAlertItem['gravity']) - header = FieldProperty(IAlertItem['header']) message = FieldProperty(IAlertItem['message']) reference = FieldProperty(IAlertItem['reference']) - _pictogram_name = FieldProperty(IAlertItem['pictogram_name']) start_date = FieldProperty(IAlertItem['start_date']) end_date = FieldProperty(IAlertItem['end_date']) maximum_interval = FieldProperty(IAlertItem['maximum_interval']) - @property - def pictogram_name(self): - return self._pictogram_name - - @pictogram_name.setter - def pictogram_name(self, value): - if value != self._pictogram_name: - self._pictogram_name = value - del self.pictogram - - @volatile_property - def pictogram(self): - table = query_utility(IPictogramTable) - return table.get(self.pictogram_name) - - @volatile_property - def target(self): - return get_reference_target(self.reference) - - def get_target(self, state=None): - if not state: - return self.target - else: - return get_reference_target(self.reference) - @adapter_config(context=IAlertItem, provides=IFormContextPermissionChecker) class AlertitemPermissionChecker(ContextAdapter): diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/features/alert/interfaces.py --- a/src/pyams_content/features/alert/interfaces.py Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/features/alert/interfaces.py Thu Sep 06 11:27:55 2018 +0200 @@ -18,7 +18,6 @@ # import interfaces from pyams_content.interfaces.container import IOrderedContainer -from pyams_content.reference.pictograms.interfaces import PICTOGRAM_VOCABULARY from pyams_sequence.interfaces import IInternalReference from zope.annotation import IAttributeAnnotatable @@ -37,10 +36,11 @@ ALERT_GRAVITY_NAMES = OrderedDict(( - ('success', _("Success")), + ('alert', _("Alert")), + ('alert_end', _("End of alert")), ('info', _("Information")), ('warning', _("Warning")), - ('danger', _("Danger")) + ('recommend', _("Recommendation")) )) ALERT_GRAVITY_VOCABULARY = SimpleVocabulary([SimpleTerm(v, title=t) for v, t in ALERT_GRAVITY_NAMES.items()]) @@ -62,27 +62,16 @@ default='info', vocabulary=ALERT_GRAVITY_VOCABULARY) - header = I18nTextLineField(title=_('alert-header', default="Heading"), - description=_("Short alert header (Alert, Important...)"), - required=False) - message = I18nTextLineField(title=_("Message"), description=_("Alert message"), required=True) reference = InternalReferenceField(title=_("Internal reference"), description=_("Internal link target reference. You can search a reference using " - "'+' followed by internal number, of by entering text matching " - "content title."), + "'+' followed by internal number, of by entering text matching " + "content title."), required=False) - pictogram_name = Choice(title=_("Pictogram"), - description=_("Name of the pictogram to select"), - required=False, - vocabulary=PICTOGRAM_VOCABULARY) - - pictogram = Attribute("Selected pictogram object") - start_date = Datetime(title=_("Display start date"), description=_("First date at which alert should be displayed"), required=False) diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/features/alert/zmi/container.py --- a/src/pyams_content/features/alert/zmi/container.py Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/features/alert/zmi/container.py Thu Sep 06 11:27:55 2018 +0200 @@ -75,11 +75,18 @@ 'data-ams-plugin-pyams_content-src': get_resource_path(pyams_content), 'data-ams-location': absolute_url(IAlertContainer(self.context), self.request), 'data-ams-tablednd-drag-handle': 'td.sorter', - 'data-ams-tablednd-drop-target': 'set-alerts-order.json', - 'data-ams-visibility-switcher': 'switch-alert-visibility.json' + 'data-ams-tablednd-drop-target': 'set-alerts-order.json' + }) + attributes.setdefault('td', {}).update({ + 'data-ams-attribute-switcher': self.get_switcher_target }) return attributes + @staticmethod + def get_switcher_target(element, column): + if column.__name__ == 'show-hide': + return 'switch-alert-visibility.json' + @reify def values(self): return list(super(AlertContainerTable, self).values) @@ -131,44 +138,13 @@ return {'visible': alert.visible} -@adapter_config(name='pictogram', context=(IAlertTarget, IPyAMSLayer, AlertContainerTable), provides=IColumn) -class AlertContainerPictogramColumn(GetAttrColumn): - """Alert container pictogram image column""" - - header = '' - weight = 10 - - cssClasses = {'td': 'text-center width-50'} - dt_sortable = 'false' - - def getValue(self, obj): - pictogram = obj.pictogram - if pictogram is not None: - image = II18n(pictogram).query_attribute('image', request=self.request) - if image: - return ''.format(absolute_url(image, self.request, '++thumb++32x32')) - return '--' - - -@adapter_config(name='header', context=(IAlertTarget, IPyAMSLayer, AlertContainerTable), provides=IColumn) -class AlertContainerHeaderColumn(I18nColumn, I18nAttrColumn): - """Alert container header column""" - - _header = _('alert-header', default="Heading") - attrName = 'header' - weight = 20 - - def getValue(self, obj): - return super(AlertContainerHeaderColumn, self).getValue(obj) or '--' - - @adapter_config(name='name', context=(IAlertTarget, IPyAMSLayer, AlertContainerTable), provides=IColumn) class AlertContainerNameColumn(I18nColumn, I18nAttrColumn): """Alert container message column""" _header = _("Message") attrName = 'message' - weight = 30 + weight = 10 def getValue(self, obj): value = super(AlertContainerNameColumn, self).getValue(obj) diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/features/footer/__init__.py --- a/src/pyams_content/features/footer/__init__.py Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/features/footer/__init__.py Thu Sep 06 11:27:55 2018 +0200 @@ -18,16 +18,20 @@ # import interfaces from pyams_content.features.footer.interfaces import FOOTER_RENDERERS, IFooterRenderer, IFooterSettings, IFooterTarget, \ FOOTER_SETTINGS_KEY, IFooterRendererSettings, FOOTER_RENDERER_SETTINGS_KEY +from zope.lifecycleevent.interfaces import IObjectModifiedEvent from zope.traversing.interfaces import ITraversable # import packages from persistent import Persistent +from pyams_cache.beaker import get_cache from pyams_content.features.renderer import RenderedContentMixin +from pyams_portal.portlet import PORTLETS_CACHE_REGION, PORTLETS_CACHE_NAME from pyams_utils.adapter import adapter_config, ContextAdapter, get_annotation_adapter from pyams_utils.inherit import BaseInheritInfo, InheritedFieldProperty from pyams_utils.request import check_request from pyams_utils.traversing import get_parent from pyams_utils.vocabulary import vocabulary_config +from pyramid.events import subscriber from zope.interface import implementer, noLongerProvides, alsoProvides from zope.location import Location, locate from zope.schema.fieldproperty import FieldProperty @@ -77,6 +81,14 @@ return get_annotation_adapter(context, FOOTER_SETTINGS_KEY, FooterSettings, name='++footer++') +@subscriber(IObjectModifiedEvent, context_selector=IFooterSettings) +def handle_modified_footer_settings(event): + """Clear cache if modified footer settings""" + renderer = event.object.get_renderer() + portlets_cache = get_cache(PORTLETS_CACHE_REGION, PORTLETS_CACHE_NAME) + portlets_cache.remove(renderer.cache_key) + + @adapter_config(name='footer', context=IFooterTarget, provides=ITraversable) class FooterTargetNamespace(ContextAdapter): """Footer target '++footer++' namespace traverser""" diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/features/footer/interfaces/__init__.py --- a/src/pyams_content/features/footer/interfaces/__init__.py Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/features/footer/interfaces/__init__.py Thu Sep 06 11:27:55 2018 +0200 @@ -54,6 +54,7 @@ name = Attribute("Renderer name") settings_key = Attribute("Renderer settings key") + cache_key = Attribute("Renderer cache key") class IFooterRendererSettings(IRendererSettings): diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/features/footer/skin/__init__.py --- a/src/pyams_content/features/footer/skin/__init__.py Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/features/footer/skin/__init__.py Thu Sep 06 11:27:55 2018 +0200 @@ -32,13 +32,18 @@ from pyams_utils.adapter import adapter_config from pyams_utils.traversing import get_parent from pyramid.decorator import reify +from zope.interface import implementer from pyams_content import _ +@implementer(IFooterRenderer) class BaseFooterRenderer(BaseContentRenderer): """Base footer renderer""" + name = None + settings_key = None + @reify def settings_target(self): context = self.request.annotations.get(DISPLAY_CONTEXT) @@ -56,23 +61,28 @@ settings = IFooterSettings(settings.parent) return settings.settings + @reify + def cache_key(self): + return PORTLETS_CACHE_KEY.format(portlet=self.name, + context=ICacheKeyValue(self.settings_target), + lang=self.request.locale_name) + def render(self): preview_mode = self.request.annotations.get(PREVIEW_MODE, False) if preview_mode: return super(BaseFooterRenderer, self).render() else: portlets_cache = get_cache(PORTLETS_CACHE_REGION, PORTLETS_CACHE_NAME) - cache_key = PORTLETS_CACHE_KEY.format(portlet=self.name, - context=ICacheKeyValue(self.settings_target)) + cache_key = self.cache_key if self.context is not self.request.context: # display shared content cache_key = '{0}::shared'.format(cache_key) try: result = portlets_cache.get_value(cache_key) - logger.debug("Retrieving header content from cache key {0}".format(cache_key)) + logger.debug("Retrieving footer content from cache key {0}".format(cache_key)) except KeyError: result = super(BaseFooterRenderer, self).render() portlets_cache.set_value(cache_key, result) - logger.debug("Storing header content for cache key {0}".format(cache_key)) + logger.debug("Storing footer content to cache key {0}".format(cache_key)) return result diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/features/header/__init__.py --- a/src/pyams_content/features/header/__init__.py Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/features/header/__init__.py Thu Sep 06 11:27:55 2018 +0200 @@ -18,16 +18,20 @@ # import interfaces from pyams_content.features.header.interfaces import HEADER_RENDERERS, IHeaderRenderer, IHeaderSettings, IHeaderTarget, \ HEADER_SETTINGS_KEY, IHeaderRendererSettings, HEADER_RENDERER_SETTINGS_KEY +from zope.lifecycleevent.interfaces import IObjectModifiedEvent from zope.traversing.interfaces import ITraversable # import packages from persistent import Persistent +from pyams_cache.beaker import get_cache from pyams_content.features.renderer import RenderedContentMixin +from pyams_portal.portlet import PORTLETS_CACHE_REGION, PORTLETS_CACHE_NAME from pyams_utils.adapter import adapter_config, ContextAdapter, get_annotation_adapter from pyams_utils.inherit import BaseInheritInfo, InheritedFieldProperty from pyams_utils.request import check_request from pyams_utils.traversing import get_parent from pyams_utils.vocabulary import vocabulary_config +from pyramid.events import subscriber from zope.interface import implementer, noLongerProvides, alsoProvides from zope.location import Location, locate from zope.schema.fieldproperty import FieldProperty @@ -77,6 +81,14 @@ return get_annotation_adapter(context, HEADER_SETTINGS_KEY, HeaderSettings, name='++header++') +@subscriber(IObjectModifiedEvent, context_selector=IHeaderSettings) +def handle_modified_header_settings(event): + """Clear cache if modified header settings""" + renderer = event.object.get_renderer() + portlets_cache = get_cache(PORTLETS_CACHE_REGION, PORTLETS_CACHE_NAME) + portlets_cache.remove(renderer.cache_key) + + @adapter_config(name='header', context=IHeaderTarget, provides=ITraversable) class HeaderTargetNamespace(ContextAdapter): """Header target '++header++' namespace traverser""" diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/features/header/interfaces/__init__.py --- a/src/pyams_content/features/header/interfaces/__init__.py Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/features/header/interfaces/__init__.py Thu Sep 06 11:27:55 2018 +0200 @@ -54,6 +54,8 @@ name = Attribute("Renderer name") settings_key = Attribute("Renderer settings key") + cache_key = Attribute("Renderer cache key") + class IHeaderRendererSettings(IRendererSettings): diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/features/header/skin/__init__.py --- a/src/pyams_content/features/header/skin/__init__.py Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/features/header/skin/__init__.py Thu Sep 06 11:27:55 2018 +0200 @@ -33,13 +33,18 @@ from pyams_utils.adapter import adapter_config from pyams_utils.traversing import get_parent from pyramid.decorator import reify +from zope.interface import implementer from pyams_content import _ +@implementer(IHeaderRenderer) class BaseHeaderRenderer(BaseContentRenderer): """Base header renderer""" + name = None + settings_key = None + @reify def settings_target(self): context = self.request.annotations.get(DISPLAY_CONTEXT) @@ -57,6 +62,12 @@ settings = IHeaderSettings(settings.parent) return settings.settings + @reify + def cache_key(self): + return PORTLETS_CACHE_KEY.format(portlet=self.name, + context=ICacheKeyValue(self.settings_target), + lang=self.request.locale_name) + @property def main_header_class(self): request = self.request @@ -68,8 +79,7 @@ return super(BaseHeaderRenderer, self).render() else: portlets_cache = get_cache(PORTLETS_CACHE_REGION, PORTLETS_CACHE_NAME) - cache_key = PORTLETS_CACHE_KEY.format(portlet=self.name, - context=ICacheKeyValue(self.settings_target)) + cache_key = self.cache_key if self.context is not self.request.context: # display shared content cache_key = '{0}::shared'.format(cache_key) try: @@ -78,7 +88,7 @@ except KeyError: result = super(BaseHeaderRenderer, self).render() portlets_cache.set_value(cache_key, result) - logger.debug("Storing header content for cache key {0}".format(cache_key)) + logger.debug("Storing header content to cache key {0}".format(cache_key)) return result diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/features/menu/__init__.py --- a/src/pyams_content/features/menu/__init__.py Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/features/menu/__init__.py Thu Sep 06 11:27:55 2018 +0200 @@ -20,8 +20,7 @@ # import packages from pyams_content.component.association.container import AssociationContainer -from pyams_sequence.reference import get_reference_target -from pyams_utils.zodb import volatile_property +from pyams_content.component.links import InternalReferenceMixin from zope.interface import implementer from zope.schema.fieldproperty import FieldProperty @@ -31,23 +30,13 @@ # @implementer(IMenu) -class Menu(AssociationContainer): +class Menu(AssociationContainer, InternalReferenceMixin): """Associations menu""" visible = FieldProperty(IMenu['visible']) title = FieldProperty(IMenu['title']) reference = FieldProperty(IMenu['reference']) - @volatile_property - def target(self): - return get_reference_target(self.reference) - - def get_target(self, state=None): - if not state: - return self.target - else: - return get_reference_target(self.reference) - @implementer(IMenusContainer) class MenusContainer(AssociationContainer): diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/features/menu/portlet/navigation/double.py --- a/src/pyams_content/features/menu/portlet/navigation/double.py Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/features/menu/portlet/navigation/double.py Thu Sep 06 11:27:55 2018 +0200 @@ -46,7 +46,6 @@ """Double navigation portlet settings""" title = FieldProperty(IDoubleNavigationPortletSettings['title']) - subtitle = FieldProperty(IDoubleNavigationPortletSettings['subtitle']) @property def menus(self): diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/features/menu/portlet/navigation/interfaces/double.py --- a/src/pyams_content/features/menu/portlet/navigation/interfaces/double.py Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/features/menu/portlet/navigation/interfaces/double.py Thu Sep 06 11:27:55 2018 +0200 @@ -32,10 +32,6 @@ description=_("Portlet main title"), required=False) - subtitle = I18nTextLineField(title=_("Subtitle"), - description=_("Portlet subtitle"), - required=False) - menus = Attribute("Navigation menus") diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/features/menu/portlet/navigation/zmi/simple.py --- a/src/pyams_content/features/menu/portlet/navigation/zmi/simple.py Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/features/menu/portlet/navigation/zmi/simple.py Thu Sep 06 11:27:55 2018 +0200 @@ -35,7 +35,7 @@ from pyams_portal.zmi.portlet import PortletSettingsEditor, PortletSettingsPropertiesEditor from pyams_template.template import template_config from pyams_utils.adapter import adapter_config -from zope.interface import alsoProvides, Interface +from zope.interface import Interface from pyams_content import _ @@ -87,4 +87,7 @@ if not IInternalLink.providedBy(link): return True target = link.get_target() - return (target is not None) and IWorkflowPublicationInfo(target).is_published() + if target is not None: + publication_info = IWorkflowPublicationInfo(target, None) + if publication_info is not None: + return publication_info.is_published() diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/features/menu/zmi/__init__.py --- a/src/pyams_content/features/menu/zmi/__init__.py Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/features/menu/zmi/__init__.py Thu Sep 06 11:27:55 2018 +0200 @@ -17,8 +17,6 @@ import json # import interfaces -from pyams_content.component.links.zmi import InternalLinkAddMenu, InternalLinkAddForm, InternalLinkPropertiesEditForm, \ - ExternalLinkAddMenu, ExternalLinkAddForm, ExternalLinkPropertiesEditForm from pyams_content.features.menu import IMenusContainer, IMenu, Menu, IMenuLink from pyams_content.features.menu.interfaces import IMenusContainerTarget, IMenuLinksContainer, IMenuInternalLink, \ IMenuExternalLink, IMenuLinksContainerTarget @@ -33,15 +31,17 @@ # import packages from pyams_content.component.association.zmi import AssociationsTable, AssociationsTablePublicNameColumn +from pyams_content.component.links.zmi import \ + InternalLinkAddMenu, InternalLinkAddForm, InternalLinkPropertiesEditForm, \ + ExternalLinkAddMenu, ExternalLinkAddForm, ExternalLinkPropertiesEditForm from pyams_form.form import ajax_config, AJAXAddForm, AJAXEditForm from pyams_i18n.column import I18nAttrColumn from pyams_pagelet.pagelet import pagelet_config from pyams_skin.container import switch_element_visibility, delete_container_element from pyams_skin.event import get_json_switched_table_refresh_event, get_json_table_row_refresh_event -from pyams_skin.table import BaseTable, SorterColumn, VisibilitySwitcherColumn, I18nColumn, TrashColumn, NameColumn, \ - get_table_id +from pyams_skin.table import BaseTable, SorterColumn, VisibilitySwitcherColumn, I18nColumn, TrashColumn, get_table_id from pyams_skin.viewlet.toolbar import ToolbarAction -from pyams_utils.adapter import adapter_config, ContextRequestViewAdapter, ContextAdapter, NullAdapter +from pyams_utils.adapter import adapter_config, ContextRequestViewAdapter, ContextAdapter from pyams_utils.traversing import get_parent from pyams_utils.url import absolute_url from pyams_viewlet.viewlet import viewlet_config @@ -170,11 +170,18 @@ attributes.setdefault('table', {}).update({ 'data-ams-location': absolute_url(menus, self.request), 'data-ams-tablednd-drag-handle': 'td.sorter', - 'data-ams-tablednd-drop-target': 'set-menus-order.json', - 'data-ams-visibility-switcher': 'switch-menu-visibility.json' + 'data-ams-tablednd-drop-target': 'set-menus-order.json' + }) + attributes.setdefault('td', {}).update({ + 'data-ams-attribute-switcher': self.get_switcher_target }) return attributes + @staticmethod + def get_switcher_target(element, column): + if column.__name__ == 'show-hide': + return 'switch-menu-visibility.json' + @adapter_config(context=(IMenusContainerTarget, IPyAMSLayer, MenusTable), provides=IValues) class MenusTableValuesAdapter(ContextRequestViewAdapter): @@ -487,13 +494,3 @@ """Menu external link properties edit form""" edit_permission = None # managed by IFormContextPermissionChecker adapter - - -# -# Custom adapters -# - -@adapter_config(name='size', context=(IMenu, IPyAMSLayer, MenuLinksTable), provides=IColumn) -@adapter_config(name='size', context=(IMenuLinksContainerTarget, IPyAMSLayer, LinksTable), provides=IColumn) -class MenuLinksTableSizeColumn(NullAdapter): - """Menu links table size column""" diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/features/redirect/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_content/features/redirect/__init__.py Thu Sep 06 11:27:55 2018 +0200 @@ -0,0 +1,78 @@ +# +# Copyright (c) 2008-2018 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 + +# import interfaces +from pyams_content.features.redirect.interfaces import IRedirectionRule +from pyams_content.interfaces import MANAGE_SITE_ROOT_PERMISSION +from pyams_form.interfaces.form import IFormContextPermissionChecker + +# import packages +from persistent import Persistent +from pyams_content.component.links import InternalReferenceMixin +from pyams_utils.adapter import adapter_config, ContextAdapter +from pyams_utils.url import canonical_url +from pyams_utils.zodb import volatile_property +from zope.container.contained import Contained +from zope.interface import implementer +from zope.schema.fieldproperty import FieldProperty + + +@implementer(IRedirectionRule) +class RedirectionRule(Persistent, Contained, InternalReferenceMixin): + """Redirection rule persistent class""" + + active = FieldProperty(IRedirectionRule['active']) + chained = FieldProperty(IRedirectionRule['chained']) + permanent = FieldProperty(IRedirectionRule['permanent']) + _url_pattern = FieldProperty(IRedirectionRule['url_pattern']) + reference = FieldProperty(IRedirectionRule['reference']) + target_url = FieldProperty(IRedirectionRule['target_url']) + + @property + def url_pattern(self): + return self._url_pattern + + @url_pattern.setter + def url_pattern(self, value): + if value != self._url_pattern: + self._url_pattern = value + del self.pattern + + @volatile_property + def pattern(self): + return re.compile(self.url_pattern) + + def match(self, source_url): + return self.pattern.match(source_url) + + def rewrite(self, source_url, request): + target_url = None + if self.reference: + target = self.target + if target is not None: + target_url = canonical_url(target, request) + else: + target_url = self.pattern.sub(self.target_url, source_url) + return target_url + + +@adapter_config(context=IRedirectionRule, provides=IFormContextPermissionChecker) +class RedirectionRulePermissionChecker(ContextAdapter): + """Redirection rule permission checker""" + + edit_permission = MANAGE_SITE_ROOT_PERMISSION diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/features/redirect/container.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_content/features/redirect/container.py Thu Sep 06 11:27:55 2018 +0200 @@ -0,0 +1,103 @@ +# +# Copyright (c) 2008-2018 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.features.redirect.interfaces import IRedirectionManager, IRedirectionRule, IRedirectionManagerTarget, \ + REDIRECT_MANAGER_KEY +from zope.location.interfaces import ISublocations +from zope.traversing.interfaces import ITraversable + +# import packages +from pyams_catalog.utils import index_object +from pyams_utils.adapter import adapter_config, get_annotation_adapter, ContextAdapter +from pyramid.response import Response +from zope.container.ordered import OrderedContainer +from zope.interface import implementer +from zope.location.location import locate + +from pyams_content import _ + + +@implementer(IRedirectionManager) +class RedirectManager(OrderedContainer): + """Redirect manager""" + + last_id = 1 + + def append(self, value, notify=True): + key = str(self.last_id) + if not notify: + # pre-locate item to avoid multiple notifications + locate(value, self, key) + self[key] = value + self.last_id += 1 + if not notify: + # make sure that item is correctly indexed + index_object(value) + + def get_active_items(self): + yield from filter(lambda x: IRedirectionRule(x).active, self.values()) + + def get_response(self, request): + target_url = request.path_qs + for rule in self.get_active_items(): + match = rule.match(target_url) + if match: + target_url = rule.rewrite(target_url, request) + if not rule.chained: + response = Response() + response.status_code = 301 if rule.permanent else 302 + response.location = target_url + return response + + def test_rules(self, source_url, request, check_inactive_rules=False): + if check_inactive_rules: + rules = self.values() + else: + rules = self.get_active_items() + for rule in rules: + match = rule.match(source_url) + if match: + target_url = rule.rewrite(source_url, request) + yield rule, source_url, target_url + if not rule.chained: + raise StopIteration + source_url = target_url + else: + yield rule, source_url, request.localizer.translate(_("not matching")) + + +@adapter_config(context=IRedirectionManagerTarget, provides=IRedirectionManager) +def redirection_manager_factory(context): + """Redirection manager factory""" + return get_annotation_adapter(context, REDIRECT_MANAGER_KEY, RedirectManager, name='++redirect++') + + +@adapter_config(name='redirect', context=IRedirectionManagerTarget, provides=ITraversable) +class RedirectionManagerNamespace(ContextAdapter): + """Redirection manager ++redirect++ namespace""" + + def traverse(self, name, furtherpath=None): + return IRedirectionManager(self.context) + + +@adapter_config(name='redirect', context=IRedirectionManagerTarget, provides=ISublocations) +class RedirectManagerSublocations(ContextAdapter): + """redirection manager sub-locations adapter""" + + def sublocations(self): + return IRedirectionManager(self.context).values() diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/features/redirect/interfaces/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_content/features/redirect/interfaces/__init__.py Thu Sep 06 11:27:55 2018 +0200 @@ -0,0 +1,102 @@ +# +# Copyright (c) 2008-2018 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.interfaces.container import IOrderedContainer +from pyams_sequence.interfaces import IInternalReference + +# import packages +from pyams_sequence.schema import InternalReferenceField +from zope.container.constraints import contains, containers +from zope.interface import Interface, Attribute, invariant, Invalid +from zope.schema import Bool, TextLine, Choice + +from pyams_content import _ + + +REDIRECT_MANAGER_KEY = 'pyams_content.redirect' + + +class IRedirectionRule(IInternalReference): + """Redirection rule interface""" + + containers('.IRedirectManager') + + active = Bool(title=_("Active rule?"), + description=_("If 'no', selected rule is inactive"), + required=True, + default=False) + + chained = Bool(title=_("Chained rule?"), + description=_("If 'no', and if this rule is matching received request URL, the rule " + "returns a redirection response; otherwise, the rule just rewrites the " + "input URL which is forwarded to the next rule"), + required=True, + default=False) + + permanent = Bool(title=_("Permanent redirect?"), + description=_("Define if this redirection should be permanent or temporary"), + required=True, + default=True) + + url_pattern = TextLine(title=_("URL pattern"), + description=_("Regexp pattern of matching URLs for this redirection rule"), + required=True) + + pattern = Attribute("Compiled URL pattern") + + reference = InternalReferenceField(title=_("Internal redirection target"), + description=_("Internal redirection reference. You can search a reference using " + "'+' followed by internal number, of by entering text matching " + "content title."), + required=False) + + target_url = TextLine(title=_("Target URL"), + description=_("URL to which source URL should be redirected"), + required=False) + + @invariant + def check_reference_and_target(self): + if self.reference and self.target_url: + raise Invalid(_("You can only provide an internal reference OR a target URL")) + elif not (self.reference or self.target_url): + raise Invalid(_("You must provide an internal reference OR a target URL")) + + def match(self, source_url): + """Return regexp URL match on given URL""" + + def rewrite(self, source_url, request): + """Rewrite given source URL""" + + +class IRedirectionManager(IOrderedContainer): + """Redirection manager""" + + contains(IRedirectionRule) + + def get_active_items(self): + """Get iterator over active items""" + + def get_response(self, request): + """Get new response for given request""" + + def test_rules(self, source_url, request, check_inactive_rules=False): + """Test rules against given URL""" + + +class IRedirectionManagerTarget(Interface): + """Redirection manager target marker interface""" diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/features/redirect/tween.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_content/features/redirect/tween.py Thu Sep 06 11:27:55 2018 +0200 @@ -0,0 +1,49 @@ +# +# Copyright (c) 2008-2018 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.features.redirect.interfaces import IRedirectionManager + +# import packages +from pyramid.exceptions import NotFound +from pyramid.httpexceptions import HTTPNotFound + + +def redirect_tween_factory(handler, registry): + """Redirect tween factory + + This tween is used to handle NotFound errors: when a request which raises + a NotFound error is served, we look info redirects configuration to check if + given URL is matching any defined redirection, in which case another HTTPRedirect + response is returned with a new location; another content using a proxy request + can also be returned. + """ + + def redirect_tween(request): + try: + response = handler(request) + except (NotFound, HTTPNotFound): + manager = IRedirectionManager(request.root, None) + if manager is not None: + response = manager.get_response(request) + if response is not None: + return response + raise + else: + return response + + return redirect_tween diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/features/redirect/zmi/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_content/features/redirect/zmi/__init__.py Thu Sep 06 11:27:55 2018 +0200 @@ -0,0 +1,126 @@ +# +# Copyright (c) 2008-2018 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_form.help import FormHelp +from pyams_form.interfaces.form import IFormHelp +from pyams_utils.adapter import adapter_config +from pyams_zmi.layer import IAdminLayer + +__docformat__ = 'restructuredtext' + + +# import standard library + +# import interfaces +from pyams_content.features.redirect.interfaces import IRedirectionRule, IRedirectionManagerTarget, IRedirectionManager +from pyams_content.interfaces import MANAGE_SITE_ROOT_PERMISSION +from pyams_skin.interfaces.viewlet import IWidgetTitleViewletManager +from pyams_skin.layer import IPyAMSLayer + +# import packages +from pyams_content.features.redirect import RedirectionRule +from pyams_content.features.redirect.zmi.container import RedirectionsContainerView, RedirectionsContainerTable +from pyams_form.form import ajax_config, AJAXAddForm +from pyams_pagelet.pagelet import pagelet_config +from pyams_skin.event import get_json_table_row_refresh_event +from pyams_skin.viewlet.toolbar import ToolbarAction +from pyams_utils.traversing import get_parent +from pyams_utils.url import absolute_url +from pyams_viewlet.viewlet import viewlet_config +from pyams_zmi.form import AdminDialogAddForm, AdminDialogEditForm +from z3c.form import field + +from pyams_content import _ + + +@viewlet_config(name='add-rule.action', context=IRedirectionManagerTarget, layer=IPyAMSLayer, + view=RedirectionsContainerView, manager=IWidgetTitleViewletManager, + permission=MANAGE_SITE_ROOT_PERMISSION, weight=1) +class RedirectionRuleAddAction(ToolbarAction): + """Redirection rule add action""" + + label = _("Add rule") + label_css_class = 'fa fa-fw fa-plus' + url = 'add-rule.html' + modal_target = True + + +@pagelet_config(name='add-rule.html', context=IRedirectionManagerTarget, layer=IPyAMSLayer, + permission=MANAGE_SITE_ROOT_PERMISSION) +@ajax_config(name='add-rule.json', context=IRedirectionManagerTarget, layer=IPyAMSLayer, base=AJAXAddForm) +class RedirectionRuleAddForm(AdminDialogAddForm): + """Redirection rule add form""" + + dialog_class = 'modal-large' + legend = _("Add new redirection rule") + icon_css_class = 'fa fa-fw fa-map-signs' + + fields = field.Fields(IRedirectionRule).omit('__parent__', '__name__', 'active', 'chained') + edit_permission = MANAGE_SITE_ROOT_PERMISSION + + def create(self, data): + return RedirectionRule() + + def add(self, object): + IRedirectionManager(self.context).append(object) + + def nextURL(self): + return absolute_url(self.context, self.request, 'redirections.html') + + +@pagelet_config(name='properties.html', context=IRedirectionRule, layer=IPyAMSLayer, + permission=MANAGE_SITE_ROOT_PERMISSION) +@ajax_config(name='properties.json', context=IRedirectionRule, layer=IPyAMSLayer) +class RedirectionRulePropertiesEditForm(AdminDialogEditForm): + """Redirection rule properties edit form""" + + dialog_class = 'modal-large' + prefix = 'rule_properties.' + + legend = _("Edit redirection rule properties") + icon_css_class = 'fa fa-fw fa-map-signs' + + fields = field.Fields(IRedirectionRule).omit('__parent__', '__name__', 'active', 'chained') + edit_permission = MANAGE_SITE_ROOT_PERMISSION + + def get_ajax_output(self, changes): + output = super(self.__class__, self).get_ajax_output(changes) + updated = changes.get(IRedirectionRule, ()) + if updated: + target = get_parent(self.context, IRedirectionManagerTarget) + output.setdefault('events', []).append( + get_json_table_row_refresh_event(target, self.request, RedirectionsContainerTable, self.context)) + return output + + +@adapter_config(context=(IRedirectionManagerTarget, IAdminLayer, RedirectionRuleAddForm), provides=IFormHelp) +@adapter_config(context=(IRedirectionRule, IAdminLayer, RedirectionRulePropertiesEditForm), provides=IFormHelp) +class RedirectionRuleFormHelp(FormHelp): + """Redirection rule form help""" + + message = _("""URL pattern and target URL are defined by *regular expressions* (see |regexp|). + +In URL pattern, you can use any valid regular expression element, notably: + +- « .* » to match any list of characters + +- « ( ) » to "memorize" parts of the URL which can be replaced into target URL + +- special characters (like "+") must be escaped with an « \\\\ ». + +In target URL, memorized parts can be reused using « \\\\1 », « \\\\2 » and so on, where given number is +the order of the matching pattern element. + +.. |regexp| raw:: html + + Python Regular Expressions +""") + message_format = 'rest' diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/features/redirect/zmi/container.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_content/features/redirect/zmi/container.py Thu Sep 06 11:27:55 2018 +0200 @@ -0,0 +1,376 @@ +# +# Copyright (c) 2008-2018 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 json + +# import interfaces +from pyams_content.features.redirect.interfaces import IRedirectionManagerTarget, IRedirectionManager +from pyams_content.interfaces import MANAGE_SITE_ROOT_PERMISSION +from pyams_form.interfaces.form import IWidgetsSuffixViewletsManager +from pyams_i18n.interfaces import II18n +from pyams_sequence.interfaces import ISequentialIdInfo +from pyams_skin.interfaces import IPageHeader, IUserSkinnable, IContentHelp +from pyams_skin.interfaces.viewlet import IToolbarViewletManager +from pyams_skin.layer import IPyAMSLayer +from pyams_zmi.interfaces.menu import ISiteManagementMenu +from pyams_zmi.layer import IAdminLayer +from z3c.table.interfaces import IValues, IColumn + +# import packages +from pyams_content.skin import pyams_content +from pyams_form.form import AJAXAddForm +from pyams_form.schema import CloseButton +from pyams_pagelet.pagelet import pagelet_config +from pyams_skin.help import ContentHelp +from pyams_skin.page import DefaultPageHeaderAdapter +from pyams_skin.skin import apply_skin +from pyams_skin.table import BaseTable, SorterColumn, TrashColumn, I18nColumn, AttributeSwitcherColumn +from pyams_skin.viewlet.menu import MenuItem +from pyams_skin.viewlet.toolbar import ToolbarAction +from pyams_template.template import template_config +from pyams_utils.adapter import adapter_config, ContextRequestViewAdapter +from pyams_utils.fanstatic import get_resource_path +from pyams_utils.request import copy_request +from pyams_utils.url import absolute_url +from pyams_viewlet.viewlet import viewlet_config, Viewlet +from pyams_zmi.form import AdminDialogAddForm +from pyams_zmi.view import ContainerAdminView +from pyramid.decorator import reify +from pyramid.exceptions import NotFound +from pyramid.view import view_config +from z3c.form import field, button +from z3c.table.column import GetAttrColumn +from zope.interface import Interface +from zope.schema import TextLine, Bool + +from pyams_content import _ + + +@viewlet_config(name='redirections.menu', context=IRedirectionManagerTarget, layer=IPyAMSLayer, + manager=ISiteManagementMenu, permission=MANAGE_SITE_ROOT_PERMISSION, weight=35) +class RedirectionMenu(MenuItem): + """Redirection manager menu""" + + label = _("Redirections") + icon_class = 'fa-map-signs' + url = '#redirections.html' + + +class RedirectionsContainerTable(BaseTable): + """Redirections container table""" + + prefix = 'redirections' + + hide_header = True + sortOn = None + + cssClasses = {'table': 'table table-bordered table-striped table-hover table-tight table-dnd'} + + @property + def data_attributes(self): + attributes = super(RedirectionsContainerTable, self).data_attributes + attributes.setdefault('table', {}).update({ + 'data-ams-plugins': 'pyams_content', + 'data-ams-plugin-pyams_content-src': get_resource_path(pyams_content), + 'data-ams-location': absolute_url(IRedirectionManager(self.context), self.request), + 'data-ams-tablednd-drag-handle': 'td.sorter', + 'data-ams-tablednd-drop-target': 'set-rules-order.json', + 'data-ams-active-icon-on': 'fa fa-fw fa-check-square-o', + 'data-ams-active-icon-off': 'fa fa-fw fa-square-o txt-color-silver opacity-75', + 'data-ams-chained-icon-on': 'fa fa-fw fa-chain', + 'data-ams-chained-icon-off': 'fa fa-fw fa-chain txt-color-silver opacity-50' + }) + attributes.setdefault('td', {}).update({ + 'data-ams-attribute-switcher': self.get_switcher_target, + 'data-ams-switcher-attribute-name': self.get_switcher_attribute + }) + return attributes + + @staticmethod + def get_switcher_target(element, column): + if column.__name__ == 'enable-disable': + return 'switch-rule-activity.json' + elif column.__name__ == 'chain-unchain': + return 'switch-rule-chain.json' + + @staticmethod + def get_switcher_attribute(element, column): + if column.__name__ == 'enable-disable': + return 'active' + elif column.__name__ == 'chain-unchain': + return 'chained' + + @reify + def values(self): + return list(super(RedirectionsContainerTable, self).values) + + def render(self): + if not self.values: + translate = self.request.localizer.translate + return translate(_("No currently defined redirection rule.")) + return super(RedirectionsContainerTable, self).render() + + +@adapter_config(context=(IRedirectionManagerTarget, IPyAMSLayer, RedirectionsContainerTable), provides=IValues) +class RedirectionsContainerValues(ContextRequestViewAdapter): + """Redirections container values""" + + @property + def values(self): + return IRedirectionManager(self.context).values() + + +@adapter_config(name='sorter', context=(IRedirectionManagerTarget, IPyAMSLayer, RedirectionsContainerTable), + provides=IColumn) +class RedirectionsContainerSorterColumn(SorterColumn): + """Redirections container sorter column""" + + +@view_config(name='set-rules-order.json', context=IRedirectionManager, request_type=IPyAMSLayer, + permission=MANAGE_SITE_ROOT_PERMISSION, renderer='json', xhr=True) +def set_rules_order(request): + """Update redirection rules order""" + order = list(map(str, json.loads(request.params.get('names')))) + request.context.updateOrder(order) + return {'status': 'success'} + + +@adapter_config(name='enable-disable', context=(IRedirectionManagerTarget, IPyAMSLayer, RedirectionsContainerTable), + provides=IColumn) +class RedirectionsContainerEnablerColumn(AttributeSwitcherColumn): + """Redirections container enabler switcher column""" + + switch_attribute = 'active' + + on_icon_class = 'fa fa-fw fa-check-square-o' + off_icon_class = 'fa fa-fw fa-square-o txt-color-silver opacity-75' + + icon_hint = _("Enable/disable rule") + + weight = 6 + + +@view_config(name='switch-rule-activity.json', context=IRedirectionManager, request_type=IPyAMSLayer, + permission=MANAGE_SITE_ROOT_PERMISSION, renderer='json', xhr=True) +def switch_rule_activity(request): + """Switch rule activity""" + container = IRedirectionManager(request.context) + rule = container.get(str(request.params.get('object_name'))) + if rule is None: + raise NotFound() + rule.active = not rule.active + return {'on': rule.active} + + +@adapter_config(name='chain-unchain', context=(IRedirectionManagerTarget, IPyAMSLayer, RedirectionsContainerTable), + provides=IColumn) +class RedirectionsContainerChainedColumn(AttributeSwitcherColumn): + """Redirections container chained switcher column""" + + switch_attribute = 'chained' + + on_icon_class = 'fa fa-fw fa-chain' + off_icon_class = 'fa fa-fw fa-chain txt-color-silver opacity-50' + + icon_hint = _("Chain/unchain rule") + + weight = 7 + + +@view_config(name='switch-rule-chain.json', context=IRedirectionManager, request_type=IPyAMSLayer, + permission=MANAGE_SITE_ROOT_PERMISSION, renderer='json', xhr=True) +def switch_rule_chain(request): + """Switch rule chain""" + container = IRedirectionManager(request.context) + rule = container.get(str(request.params.get('object_name'))) + if rule is None: + raise NotFound() + rule.chained = not rule.chained + return {'chained': rule.chained} + + +@adapter_config(name='name', context=(IRedirectionManagerTarget, IPyAMSLayer, RedirectionsContainerTable), + provides=IColumn) +class RedirectionsContainerNameColumn(I18nColumn, GetAttrColumn): + """Redirections container name column""" + + _header = _("URL pattern") + attrName = 'url_pattern' + weight = 10 + + +@adapter_config(name='target', context=(IRedirectionManagerTarget, IPyAMSLayer, RedirectionsContainerTable), + provides=IColumn) +class RedirectionsContainerTargetColumn(I18nColumn, GetAttrColumn): + """Redirections container target column""" + + _header = _("Target") + attrName = 'target_url' + weight = 20 + + def getValue(self, obj): + if obj.reference: + target = obj.target + return '{0} ({1})'.format(II18n(target).query_attribute('title', request=self.request), + ISequentialIdInfo(target).get_short_oid()) + else: + return super(RedirectionsContainerTargetColumn, self).getValue(obj) + + +@adapter_config(name='trash', context=(IRedirectionManagerTarget, IPyAMSLayer, RedirectionsContainerTable), + provides=IColumn) +class RedirectionsContainerTrashColumn(TrashColumn): + """Redirections container trash column""" + + permission = MANAGE_SITE_ROOT_PERMISSION + + +@pagelet_config(name='redirections.html', context=IRedirectionManagerTarget, layer=IPyAMSLayer, + permission=MANAGE_SITE_ROOT_PERMISSION) +class RedirectionsContainerView(ContainerAdminView): + """Redirections container view""" + + title = _("Redirections list") + table_class = RedirectionsContainerTable + + +@adapter_config(context=(IRedirectionManagerTarget, IAdminLayer, RedirectionsContainerView), provides=IPageHeader) +class RedirectionsContainerViewHeaderAdapter(DefaultPageHeaderAdapter): + """Redirections container view header adapter""" + + icon_class = 'fa fa-fw fa-map-signs' + + +@adapter_config(context=(IRedirectionManagerTarget, IAdminLayer, RedirectionsContainerView), provides=IContentHelp) +class RedirectionsContainerHelpAdapter(ContentHelp): + """Redirections container help adapter""" + + header = _("Redirection rules") + message = _("""Redirection rules are use to handle redirections responses when a request generates +a famous « 404 NotFound » error. + +Redirections are particularly useful when you are migrating from a previous site and don't want to lose +your SEO. + +You can define a set of rules which will be applied to every \"NotFound\" request; rules are based on +regular expressions which are applied to input URL: if the rule is \"matching\", the target URL is rewritten +and a \"Redirect\" response is send. + +You can chain rules together: when a rule is chained, it's rewritten URL is passed as input URL to the +next rule, until a matching rule is found. +""") + message_format = 'rest' + + +# +# Redirections container test form +# + +@viewlet_config(name='test.action', context=IRedirectionManagerTarget, layer=IAdminLayer, + view=RedirectionsContainerView, manager=IToolbarViewletManager, + permission=MANAGE_SITE_ROOT_PERMISSION, weight=75) +class RedirectionsContainerTestAction(ToolbarAction): + """redirections container test action""" + + label = _("Test") + + group_css_class = 'btn-group margin-left-5' + label_css_class = 'fa fa-fw fa-magic' + css_class = 'btn btn-xs btn-default' + + url = 'test-redirection-rules.html' + modal_target = True + + +class IRedirectionsContainerTestFields(Interface): + """Redirections container test fields""" + + source_url = TextLine(title=_("Test URL"), + required=True) + + check_inactive_rules = Bool(title=_("Check inactive rules?"), + description=_("If 'yes', inactive rules will also be tested"), + required=True, + default=False) + + +class IRedirectionsContainerTestButtons(Interface): + """Redirections container test form buttons""" + + close = CloseButton(name='close', title=_("Close")) + test = button.Button(name='test', title=_("Test rules")) + + +@pagelet_config(name='test-redirection-rules.html', context=IRedirectionManagerTarget, layer=IPyAMSLayer, + permission=MANAGE_SITE_ROOT_PERMISSION) +class RedirectionsContainerTestForm(AdminDialogAddForm): + """Redirections container test form""" + + dialog_class = 'modal-max' + legend = _("Test redirection rules") + icon_css_class = 'fa fa-fw fa-magic' + + prefix = 'rules_test_form.' + fields = field.Fields(IRedirectionsContainerTestFields) + buttons = button.Buttons(IRedirectionsContainerTestButtons) + ajax_handler = 'test-redirection-rules.json' + edit_permission = MANAGE_SITE_ROOT_PERMISSION + + @property + def form_target(self): + return '#{0}_test_result'.format(self.id) + + def updateActions(self): + super(RedirectionsContainerTestForm, self).updateActions() + if 'test' in self.actions: + self.actions['test'].addClass('btn-primary') + + def createAndAdd(self, data): + data = data.get(self, data) + request = copy_request(self.request) + apply_skin(request, IUserSkinnable(self.context).get_skin()) + return IRedirectionManager(self.context).test_rules(data['source_url'], request, data['check_inactive_rules']) + + +@viewlet_config(name='test-indexer-process.suffix', layer=IAdminLayer, manager=IWidgetsSuffixViewletsManager, + view=RedirectionsContainerTestForm, weight=50) +@template_config(template='templates/manager-test.pt') +class RedirectionsContainerTestSuffix(Viewlet): + """Redirections container test form suffix""" + + +@view_config(name='test-redirection-rules.json', context=IRedirectionManagerTarget, request_type=IPyAMSLayer, + permission=MANAGE_SITE_ROOT_PERMISSION, renderer='json', xhr=True) +class RedirectionsContainerAJAXTestForm(AJAXAddForm, RedirectionsContainerTestForm): + """Redirections container test form, JSON renderer""" + + def get_ajax_output(self, changes): + message = [] + translate = self.request.localizer.translate + for rule, source_url, target_url in changes: + if not message: + message.append('{:<40} | {:<40} | {:<40}'.format(translate(_("Input URL")), + translate(_("URL pattern")), + translate(_("Output URL")))) + message.append('{:<40}-|-{:<40}-|-{:<40}'.format('-' * 40, '-' * 40, '-' * 40)) + message.append('{:<40} | {:<40} | {:<40}'.format(source_url, rule.url_pattern, target_url)) + if not message: + message.append(translate(_("No matching rule!"))) + return { + 'status': 'success', + 'content': {'html': '\n'.join(message)}, + 'close_form': False + } diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/features/redirect/zmi/templates/manager-test.pt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_content/features/redirect/zmi/templates/manager-test.pt Thu Sep 06 11:27:55 2018 +0200 @@ -0,0 +1,4 @@ +
+

+
diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/features/renderer/__init__.py --- a/src/pyams_content/features/renderer/__init__.py Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/features/renderer/__init__.py Thu Sep 06 11:27:55 2018 +0200 @@ -45,7 +45,7 @@ request = check_request() renderer = request.registry.queryMultiAdapter((self, request), self.renderer_interface, name=self.renderer or '') - if 'lang' in request.params: + if (renderer is not None) and ('lang' in request.params): renderer.language = request.params['lang'] return renderer diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/features/renderer/interfaces/__init__.py --- a/src/pyams_content/features/renderer/interfaces/__init__.py Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/features/renderer/interfaces/__init__.py Thu Sep 06 11:27:55 2018 +0200 @@ -39,9 +39,14 @@ """Content renderer interface""" label = Attribute("Renderer label") - weight = Attribute("Renderer weight") - settings_interface = Attribute("Renderer target interface") + weight = Attribute("Renderer weight, used for ordering") + + settings_interface = Attribute("Renderer settings interface") + resources = Attribute("Iterable of needed Fanstatic resources") + language = Attribute("Renderer language (if forced)") + context_attrs = Attribute("Context attributes defined into renderer") + i18n_context_attrs = Attribute("I18n context attributes defined into renderer") class ISharedContentRenderer(IContentRenderer): diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/features/renderer/skin/__init__.py --- a/src/pyams_content/features/renderer/skin/__init__.py Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/features/renderer/skin/__init__.py Thu Sep 06 11:27:55 2018 +0200 @@ -36,7 +36,9 @@ label = None weight = 0 + settings_interface = None + resources = () language = None context_attrs = () @@ -49,6 +51,8 @@ return IRendererSettings(self.context) def update(self): + for resource in self.resources: + resource.need() for attr in self.context_attrs: setattr(self, attr, getattr(self.context, attr, None)) if self.i18n_context_attrs: diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/features/renderer/zmi/__init__.py --- a/src/pyams_content/features/renderer/zmi/__init__.py Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/features/renderer/zmi/__init__.py Thu Sep 06 11:27:55 2018 +0200 @@ -18,6 +18,7 @@ # import interfaces from pyams_content.features.renderer.interfaces import IRenderedContent, IContentRenderer, IRendererSettings from pyams_content.interfaces import MANAGE_CONTENT_PERMISSION +from pyams_form.interfaces.form import IFormManager from pyams_skin.layer import IPyAMSLayer # import packages @@ -25,6 +26,7 @@ from pyams_pagelet.pagelet import pagelet_config from pyams_viewlet.viewlet import BaseContentProvider from pyams_zmi.form import AdminDialogEditForm +from pyramid.decorator import reify from z3c.form import field from zope.interface import Interface @@ -70,12 +72,45 @@ legend = _("Edit renderer properties") icon_css_class = 'fa fa-fw fa-pencil-square-o' - @property + @reify + def manager(self): + content = self.getContent() + return self.request.registry.queryMultiAdapter((content, self.request, self), IFormManager) + + @reify def fields(self): - renderer = IContentRenderer(self.context) - return field.Fields(renderer.settings_interface or Interface) + if self.manager is not None: + return self.manager.getFields() + else: + renderer = IContentRenderer(self.context) + return field.Fields(renderer.settings_interface or Interface) edit_permission = MANAGE_CONTENT_PERMISSION def getContent(self): return IRendererSettings(self.context) + + def update(self): + if self.manager is not None: + self.manager.update() + else: + super(RendererPropertiesEditForm, self).update() + + def updateWidgets(self, prefix=None): + if self.manager is not None: + self.manager.updateWidgets(prefix) + else: + super(RendererPropertiesEditForm, self).updateWidgets(prefix) + + def updateActions(self): + if self.manager is not None: + self.manager.updateActions() + else: + super(RendererPropertiesEditForm, self).updateActions() + + def updateGroups(self): + if self.manager is not None: + self.manager.updateGroups() + else: + super(RendererPropertiesEditForm, self).updateGroups() + diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/features/renderer/zmi/templates/renderer-input.pt --- a/src/pyams_content/features/renderer/zmi/templates/renderer-input.pt Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/features/renderer/zmi/templates/renderer-input.pt Thu Sep 06 11:27:55 2018 +0200 @@ -1,6 +1,6 @@
-
@@ -94,16 +94,18 @@
-
+
+
-
-
and
-
@@ -154,27 +156,34 @@ Modified between
-
and
-
+
+ Tags +
+ +
+
-
diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/shared/common/types.py --- a/src/pyams_content/shared/common/types.py Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/shared/common/types.py Thu Sep 06 11:27:55 2018 +0200 @@ -21,9 +21,10 @@ from pyams_content.component.paragraph.interfaces import IParagraphContainerTarget from pyams_content.component.theme.interfaces import IThemesTarget, IThemesInfo from pyams_content.interfaces import MANAGE_TOOL_PERMISSION +from pyams_content.shared.common.interfaces import ISharedContentFactory from pyams_content.shared.common.interfaces.types import IDataType, ISubType, IBaseDataType, ITypedSharedTool, \ ITypedDataManager, DATA_MANAGER_ANNOTATION_KEY, DATA_TYPES_VOCABULARY, DATA_TYPE_FIELDS_VOCABULARY, \ - IWfTypedSharedContent, DATA_SUBTYPES_VOCABULARY + IWfTypedSharedContent, DATA_SUBTYPES_VOCABULARY, ALL_DATA_TYPES_VOCABULARY from pyams_form.interfaces.form import IFormContextPermissionChecker from pyams_i18n.interfaces import II18n from zope.lifecycleevent.interfaces import IObjectAddedEvent @@ -32,9 +33,10 @@ # import packages from persistent import Persistent -from pyams_content.shared.common import WfSharedContent +from pyams_content.shared.common import WfSharedContent, IWfSharedContentFactory from pyams_content.shared.common.manager import SharedTool from pyams_utils.adapter import adapter_config, ContextAdapter, get_annotation_adapter +from pyams_utils.registry import get_local_registry from pyams_utils.request import check_request from pyams_utils.traversing import get_parent from pyams_utils.vocabulary import vocabulary_config @@ -147,6 +149,43 @@ # Data types vocabularies # +@vocabulary_config(name=ALL_DATA_TYPES_VOCABULARY) +class AllTypedSharedToolDataTypesVocabulary(SimpleVocabulary): + """Vocabulary consolidating all data types""" + + def __init__(self, context): + terms = [] + request = check_request() + registry = get_local_registry() + for tool in registry.getAllUtilitiesRegisteredFor(ITypedSharedTool): + manager = ITypedDataManager(tool) + terms.extend([SimpleTerm(datatype.__name__, + title=II18n(datatype).query_attribute('label', request=request)) + for datatype in manager.values()]) + terms.sort(key=lambda x: x.title) + super(AllTypedSharedToolDataTypesVocabulary, self).__init__(terms) + + +def get_all_data_types(request): + """Get list of all registered data types as JSON object""" + results = [] + registry = get_local_registry() + for tool in sorted(registry.getAllUtilitiesRegisteredFor(ITypedSharedTool), + key=lambda x: II18n(x).query_attribute('title', request=request)): + manager = ITypedDataManager(tool) + terms = [{ + 'id': datatype.__name__, + 'text': II18n(datatype).query_attribute('label', request=request) + } for datatype in manager.values()] + content_factory = IWfSharedContentFactory(ISharedContentFactory(tool)) + results.append({ + 'text': request.localizer.translate(content_factory.content_name), + 'disabled': True, + 'children': terms + }) + return results + + @vocabulary_config(name=DATA_TYPES_VOCABULARY) class TypedSharedToolDataTypesVocabulary(SimpleVocabulary): """Typed shared tool data types vocabulary""" diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/shared/common/zmi/__init__.py --- a/src/pyams_content/shared/common/zmi/__init__.py Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/shared/common/zmi/__init__.py Thu Sep 06 11:27:55 2018 +0200 @@ -87,12 +87,12 @@ return self.context.shared_content_factory.content_class() def update_content(self, content, data): - # generic content update changes = super(SharedContentAddForm, self).update_content(content, data) + # initialize content fields + lang = get_utility(INegotiator).server_language content.creator = self.request.principal.id content.owner = self.request.principal.id content.short_name = content.title.copy() - lang = get_utility(INegotiator).server_language content.content_url = generate_url(content.title.get(lang, '')) # init content languages languages = II18nManager(self.context).languages diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/shared/common/zmi/dashboard.py --- a/src/pyams_content/shared/common/zmi/dashboard.py Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/shared/common/zmi/dashboard.py Thu Sep 06 11:27:55 2018 +0200 @@ -542,7 +542,7 @@ def title(self): return II18n(self.context).query_attribute('title', request=self.request) - subtitle = _("Your favorites") + subtitle = _("Your favorite contents") # diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/shared/common/zmi/manager.py --- a/src/pyams_content/shared/common/zmi/manager.py Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/shared/common/zmi/manager.py Thu Sep 06 11:27:55 2018 +0200 @@ -56,7 +56,7 @@ @property def label(self): - return II18n(self.context).query_attribute('short_name', request=self.request) + return II18n(self.context).query_attribute('title', request=self.request) css_class = 'strong' @@ -87,7 +87,7 @@ # @viewlet_config(name='properties.menu', context=IBaseSharedTool, layer=IAdminLayer, - manager=ISiteManagementMenu, permission=MANAGE_TOOL_PERMISSION, weight=40) + manager=ISiteManagementMenu, permission=MANAGE_TOOL_PERMISSION, weight=15) @viewletmanager_config(name='properties.menu', layer=IAdminLayer, provides=IPropertiesMenu) @implementer(IPropertiesMenu) class SharedToolPropertiesMenu(MenuItem): diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/shared/common/zmi/portal.py --- a/src/pyams_content/shared/common/zmi/portal.py Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/shared/common/zmi/portal.py Thu Sep 06 11:27:55 2018 +0200 @@ -15,23 +15,21 @@ # import standard library +from pyams_content import _ # import interfaces from pyams_content.shared.common import IWfSharedContent -from pyams_content.shared.common.interfaces import IWfSharedContentPortalContext, ISharedToolPortalContext, ISharedTool +from pyams_content.shared.common.interfaces import IWfSharedContentPortalContext, ISharedToolPortalContext from pyams_content.shared.common.interfaces.types import ITypedSharedToolPortalContext -from pyams_form.interfaces.form import IFormHelp -from pyams_portal.interfaces import MANAGE_TEMPLATE_PERMISSION -from pyams_skin.layer import IPyAMSLayer -from pyams_zmi.layer import IAdminLayer - # import packages from pyams_form.form import ajax_config from pyams_form.help import FormHelp +from pyams_form.interfaces.form import IFormHelp from pyams_pagelet.pagelet import pagelet_config +from pyams_portal.interfaces import MANAGE_TEMPLATE_PERMISSION from pyams_portal.zmi.page import PortalContextTemplatePropertiesEditForm +from pyams_skin.layer import IPyAMSLayer from pyams_utils.adapter import adapter_config - -from pyams_content import _ +from pyams_zmi.layer import IAdminLayer @pagelet_config(name='template-properties.html', context=ISharedToolPortalContext, layer=IPyAMSLayer, @@ -69,4 +67,4 @@ class SharedContentTemplatePropertiesEditForm(PortalContextTemplatePropertiesEditForm): """Shared content template properties edit form""" - override_legend = _("Override tool default template") + inherit_legend = _("Use tool default template") diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/shared/common/zmi/properties.py --- a/src/pyams_content/shared/common/zmi/properties.py Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/shared/common/zmi/properties.py Thu Sep 06 11:27:55 2018 +0200 @@ -15,35 +15,34 @@ # import standard library +from pyramid.events import subscriber +from z3c.form import field +from z3c.form.interfaces import IDataExtractedEvent, HIDDEN_MODE +from zope.interface import implementer + +from pyams_content import _ # import interfaces from pyams_content.interfaces import MANAGE_CONTENT_PERMISSION, IBaseContent from pyams_content.shared.common.interfaces import IWfSharedContent -from pyams_form.interfaces.form import IWidgetForm -from pyams_skin.interfaces import IInnerPage, IPageHeader -from pyams_skin.layer import IPyAMSLayer -from pyams_utils.interfaces import VIEW_SYSTEM_PERMISSION -from pyams_zmi.interfaces import IPropertiesEditForm -from pyams_zmi.interfaces.menu import IContentManagementMenu, IPropertiesMenu -from pyams_zmi.layer import IAdminLayer -from z3c.form.interfaces import IDataExtractedEvent, HIDDEN_MODE - # import packages from pyams_content.shared.common.zmi import WfSharedContentHeaderAdapter from pyams_form.form import ajax_config +from pyams_form.interfaces.form import IWidgetForm from pyams_i18n.widget import I18nSEOTextLineFieldWidget from pyams_pagelet.pagelet import pagelet_config from pyams_skin.event import get_json_widget_refresh_event +from pyams_skin.interfaces import IInnerPage, IPageHeader +from pyams_skin.layer import IPyAMSLayer from pyams_skin.viewlet.menu import MenuItem from pyams_utils.adapter import adapter_config +from pyams_utils.interfaces import VIEW_SYSTEM_PERMISSION from pyams_utils.url import generate_url from pyams_viewlet.manager import viewletmanager_config from pyams_viewlet.viewlet import viewlet_config from pyams_zmi.form import AdminEditForm -from pyramid.events import subscriber -from z3c.form import field -from zope.interface import implementer - -from pyams_content import _ +from pyams_zmi.interfaces import IPropertiesEditForm +from pyams_zmi.interfaces.menu import IContentManagementMenu, IPropertiesMenu +from pyams_zmi.layer import IAdminLayer # @@ -82,9 +81,14 @@ legend = _("Content properties") - fields = field.Fields(IWfSharedContent).select('title', 'short_name', 'content_url', - 'description', 'notepad') - fields['title'].widgetFactory = I18nSEOTextLineFieldWidget + @property + def fields(self): + fields = field.Fields(IWfSharedContent).select('title', 'short_name', 'content_url', + 'header', 'description', 'notepad') + fields['title'].widgetFactory = I18nSEOTextLineFieldWidget + if not self.context.handle_header: + fields = fields.omit('header') + return fields def updateWidgets(self, prefix=None): super(SharedContentPropertiesEditForm, self).updateWidgets(prefix) diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/shared/common/zmi/reverse.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_content/shared/common/zmi/reverse.py Thu Sep 06 11:27:55 2018 +0200 @@ -0,0 +1,109 @@ +# +# 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 hypatia.interfaces import ICatalog +from pyams_content.shared.common.interfaces import IWfSharedContent +from pyams_content.shared.common.interfaces.zmi import ISiteRootDashboardTable +from pyams_content.shared.site.interfaces import ISiteContainer +from pyams_portal.interfaces import IPortalTemplate +from pyams_sequence.interfaces import ISequentialIdInfo +from pyams_skin.interfaces import IInnerPage +from pyams_skin.layer import IPyAMSLayer +from pyams_utils.interfaces import VIEW_SYSTEM_PERMISSION +from pyams_workflow.interfaces import IWorkflowVersions +from pyams_zmi.interfaces.menu import IContentManagementMenu +from pyams_zmi.layer import IAdminLayer +from z3c.table.interfaces import IValues, IColumn + +# import packages +from hypatia.catalog import CatalogQuery +from hypatia.query import Eq, Or, Any +from pyams_catalog.query import CatalogResultSet +from pyams_pagelet.pagelet import pagelet_config +from pyams_skin.container import ContainerView +from pyams_skin.table import BaseTable, NameColumn +from pyams_skin.viewlet.menu import MenuItem +from pyams_utils.adapter import adapter_config, ContextRequestViewAdapter +from pyams_utils.list import unique_iter +from pyams_utils.registry import get_utility +from pyams_utils.traversing import get_parent +from pyams_viewlet.viewlet import viewlet_config +from pyams_zmi.view import AdminView +from zope.interface import implementer, Interface + +from pyams_content import _ + + +@viewlet_config(name='reverse-links.menu', context=IWfSharedContent, layer=IAdminLayer, + manager=IContentManagementMenu, permission=VIEW_SYSTEM_PERMISSION, weight=40) +class SequentialITargetReverseLinksMenu(MenuItem): + """Sequential ID target reverse links menu""" + + label = _("Reverse links") + icon_class = 'fa-anchor' + url = '#reverse-links.html' + + +@implementer(ISiteRootDashboardTable) +class SequentialIdTargetReverseLinkTable(BaseTable): + """Sequential ID target reverse links table""" + + title = _("Content's internal links") + + +@adapter_config(name='name', context=(Interface, IPyAMSLayer, SequentialIdTargetReverseLinkTable), provides=IColumn) +class ReverseLinkNameColumn(NameColumn): + """Reverse link name column""" + + _header = _("Title") + + +@adapter_config(context=(IWfSharedContent, IPyAMSLayer, SequentialIdTargetReverseLinkTable), provides=IValues) +class SequentialIdTargetReverseLinkValues(ContextRequestViewAdapter): + """Sequential ID target reverse links values""" + + @property + def values(self): + + def get_item(result): + parent = get_parent(result, IWfSharedContent) + if parent is not None: + return IWorkflowVersions(parent).get_last_versions(count=1)[0] + parent = get_parent(result, IPortalTemplate) + if parent is None: + parent = get_parent(result, ISiteContainer) + if parent is None: + parent = self.request.root + return parent + + catalog = get_utility(ICatalog) + oid = ISequentialIdInfo(self.context).hex_oid + params = Or(Eq(catalog['link_reference'], oid), + Any(catalog['link_references'], {oid})) + return unique_iter(map(get_item, + CatalogResultSet(CatalogQuery(catalog).query(params, + sort_index='modified_date')))) + + +@pagelet_config(name='reverse-links.html', context=IWfSharedContent, layer=IPyAMSLayer, + permission=VIEW_SYSTEM_PERMISSION) +@implementer(IInnerPage) +class SequentialIdTargetReverseLinkView(AdminView, ContainerView): + """Sequential ID target reverse links view""" + + table_class = SequentialIdTargetReverseLinkTable diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/shared/common/zmi/search.py --- a/src/pyams_content/shared/common/zmi/search.py Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/shared/common/zmi/search.py Thu Sep 06 11:27:55 2018 +0200 @@ -17,6 +17,8 @@ # import interfaces from hypatia.interfaces import ICatalog +from pyams_content.component.theme.interfaces import ITagsManager, IThemesManagerTarget, IThemesManager, \ + ICollectionsManagerTarget, ICollectionsManager from pyams_content.profile.interfaces import IAdminProfile from pyams_content.shared.common.interfaces import IBaseSharedTool from pyams_content.shared.common.interfaces.zmi import ISharedToolDashboardTable @@ -46,6 +48,8 @@ from pyams_skin.table import BaseTable from pyams_skin.viewlet.menu import MenuItem from pyams_template.template import template_config +from pyams_thesaurus.schema import ThesaurusTermsListField +from pyams_thesaurus.zmi.widget import ThesaurusTermsTreeFieldWidget from pyams_utils.adapter import adapter_config, ContextRequestViewAdapter from pyams_utils.list import unique from pyams_utils.registry import get_utility @@ -92,12 +96,13 @@ title = _("Quick search results") sortOn = None + dt_sort_order = 'desc' @property def data_attributes(self): attributes = super(SharedToolQuickSearchResults, self).data_attributes attributes['table'] = { - 'data-ams-datatable-sorting': '[]', + 'data-ams-datatable-sorting': '{0},{1}'.format(len(self.columns) - 1, self.dt_sort_order), 'data-ams-datatable-display-length': IAdminProfile(self.request.principal).table_page_length } return attributes @@ -181,6 +186,15 @@ modified_before = Datetime(title=_("Modified before..."), required=False) + tags = ThesaurusTermsListField(title=_("Tags"), + required=False) + + themes = ThesaurusTermsListField(title=_("Themes"), + required=False) + + collections = ThesaurusTermsListField(title=_("Collections"), + required=False) + @template_config(template='templates/advanced-search.pt', layer=IPyAMSLayer) @implementer(IInnerPage) @@ -201,8 +215,35 @@ workflow = IWorkflow(self.context) fields = field.Fields(ISharedToolAdvancedSearchFields) fields['status'].vocabulary = workflow.states + fields['tags'].widgetFactory = ThesaurusTermsTreeFieldWidget + if IThemesManagerTarget.providedBy(self.context): + fields['themes'].widgetFactory = ThesaurusTermsTreeFieldWidget + else: + fields = fields.omit('themes') + if ICollectionsManagerTarget.providedBy(self.context): + fields['collections'].widgetFactory = ThesaurusTermsTreeFieldWidget + else: + fields = fields.omit('collections') return fields + def updateWidgets(self, prefix=None): + super(SharedToolAdvancedSearchForm, self).updateWidgets(prefix) + if 'tags' in self.widgets: + widget = self.widgets['tags'] + manager = ITagsManager(self.request.root) + widget.thesaurus_name = manager.thesaurus_name + widget.extract_name = manager.extract_name + if 'themes' in self.widgets: + widget = self.widgets['themes'] + manager = IThemesManager(self.context) + widget.thesaurus_name = manager.thesaurus_name + widget.extract_name = manager.extract_name + if 'collections' in self.widgets: + widget = self.widgets['collections'] + manager = ICollectionsManager(self.context) + widget.thesaurus_name = manager.thesaurus_name + widget.extract_name = manager.extract_name + @adapter_config(context=(IBaseSharedTool, IPyAMSLayer, SharedToolAdvancedSearchForm), provides=IContentSearch) class SharedToolAdvancedSearchFormSearchAdapter(ContextRequestViewAdapter): @@ -242,6 +283,15 @@ params &= Ge(catalog['modified_date'], data['modified_after']) if data.get('modified_before'): params &= Le(catalog['modified_date'], data['modified_before']) + if data.get('tags'): + tags = [intids.register(term) for term in data['tags']] + params &= Any(catalog['tags'], tags) + if data.get('themes'): + tags = [intids.register(term) for term in data['themes']] + params &= Any(catalog['themes'], tags) + if data.get('collections'): + tags = [intids.register(term) for term in data['collections']] + params &= Any(catalog['collections'], tags) if data.get('status'): return unique(map(lambda x: sorted(IWorkflowVersions(x).get_versions(data['status']), key=lambda y: IZopeDublinCore(y).modified)[0], diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/shared/common/zmi/summary.py --- a/src/pyams_content/shared/common/zmi/summary.py Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/shared/common/zmi/summary.py Thu Sep 06 11:27:55 2018 +0200 @@ -9,6 +9,8 @@ # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # +from pyams_content.shared.common.interfaces.types import IWfTypedSharedContent +from pyams_sequence.interfaces import ISequentialIdInfo __docformat__ = 'restructuredtext' @@ -74,7 +76,13 @@ tab_label = _("Identity card") css_class = 'form-tight' - fields = field.Fields(Interface) + @property + def fields(self): + fields = field.Fields(IWfSharedContent).select('title') + if IWfTypedSharedContent.providedBy(self.context): + fields += field.Fields(IWfTypedSharedContent).select('data_type') + fields += field.Fields(ISequentialIdInfo).select('public_oid') + return fields @adapter_config(name='workflow-waiting-state', diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/shared/common/zmi/templates/advanced-search.pt --- a/src/pyams_content/shared/common/zmi/templates/advanced-search.pt Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/shared/common/zmi/templates/advanced-search.pt Thu Sep 06 11:27:55 2018 +0200 @@ -73,12 +73,12 @@
-
@@ -94,16 +94,18 @@
-
+
+
-
@@ -128,25 +130,25 @@ Created between
-
and
-
@@ -154,27 +156,37 @@ Modified between
-
and
-
+ +
+ + ${widget.label} +
+ +
+
+
+
-
diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/shared/logo/__init__.py --- a/src/pyams_content/shared/logo/__init__.py Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/shared/logo/__init__.py Thu Sep 06 11:27:55 2018 +0200 @@ -15,20 +15,19 @@ # import standard library -# import interfaces -from pyams_content.features.checker.interfaces import IContentChecker, MISSING_VALUE -from pyams_content.shared.logo.interfaces import IWfLogo, LOGO_CONTENT_TYPE, LOGO_CONTENT_NAME, ILogo, IWfLogoFactory -from pyams_content.features.review import IReviewTarget - -# import packages -from pyams_content.shared.common import WfSharedContent, register_content_type, SharedContent, WfSharedContentChecker, \ - IWfSharedContentFactory -from pyams_file.property import FileProperty -from pyams_utils.adapter import adapter_config from zope.interface import implementer, provider from zope.schema.fieldproperty import FieldProperty from pyams_content import _ +# import interfaces +from pyams_content.features.checker.interfaces import IContentChecker, MISSING_VALUE +from pyams_content.features.review import IReviewTarget +# import packages +from pyams_content.shared.common import WfSharedContent, register_content_type, SharedContent, WfSharedContentChecker, \ + IWfSharedContentFactory +from pyams_content.shared.logo.interfaces import IWfLogo, LOGO_CONTENT_TYPE, LOGO_CONTENT_NAME, ILogo, IWfLogoFactory +from pyams_file.property import FileProperty +from pyams_utils.adapter import adapter_config @implementer(IWfLogo, IReviewTarget) @@ -38,6 +37,8 @@ content_type = LOGO_CONTENT_TYPE content_name = LOGO_CONTENT_NAME + handle_header = False + image = FileProperty(IWfLogo['image']) monochrome_image = FileProperty(IWfLogo['monochrome_image']) url = FieldProperty(IWfLogo['url']) diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/shared/logo/paragraph.py --- a/src/pyams_content/shared/logo/paragraph.py Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/shared/logo/paragraph.py Thu Sep 06 11:27:55 2018 +0200 @@ -9,7 +9,6 @@ # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # -from pyams_utils.zodb import volatile_property __docformat__ = 'restructuredtext' diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/shared/site/folder.py --- a/src/pyams_content/shared/site/folder.py Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/shared/site/folder.py Thu Sep 06 11:27:55 2018 +0200 @@ -53,8 +53,11 @@ roles_interface = ISiteFolderRoles heading = FieldProperty(ISiteFolder['heading']) + notepad = FieldProperty(ISiteFolder['notepad']) + + visible_in_list = FieldProperty(ISiteFolder['visible_in_list']) navigation_title = FieldProperty(ISiteFolder['navigation_title']) - notepad = FieldProperty(ISiteFolder['notepad']) + navigation_mode = FieldProperty(ISiteFolder['navigation_mode']) content_name = _("Site folder") diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/shared/site/interfaces/__init__.py --- a/src/pyams_content/shared/site/interfaces/__init__.py Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/shared/site/interfaces/__init__.py Thu Sep 06 11:27:55 2018 +0200 @@ -14,6 +14,7 @@ # import standard library +from collections import OrderedDict # import interfaces from pyams_content.interfaces import IBaseContent @@ -28,11 +29,23 @@ from pyams_i18n.schema import I18nTextLineField, I18nTextField from zope.container.constraints import containers, contains from zope.interface import Interface, Attribute -from zope.schema import Text, Bool +from zope.schema import Text, Bool, Choice +from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm from pyams_content import _ +FOLDER_REDIRECT_DISPLAY_MODE = 'redirect' +FOLDER_TEMPLATE_DISPLAY_MODE = 'template' + +FOLDER_DISPLAY_MODES = OrderedDict(( + (FOLDER_REDIRECT_DISPLAY_MODE, _("Redirect to first visible sub-folder or content")), + (FOLDER_TEMPLATE_DISPLAY_MODE, _("Use presentation template")) +)) + +FOLDER_DISPLAY_MODE_VOCABULARY = SimpleVocabulary([SimpleTerm(v, title=t) for v, t in FOLDER_DISPLAY_MODES.items()]) + + class ISiteElement(IContained, IDeletableElement): """Base site element interface""" @@ -60,14 +73,26 @@ description=_("Heading displayed according to presentation template"), required=False) - navigation_title = I18nTextLineField(title=_("Navigation title"), - description=_("Title displayed in navigation items"), - required=False) - notepad = Text(title=_("Notepad"), description=_("Internal information to be known about this content"), required=False) + visible_in_list = Bool(title=_("Visible in folders list"), + description=_("If 'no', folder will not be displayed into folders list"), + required=True, + default=True) + + navigation_title = I18nTextLineField(title=_("Navigation title"), + description=_("Folder's title displayed in navigation pages; " + "original title will be used if none is specified"), + required=False) + + navigation_mode = Choice(title=_("Navigation mode"), + description=_("Folder behaviour when navigating to folder URL"), + required=True, + vocabulary=FOLDER_DISPLAY_MODE_VOCABULARY, + default=FOLDER_REDIRECT_DISPLAY_MODE) + class ISiteFolderFactory(Interface): """Site folder factory interface""" @@ -111,9 +136,10 @@ class IContentLink(ISiteElement, IInternalReference, IAttributeAnnotatable): """Rented content interface""" - alt_title = I18nTextLineField(title=_("Alternate title"), - description=_("Content title, as shown in front-office"), - required=False) + navigation_title = I18nTextLineField(title=_("Navigation title"), + description=_("Alternate content's title displayed in navigation pages; " + "original title will be used if none is specified"), + required=False) visible = Bool(title=_("Visible?"), description=_("If 'no', link is not visible"), diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/shared/site/link.py --- a/src/pyams_content/shared/site/link.py Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/shared/site/link.py Thu Sep 06 11:27:55 2018 +0200 @@ -43,7 +43,7 @@ """ reference = FieldProperty(IContentLink['reference']) - alt_title = FieldProperty(IContentLink['alt_title']) + navigation_title = FieldProperty(IContentLink['navigation_title']) visible = FieldProperty(IContentLink['visible']) content_name = _("Content link") diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/shared/site/zmi/__init__.py --- a/src/pyams_content/shared/site/zmi/__init__.py Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/shared/site/zmi/__init__.py Thu Sep 06 11:27:55 2018 +0200 @@ -20,7 +20,7 @@ from pyams_content.interfaces import CREATE_CONTENT_PERMISSION from pyams_content.shared.common.interfaces import IWfSharedContent from pyams_content.shared.site.interfaces import ISiteContainer, ISiteManager, IWfTopic -from pyams_i18n.interfaces import II18nManager +from pyams_i18n.interfaces import II18nManager, INegotiator from pyams_skin.interfaces.viewlet import IToolbarAddingMenu, IMenuHeader from pyams_skin.layer import IPyAMSLayer from pyams_workflow.interfaces import IWorkflowInfo, IWorkflowVersions @@ -37,7 +37,7 @@ from pyams_utils.adapter import adapter_config, ContextRequestAdapter from pyams_utils.registry import get_utility from pyams_utils.traversing import get_parent -from pyams_utils.url import absolute_url +from pyams_utils.url import absolute_url, generate_url from pyams_viewlet.viewlet import viewlet_config from pyramid.decorator import reify from pyramid.path import DottedNameResolver @@ -114,11 +114,13 @@ def update_content(self, content, data): data = data.get(self, data) # initialize content fields + lang = get_utility(INegotiator).server_language + content.creator = self.request.principal.id + content.owner = self.request.principal.id content.title = data['title'] content.short_name = content.title.copy() + content.content_url = generate_url(content.title.get(lang, '')) content.notepad = data.get('notepad') - content.creator = self.request.principal.id - content.owner = self.request.principal.id # get parent intids = get_utility(IIntIds) parent = intids.queryObject(data.get('parent')) diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/shared/site/zmi/container.py --- a/src/pyams_content/shared/site/zmi/container.py Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/shared/site/zmi/container.py Thu Sep 06 11:27:55 2018 +0200 @@ -80,7 +80,7 @@ @property def label(self): - return II18n(self.context).query_attribute('short_name', request=self.request) + return II18n(self.context).query_attribute('title', request=self.request) @adapter_config(context=(ISiteContainer, IAdminLayer), provides=IUserAddingsMenuLabel) @@ -334,7 +334,7 @@ icon_class = 'fa-eye-slash text-danger opaque' return ''.format( icon_class=icon_class, - title=self.request.localizer.translate(self.icon_hint)) + title=self.request.localizer.translate(self.get_icon_hint(item))) def get_icon_hint(self, item): translate = self.request.localizer.translate diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/shared/site/zmi/folder.py --- a/src/pyams_content/shared/site/zmi/folder.py Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/shared/site/zmi/folder.py Thu Sep 06 11:27:55 2018 +0200 @@ -9,6 +9,8 @@ # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # +from pyams_form.group import NamedWidgetsGroup +from pyams_form.interfaces.form import IInnerSubForm __docformat__ = 'restructuredtext' @@ -31,7 +33,7 @@ # import packages from pyams_content.shared.common.zmi.manager import SharedToolPropertiesEditForm from pyams_content.shared.site.zmi.widget import SiteManagerFoldersSelectorFieldWidget -from pyams_form.form import AJAXAddForm, AJAXEditForm, ajax_config +from pyams_form.form import AJAXAddForm, ajax_config from pyams_i18n.schema import I18nTextLineField from pyams_pagelet.pagelet import pagelet_config from pyams_skin.table import DefaultElementEditorAdapter @@ -42,10 +44,9 @@ from pyams_utils.unicode import translate_string from pyams_utils.url import absolute_url from pyams_viewlet.viewlet import viewlet_config -from pyams_zmi.form import AdminDialogAddForm +from pyams_zmi.form import AdminDialogAddForm, InnerAdminEditForm from pyramid.events import subscriber from pyramid.path import DottedNameResolver -from pyramid.view import view_config from z3c.form import field from zope.interface import Interface, Invalid from zope.schema import Text, Int @@ -182,16 +183,45 @@ @pagelet_config(name='properties.html', context=ISiteFolder, layer=IPyAMSLayer, permission=MANAGE_TOOL_PERMISSION) +@ajax_config(name='properties.json', context=ISiteFolder, layer=IPyAMSLayer) class SiteFolderPropertiesEditForm(SharedToolPropertiesEditForm): """Site folder properties edit form""" legend = _("Site folder properties") - fields = field.Fields(ISiteFolder).select('title', 'short_name', 'heading', 'navigation_title', 'notepad') + \ + fields = field.Fields(ISiteFolder).select('title', 'short_name', 'heading', 'notepad') + \ field.Fields(IBaseSharedTool).select('shared_content_workflow') -@view_config(name='properties.json', context=ISiteFolder, request_type=IPyAMSLayer, - permission=MANAGE_TOOL_PERMISSION, renderer='json', xhr=True) -class SiteFolderPropertiesAJAXEditForm(AJAXEditForm, SiteFolderPropertiesEditForm): - """Site folder properties edit form, JSON renderer""" +@adapter_config(name='navigation', context=(ISiteFolder, IPyAMSLayer, SiteFolderPropertiesEditForm), + provides=IInnerSubForm) +class SiteFolderNavigationPropertiesEditForm(InnerAdminEditForm): + """Site folder navigation properties edit form""" + + prefix = 'navigation_form.' + + css_class = 'form-group' + padding_class = '' + fieldset_class = 'bordered margin-top-10 padding-y-5' + + legend = None + main_group_legend = _("Navigation properties") + main_group_class = 'inner switcher no-y-padding' + + fields = field.Fields(ISiteFolder).select('visible_in_list', 'navigation_title', 'navigation_mode') + + weight = 5 + + def check_mode(self): + if self.parent_form is not None: + self.mode = self.parent_form.mode + + def updateGroups(self): + self.add_group(NamedWidgetsGroup(self, 'navigation', self.widgets, + ('visible_in_list', 'navigation_title', 'navigation_mode'), + fieldset_class=self.fieldset_class, + legend=self.main_group_legend, + css_class=self.main_group_class, + switch=True, + display_mode='auto')) + super(SiteFolderNavigationPropertiesEditForm, self).updateGroups() diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/shared/site/zmi/link.py --- a/src/pyams_content/shared/site/zmi/link.py Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/shared/site/zmi/link.py Thu Sep 06 11:27:55 2018 +0200 @@ -9,7 +9,6 @@ # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # -from pyams_skin.event import get_json_table_row_refresh_event __docformat__ = 'restructuredtext' @@ -76,7 +75,7 @@ legend = _("Rent existing content") - fields = field.Fields(IContentLinkAddFormFields).select('reference', 'alt_title', 'parent') + fields = field.Fields(IContentLinkAddFormFields).select('reference', 'navigation_title', 'parent') fields['parent'].widgetFactory = SiteManagerFoldersSelectorFieldWidget edit_permission = CREATE_CONTENT_PERMISSION @@ -94,7 +93,7 @@ def update_content(self, content, data): data = data.get(self, data) content.reference = data.get('reference') - content.alt_title = data['alt_title'] + content.navigation_title = data['navigation_title'] intids = get_utility(IIntIds) parent = intids.queryObject(data.get('parent')) if parent is not None: @@ -115,7 +114,7 @@ @property def name(self): - title = II18n(self.context).query_attribute('alt_title', request=self.request) + title = II18n(self.context).query_attribute('navigation_title', request=self.request) if not title: target = self.context.get_target() if target is not None: @@ -134,7 +133,7 @@ legend = _("Edit content link properties") - fields = field.Fields(IContentLink).omit('__parent__', '__name__') + fields = field.Fields(IContentLink).omit('__parent__', '__name__', 'visible') edit_permission = MANAGE_CONTENT_PERMISSION def get_ajax_output(self, changes): diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/shared/site/zmi/manager.py --- a/src/pyams_content/shared/site/zmi/manager.py Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/shared/site/zmi/manager.py Thu Sep 06 11:27:55 2018 +0200 @@ -63,7 +63,7 @@ @property def label(self): - return II18n(self.context).query_attribute('short_name', request=self.request) + return II18n(self.context).query_attribute('title', request=self.request) css_class = 'strong' diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/shared/view/__init__.py --- a/src/pyams_content/shared/view/__init__.py Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/shared/view/__init__.py Thu Sep 06 11:27:55 2018 +0200 @@ -24,6 +24,7 @@ from hypatia.interfaces import ICatalog from pyams_content.features.preview.interfaces import IPreviewTarget from pyams_content.features.review.interfaces import IReviewTarget +from pyams_content.shared.common.interfaces.types import IWfTypedSharedContent from pyams_content.shared.view.interfaces import IView, IWfView, IWfViewFactory, IViewQuery, \ IViewQueryParamsExtension, IViewQueryFilterExtension, VIEW_CONTENT_TYPE, VIEW_CONTENT_NAME, IViewSettings from pyams_utils.interfaces import ICacheKeyValue @@ -63,8 +64,12 @@ content_type = VIEW_CONTENT_TYPE content_name = VIEW_CONTENT_NAME + handle_header = False + select_context_type = FieldProperty(IWfView['select_context_type']) selected_content_types = FieldProperty(IWfView['selected_content_types']) + select_context_datatype = FieldProperty(IWfView['select_context_datatype']) + selected_datatypes = FieldProperty(IWfView['selected_datatypes']) order_by = FieldProperty(IWfView['order_by']) reversed_order = FieldProperty(IWfView['reversed_order']) limit = FieldProperty(IWfView['limit']) @@ -89,6 +94,16 @@ content_types |= set(self.selected_content_types) return list(content_types) + def get_data_types(self, context): + data_types = set() + if self.select_context_datatype: + content = IWfTypedSharedContent(context, None) + if content is not None: + data_types.add(content.data_type) + if self.selected_datatypes: + data_types |= set(self.selected_datatypes) + return list(data_types) + def get_results(self, context, sort_index=None, reverse=None, limit=None, ignore_cache=False): results = _MARKER if not ignore_cache: @@ -153,6 +168,10 @@ content_types = view.get_content_types(context) if content_types: params &= Any(catalog['content_type'], content_types) + # check data types + data_types = view.get_data_types(context) + if data_types: + params &= Any(catalog['data_type'], data_types) # check workflow states wf_params = None for workflow in registry.getAllUtilitiesRegisteredFor(IWorkflow): diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/shared/view/interfaces/__init__.py --- a/src/pyams_content/shared/view/interfaces/__init__.py Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/shared/view/interfaces/__init__.py Thu Sep 06 11:27:55 2018 +0200 @@ -16,13 +16,15 @@ # import standard library # import interfaces -from pyams_content.shared.common.interfaces import ISharedContent, IWfSharedContent, ISharedTool +from pyams_content.shared.common.interfaces import ISharedContent, IWfSharedContent, ISharedTool, \ + CONTENT_TYPES_VOCABULARY +from pyams_content.shared.common.interfaces.types import ALL_DATA_TYPES_VOCABULARY from pyams_sequence.interfaces import IInternalReferencesList # import packages from pyams_thesaurus.schema import ThesaurusTermsListField from zope.interface import Interface, Attribute -from zope.schema import List, Choice, Bool, Int +from zope.schema import List, Set, Choice, Bool, Int from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm from pyams_content import _ @@ -64,14 +66,28 @@ required=True, default=False) - selected_content_types = List(title=_("Other content types"), - description=_("Selected content types; leave empty for all"), - value_type=Choice(vocabulary='PyAMS content types'), - required=False) + selected_content_types = Set(title=_("Other content types"), + description=_("Selected content types; leave empty for all"), + value_type=Choice(vocabulary=CONTENT_TYPES_VOCABULARY), + required=False) def get_content_types(self, context): """Get content types for given context""" + select_context_datatype = Bool(title=_("Select context data type?"), + description=_("If 'yes', content data type (if available) will be extracted from " + "context"), + required=True, + default=False) + + selected_datatypes = Set(title=_("Other data types"), + description=_("Selected data types; leave empty for all"), + value_type=Choice(vocabulary=ALL_DATA_TYPES_VOCABULARY), + required=False) + + def get_data_types(self, context): + """Get data types for given context""" + order_by = Choice(title=_("Order by"), description=_("Property to use to sort results"), vocabulary=VIEW_ORDER_VOCABULARY, diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/shared/view/zmi/properties.py --- a/src/pyams_content/shared/view/zmi/properties.py Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/shared/view/zmi/properties.py Thu Sep 06 11:27:55 2018 +0200 @@ -14,17 +14,22 @@ # import standard library +import json # import interfaces from pyams_content.shared.view.interfaces import IWfView from pyams_form.interfaces.form import IInnerSubForm from pyams_skin.layer import IPyAMSLayer +from pyams_utils.interfaces.data import IObjectData # import packages +from pyams_content.shared.common.types import get_all_data_types from pyams_content.shared.common.zmi.properties import SharedContentPropertiesEditForm +from pyams_form.widget import HiddenSelect2FieldWidget from pyams_utils.adapter import adapter_config from pyams_zmi.form import InnerAdminEditForm from z3c.form import field +from zope.interface import alsoProvides from pyams_content import _ @@ -41,5 +46,19 @@ fieldset_class = 'bordered no-x-margin margin-y-10' fields = field.Fields(IWfView).select('select_context_type', 'selected_content_types', + 'select_context_datatype', 'selected_datatypes', 'order_by', 'reversed_order', 'limit') + fields['selected_datatypes'].widgetFactory = HiddenSelect2FieldWidget + weight = 1 + + def updateWidgets(self, prefix=None): + super(ViewPropertiesEditForm, self).updateWidgets(prefix) + if 'selected_datatypes' in self.widgets: + widget = self.widgets['selected_datatypes'] + # widget.multiple = True + widget.object_data = { + 'ams-select2-multiple': True, + 'ams-select2-data': json.dumps(get_all_data_types(self.request)) + } + alsoProvides(widget, IObjectData) diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/site.py --- a/src/pyams_content/site.py Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/site.py Thu Sep 06 11:27:55 2018 +0200 @@ -18,14 +18,13 @@ # import interfaces from hypatia.interfaces import ICatalog -from pyams_content.shared.common.interfaces import IWfSharedContent -from pyams_content.shared.site.interfaces import IContentLink from zope.intid.interfaces import IIntIds # import packages from pyams_utils.container import find_objects_providing from pyams_utils.registry import set_local_registry, get_utility from pyams_utils.site import site_factory +from zope.interface import Interface def site_index(request): @@ -35,13 +34,14 @@ try: set_local_registry(application.getSiteManager()) catalog = get_utility(ICatalog) + catalog.reset() + transaction.savepoint() intids = get_utility(IIntIds) - for document in find_objects_providing(application, IWfSharedContent): + for index, document in enumerate(find_objects_providing(application, Interface)): print("Indexing: {0!r}".format(document)) catalog.reindex_doc(intids.register(document), document) - for document in find_objects_providing(application, IContentLink): - print("Indexing: {0!r}".format(document)) - catalog.reindex_doc(intids.register(document), document) + if not index % 100: + transaction.savepoint() finally: set_local_registry(None) transaction.commit() diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/skin/resources/css/pyams_content.css --- a/src/pyams_content/skin/resources/css/pyams_content.css Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/skin/resources/css/pyams_content.css Thu Sep 06 11:27:55 2018 +0200 @@ -46,3 +46,6 @@ .pictograms-manager .pictogram:last-child { border-bottom: none; } +.sortable.gallery { + max-height: 550px; +} diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/skin/resources/css/pyams_content.min.css --- a/src/pyams_content/skin/resources/css/pyams_content.min.css Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/skin/resources/css/pyams_content.min.css Thu Sep 06 11:27:55 2018 +0200 @@ -1,1 +1,1 @@ -.ams-widget.comments .widget-body{position:fixed;height:calc(100% - 337px)}.ams-widget.comments .widget-body .chat-body{position:relative;height:100%}.ams-widget.comments .widget-body .chat-footer{position:fixed;bottom:10px}.ams-widget.comments .widget-body,.ams-widget.comments .widget-body .chat-footer{width:calc(100% - 240px)}@media (max-width:767px){.ams-widget.comments .widget-body,.ams-widget.comments .widget-body .chat-footer{width:calc(100% - 10px)}}@media (min-width:768px) and (max-width:979px){.ams-widget.comments .widget-body,.ams-widget.comments .widget-body .chat-footer{width:calc(100% - 20px)}}.minified .ams-widget.comments .widget-body,.minified .ams-widget.comments .widget-body .chat-footer{width:calc(100% - 65px)}@media (max-width:767px){.minified .ams-widget.comments .widget-body,.minified .ams-widget.comments .widget-body .chat-footer{width:calc(100% - 55px)}}.hidden-menu .ams-widget.comments .widget-body,.hidden-menu .ams-widget.comments .widget-body .chat-footer{width:calc(100% - 30px)}.pictograms-manager .pictogram{border-bottom:1px solid silver}.pictograms-manager .pictogram:last-child{border-bottom:none} +.ams-widget.comments .widget-body{position:fixed;height:calc(100% - 337px)}.ams-widget.comments .widget-body .chat-body{position:relative;height:100%}.ams-widget.comments .widget-body .chat-footer{position:fixed;bottom:10px}.ams-widget.comments .widget-body,.ams-widget.comments .widget-body .chat-footer{width:calc(100% - 240px)}@media (max-width:767px){.ams-widget.comments .widget-body,.ams-widget.comments .widget-body .chat-footer{width:calc(100% - 10px)}}@media (min-width:768px) and (max-width:979px){.ams-widget.comments .widget-body,.ams-widget.comments .widget-body .chat-footer{width:calc(100% - 20px)}}.minified .ams-widget.comments .widget-body,.minified .ams-widget.comments .widget-body .chat-footer{width:calc(100% - 65px)}@media (max-width:767px){.minified .ams-widget.comments .widget-body,.minified .ams-widget.comments .widget-body .chat-footer{width:calc(100% - 55px)}}.hidden-menu .ams-widget.comments .widget-body,.hidden-menu .ams-widget.comments .widget-body .chat-footer{width:calc(100% - 30px)}.pictograms-manager .pictogram{border-bottom:1px solid silver}.pictograms-manager .pictogram:last-child{border-bottom:none}.sortable.gallery{max-height:550px} diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/skin/resources/js/pyams_content.js --- a/src/pyams_content/skin/resources/js/pyams_content.js Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/skin/resources/js/pyams_content.js Thu Sep 06 11:27:55 2018 +0200 @@ -173,6 +173,7 @@ var source = $(this); var media = source.parents('.media'); var gallery = media.parents('.gallery'); + $('i', source).attr('class', 'fa fa-fw fa-spinner fa-pulse'); MyAMS.ajax.post(gallery.data('ams-location') + '/set-media-visibility.json', {object_name: media.data('ams-element-name')}, function(result, status) { @@ -350,24 +351,6 @@ MyAMS.initContent(marker); } MyAMS.helpers.sort(toolbar, 'weight'); - }, - - switchAnchor: function() { - return function () { - var source = $(this); - var element = source.parents('tr').first(); - var container = element.parents('table'); - MyAMS.ajax.post(container.data('ams-location') + '/' + - container.data('ams-anchor-switcher'), - {object_name: element.data('ams-element-name')}, - function (result, status) { - if (result.anchor) { - $('i', source).attr('class', 'fa fa-fw fa-anchor'); - } else { - $('i', source).attr('class', 'fa fa-fw fa-anchor txt-color-silver opacity-50'); - } - }); - } } }, diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/skin/resources/js/pyams_content.min.js --- a/src/pyams_content/skin/resources/js/pyams_content.min.js Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/skin/resources/js/pyams_content.min.js Thu Sep 06 11:27:55 2018 +0200 @@ -1,1 +1,1 @@ -!function(t,e){"use strict";var a=e.MyAMS,i={widget:{treeview:{selectFolder:function(e,a){t(e.target).siblings('input[type="hidden"]').val(a.id)},unselectFolder:function(e,a){t(e.target).siblings('input[type="hidden"]').val(null)}}},TinyMCE:{initEditor:function(t){return tinyMCE.addI18n("fr",{"Link list":"Liste de liens","Toggle h3 header":"En-tête H3","Toggle h4 header":"En-tête H4","Insert internal link":"Insérer un lien interne","Link title":"Texte à afficher","Internal number":"N° interne"}),tinymce.PluginManager.add("internal_links",function(t,e){t.addButton("internal_links",{icon:"cloud-check",tooltip:"Insert internal link",image:"/--static--/pyams_content/img/internal-link.png",onclick:function(){t.windowManager.open({title:"Insert internal link",body:[{type:"textbox",name:"oid",label:"Internal number"},{type:"textbox",name:"title",label:"Link title",value:t.selection.getContent()}],onsubmit:function(e){t.insertContent(''+e.data.title+"")}})}})}),tinyMCE.PluginManager.add("headers",function(t,e){["h3","h4"].forEach(function(e){t.addButton("header-"+e,{tooltip:"Toggle "+e+" header",text:e.toUpperCase(),onClick:function(){t.execCommand("mceToggleFormat",!1,e)},onPostRender:function(){var a=this,i=function(){t.formatter.formatChanged(e,function(t){a.active(t)})};t.formatter?i():t.on("init",i)}})})}),t.image_list=i.TinyMCE.getImagesList,t.link_list=i.TinyMCE.getLinksList,t.style_formats=[{title:"Inline",items:[{title:"Bold",icon:"bold",format:"bold"},{title:"Italic",icon:"italic",format:"italic"},{title:"Underline",icon:"underline",format:"underline"},{title:"Strikethrough",icon:"strikethrough",format:"strikethrough"},{title:"Superscript",icon:"superscript",format:"superscript"},{title:"Subscript",icon:"subscript",format:"subscript"},{title:"Code",icon:"code",format:"code"}]},{title:"Blocks",items:[{title:"Paragraph",format:"p"},{title:"Blockquote",format:"blockquote"},{title:"Div",format:"div"},{title:"Pre",format:"pre"}]},{title:"Alignment",items:[{title:"Left",icon:"alignleft",format:"alignleft"},{title:"Center",icon:"aligncenter",format:"aligncenter"},{title:"Right",icon:"alignright",format:"alignright"},{title:"Justify",icon:"alignjustify",format:"alignjustify"}]}],t.plugins+=" internal_links headers",t.toolbar1&&(t.toolbar1="undo redo | header-h3 header-h4 styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent"),t.toolbar2&&(t.toolbar2="forecolor backcolor | charmap internal_links link | fullscreen preview print | code"),t},getImagesList:function(e){var i=t(document.activeElement).parents("form");if(i.exists()){var n=i.attr("data-ams-form-handler")||i.attr("action"),s=n.substr(0,n.lastIndexOf("/")+1);return a.ajax.post(s+"get-images-list.json",{},e)}},getLinksList:function(e){var i=t(document.activeElement).parents("form");if(i.exists()){var n=i.attr("data-ams-form-handler")||i.attr("action"),s=n.substr(0,n.lastIndexOf("/")+1);return a.ajax.post(s+"get-links-list.json",{},e)}}},galleries:{updateMediaTitle:function(e){t('img[id="'+e.media_id+'"]').attr("original-title",e.title)},switchMediaVisibility:function(e){return function(){var e=t(this),i=e.parents(".media"),n=i.parents(".gallery");a.ajax.post(n.data("ams-location")+"/set-media-visibility.json",{object_name:i.data("ams-element-name")},function(a,i){a.visible?(t("i",e).attr("class","fa fa-fw fa-eye"),e.parents(".btn-group").siblings("a.fancyimg").removeClass("not-visible")):(t("i",e).attr("class","fa fa-fw fa-eye-slash text-danger"),e.parents(".btn-group").siblings("a.fancyimg").addClass("not-visible"))})}},setOrder:function(e,i){if(!i||!i.item.hasClass("already-dropped")){var n=i.item.parents(".gallery"),s=t(".media",n).listattr("data-ams-element-name");a.ajax.post(n.data("ams-location")+"/set-medias-order.json",{medias:JSON.stringify(s)})}},removeMedia:function(e){return function(){var e=t(this);a.skin.bigBox({title:a.i18n.WARNING,content:'  '+a.i18n.DELETE_WARNING,buttons:a.i18n.BTN_OK_CANCEL},function(t){if(t===a.i18n.BTN_OK){var i=e.parents(".gallery").data("ams-location"),n=e.parents(".media"),s=n.data("ams-element-name");a.ajax.post(i+"/delete-element.json",{object_name:s},function(t,e){n.remove()})}})}},afterFancyboxLoad:function(t,e){t.element.hasClass("not-visible")&&t.inner.prepend('
')}},paragraphs:{preReload:function(){i.paragraphs.switched=t("i.switch.fa-minus-square-o","#paragraphs_list").parents("tr").listattr("id")},postReload:function(){t(i.paragraphs.switched).each(function(){t("i.switch.fa-plus-square-o",'[id="'+this+'"]').parents("div").first().click()}),delete i.paragraphs.switched},refreshParagraph:function(e){var a=t('tr[id="'+e.object_id+'"]');t("span.title",a).html(e.title||" - - - - - - - -")},switchEditor:function(e){var i=t(this),n=t("i.switch",i),s=i.parents("td"),r=t(".editor",s),o=i.parents("tr");if(n.hasClass("fa-plus-square-o")){var l=o.parents("table");r.html('

'),a.ajax.post(l.data("ams-location")+"/get-paragraph-editor.json",{object_name:o.data("ams-element-name")},function(t){r.html(t),t&&(a.initContent(r),n.removeClass("fa-plus-square-o").addClass("fa-minus-square-o"),o.data("ams-disabled-handlers",!0),a.skin.scrollTo(r,{offset:r.height()-o.height()}))})}else a.skin.cleanContainer(r),r.empty(),n.removeClass("fa-minus-square-o").addClass("fa-plus-square-o"),o.removeData("ams-disabled-handlers")},switchLastEditor:function(e){var a=t('table[id="'+e+'"]'),i=t("tr:last",a);t('[data-ams-click-handler="PyAMS_content.paragraphs.switchEditor"]',i).click()},switchAllEditors:function(e){var i=t(this),n=t("i",i),s=i.parents("table");n.hasClass("fa-plus-square-o")?(n.removeClass("fa-plus-square-o").addClass("fa-cog fa-spin"),a.ajax.post(s.data("ams-location")+"/get-paragraphs-editors.json",{},function(e){for(var i in e)if(e.hasOwnProperty(i)){var r=t('tr[data-ams-element-name="'+i+'"]',s),o=t(".editor",r);o.is(":empty")&&o.html(e[i]),t(".fa-plus-square-o",r).removeClass("fa-plus-square-o").addClass("fa-minus-square-o"),r.data("ams-disabled-handlers",!0)}t("i.fa-plus-square-o",t("tbody",s)).exists()||n.removeClass("fa-cog fa-spin").addClass("fa-minus-square-o"),a.initContent(s)})):(t(".editor",s).each(function(){a.skin.cleanContainer(t(this)),t(this).empty()}),t(".fa-minus-square-o",s).removeClass("fa-minus-square-o").addClass("fa-plus-square-o"),t("tr",s).removeData("ams-disabled-handlers"))},updateToolbar:function(e){var i=t('tr[id="'+e.object_id+'"]'),n=t(".title-toolbar",i);n.replaceWith(e.toolbar_tag),n=t(".title-toolbar",i),a.initContent(n)},updateMarkers:function(e){var i=t('tr[id="'+e.object_id+'"]'),n=t(".title-toolbar",i),s=t("DIV.action."+e.marker_type,n);s.exists()?s.replaceWith(e.marker_tag):t(e.marker_tag).appendTo(n),e.marker_tag&&(s=t("DIV.action."+e.marker_type,n),a.initContent(s)),a.helpers.sort(n,"weight")},switchAnchor:function(){return function(){var e=t(this),i=e.parents("tr").first(),n=i.parents("table");a.ajax.post(n.data("ams-location")+"/"+n.data("ams-anchor-switcher"),{object_name:i.data("ams-element-name")},function(a,i){a.anchor?t("i",e).attr("class","fa fa-fw fa-anchor"):t("i",e).attr("class","fa fa-fw fa-anchor txt-color-silver opacity-50")})}}},pictograms:{initManagerSelection:function(){var e=t(this),a=t('input[type="hidden"]',t(".selected-pictograms",e)).listattr("value");return{selected:JSON.stringify(a)}},switchPictogram:function(){var e=t(this),a=e.parents(".pictograms"),i=a.parents(".pictograms-manager");a.hasClass("available-pictograms")?t(".selected-pictograms",i).append(e):t(".available-pictograms",i).append(e)}},themes:{initExtracts:function(e){var i=t('select[name="manager_themes.widgets.thesaurus_name:list"]',e).val(),n=t('select[name="manager_themes.widgets.extract_name:list"]',e),s=n.val();i&&a.jsonrpc.post("getExtracts",{thesaurus_name:i},{url:"/api/thesaurus/json"},function(e){n.empty(),t(e.result).each(function(){t("").attr("value",this.id).attr("selected",this.id===s).text(this.text).appendTo(n)})})},getExtracts:function(e){var i=t(e.currentTarget).parents("form"),n=t('select[name="manager_themes.widgets.thesaurus_name:list"]',i).val(),s=t('select[name="manager_themes.widgets.extract_name:list"]',i),r=s.data("select2");n&&"--NOVALUE--"!==n?a.jsonrpc.post("getExtracts",{thesaurus_name:n},{url:"/api/thesaurus/json"},function(t){r.results.empty(),r.opts.populateResults.call(r,r.results,t.result,{term:""})}):(s.select2("data",null),r.results.empty(),r.opts.populateResults.call(r,r.results,[],{term:""}))}},fields:{refreshField:function(e){var a=t('table[id="form_fields_list"]'),i=t('tr[data-ams-element-name="'+e.object_name+'"]',a);t("td:nth-child(4)",i).html(e.title)}},imgmap:{init:function(){var e=t(this);a.ajax.check(t.fn.canvasAreaDraw,"/--static--/pyams_content/js/jquery-canvasAreaDraw"+a.devext+".js",function(){e.canvasAreaDraw({imageUrl:e.data("ams-image-url")})})},initPreview:function(){var e=t(this);a.ajax.check(t.fn.mapster,"/--static--/pyams_content/js/jquery-imagemapster-1.2.10"+a.devext+".js",function(){e.mapster({fillColor:"ff0000",fillOpacity:.35,selected:!0,highlight:!0,staticState:!0})})}},site:{switchVisibility:function(){return function(){var e=t(this),i=e.parents("tr").first();a.ajax.post(i.data("ams-location")+"/switch-content-visibility.json",{object_name:i.data("ams-element-name")},function(a,i){var n="fa-eye";a.visible||(n+="-slash"),a.published||(n+=" text-danger"),t("i",e).attr("class","fa fa-fw "+n)})}}},review:{timer:null,timer_duration:{general:3e4,chat:5e3},initComments:function(e){var n=t(".chat-body",e);n.animate({scrollTop:n[0].scrollHeight},1e3),clearInterval(i.review.timer),i.review.timer=setInterval(i.review.updateComments,i.review.timer_duration.chat),a.skin.registerCleanCallback(i.review.cleanCommentsCallback)},cleanCommentsCallback:function(){clearInterval(i.review.timer),i.review.timer=setInterval(i.review.updateComments,i.review.timer_duration.general)},updateComments:function(){var e,i=t(".badge",'nav a[href="#review-comments.html"]'),n=t(".chat-body",".widget-body");e=n.exists()?t(".message",n).length:parseInt(i.text()),a.ajax.post("get-last-review-comments.json",{count:e},function(a){n.exists()&&i.removeClass("bg-color-danger").addClass("bg-color-info"),e!==a.count&&(i.text(a.count).removeClass("hidden"),n.exists()&&(t(".messages",n).append(a.content),n.animate({scrollTop:n[0].scrollHeight},1e3)),n.exists()||i.removeClass("bg-color-info").addClass("bg-color-danger").animate({padding:"3px 12px 2px","margin-right":"9px"},"slow",function(){t(this).animate({padding:"3px 6px 2px","margin-right":"15px"},"slow")}))})},initCommentData:function(e){var a=t(".chat-body",".widget-body");return{count:t(".message",a).length}},addCommentAction:function(){return function(){t('textarea[name="comment"]').focus()}},addCommentCallback:function(e){var a=t(this),i=a.parents(".widget-body");t(".messages",i).append(e.content),t('textarea[name="comment"]',a).val("");var n=t(".chat-body",i);n.animate({scrollTop:n[0].scrollHeight},1e3),t(".badge",'nav a[href="#review-comments.html"]').text(e.count).removeClass("hidden")}},header:{submitEditForm:function(){var e=t(this).parents("form").first();a.form.submit(e,{form_data:{autosubmit:!0}})}},footer:{submitEditForm:function(){var e=t(this).parents("form").first();a.form.submit(e,{form_data:{autosubmit:!0}})}},profile:{switchFavorite:function(){var e=t(this),i=e.data("sequence-oid");a.ajax.post("switch-user-favorite.json",{oid:i},function(t,a){t.favorite?e.removeClass("fa-star-o").addClass("fa-star"):e.removeClass("fa-star").addClass("fa-star-o")})}}};t(".badge",'nav a[href="#review-comments.html"]').exists()&&(i.review.timer=setInterval(i.review.updateComments,i.review.timer_duration.general)),e.PyAMS_content=i}(jQuery,this); +!function(t,e){"use strict";var a=e.MyAMS,i={widget:{treeview:{selectFolder:function(e,a){t(e.target).siblings('input[type="hidden"]').val(a.id)},unselectFolder:function(e,a){t(e.target).siblings('input[type="hidden"]').val(null)}}},TinyMCE:{initEditor:function(t){return tinyMCE.addI18n("fr",{"Link list":"Liste de liens","Toggle h3 header":"En-tête H3","Toggle h4 header":"En-tête H4","Insert internal link":"Insérer un lien interne","Link title":"Texte à afficher","Internal number":"N° interne"}),tinymce.PluginManager.add("internal_links",function(t,e){t.addButton("internal_links",{icon:"cloud-check",tooltip:"Insert internal link",image:"/--static--/pyams_content/img/internal-link.png",onclick:function(){t.windowManager.open({title:"Insert internal link",body:[{type:"textbox",name:"oid",label:"Internal number"},{type:"textbox",name:"title",label:"Link title",value:t.selection.getContent()}],onsubmit:function(e){t.insertContent(''+e.data.title+"")}})}})}),tinyMCE.PluginManager.add("headers",function(t,e){["h3","h4"].forEach(function(e){t.addButton("header-"+e,{tooltip:"Toggle "+e+" header",text:e.toUpperCase(),onClick:function(){t.execCommand("mceToggleFormat",!1,e)},onPostRender:function(){var a=this,i=function(){t.formatter.formatChanged(e,function(t){a.active(t)})};t.formatter?i():t.on("init",i)}})})}),t.image_list=i.TinyMCE.getImagesList,t.link_list=i.TinyMCE.getLinksList,t.style_formats=[{title:"Inline",items:[{title:"Bold",icon:"bold",format:"bold"},{title:"Italic",icon:"italic",format:"italic"},{title:"Underline",icon:"underline",format:"underline"},{title:"Strikethrough",icon:"strikethrough",format:"strikethrough"},{title:"Superscript",icon:"superscript",format:"superscript"},{title:"Subscript",icon:"subscript",format:"subscript"},{title:"Code",icon:"code",format:"code"}]},{title:"Blocks",items:[{title:"Paragraph",format:"p"},{title:"Blockquote",format:"blockquote"},{title:"Div",format:"div"},{title:"Pre",format:"pre"}]},{title:"Alignment",items:[{title:"Left",icon:"alignleft",format:"alignleft"},{title:"Center",icon:"aligncenter",format:"aligncenter"},{title:"Right",icon:"alignright",format:"alignright"},{title:"Justify",icon:"alignjustify",format:"alignjustify"}]}],t.plugins+=" internal_links headers",t.toolbar1&&(t.toolbar1="undo redo | header-h3 header-h4 styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent"),t.toolbar2&&(t.toolbar2="forecolor backcolor | charmap internal_links link | fullscreen preview print | code"),t},getImagesList:function(e){var i=t(document.activeElement).parents("form");if(i.exists()){var n=i.attr("data-ams-form-handler")||i.attr("action"),s=n.substr(0,n.lastIndexOf("/")+1);return a.ajax.post(s+"get-images-list.json",{},e)}},getLinksList:function(e){var i=t(document.activeElement).parents("form");if(i.exists()){var n=i.attr("data-ams-form-handler")||i.attr("action"),s=n.substr(0,n.lastIndexOf("/")+1);return a.ajax.post(s+"get-links-list.json",{},e)}}},galleries:{updateMediaTitle:function(e){t('img[id="'+e.media_id+'"]').attr("original-title",e.title)},switchMediaVisibility:function(e){return function(){var e=t(this),i=e.parents(".media"),n=i.parents(".gallery");t("i",e).attr("class","fa fa-fw fa-spinner fa-pulse"),a.ajax.post(n.data("ams-location")+"/set-media-visibility.json",{object_name:i.data("ams-element-name")},function(a,i){a.visible?(t("i",e).attr("class","fa fa-fw fa-eye"),e.parents(".btn-group").siblings("a.fancyimg").removeClass("not-visible")):(t("i",e).attr("class","fa fa-fw fa-eye-slash text-danger"),e.parents(".btn-group").siblings("a.fancyimg").addClass("not-visible"))})}},setOrder:function(e,i){if(!i||!i.item.hasClass("already-dropped")){var n=i.item.parents(".gallery"),s=t(".media",n).listattr("data-ams-element-name");a.ajax.post(n.data("ams-location")+"/set-medias-order.json",{medias:JSON.stringify(s)})}},removeMedia:function(e){return function(){var e=t(this);a.skin.bigBox({title:a.i18n.WARNING,content:'  '+a.i18n.DELETE_WARNING,buttons:a.i18n.BTN_OK_CANCEL},function(t){if(t===a.i18n.BTN_OK){var i=e.parents(".gallery").data("ams-location"),n=e.parents(".media"),s=n.data("ams-element-name");a.ajax.post(i+"/delete-element.json",{object_name:s},function(t,e){n.remove()})}})}},afterFancyboxLoad:function(t,e){t.element.hasClass("not-visible")&&t.inner.prepend('
')}},paragraphs:{preReload:function(){i.paragraphs.switched=t("i.switch.fa-minus-square-o","#paragraphs_list").parents("tr").listattr("id")},postReload:function(){t(i.paragraphs.switched).each(function(){t("i.switch.fa-plus-square-o",'[id="'+this+'"]').parents("div").first().click()}),delete i.paragraphs.switched},refreshParagraph:function(e){var a=t('tr[id="'+e.object_id+'"]');t("span.title",a).html(e.title||" - - - - - - - -")},switchEditor:function(e){var i=t(this),n=t("i.switch",i),s=i.parents("td"),r=t(".editor",s),o=i.parents("tr");if(n.hasClass("fa-plus-square-o")){var l=o.parents("table");r.html('

'),a.ajax.post(l.data("ams-location")+"/get-paragraph-editor.json",{object_name:o.data("ams-element-name")},function(t){r.html(t),t&&(a.initContent(r),n.removeClass("fa-plus-square-o").addClass("fa-minus-square-o"),o.data("ams-disabled-handlers",!0),a.skin.scrollTo(r,{offset:r.height()-o.height()}))})}else a.skin.cleanContainer(r),r.empty(),n.removeClass("fa-minus-square-o").addClass("fa-plus-square-o"),o.removeData("ams-disabled-handlers")},switchLastEditor:function(e){var a=t('table[id="'+e+'"]'),i=t("tr:last",a);t('[data-ams-click-handler="PyAMS_content.paragraphs.switchEditor"]',i).click()},switchAllEditors:function(e){var i=t(this),n=t("i",i),s=i.parents("table");n.hasClass("fa-plus-square-o")?(n.removeClass("fa-plus-square-o").addClass("fa-cog fa-spin"),a.ajax.post(s.data("ams-location")+"/get-paragraphs-editors.json",{},function(e){for(var i in e)if(e.hasOwnProperty(i)){var r=t('tr[data-ams-element-name="'+i+'"]',s),o=t(".editor",r);o.is(":empty")&&o.html(e[i]),t(".fa-plus-square-o",r).removeClass("fa-plus-square-o").addClass("fa-minus-square-o"),r.data("ams-disabled-handlers",!0)}t("i.fa-plus-square-o",t("tbody",s)).exists()||n.removeClass("fa-cog fa-spin").addClass("fa-minus-square-o"),a.initContent(s)})):(t(".editor",s).each(function(){a.skin.cleanContainer(t(this)),t(this).empty()}),t(".fa-minus-square-o",s).removeClass("fa-minus-square-o").addClass("fa-plus-square-o"),t("tr",s).removeData("ams-disabled-handlers"))},updateToolbar:function(e){var i=t('tr[id="'+e.object_id+'"]'),n=t(".title-toolbar",i);n.replaceWith(e.toolbar_tag),n=t(".title-toolbar",i),a.initContent(n)},updateMarkers:function(e){var i=t('tr[id="'+e.object_id+'"]'),n=t(".title-toolbar",i),s=t("DIV.action."+e.marker_type,n);s.exists()?s.replaceWith(e.marker_tag):t(e.marker_tag).appendTo(n),e.marker_tag&&(s=t("DIV.action."+e.marker_type,n),a.initContent(s)),a.helpers.sort(n,"weight")}},pictograms:{initManagerSelection:function(){var e=t(this),a=t('input[type="hidden"]',t(".selected-pictograms",e)).listattr("value");return{selected:JSON.stringify(a)}},switchPictogram:function(){var e=t(this),a=e.parents(".pictograms"),i=a.parents(".pictograms-manager");a.hasClass("available-pictograms")?t(".selected-pictograms",i).append(e):t(".available-pictograms",i).append(e)}},themes:{initExtracts:function(e){var i=t('select[name="manager_themes.widgets.thesaurus_name:list"]',e).val(),n=t('select[name="manager_themes.widgets.extract_name:list"]',e),s=n.val();i&&a.jsonrpc.post("getExtracts",{thesaurus_name:i},{url:"/api/thesaurus/json"},function(e){n.empty(),t(e.result).each(function(){t("").attr("value",this.id).attr("selected",this.id===s).text(this.text).appendTo(n)})})},getExtracts:function(e){var i=t(e.currentTarget).parents("form"),n=t('select[name="manager_themes.widgets.thesaurus_name:list"]',i).val(),s=t('select[name="manager_themes.widgets.extract_name:list"]',i),r=s.data("select2");n&&"--NOVALUE--"!==n?a.jsonrpc.post("getExtracts",{thesaurus_name:n},{url:"/api/thesaurus/json"},function(t){r.results.empty(),r.opts.populateResults.call(r,r.results,t.result,{term:""})}):(s.select2("data",null),r.results.empty(),r.opts.populateResults.call(r,r.results,[],{term:""}))}},fields:{refreshField:function(e){var a=t('table[id="form_fields_list"]'),i=t('tr[data-ams-element-name="'+e.object_name+'"]',a);t("td:nth-child(4)",i).html(e.title)}},imgmap:{init:function(){var e=t(this);a.ajax.check(t.fn.canvasAreaDraw,"/--static--/pyams_content/js/jquery-canvasAreaDraw"+a.devext+".js",function(){e.canvasAreaDraw({imageUrl:e.data("ams-image-url")})})},initPreview:function(){var e=t(this);a.ajax.check(t.fn.mapster,"/--static--/pyams_content/js/jquery-imagemapster-1.2.10"+a.devext+".js",function(){e.mapster({fillColor:"ff0000",fillOpacity:.35,selected:!0,highlight:!0,staticState:!0})})}},site:{switchVisibility:function(){return function(){var e=t(this),i=e.parents("tr").first();a.ajax.post(i.data("ams-location")+"/switch-content-visibility.json",{object_name:i.data("ams-element-name")},function(a,i){var n="fa-eye";a.visible||(n+="-slash"),a.published||(n+=" text-danger"),t("i",e).attr("class","fa fa-fw "+n)})}}},review:{timer:null,timer_duration:{general:3e4,chat:5e3},initComments:function(e){var n=t(".chat-body",e);n.animate({scrollTop:n[0].scrollHeight},1e3),clearInterval(i.review.timer),i.review.timer=setInterval(i.review.updateComments,i.review.timer_duration.chat),a.skin.registerCleanCallback(i.review.cleanCommentsCallback)},cleanCommentsCallback:function(){clearInterval(i.review.timer),i.review.timer=setInterval(i.review.updateComments,i.review.timer_duration.general)},updateComments:function(){var e,i=t(".badge",'nav a[href="#review-comments.html"]'),n=t(".chat-body",".widget-body");e=n.exists()?t(".message",n).length:parseInt(i.text()),a.ajax.post("get-last-review-comments.json",{count:e},function(a){n.exists()&&i.removeClass("bg-color-danger").addClass("bg-color-info"),e!==a.count&&(i.text(a.count).removeClass("hidden"),n.exists()&&(t(".messages",n).append(a.content),n.animate({scrollTop:n[0].scrollHeight},1e3)),n.exists()||i.removeClass("bg-color-info").addClass("bg-color-danger").animate({padding:"3px 12px 2px","margin-right":"9px"},"slow",function(){t(this).animate({padding:"3px 6px 2px","margin-right":"15px"},"slow")}))})},initCommentData:function(e){var a=t(".chat-body",".widget-body");return{count:t(".message",a).length}},addCommentAction:function(){return function(){t('textarea[name="comment"]').focus()}},addCommentCallback:function(e){var a=t(this),i=a.parents(".widget-body");t(".messages",i).append(e.content),t('textarea[name="comment"]',a).val("");var n=t(".chat-body",i);n.animate({scrollTop:n[0].scrollHeight},1e3),t(".badge",'nav a[href="#review-comments.html"]').text(e.count).removeClass("hidden")}},header:{submitEditForm:function(){var e=t(this).parents("form").first();a.form.submit(e,{form_data:{autosubmit:!0}})}},footer:{submitEditForm:function(){var e=t(this).parents("form").first();a.form.submit(e,{form_data:{autosubmit:!0}})}},profile:{switchFavorite:function(){var e=t(this),i=e.data("sequence-oid");a.ajax.post("switch-user-favorite.json",{oid:i},function(t,a){t.favorite?e.removeClass("fa-star-o").addClass("fa-star"):e.removeClass("fa-star").addClass("fa-star-o")})}}};t(".badge",'nav a[href="#review-comments.html"]').exists()&&(i.review.timer=setInterval(i.review.updateComments,i.review.timer_duration.general)),e.PyAMS_content=i}(jQuery,this); diff -r 1afd36ed6947 -r a2f3b82f93c3 src/pyams_content/skin/resources/less/pyams_content.less --- a/src/pyams_content/skin/resources/less/pyams_content.less Tue Jul 17 15:12:43 2018 +0200 +++ b/src/pyams_content/skin/resources/less/pyams_content.less Thu Sep 06 11:27:55 2018 +0200 @@ -50,3 +50,10 @@ border-bottom: none; } } + + +.sortable { + &.gallery { + max-height: 550px; + } +} \ No newline at end of file