# HG changeset patch # User tflorac@dagon.home # Date 1529246484 -7200 # Node ID 6490cb72a1263a0f0afa7c8f9ac50eeaecbdcd49 # Parent 9a84fae4ec97ab68b94bb18e8c93b05e00b9c745# Parent 5d1e4e777dbc8a63b83e236d92cd50a120eb9052 Include keynumber portlet (merge dev-dc) diff -r 5d1e4e777dbc -r 6490cb72a126 docs/HISTORY.txt --- a/docs/HISTORY.txt Fri Jun 15 17:42:23 2018 +0200 +++ b/docs/HISTORY.txt Sun Jun 17 16:41:24 2018 +0200 @@ -1,6 +1,13 @@ History ======= +0.1.15 +------ + - added "basic" illustration component + - added pictogram selection widget + - added optional pictogram to links + - added generic menu feature + 0.1.14 ------ - added header and footer management features diff -r 5d1e4e777dbc -r 6490cb72a126 src/pyams_content.egg-info/PKG-INFO --- a/src/pyams_content.egg-info/PKG-INFO Fri Jun 15 17:42:23 2018 +0200 +++ b/src/pyams_content.egg-info/PKG-INFO Sun Jun 17 16:41:24 2018 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 1.1 Name: pyams-content -Version: 0.1.14 +Version: 0.1.15 Summary: PyAMS base content interfaces and classes Home-page: http://hg.ztfy.org/pyams/pyams_content Author: Thierry Florac @@ -73,6 +73,13 @@ History ======= + 0.1.15 + ------ + - added "basic" illustration component + - added pictogram selection widget + - added optional pictogram to links + - added generic menu feature + 0.1.14 ------ - added header and footer management features diff -r 5d1e4e777dbc -r 6490cb72a126 src/pyams_content.egg-info/SOURCES.txt --- a/src/pyams_content.egg-info/SOURCES.txt Fri Jun 15 17:42:23 2018 +0200 +++ b/src/pyams_content.egg-info/SOURCES.txt Sun Jun 17 16:41:24 2018 +0200 @@ -43,6 +43,13 @@ src/pyams_content/component/illustration/zmi/paragraph.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 +src/pyams_content/component/keynumber/interfaces/__init__.py +src/pyams_content/component/keynumber/portlet/__init__.py +src/pyams_content/component/keynumber/portlet/interfaces/__init__.py +src/pyams_content/component/keynumber/portlet/zmi/__init__.py +src/pyams_content/component/keynumber/portlet/zmi/templates/keynumber-preview.pt +src/pyams_content/component/keynumber/zmi/__init__.py src/pyams_content/component/links/__init__.py src/pyams_content/component/links/interfaces/__init__.py src/pyams_content/component/links/zmi/__init__.py @@ -136,6 +143,22 @@ src/pyams_content/features/header/skin/__init__.py src/pyams_content/features/header/zmi/__init__.py src/pyams_content/features/header/zmi/templates/renderer-settings.pt +src/pyams_content/features/menu/__init__.py +src/pyams_content/features/menu/interfaces/__init__.py +src/pyams_content/features/menu/portlet/__init__.py +src/pyams_content/features/menu/portlet/navigation/__init__.py +src/pyams_content/features/menu/portlet/navigation/double.py +src/pyams_content/features/menu/portlet/navigation/simple.py +src/pyams_content/features/menu/portlet/navigation/interfaces/__init__.py +src/pyams_content/features/menu/portlet/navigation/interfaces/double.py +src/pyams_content/features/menu/portlet/navigation/interfaces/simple.py +src/pyams_content/features/menu/portlet/navigation/zmi/__init__.py +src/pyams_content/features/menu/portlet/navigation/zmi/double.py +src/pyams_content/features/menu/portlet/navigation/zmi/simple.py +src/pyams_content/features/menu/portlet/navigation/zmi/templates/double-preview.pt +src/pyams_content/features/menu/portlet/navigation/zmi/templates/simple-preview.pt +src/pyams_content/features/menu/zmi/__init__.py +src/pyams_content/features/menu/zmi/templates/menu-name-cell.pt src/pyams_content/features/preview/__init__.py src/pyams_content/features/preview/interfaces.py src/pyams_content/features/preview/zmi/__init__.py @@ -160,12 +183,6 @@ src/pyams_content/locales/pyams_content.pot src/pyams_content/locales/fr/LC_MESSAGES/pyams_content.mo src/pyams_content/locales/fr/LC_MESSAGES/pyams_content.po -src/pyams_content/portlet/__init__.py -src/pyams_content/portlet/content/__init__.py -src/pyams_content/portlet/content/interfaces.py -src/pyams_content/portlet/content/skin/__init__.py -src/pyams_content/portlet/content/zmi/__init__.py -src/pyams_content/portlet/content/zmi/preview.pt src/pyams_content/profile/__init__.py src/pyams_content/profile/admin.py src/pyams_content/profile/interfaces/__init__.py @@ -177,6 +194,7 @@ src/pyams_content/reference/pictograms/interfaces/__init__.py src/pyams_content/reference/pictograms/zmi/__init__.py src/pyams_content/reference/pictograms/zmi/manager.py +src/pyams_content/reference/pictograms/zmi/widget.py src/pyams_content/reference/pictograms/zmi/templates/manager-selection.pt src/pyams_content/reference/pictograms/zmi/templates/pictogram-header.pt src/pyams_content/reference/zmi/__init__.py @@ -204,6 +222,12 @@ src/pyams_content/shared/common/interfaces/__init__.py src/pyams_content/shared/common/interfaces/types.py src/pyams_content/shared/common/interfaces/zmi.py +src/pyams_content/shared/common/portlet/__init__.py +src/pyams_content/shared/common/portlet/content/__init__.py +src/pyams_content/shared/common/portlet/content/interfaces/__init__.py +src/pyams_content/shared/common/portlet/content/skin/__init__.py +src/pyams_content/shared/common/portlet/content/zmi/__init__.py +src/pyams_content/shared/common/portlet/content/zmi/preview.pt src/pyams_content/shared/common/zmi/__init__.py src/pyams_content/shared/common/zmi/dashboard.py src/pyams_content/shared/common/zmi/header.py @@ -300,9 +324,7 @@ src/pyams_content/shared/view/zmi/__init__.py src/pyams_content/shared/view/zmi/properties.py src/pyams_content/shared/view/zmi/reference.py -src/pyams_content/shared/view/zmi/render.py src/pyams_content/shared/view/zmi/theme.py -src/pyams_content/shared/view/zmi/templates/render.pt src/pyams_content/skin/__init__.py src/pyams_content/skin/routes.py src/pyams_content/skin/resources/css/pyams_content.css diff -r 5d1e4e777dbc -r 6490cb72a126 src/pyams_content/component/illustration/__init__.py --- a/src/pyams_content/component/illustration/__init__.py Fri Jun 15 17:42:23 2018 +0200 +++ b/src/pyams_content/component/illustration/__init__.py Sun Jun 17 16:41:24 2018 +0200 @@ -17,7 +17,8 @@ # import interfaces from pyams_content.component.illustration.interfaces import IIllustration, IIllustrationTarget, \ - ILLUSTRATION_KEY, ILLUSTRATION_RENDERERS, IBasicIllustration, IBasicIllustrationTarget, BASIC_ILLUSTRATION_KEY + ILLUSTRATION_KEY, ILLUSTRATION_RENDERERS, IBasicIllustration, IBasicIllustrationTarget, BASIC_ILLUSTRATION_KEY, \ + ILinkIllustrationTarget, LINK_ILLUSTRATION_KEY, ILinkIllustration from pyams_content.features.checker.interfaces import IContentChecker, MISSING_VALUE, MISSING_LANG_VALUE from pyams_file.interfaces import IFileInfo, IImage, IResponsiveImage from pyams_i18n.interfaces import INegotiator, II18n, II18nManager @@ -109,6 +110,20 @@ callback=illustration_callback) +@adapter_config(context=ILinkIllustrationTarget, provides=ILinkIllustration) +@adapter_config(name='link', context=ILinkIllustrationTarget, provides=IIllustration) +def link_illustration_factory(context): + """Link illustration factory""" + + def illustration_callback(illustration): + get_current_registry().notify(ObjectAddedEvent(illustration, context, illustration.__name__)) + + return get_annotation_adapter(context, LINK_ILLUSTRATION_KEY, BasicIllustration, + markers=ILinkIllustration, + name='++illustration++link', + callback=illustration_callback) + + def update_illustration_properties(illustration): """Update missing file properties""" request = check_request() diff -r 5d1e4e777dbc -r 6490cb72a126 src/pyams_content/component/illustration/interfaces/__init__.py --- a/src/pyams_content/component/illustration/interfaces/__init__.py Fri Jun 15 17:42:23 2018 +0200 +++ b/src/pyams_content/component/illustration/interfaces/__init__.py Sun Jun 17 16:41:24 2018 +0200 @@ -37,6 +37,8 @@ ILLUSTRATION_KEY = 'pyams_content.illustration' ILLUSTRATION_RENDERERS = 'PyAMS.illustration.renderers' +LINK_ILLUSTRATION_KEY = '{0}::link'.format(ILLUSTRATION_KEY) + class IBasicIllustration(Interface): """Basic illustration interface""" @@ -73,6 +75,10 @@ default='default') +class ILinkIllustration(IBasicIllustration): + """Navigation link illustration interface""" + + class IBasicIllustrationTarget(IAttributeAnnotatable): """Basic illustration target marker interface""" @@ -81,6 +87,14 @@ """Illustration target interface""" +class ILinkIllustrationTarget(IBasicIllustrationTarget): + """Link illustration target interface""" + + +# +# Illustration paragraph +# + ILLUSTRATION_PARAGRAPH_TYPE = 'Illustration' ILLUSTRATION_PARAGRAPH_NAME = _("Illustration") diff -r 5d1e4e777dbc -r 6490cb72a126 src/pyams_content/component/illustration/zmi/__init__.py --- a/src/pyams_content/component/illustration/zmi/__init__.py Fri Jun 15 17:42:23 2018 +0200 +++ b/src/pyams_content/component/illustration/zmi/__init__.py Sun Jun 17 16:41:24 2018 +0200 @@ -17,16 +17,15 @@ # import interfaces from pyams_content.component.illustration.interfaces import IBasicIllustration, IBasicIllustrationTarget, \ - IIllustration, IIllustrationTarget + IIllustration, IIllustrationTarget, ILinkIllustrationTarget from pyams_content.component.paragraph import IBaseParagraph -from pyams_content.component.paragraph.zmi.interfaces import IParagraphContainerTable, IParagraphTitleToolbar from pyams_form.interfaces.form import IInnerSubForm, IWidgetsPrefixViewletsManager from pyams_skin.layer import IPyAMSLayer -from pyams_utils.interfaces import VIEW_SYSTEM_PERMISSION from pyams_zmi.interfaces import IPropertiesEditForm from transaction.interfaces import ITransactionManager # import packages +from pyams_content.component.illustration.zmi.paragraph import ParagraphContainerIllustrationMarker from pyams_content.component.paragraph.zmi import get_json_paragraph_markers_refresh_event from pyams_content.features.renderer.zmi.widget import RendererFieldWidget from pyams_skin.event import get_json_form_refresh_event, get_json_widget_refresh_event @@ -43,41 +42,19 @@ # Illustration properties inner edit form # -@viewlet_config(name='illustration', context=IIllustrationTarget, layer=IPyAMSLayer, view=IParagraphContainerTable, - manager=IParagraphTitleToolbar, permission=VIEW_SYSTEM_PERMISSION, weight=5) -@template_config(template='templates/paragraph-illustration-icon.pt', layer=IPyAMSLayer) -class ParagraphContainerIllustrationMarker(Viewlet): - """Paragraph container illustration marker column""" - - weight = 5 - action_class = 'action illustration nowrap width-40' - icon_class = 'fa fa-fw fa-picture-o' - icon_hint = _("Illustration") - - marker_type = 'illustration' - - def render(self): - illustration = IIllustration(self.context, None) - if illustration and illustration.data: - for value in illustration.data.values(): - if value: - return super(ParagraphContainerIllustrationMarker, self).render() - return '' - - @adapter_config(name='illustration', context=(IBasicIllustrationTarget, IPyAMSLayer, IPropertiesEditForm), provides=IInnerSubForm) class BasicIllustrationPropertiesInnerEditForm(InnerAdminEditForm): """Basic illustration properties inner edit form""" - prefix = 'illustration_form.' + prefix = 'basic_illustration_form.' css_class = 'form-group' padding_class = '' - fieldset_class = '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 pull-left width-auto' + legend_class = 'illustration switcher no-y-padding padding-right-10' fields = field.Fields(IBasicIllustration).omit('__parent__', '__name__') @@ -93,18 +70,17 @@ @property def switcher_state(self): content = self.getContent() - for value in (content.data or {}).values(): - if value: - return 'open' + if content.has_data(): + return 'open' def get_ajax_output(self, changes): output = super(BasicIllustrationPropertiesInnerEditForm, self).get_ajax_output(changes) - updated = changes.get(IIllustration, ()) - events = output.setdefault('events', []) + updated = changes.get(IBasicIllustration, ()) if 'data' in updated: # we have to commit transaction to be able to handle blobs... ITransactionManager(self.context).get().commit() - events.append(get_json_form_refresh_event(self.context, self.request, self.__class__)) + output.setdefault('events', []).append( + get_json_form_refresh_event(self.context, self.request, self.__class__)) return output @@ -113,6 +89,11 @@ class IllustrationPropertiesInnerEditForm(BasicIllustrationPropertiesInnerEditForm): """Illustration properties inner edit form""" + prefix = 'illustration_form.' + + fields = field.Fields(IIllustration).omit('__parent__', '__name__') + fields['renderer'].widgetFactory = RendererFieldWidget + @property def legend(self): if IBaseParagraph.providedBy(self.context): @@ -125,13 +106,22 @@ if IBaseParagraph.providedBy(self.context): return 'illustration switcher no-y-padding padding-right-10 pull-left width-auto' else: - return 'illustration no-y-padding' + return 'illustration switcher no-y-padding' - fields = field.Fields(IIllustration).omit('__parent__', '__name__') - fields['renderer'].widgetFactory = RendererFieldWidget + @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' + def updateWidgets(self, prefix=None): super(IllustrationPropertiesInnerEditForm, self).updateWidgets(prefix) if 'description' in self.widgets: @@ -139,9 +129,8 @@ def get_ajax_output(self, changes): output = super(IllustrationPropertiesInnerEditForm, self).get_ajax_output(changes) - updated = changes.get(IIllustration, ()) events = output.setdefault('events', []) - if 'data' in updated: + if 'data' in changes.get(IBasicIllustration, ()): if IBaseParagraph.providedBy(self.context): if self.getContent().data: events.append(get_json_paragraph_markers_refresh_event(self.context, self.request, self, @@ -150,12 +139,29 @@ events.append(get_json_paragraph_markers_refresh_event(self.context, self.request, self, EmptyViewlet, ParagraphContainerIllustrationMarker.marker_type)) - elif 'renderer' in updated: + elif 'renderer' in changes.get(IIllustration, ()): events.append(get_json_widget_refresh_event(self.context, self.request, IllustrationPropertiesInnerEditForm, 'renderer')) return output +@adapter_config(name='link-illustration', context=(ILinkIllustrationTarget, IPyAMSLayer, IPropertiesEditForm), + provides=IInnerSubForm) +class LinkIllustrationPropertiesInnerEditForm(BasicIllustrationPropertiesInnerEditForm): + """Link illustration properties inner edit form""" + + prefix = 'link_illustration_form.' + + legend = _("Navigation link illustration") + legend_class = 'illustration switcher no-y-padding' + + weight = 11 + + def getContent(self): + registry = self.request.registry + return registry.getAdapter(self.context, IIllustration, name='link') + + @viewlet_config(name='illustration-thumbnail', context=IBasicIllustrationTarget, layer=IPyAMSLayer, view=BasicIllustrationPropertiesInnerEditForm, manager=IWidgetsPrefixViewletsManager) @template_config(template='templates/illustration-thumbnail.pt', layer=IPyAMSLayer) diff -r 5d1e4e777dbc -r 6490cb72a126 src/pyams_content/component/illustration/zmi/paragraph.py --- a/src/pyams_content/component/illustration/zmi/paragraph.py Fri Jun 15 17:42:23 2018 +0200 +++ b/src/pyams_content/component/illustration/zmi/paragraph.py Sun Jun 17 16:41:24 2018 +0200 @@ -18,13 +18,15 @@ # import interfaces from pyams_content.component.paragraph.interfaces import IParagraphContainerTarget, \ IParagraphContainer -from pyams_content.component.illustration.interfaces import IIllustration, IIllustrationParagraph, \ - ILLUSTRATION_PARAGRAPH_TYPE -from pyams_content.component.paragraph.zmi.interfaces import IParagraphInnerEditor, IParagraphContainerView +from pyams_content.component.illustration.interfaces import IIllustrationTarget, IIllustration, \ + IIllustrationParagraph, ILLUSTRATION_PARAGRAPH_TYPE +from pyams_content.component.paragraph.zmi.interfaces import IParagraphInnerEditor, IParagraphContainerView, \ + IParagraphContainerTable, IParagraphTitleToolbar 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 pyams_utils.interfaces import VIEW_SYSTEM_PERMISSION from transaction.interfaces import ITransactionManager from z3c.form.interfaces import INPUT_MODE @@ -36,8 +38,9 @@ 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_template.template import template_config from pyams_utils.adapter import adapter_config -from pyams_viewlet.viewlet import viewlet_config +from pyams_viewlet.viewlet import viewlet_config, Viewlet from pyams_zmi.form import AdminDialogAddForm from z3c.form import field, button from zope.interface import implementer @@ -147,3 +150,29 @@ output.setdefault('events', []).append(get_json_form_refresh_event(self.context, self.request, IllustrationInnerEditForm)) return output + + +# +# Paragraph container illustration marker +# + +@viewlet_config(name='illustration', context=IIllustrationTarget, layer=IPyAMSLayer, view=IParagraphContainerTable, + manager=IParagraphTitleToolbar, permission=VIEW_SYSTEM_PERMISSION, weight=5) +@template_config(template='templates/paragraph-illustration-icon.pt', layer=IPyAMSLayer) +class ParagraphContainerIllustrationMarker(Viewlet): + """Paragraph container illustration marker column""" + + weight = 5 + action_class = 'action illustration nowrap width-40' + icon_class = 'fa fa-fw fa-picture-o' + icon_hint = _("Illustration") + + marker_type = 'illustration' + + def render(self): + illustration = IIllustration(self.context, None) + if illustration and illustration.data: + for value in illustration.data.values(): + if value: + return super(ParagraphContainerIllustrationMarker, self).render() + return '' diff -r 5d1e4e777dbc -r 6490cb72a126 src/pyams_content/component/keynumber/portlet/__init__.py --- a/src/pyams_content/component/keynumber/portlet/__init__.py Fri Jun 15 17:42:23 2018 +0200 +++ b/src/pyams_content/component/keynumber/portlet/__init__.py Sun Jun 17 16:41:24 2018 +0200 @@ -15,21 +15,24 @@ # import standard library # import interfaces -from pyams_content.component.keynumber import IKeyNumberContainerTarget, IKeyNumberContainer +from pyams_content.component.keynumber.interfaces import IKeyNumberContainerTarget, IKeyNumberContainer from pyams_content.component.keynumber.portlet.interfaces import IKeyNumberPortletSettings from pyams_utils.interfaces import VIEW_PERMISSION # import packages +from pyams_portal.portlet import PortletSettings, portlet_config, Portlet +from pyams_utils.factory import factory_config from zope.interface import implementer from zope.schema.fieldproperty import FieldProperty -from pyams_portal.portlet import PortletSettings, portlet_config, Portlet from pyams_content import _ + KEYNUMBER_PORTLET_NAME = "pyams_portal.portlet.keynumber" @implementer(IKeyNumberPortletSettings, IKeyNumberContainerTarget) +@factory_config(provided=IKeyNumberPortletSettings) class KeyNumberPortletSettings(PortletSettings): """Key Number portlet settings""" @@ -51,4 +54,4 @@ toolbar_image = None toolbar_css_class = 'fa fa-fw fa-2x fa-dashboard' - settings_factory = KeyNumberPortletSettings + settings_factory = IKeyNumberPortletSettings diff -r 5d1e4e777dbc -r 6490cb72a126 src/pyams_content/component/keynumber/portlet/interfaces/__init__.py --- a/src/pyams_content/component/keynumber/portlet/interfaces/__init__.py Fri Jun 15 17:42:23 2018 +0200 +++ b/src/pyams_content/component/keynumber/portlet/interfaces/__init__.py Sun Jun 17 16:41:24 2018 +0200 @@ -19,16 +19,18 @@ from pyams_portal.interfaces import IPortletSettings # import packages -from zope.schema import Text, TextLine +from pyams_i18n.schema import I18nTextLineField, I18nTextField + from pyams_content import _ class IKeyNumberPortletSettings(IPortletSettings): """Key numbers portlet settings interface""" - title = TextLine(title=_("Title"), - required=False) + title = I18nTextLineField(title=_("Title"), + description=_("Portlet title"), + required=False) - teaser = Text(title=_("Teaser"), - required=False) - + teaser = I18nTextField(title=_("Teaser"), + description=_("Short text displayed above key numbers"), + required=False) diff -r 5d1e4e777dbc -r 6490cb72a126 src/pyams_content/component/keynumber/portlet/zmi/__init__.py --- a/src/pyams_content/component/keynumber/portlet/zmi/__init__.py Fri Jun 15 17:42:23 2018 +0200 +++ b/src/pyams_content/component/keynumber/portlet/zmi/__init__.py Sun Jun 17 16:41:24 2018 +0200 @@ -12,15 +12,15 @@ __docformat__ = 'restructuredtext' + # import standard library # import interfaces -from zope.interface import Interface +from pyams_content.component.keynumber.portlet.interfaces import IKeyNumberPortletSettings from pyams_portal.interfaces import IPortletPreviewer from pyams_form.interfaces.form import IInnerSubForm, IInnerTabForm from pyams_pagelet.interfaces import IPagelet from pyams_skin.layer import IPyAMSLayer -from pyams_content.component.keynumber.portlet.interfaces import IKeyNumberPortletSettings from pyams_utils.interfaces import VIEW_SYSTEM_PERMISSION # import packages @@ -31,12 +31,13 @@ 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 Interface from pyams_content import _ @adapter_config(context=(Interface, IPyAMSLayer, Interface, IKeyNumberPortletSettings), provides=IPortletPreviewer) -@template_config(template='templates/preview.pt', layer=IPyAMSLayer) +@template_config(template='templates/keynumber-preview.pt', layer=IPyAMSLayer) class KeyNumberPortletPreview(PortletPreviewer): """Key number portlet previewer""" @@ -64,7 +65,7 @@ """Key number portlet settings editor, JSON renderer""" -@adapter_config(name='portlet-keynumbers', +@adapter_config(name='keynumber-portlet-numbers', context=(IKeyNumberPortletSettings, IPyAMSLayer, PortletSettingsPropertiesEditor), provides=IInnerSubForm) class PortletKeynumberLinksView(KeyNumbersView): diff -r 5d1e4e777dbc -r 6490cb72a126 src/pyams_content/component/keynumber/portlet/zmi/templates/keynumber-preview.pt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_content/component/keynumber/portlet/zmi/templates/keynumber-preview.pt Sun Jun 17 16:41:24 2018 +0200 @@ -0,0 +1,20 @@ +