diff --git a/core/docs/changelog/ZO-6370.change b/core/docs/changelog/ZO-6370.change
new file mode 100644
index 0000000000..49f80d6ada
--- /dev/null
+++ b/core/docs/changelog/ZO-6370.change
@@ -0,0 +1 @@
+ZO-6370: add image row module
\ No newline at end of file
diff --git a/core/src/zeit/content/article/edit/browser/configure.zcml b/core/src/zeit/content/article/edit/browser/configure.zcml
index f3125cf720..6d6c051710 100644
--- a/core/src/zeit/content/article/edit/browser/configure.zcml
+++ b/core/src/zeit/content/article/edit/browser/configure.zcml
@@ -827,4 +827,46 @@
permission="zope.View"
/>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/core/src/zeit/content/article/edit/browser/edit.py b/core/src/zeit/content/article/edit/browser/edit.py
index c98746df03..ae1842fa2b 100644
--- a/core/src/zeit/content/article/edit/browser/edit.py
+++ b/core/src/zeit/content/article/edit/browser/edit.py
@@ -1,6 +1,7 @@
import json
import logging
+import grokcore.component as grok
import zope.cachedescriptors.property
import zope.component
import zope.security
@@ -575,3 +576,66 @@ class EditAnimation(zeit.cms.browser.manual.FormMixin, zeit.edit.browser.form.In
@property
def prefix(self):
return 'animation.{0}'.format(self.context.__name__)
+
+
+class IImageRowForm(zeit.content.article.edit.interfaces.IImageRow):
+ pass
+
+
+@grok.implementer(IImageRowForm)
+class ImageRowForm(grok.Adapter):
+ """
+ We'd prefer Tuple(ReferenceField) over Tuple(Combination), but ReferenceSequenceWidget
+ nesting fails due to Mochikit-Sortable's limitations.
+ """
+
+ grok.context(zeit.content.article.edit.interfaces.IImageRow)
+
+ @property
+ def images(self):
+ return [(x.target, x.caption, x.alt) for x in self.context.images]
+
+ @images.setter
+ def images(self, value):
+ if not value:
+ self.context.images = ()
+ return
+ result = []
+ for img, caption, alt in value:
+ ref = self.context.images.create(img)
+ ref.caption = caption
+ ref.alt = alt
+ result.append(ref)
+ self.context.images = result
+
+
+class EditImageRow(zeit.edit.browser.form.InlineForm):
+ legend = ''
+ form_fields = zope.formlib.form.FormFields(zeit.content.article.edit.interfaces.IImageRow).omit(
+ 'images', *list(zeit.edit.interfaces.IBlock)
+ ) + zope.formlib.form.FormFields(IImageRowForm).select('images')
+
+ @property
+ def prefix(self):
+ return 'image_row.{0}'.format(self.context.__name__)
+
+ def setUpWidgets(self, *args, **kw):
+ super().setUpWidgets(*args, **kw)
+ # s : float, m: column-width, l: large
+ max_images_dict = {'float': 1, 'column-width': 2, 'large': 3}
+ display_mode = self.context.display_mode
+ if display_mode is None:
+ return
+ max_images = max_images_dict.get(display_mode)
+ self.widgets['images'].context.max_length = max_images
+
+
+class EditImageParallaxProperties(zeit.edit.browser.form.InlineForm):
+ legend = ''
+ form_fields = zope.formlib.form.FormFields(
+ zeit.content.article.edit.interfaces.IImageParallaxProperties
+ ).omit(*list(zeit.edit.interfaces.IBlock))
+
+ @property
+ def prefix(self):
+ return 'image_parallax_properties.{0}'.format(self.context.__name__)
diff --git a/core/src/zeit/content/article/edit/browser/resources/editor.css b/core/src/zeit/content/article/edit/browser/resources/editor.css
index 3da1ce0876..e1f7b3bc9b 100644
--- a/core/src/zeit/content/article/edit/browser/resources/editor.css
+++ b/core/src/zeit/content/article/edit/browser/resources/editor.css
@@ -2069,6 +2069,10 @@
height: 140px;
}
+.type-article .block.type-image_row .block-inner {
+ height: 800px;
+}
+
/* .type-article .block.type-raw .block-inner
is obsolete and can not be added anymore, see ZO-5487
*/
diff --git a/core/src/zeit/content/article/edit/configure.zcml b/core/src/zeit/content/article/edit/configure.zcml
index 53afc662a7..f9c24852e0 100644
--- a/core/src/zeit/content/article/edit/configure.zcml
+++ b/core/src/zeit/content/article/edit/configure.zcml
@@ -462,6 +462,28 @@
/>
+
+
+
+
+
+
+
+
+
+
diff --git a/core/src/zeit/content/article/edit/image_row.py b/core/src/zeit/content/article/edit/image_row.py
new file mode 100644
index 0000000000..4c482007d6
--- /dev/null
+++ b/core/src/zeit/content/article/edit/image_row.py
@@ -0,0 +1,41 @@
+import grokcore.component as grok
+
+from zeit.cms.content.property import ObjectPathAttributeProperty
+from zeit.cms.i18n import MessageFactory as _
+import zeit.cms.content.reference
+import zeit.content.article.edit.block
+import zeit.content.article.edit.interfaces
+
+
+@grok.implementer(zeit.content.article.edit.interfaces.IImageRow)
+class ImageRow(zeit.content.article.edit.block.Block):
+ type = 'image_row'
+ display_mode = zeit.cms.content.property.ObjectPathAttributeProperty('.', 'display_mode')
+ variant_name = zeit.cms.content.property.ObjectPathAttributeProperty('.', 'variant_name')
+ images = zeit.cms.content.reference.ReferenceProperty('.image', 'image')
+
+
+class RowFactory(zeit.content.article.edit.block.BlockFactory):
+ produces = ImageRow
+ title = _('Image Row')
+
+
+@grok.implementer(zeit.content.article.edit.interfaces.IImageParallaxProperties)
+class ImageParallaxProperties(zeit.content.article.edit.block.Block):
+ type = 'image_parallax_properties'
+
+ show_caption = ObjectPathAttributeProperty(
+ '.',
+ 'show_caption',
+ zeit.content.article.edit.interfaces.IImageParallaxProperties['show_caption'],
+ )
+ show_source = ObjectPathAttributeProperty(
+ '.',
+ 'show_source',
+ zeit.content.article.edit.interfaces.IImageParallaxProperties['show_source'],
+ )
+
+
+class PropertiesFactory(zeit.content.article.edit.block.BlockFactory):
+ produces = ImageParallaxProperties
+ title = _('Image Parallax Properties')
diff --git a/core/src/zeit/content/article/edit/interfaces.py b/core/src/zeit/content/article/edit/interfaces.py
index ee9672e368..5a50bc0696 100644
--- a/core/src/zeit/content/article/edit/interfaces.py
+++ b/core/src/zeit/content/article/edit/interfaces.py
@@ -658,3 +658,39 @@ class IIngredientDice(IBlock):
"""
pass
+
+
+class IImageRow(IBlock):
+ images = zope.schema.Tuple(
+ title=_('Images'),
+ max_length=3,
+ default=(),
+ required=False,
+ value_type=zc.form.field.Combination(
+ fields=(
+ zope.schema.Choice(
+ title=_('Image'), source=zeit.content.image.interfaces.ImageGroupSource()
+ ),
+ zope.schema.TextLine(title=_('Image caption'), required=False),
+ zope.schema.TextLine(title=_('Alternative Text'), required=False),
+ )
+ ),
+ )
+ display_mode = zope.schema.Choice(
+ title=_('Display Mode'),
+ source=zeit.content.article.source.IMAGE_DISPLAY_MODE_SOURCE,
+ default='column-width',
+ required=False,
+ )
+ # Currently need default for bw compat.
+ variant_name = zope.schema.Choice(
+ title=_('Variant Name'),
+ source=zeit.content.article.source.IMAGE_VARIANT_NAME_SOURCE,
+ default='wide',
+ required=False,
+ )
+
+
+class IImageParallaxProperties(IBlock):
+ show_caption = zope.schema.Bool(title=_('Show caption'), required=False, default=True)
+ show_source = zope.schema.Bool(title=_('Show source'), required=False, default=True)
diff --git a/core/src/zeit/content/article/edit/tests/modules.xml b/core/src/zeit/content/article/edit/tests/modules.xml
index de62ac7594..85dc913582 100644
--- a/core/src/zeit/content/article/edit/tests/modules.xml
+++ b/core/src/zeit/content/article/edit/tests/modules.xml
@@ -29,4 +29,6 @@
Video
Ausgabe
ARD Video
+ Bilderreihe
+ Bilder-Parallax-Modul
diff --git a/core/src/zeit/content/article/edit/tests/test_image_row.py b/core/src/zeit/content/article/edit/tests/test_image_row.py
new file mode 100644
index 0000000000..2ccfbf516e
--- /dev/null
+++ b/core/src/zeit/content/article/edit/tests/test_image_row.py
@@ -0,0 +1,40 @@
+import lxml.builder
+
+from zeit.cms.content.reference import ReferenceProperty
+from zeit.cms.interfaces import ICMSContent
+from zeit.cms.testcontenttype.testcontenttype import ExampleContentType
+from zeit.content.article.edit.image_row import ImageParallaxProperties, ImageRow
+import zeit.content.article.testing
+import zeit.edit.interfaces
+
+
+class ImageRowTest(zeit.content.article.testing.FunctionalTestCase):
+ def get_image_row(self):
+ image_row = ImageRow(None, lxml.builder.E.image_row())
+ return image_row
+
+ def test_image_row_should_be_set(self):
+ ExampleContentType.images = ReferenceProperty('.body.image', 'image')
+ image_row = self.get_image_row()
+ image_row.display_mode = 'square'
+ image_row.variant_name = 'default'
+ image = ICMSContent('http://xml.zeit.de/2006/DSC00109_2.JPG')
+ content = self.repository['testcontent']
+ ref = content.images.create(image)
+ content.images = (ref,)
+ ref.title = 'localtitle'
+ ref.caption = 'localcaption'
+ image_row.images = (ref,)
+ self.assertEqual('square', image_row.xml.xpath('.')[0].get('display_mode'))
+ self.assertEqual('default', image_row.xml.xpath('.')[0].get('variant_name'))
+ self.assertEqual(
+ 'http://xml.zeit.de/2006/DSC00109_2.JPG', image_row.xml.xpath('./image')[0].get('src')
+ )
+
+ def test_image_parallax_properties_should_be_set(self):
+ image_properties = ImageParallaxProperties(None, lxml.builder.E.image_parallax_properties())
+
+ image_properties.show_source = True
+ image_properties.show_caption = True
+ self.assertEqual('True', image_properties.xml.xpath('.')[0].get('show_source'))
+ self.assertEqual('True', image_properties.xml.xpath('.')[0].get('show_caption'))
diff --git a/core/src/zeit/locales/de/LC_MESSAGES/zeit.cms.mo b/core/src/zeit/locales/de/LC_MESSAGES/zeit.cms.mo
index 3b814f613d..1b4dab1fee 100644
Binary files a/core/src/zeit/locales/de/LC_MESSAGES/zeit.cms.mo and b/core/src/zeit/locales/de/LC_MESSAGES/zeit.cms.mo differ
diff --git a/core/src/zeit/locales/de/LC_MESSAGES/zeit.cms.po b/core/src/zeit/locales/de/LC_MESSAGES/zeit.cms.po
index d82044a124..76628dacf3 100644
--- a/core/src/zeit/locales/de/LC_MESSAGES/zeit.cms.po
+++ b/core/src/zeit/locales/de/LC_MESSAGES/zeit.cms.po
@@ -14,7 +14,7 @@
msgid ""
msgstr ""
"Project-Id-Version: Unknown\n"
-"POT-Creation-Date: Tue Nov 19 14:24:41 2024\n"
+"POT-Creation-Date: Tue Nov 26 17:02:26 2024\n"
"PO-Revision-Date: \n"
"Last-Translator: \n"
"Language-Team: \n"
@@ -190,7 +190,7 @@ msgid "Edit"
msgstr "Bearbeiten"
#: zeit/cms/browser/form.py:293 zeit/cms/repository/browser/file.py:89
-#: zeit/content/article/edit/browser/edit.py:299
+#: zeit/content/article/edit/browser/edit.py:300
#: zeit/content/article/edit/browser/form.py:469
#: zeit/content/article/edit/browser/push.py:52
#: zeit/content/article/edit/browser/reference.py:54
@@ -1504,6 +1504,7 @@ msgstr "Farbe Button"
#: zeit/content/advertisement/interfaces.py:33
#: zeit/content/article/edit/image.py:85
#: zeit/content/article/edit/interfaces.py:199
+#: zeit/content/article/edit/interfaces.py:672
#: zeit/content/article/interfaces.py:68 zeit/content/cp/interfaces.py:126
#: zeit/content/cp/interfaces.py:284 zeit/content/cp/interfaces.py:626
#: zeit/content/image/browser/configure.zcml:262
@@ -1560,7 +1561,7 @@ msgid "View metadata"
msgstr "Metadaten anzeigen"
#: zeit/content/animation/browser/form.py:21
-#: zeit/content/animation/interfaces.py:28 zeit/content/article/article.py:232
+#: zeit/content/animation/interfaces.py:28 zeit/content/article/article.py:217
#: zeit/content/article/browser/configure.zcml:74
#: zeit/content/article/edit/browser/form.py:58
msgid "Article"
@@ -1607,6 +1608,7 @@ msgid "Video to use for animation"
msgstr "Video"
#: zeit/content/animation/interfaces.py:44
+#: zeit/content/article/edit/interfaces.py:665
#: zeit/content/gallery/browser/configure.zcml:79
#: zeit/content/image/browser/configure.zcml:167
#: zeit/content/image/browser/imagebrowser.py:6
@@ -1689,21 +1691,21 @@ msgstr "zuletzt veröffentlicht am ${date} um ${time} von ${by}"
msgid "not published"
msgstr "Nicht veröffentlicht"
-#: zeit/content/article/edit/browser/edit.py:102
+#: zeit/content/article/edit/browser/edit.py:103
#: zeit/content/cp/browser/landing.py:53
msgid "The object \"${name}\" does not exist."
msgstr "Das Objekt »${name}« konnte nicht gefunden werden."
-#: zeit/content/article/edit/browser/edit.py:110
+#: zeit/content/article/edit/browser/edit.py:111
msgid "Could not create block for \"${name}\", because I don't know which one."
msgstr "Kann für \"${name}\" keinen Block anlegen, weil ich nicht weiß welchen"
-#: zeit/content/article/edit/browser/edit.py:303
+#: zeit/content/article/edit/browser/edit.py:304
msgid "generate-video-recommendation"
msgstr "Videoempfehlung generieren"
-#: zeit/content/article/edit/browser/edit.py:341
-#: zeit/content/article/edit/browser/edit.py:346
+#: zeit/content/article/edit/browser/edit.py:342
+#: zeit/content/article/edit/browser/edit.py:347
#: zeit/content/author/browser/form.py:170
#: zeit/content/author/browser/honorar.py:53
#: zeit/content/volume/browser/form.py:142
@@ -1924,6 +1926,14 @@ msgstr "Seitenumbruch"
msgid "Embed block"
msgstr "Easy-Embed"
+#: zeit/content/article/edit/image_parallax_properties.py:29
+msgid "Image Parallax Properties"
+msgstr "Bilder-Parallax-Eigenschaften"
+
+#: zeit/content/article/edit/image_row.py:56
+msgid "Image Row"
+msgstr "Bilderreihe"
+
#: zeit/content/article/edit/ingredientdice.py:15
msgid "Ingredient dice block"
msgstr "Zutatenwürfel bearbeiten"
@@ -1986,10 +1996,12 @@ msgid "Edited"
msgstr "redigiert"
#: zeit/content/article/edit/interfaces.py:209
+#: zeit/content/article/edit/interfaces.py:680
msgid "Display Mode"
msgstr "Darstellung"
#: zeit/content/article/edit/interfaces.py:217
+#: zeit/content/article/edit/interfaces.py:687
#: zeit/content/article/interfaces.py:76
msgid "Variant Name"
msgstr "Seitenverhältnis"
@@ -2210,6 +2222,23 @@ msgstr "Ziehen Sie einen Artikel/Link hierher"
msgid "Filter"
msgstr "Filter"
+#: zeit/content/article/edit/interfaces.py:674
+#: zeit/content/gallery/interfaces.py:102
+msgid "Image caption"
+msgstr "Bildunterschrift"
+
+#: zeit/content/article/edit/interfaces.py:675
+msgid "Alternative Text"
+msgstr "ALT-Text"
+
+#: zeit/content/article/edit/interfaces.py:695
+msgid "Show caption"
+msgstr "Bildunterschrift anzeigen"
+
+#: zeit/content/article/edit/interfaces.py:696
+msgid "Show source"
+msgstr "Bildquelle anzeigen"
+
#: zeit/content/article/edit/jobticker.py:18
msgid "Jobbox ticker block"
msgstr "Jobticker Modul"
@@ -2962,7 +2991,7 @@ msgid "Could not publish ${id} since it has validation errors."
msgstr ""
"${id} hat Validierungsfehler und kann daher nicht veröffentlicht werden."
-#: zeit/content/cp/centerpage.py:177
+#: zeit/content/cp/centerpage.py:165
msgid "Centerpage 2009"
msgstr "Center-Page"
@@ -3381,10 +3410,6 @@ msgstr ""
msgid "Is a cropped image of"
msgstr "Ist ein Zuschnitt von"
-#: zeit/content/gallery/interfaces.py:102
-msgid "Image caption"
-msgstr "Bildunterschrift"
-
#: zeit/content/gallery/interfaces.py:103
msgid "gallery-image-caption-description"
msgstr ""
@@ -4814,6 +4839,12 @@ msgstr "Format"
msgid "with info"
msgstr "mit Info"
+#~ msgid "Caption"
+#~ msgstr "Bildunterschrift"
+
+#~ msgid "Alt-Text"
+#~ msgstr "Alternativtext"
+
#~ msgid "Print Ressort"
#~ msgstr "Print-Ressort"
diff --git a/core/src/zeit/locales/zeit.cms.pot b/core/src/zeit/locales/zeit.cms.pot
index a55e7e0d66..2caff6dfa2 100644
--- a/core/src/zeit/locales/zeit.cms.pot
+++ b/core/src/zeit/locales/zeit.cms.pot
@@ -14,7 +14,7 @@
msgid ""
msgstr ""
"Project-Id-Version: Unknown\n"
-"POT-Creation-Date: Tue Nov 19 14:24:41 2024\n"
+"POT-Creation-Date: Tue Nov 26 17:02:26 2024\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: Zope 3 Developers \n"
@@ -209,7 +209,7 @@ msgstr ""
#: zeit/cms/browser/form.py:293
#: zeit/cms/repository/browser/file.py:89
-#: zeit/content/article/edit/browser/edit.py:299
+#: zeit/content/article/edit/browser/edit.py:300
#: zeit/content/article/edit/browser/form.py:469
#: zeit/content/article/edit/browser/push.py:52
#: zeit/content/article/edit/browser/reference.py:54
@@ -1541,6 +1541,7 @@ msgstr ""
#: zeit/content/advertisement/interfaces.py:33
#: zeit/content/article/edit/image.py:85
#: zeit/content/article/edit/interfaces.py:199
+#: zeit/content/article/edit/interfaces.py:672
#: zeit/content/article/interfaces.py:68
#: zeit/content/cp/interfaces.py:126
#: zeit/content/cp/interfaces.py:284
@@ -1603,7 +1604,7 @@ msgstr ""
#: zeit/content/animation/browser/form.py:21
#: zeit/content/animation/interfaces.py:28
-#: zeit/content/article/article.py:232
+#: zeit/content/article/article.py:217
#: zeit/content/article/browser/configure.zcml:74
#: zeit/content/article/edit/browser/form.py:58
msgid "Article"
@@ -1652,6 +1653,7 @@ msgid "Video to use for animation"
msgstr ""
#: zeit/content/animation/interfaces.py:44
+#: zeit/content/article/edit/interfaces.py:665
#: zeit/content/gallery/browser/configure.zcml:79
#: zeit/content/image/browser/configure.zcml:167
#: zeit/content/image/browser/imagebrowser.py:6
@@ -1736,21 +1738,21 @@ msgstr ""
msgid "not published"
msgstr ""
-#: zeit/content/article/edit/browser/edit.py:102
+#: zeit/content/article/edit/browser/edit.py:103
#: zeit/content/cp/browser/landing.py:53
msgid "The object \"${name}\" does not exist."
msgstr ""
-#: zeit/content/article/edit/browser/edit.py:110
+#: zeit/content/article/edit/browser/edit.py:111
msgid "Could not create block for \"${name}\", because I don't know which one."
msgstr ""
-#: zeit/content/article/edit/browser/edit.py:303
+#: zeit/content/article/edit/browser/edit.py:304
msgid "generate-video-recommendation"
msgstr ""
-#: zeit/content/article/edit/browser/edit.py:341
-#: zeit/content/article/edit/browser/edit.py:346
+#: zeit/content/article/edit/browser/edit.py:342
+#: zeit/content/article/edit/browser/edit.py:347
#: zeit/content/author/browser/form.py:170
#: zeit/content/author/browser/honorar.py:53
#: zeit/content/volume/browser/form.py:142
@@ -1979,6 +1981,14 @@ msgstr ""
msgid "Embed block"
msgstr ""
+#: zeit/content/article/edit/image_parallax_properties.py:29
+msgid "Image Parallax Properties"
+msgstr ""
+
+#: zeit/content/article/edit/image_row.py:56
+msgid "Image Row"
+msgstr ""
+
#: zeit/content/article/edit/ingredientdice.py:15
msgid "Ingredient dice block"
msgstr ""
@@ -2044,10 +2054,12 @@ msgid "Edited"
msgstr ""
#: zeit/content/article/edit/interfaces.py:209
+#: zeit/content/article/edit/interfaces.py:680
msgid "Display Mode"
msgstr ""
#: zeit/content/article/edit/interfaces.py:217
+#: zeit/content/article/edit/interfaces.py:687
#: zeit/content/article/interfaces.py:76
msgid "Variant Name"
msgstr ""
@@ -2274,6 +2286,23 @@ msgstr ""
msgid "Filter"
msgstr ""
+#: zeit/content/article/edit/interfaces.py:674
+#: zeit/content/gallery/interfaces.py:102
+msgid "Image caption"
+msgstr ""
+
+#: zeit/content/article/edit/interfaces.py:675
+msgid "Alternative Text"
+msgstr ""
+
+#: zeit/content/article/edit/interfaces.py:695
+msgid "Show caption"
+msgstr ""
+
+#: zeit/content/article/edit/interfaces.py:696
+msgid "Show source"
+msgstr ""
+
#: zeit/content/article/edit/jobticker.py:18
msgid "Jobbox ticker block"
msgstr ""
@@ -3034,7 +3063,7 @@ msgstr ""
msgid "Could not publish ${id} since it has validation errors."
msgstr ""
-#: zeit/content/cp/centerpage.py:177
+#: zeit/content/cp/centerpage.py:165
msgid "Centerpage 2009"
msgstr ""
@@ -3453,10 +3482,6 @@ msgstr ""
msgid "Is a cropped image of"
msgstr ""
-#: zeit/content/gallery/interfaces.py:102
-msgid "Image caption"
-msgstr ""
-
#: zeit/content/gallery/interfaces.py:103
msgid "gallery-image-caption-description"
msgstr ""