Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WCM-316: add teaser title, teaser text and background color to volume #846

Merged
merged 8 commits into from
Sep 17, 2024
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'),
Sinnaj94 marked this conversation as resolved.
Show resolved Hide resolved
)

_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