Skip to content

Commit

Permalink
Merge pull request #846 from ZeitOnline/WCM-316_add-teaser-attributes…
Browse files Browse the repository at this point in the history
…-to-volume

WCM-316: add teaser title, teaser text and background color to volume
  • Loading branch information
stollero committed Sep 17, 2024
2 parents 6370e80 + 58c5d82 commit af66db7
Show file tree
Hide file tree
Showing 13 changed files with 354 additions and 212 deletions.
1 change: 1 addition & 0 deletions core/docs/changelog/WCM-316.change
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
WCM-316: add teaser title, teaser text and background color to volume
53 changes: 39 additions & 14 deletions core/src/zeit/content/volume/browser/form.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,27 @@ def doc(self):

class Base:
form_fields = zope.formlib.form.FormFields(zeit.content.volume.interfaces.IVolume).select(
'product', 'year', 'volume', 'date_digital_published', 'teaserText'
'product',
'year',
'volume',
'date_digital_published',
'volume_note',
'title',
'teaser',
'background_color',
)

field_groups = (
gocept.form.grouped.Fields(
_('Volume'),
('product', 'year', 'volume', 'date_digital_published', 'teaserText'),
('product', 'year', 'volume', 'date_digital_published', 'volume_note'),
css_class='column-left',
),
gocept.form.grouped.Fields(
_('Teaser'),
('title', 'teaser', 'background_color'),
css_class='wide-widgets column-left',
),
)

def __init__(self, context, request):
Expand All @@ -58,10 +70,16 @@ def __init__(self, context, request):
required=False,
source=zeit.content.image.interfaces.imageGroupSource,
)
field.__name__ = 'cover_%s_%s' % (product.id, name)
field.__name__ = f'cover_{product.id}_{name}'
field.interface = ICovers
self.form_fields += zope.formlib.form.FormFields(field)
fieldnames.append(field.__name__)
# In addition to covers, we give the possibility to override the title
field = zope.schema.TextLine(title=_('Title'), required=False)
field.__name__ = f'title_{product.id}'
field.interface = ICovers
self.form_fields += zope.formlib.form.FormFields(field)
fieldnames.append(field.__name__)
self.field_groups += (
gocept.form.grouped.Fields(product.title, fieldnames, css_class='column-right'),
)
Expand Down Expand Up @@ -144,17 +162,24 @@ class Covers(grok.Adapter):
grok.context(zeit.content.volume.interfaces.IVolume)

def __getattr__(self, name):
if not name.startswith('cover_'):
return super().__getattr__(name)
name = name.replace('cover_', '', 1)
product, cover = name.split('_')
# We dont want the fallback in the UI
return self.context.get_cover(cover, product, use_fallback=False)
if name.startswith('title_'):
product = name.split('_')[1]
return self.context.get_cover_title(product)
elif name.startswith('cover_'):
name = name.replace('cover_', '', 1)
product, cover = name.split('_')
# We dont want the fallback in the UI
return self.context.get_cover(cover, product, use_fallback=False)
return super().__getattr__(name)

def __setattr__(self, name, value):
if not name.startswith('cover_'):
super().__setattr__(name, value)
if name.startswith('title_'):
product = name.split('_')[1]
self.context.set_cover_title(product, value)
return
elif name.startswith('cover_'):
name = name.replace('cover_', '', 1)
product, cover = name.split('_')
self.context.set_cover(cover, product, value)
return
name = name.replace('cover_', '', 1)
product, cover = name.split('_')
self.context.set_cover(cover, product, value)
super().__setattr__(name, value)
4 changes: 2 additions & 2 deletions core/src/zeit/content/volume/browser/reference.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@


class EditReference(zeit.edit.browser.form.InlineForm):
"""Display the additional field `teaserText` for references."""
"""Display the additional field `volume_note` for references."""

legend = ''

Expand All @@ -19,7 +19,7 @@ class EditReference(zeit.edit.browser.form.InlineForm):
# support read-only mode, see
# zeit.content.article.edit.browser.form.FormFields
render_context=zope.formlib.interfaces.DISPLAY_UNWRITEABLE,
).select('teaserText')
).select('volume_note')

@property
def prefix(self):
Expand Down
37 changes: 37 additions & 0 deletions core/src/zeit/content/volume/browser/tests/test_form.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,22 @@ def test_adds_centerpage_in_addition_to_volume(self):
self.assertEqual(2010, cp.year)
self.assertEqual(2, cp.volume)

def test_teaser_attributes_are_contained_in_volume(self):
self.open_add_form()
b = self.browser
b.getControl('Year').value = '2010'
b.getControl(name='form.volume').value = '2'
b.getControl('Add').click()
b.getControl(name='form.title').value = 'Obamas Return'
b.getControl(name='form.teaser').value = 'Obama returns you to tell about his vacation'
b.getControl(name='form.background_color').value = 'ff0000'
b.getControl('Apply').click()
b.getLink('Checkin').click()
volume = zeit.cms.interfaces.ICMSContent('http://xml.zeit.de/2010/02/ausgabe')
assert volume.title == 'Obamas Return'
assert volume.teaser == 'Obama returns you to tell about his vacation'
assert volume.background_color == 'ff0000'


class TestVolumeCoverWidget(zeit.content.volume.testing.SeleniumTestCase):
def setUp(self):
Expand All @@ -131,3 +147,24 @@ def test_only_one_cover_add_form_is_visible_at_the_time(self):
s.select('id=choose-cover', 'label=Zeit Magazin')
s.assertVisible('css=.fieldname-cover_ZMLB_portrait')
s.assertNotVisible('css=.fieldname-cover_ZEI_portrait')

def test_saves_title_for_each_cover(self):
s = self.selenium
volume = self.repository['2015']['01']['ausgabe']
title_overrides = volume.xml.makeelement('title-overrides')
text_zei = volume.xml.makeelement('title', {'product_id': 'ZEI'})
text_zei.text = 'Budgies are cool'
text_zmlb = volume.xml.makeelement('title', {'product_id': 'ZMLB'})
text_zmlb.text = 'Kingfishers are eating fish'
title_overrides.append(text_zei)
title_overrides.append(text_zmlb)
volume.xml.append(title_overrides)
self.repository['2015']['01']['ausgabe'] = volume
self.open('/repository/2015/01/ausgabe/@@checkout')
s.waitForElementPresent('css=#choose-cover')
# Set title of Die Zeit
s.select('id=choose-cover', 'label=Die Zeit')
s.assertValue('id=form.title_ZEI', 'Budgies are cool')
# Set title for Zeit Magazin
s.select('id=choose-cover', 'label=Zeit Magazin')
s.assertValue('id=form.title_ZMLB', 'Kingfishers are eating fish')
35 changes: 33 additions & 2 deletions core/src/zeit/content/volume/interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class IVolume(zeit.cms.content.interfaces.IXMLContent):

volume = zope.schema.Int(title=_('Volume'), min=1, max=54)

teaserText = zope.schema.Text(title=_('Volume text'), required=False, max_length=170)
volume_note = zope.schema.Text(title=_('Volume text'), required=False, max_length=170)

date_digital_published = zope.schema.Datetime(title=_('Date of digital publication'))

Expand All @@ -47,6 +47,19 @@ class IVolume(zeit.cms.content.interfaces.IXMLContent):

next = zope.interface.Attribute('The next IVolume object (by date_digital_published) or None')

title = zope.schema.TextLine(title=_('Title'), required=False)

teaser = zope.schema.TextLine(title=_('Teaser'), required=False)

background_color = zope.schema.TextLine(
title=_('Area background color (6 characters, no #)'),
description=_('Hex value of background color for area'),
required=False,
min_length=6,
max_length=6,
constraint=zeit.cms.content.interfaces.hex_literal,
)

def fill_template(text):
"""Fill in a string template with the placeholders year=self.year
and name=self.volume (zero-padded to two digits), e.g.
Expand All @@ -71,6 +84,24 @@ def set_cover(cover_id, product_id, image):
Set an image as a cover of product.
"""

def get_cover_title(product_id):
"""
Get a title of a product.
For example volume.get_title('ZEI') returns the title of DIE ZEIT of
this specific volume.
:param product_id: str product ID set in products.xml
:return: str
"""

def set_cover_title(product_id, title):
"""
Set cover specific title.
For example volume.set_title('ZEI', 'DIE ZEIT') sets the title of DIE
ZEIT of this specific volume.
:param product_id: str product ID set in products.xml
:param title: str - title of the product
"""

def all_content_via_search(additional_query_contstraints):
"""
Get all Content for this volume with a Elasticsearch-Lookup.
Expand Down Expand Up @@ -103,7 +134,7 @@ def content_with_references_for_publishing():


class IVolumeReference(zeit.cms.content.interfaces.IReference):
teaserText = zope.schema.Text(title=_('Volume text'), required=False, max_length=170)
volume_note = zope.schema.Text(title=_('Volume text'), required=False, max_length=170)


class ITocConnector(zope.interface.Interface):
Expand Down
8 changes: 4 additions & 4 deletions core/src/zeit/content/volume/reference.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ class RelatedReference(zeit.cms.content.reference.Reference):
grok.provides(zeit.cms.content.interfaces.IReference)
grok.name('related')

_teaserText_local = zeit.cms.content.property.ObjectPathAttributeProperty(
'.', 'teasertext_local', zeit.content.volume.interfaces.IVolumeReference['teaserText']
_volume_note_local = zeit.cms.content.property.ObjectPathAttributeProperty(
'.', 'volume_note_local', zeit.content.volume.interfaces.IVolumeReference['volume_note']
)
teaserText = zeit.cms.content.reference.OverridableProperty(
zeit.content.volume.interfaces.IVolume['teaserText'], original='target'
volume_note = zeit.cms.content.reference.OverridableProperty(
zeit.content.volume.interfaces.IVolume['volume_note'], original='target'
)
16 changes: 8 additions & 8 deletions core/src/zeit/content/volume/tests/test_reference.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def setUp(self):

super().setUp()
volume = Volume()
volume.teaserText = 'original'
volume.volume_note = 'original'
self.repository['testvolume'] = volume
self.volume = self.repository['testvolume']

Expand All @@ -31,7 +31,7 @@ def test_reference_honors_ICommonMetadata_xml_format(self):
volume = Volume()
volume.year = 2015
volume.volume = 1
volume.teaserText = 'original'
volume.volume_note = 'original'
volume.product = zeit.cms.content.sources.Product('ZEI')
self.repository['2015'] = Folder()
self.repository['2015']['01'] = Folder()
Expand All @@ -58,16 +58,16 @@ def test_volume_can_be_adapted_to_IReference(self):
)
self.assertEqual(True, IVolumeReference.providedBy(reference))

def test_teasertext_can_be_overridden(self):
def test_volume_note_can_be_overridden(self):
node = zope.component.getAdapter(
self.volume, zeit.cms.content.interfaces.IXMLReference, name='related'
)
source = zeit.content.article.edit.volume.Volume(None, lxml.builder.E.volume())
reference = zope.component.getMultiAdapter(
(source, node), zeit.cms.content.interfaces.IReference, name='related'
)
self.assertEqual('original', reference.teaserText)
reference.teaserText = 'local'
self.assertEqual('local', reference.teaserText)
reference.teaserText = None
self.assertEqual('original', reference.teaserText)
self.assertEqual('original', reference.volume_note)
reference.volume_note = 'local'
self.assertEqual('local', reference.volume_note)
reference.volume_note = None
self.assertEqual('original', reference.volume_note)
24 changes: 22 additions & 2 deletions core/src/zeit/content/volume/tests/test_volume.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import lxml.builder
import lxml.etree
import pytest
import pytz
import requests_mock
import zope.component
Expand Down Expand Up @@ -180,9 +181,9 @@ def test_looks_up_centerpage_for_depent_product_content(self):
cp = zeit.content.cp.interfaces.ICenterPage(volume)
self.assertEqual('http://xml.zeit.de/2015/01/index', cp.uniqueId)

def test_no_teaserText_present_returns_default_string(self):
def test_no_volume_note_present_returns_default_string(self):
volume = zeit.cms.interfaces.ICMSContent('http://xml.zeit.de/2015/01/ausgabe')
self.assertEqual('Teäser 01/2015', volume.teaserText)
self.assertEqual('Teäser 01/2015', volume.volume_note)

def test_covers_are_published_with_the_volume(self):
volume = self.repository['2015']['01']['ausgabe']
Expand Down Expand Up @@ -313,6 +314,25 @@ def test_volume_contents_access_dry_run_does_not_change_accces(self, mock):
self.assertEqual('free', c.access)


@pytest.mark.parametrize(
'color, raised_exception',
[
('123456', None), # Valid hex value
('abcdeg', zeit.cms.interfaces.ValidationError), # Invalid hex (faulty character)
('ff12', zope.schema._bootstrapinterfaces.TooShort), # Invalid hex value (too short)
('abcdef123456', zope.schema._bootstrapinterfaces.TooLong), # Invalid hex value (too long)
(None, None), # Absent value raises nothing
],
)
def test_background_color_is_hex_validation(color, raised_exception):
field = zeit.content.volume.interfaces.IVolume['background_color']
if raised_exception:
with pytest.raises(raised_exception):
field.validate(color)
else:
field.validate(color)


class TestWebtrekkQuery(TestVolumeQueries):
def setUp(self):
super().setUp()
Expand Down
41 changes: 33 additions & 8 deletions core/src/zeit/content/volume/volume.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,15 @@ class Volume(zeit.cms.content.xmlsupport.XMLContentBase):
<volume>
<head/>
<body/>
<title-overrides/>
<covers/>
</volume>
"""

zeit.cms.content.dav.mapProperties(
zeit.content.volume.interfaces.IVolume,
zeit.cms.interfaces.DOCUMENT_SCHEMA_NS,
('date_digital_published', 'year', 'volume'),
('date_digital_published', 'year', 'volume', 'title', 'teaser', 'background_color'),
)

_product_id = zeit.cms.content.dav.DAVProperty(
Expand All @@ -71,22 +72,28 @@ def product(self, value):
return
self._product_id = value.id if value is not None else None

_teaserText = zeit.cms.content.dav.DAVProperty(
zeit.content.volume.interfaces.IVolume['teaserText'],
_volume_note = zeit.cms.content.dav.DAVProperty(
zeit.content.volume.interfaces.IVolume['volume_note'],
zeit.cms.interfaces.DOCUMENT_SCHEMA_NS,
'volume_note',
)

_old_volume_note = zeit.cms.content.dav.DAVProperty(
zeit.content.volume.interfaces.IVolume['volume_note'],
zeit.cms.interfaces.DOCUMENT_SCHEMA_NS,
'teaserText',
)

@property
def teaserText(self):
text = self._teaserText
def volume_note(self):
text = self._volume_note or self._old_volume_note
if text is None:
text = zeit.cms.config.required('zeit.content.volume', 'default-teaser-text')
return self.fill_template(text)

@teaserText.setter
def teaserText(self, value):
self._teaserText = value
@volume_note.setter
def volume_note(self, value):
self._volume_note = value

@property
def teaserSupertitle(self): # For display in CP-editor
Expand Down Expand Up @@ -204,6 +211,24 @@ def set_cover(self, cover_id, product_id, imagegroup):
self.xml.find('covers').append(node)
super().__setattr__('_p_changed', True)

def get_cover_title(self, product_id):
path = f'//volume/title-overrides/title[@product_id="{product_id}"]'
node = self.xml.xpath(path)
return node[0].text if node else None

def set_cover_title(self, product_id, title):
title_overrides_path = '//volume/title-overrides'
if not self.xml.xpath(title_overrides_path):
self.xml.append(lxml.builder.E('title-overrides'))
path = f'//volume/title-overrides/title[@product_id="{product_id}"]'
node = self.xml.xpath(path)
if node:
self.xml.find('title-overrides').remove(node[0])
if title is not None:
node = lxml.builder.E.title(title, product_id=product_id)
self.xml.find('title-overrides').append(node)
super().__setattr__('_p_changed', True)

def _is_valid_cover_id_and_product_id(self, cover_id, product_id):
cover_ids = list(zeit.content.volume.interfaces.VOLUME_COVER_SOURCE(self))
product_ids = [prod.id for prod in self._all_products]
Expand Down
Binary file modified core/src/zeit/locales/de/LC_MESSAGES/zeit.cms.mo
Binary file not shown.
Loading

0 comments on commit af66db7

Please sign in to comment.