From 62b20fb76beeaa7167dbf413db5468ed470c0845 Mon Sep 17 00:00:00 2001 From: Martin Stolle Date: Mon, 23 Sep 2024 15:05:24 +0200 Subject: [PATCH 1/9] Revert "WCM-285: the contract is, keep the DAV as is but internally store if type is different" This reverts commit 09f670705484975312bc22928d38530769c41959. --- core/src/zeit/connector/filesystem.py | 13 +++++++++++++ core/src/zeit/connector/mock.py | 11 +++++++++++ core/src/zeit/connector/tests/test_contract.py | 4 ++-- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/core/src/zeit/connector/filesystem.py b/core/src/zeit/connector/filesystem.py index e54da289dc..ce5a6d1b36 100644 --- a/core/src/zeit/connector/filesystem.py +++ b/core/src/zeit/connector/filesystem.py @@ -11,7 +11,9 @@ import zope.app.file.image import zope.interface +from zeit.cms.content.sources import FEATURE_TOGGLES from zeit.connector.interfaces import ID_NAMESPACE, CannonicalId +from zeit.connector.models import ContentWithMetadataColumns as Content import zeit.cms.config import zeit.cms.content.dav import zeit.connector.interfaces @@ -251,6 +253,7 @@ def _get_properties(self, id): return properties properties.update(parse_properties(xml)) + self._convert_sql_types(properties) if zeit.connector.interfaces.RESOURCE_TYPE_PROPERTY not in properties: properties[zeit.connector.interfaces.RESOURCE_TYPE_PROPERTY] = self._guess_type(id) @@ -258,6 +261,16 @@ def _get_properties(self, id): self.property_cache[id] = properties return properties + def _convert_sql_types(self, properties): + if not FEATURE_TOGGLES.find('read_metadata_columns'): + return + for key, value in properties.items(): + column = Content.column_by_name(*key) + if column is None: + continue + converter = zeit.connector.interfaces.IConverter(column) + properties[key] = converter.deserialize(value) + def _guess_type(self, id): path = self._path(id) if os.path.isdir(path): diff --git a/core/src/zeit/connector/mock.py b/core/src/zeit/connector/mock.py index a73d2a2145..2815b9875b 100644 --- a/core/src/zeit/connector/mock.py +++ b/core/src/zeit/connector/mock.py @@ -14,6 +14,8 @@ import sqlalchemy import zope.event +from zeit.cms.content.sources import FEATURE_TOGGLES +from zeit.connector.converter import DefaultConverter from zeit.connector.interfaces import ( ID_NAMESPACE, UUID_PROPERTY, @@ -368,6 +370,7 @@ def _get_properties(self, id): properties = super()._get_properties(id) else: properties = properties.copy() + self._convert_sql_types(properties) return properties def _set_properties(self, id, properties): @@ -383,6 +386,14 @@ def _set_properties(self, id, properties): stored_properties.pop((name, namespace), None) continue + if FEATURE_TOGGLES.find('write_metadata_columns'): + column = Content.column_by_name(name, namespace) + converter = zeit.connector.interfaces.IConverter(column) + value = converter.serialize(value) + else: + converter = DefaultConverter(None) + if isinstance(converter, DefaultConverter) and not isinstance(value, str): + raise ValueError('Expected str, got %s: %r' % (type(value), value)) stored_properties[(name, namespace)] = value self._properties[id] = stored_properties diff --git a/core/src/zeit/connector/tests/test_contract.py b/core/src/zeit/connector/tests/test_contract.py index 3aa80afb01..fa752efacd 100644 --- a/core/src/zeit/connector/tests/test_contract.py +++ b/core/src/zeit/connector/tests/test_contract.py @@ -896,7 +896,7 @@ def setUp(self): def test_converts_scalar_types_on_read(self): self.repository.connector.changeProperties( 'http://xml.zeit.de/testcontent', - {('overscrolling', 'http://namespaces.zeit.de/CMS/document'): 'yes'}, + {('overscrolling', 'http://namespaces.zeit.de/CMS/document'): True}, ) self.assertIs(True, self.repository['testcontent'].overscrolling) @@ -905,7 +905,7 @@ def test_converts_scalar_types_on_write(self): co.overscrolling = True resource = self.repository.connector['http://xml.zeit.de/testcontent'] self.assertIs( - 'yes', resource.properties[('overscrolling', 'http://namespaces.zeit.de/CMS/document')] + True, resource.properties[('overscrolling', 'http://namespaces.zeit.de/CMS/document')] ) From ca0c35c0e9c3135cb50e08f5645364c43325db5e Mon Sep 17 00:00:00 2001 From: Martin Stolle Date: Mon, 23 Sep 2024 15:08:25 +0200 Subject: [PATCH 2/9] Revert "WCM-291: DAVPropertyConverter for datetime should not return datetime format to DAV if str is expected" This reverts commit f96682fbeaa6d687d03a59906d18c2ddeedaadef. --- core/src/zeit/cms/content/dav.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/core/src/zeit/cms/content/dav.py b/core/src/zeit/cms/content/dav.py index e882cfcb24..46eca9f18c 100644 --- a/core/src/zeit/cms/content/dav.py +++ b/core/src/zeit/cms/content/dav.py @@ -355,18 +355,20 @@ def __init__(self, context, properties, propertykey): def fromProperty(self, value): if not value: return None + if self.has_sql_type and FEATURE_TOGGLES.find('read_metadata_columns'): + return value return self._fromProperty(value) @staticmethod def _fromProperty(value): # We have _mostly_ iso8601, but some old content has the format # "Thu, 13 Mar 2008 13:48:37 GMT", so we use a lenient parser. - if not value: - return None date = pendulum.parse(value, strict=False) return date.in_tz('UTC') def toProperty(self, value): + if self.has_sql_type and FEATURE_TOGGLES.find('write_metadata_columns'): + return value return self._toProperty(value) @staticmethod From 24470791576b886b80ce394fc8fc4e2a6645eeb1 Mon Sep 17 00:00:00 2001 From: Martin Stolle Date: Mon, 23 Sep 2024 16:11:00 +0200 Subject: [PATCH 3/9] WCM-285: revert back to behaviour sql type not being converted to webdav - this is the intented behaviour, we do not want the dav anymore because we should prevent needless casting where possible --- core/src/zeit/cms/content/dav.py | 5 +++++ core/src/zeit/connector/converter.py | 14 ++++++-------- core/src/zeit/connector/filesystem.py | 2 +- core/src/zeit/connector/models.py | 19 ++----------------- 4 files changed, 14 insertions(+), 26 deletions(-) diff --git a/core/src/zeit/cms/content/dav.py b/core/src/zeit/cms/content/dav.py index 46eca9f18c..b49b1060c9 100644 --- a/core/src/zeit/cms/content/dav.py +++ b/core/src/zeit/cms/content/dav.py @@ -327,8 +327,11 @@ def toProperty(self, value): class BoolProperty: def __init__(self, context, properties, propertykey): self.context = context + self.has_sql_type = ConnectorModel.column_by_name(*propertykey) is not None def fromProperty(self, value): + if self.has_sql_type and FEATURE_TOGGLES.find('read_metadata_columns'): + return value return self._fromProperty(value) @staticmethod @@ -336,6 +339,8 @@ def _fromProperty(value): return value.lower() in ('yes', 'true') def toProperty(self, value): + if self.has_sql_type and FEATURE_TOGGLES.find('write_metadata_columns'): + return value return self._toProperty(value) @staticmethod diff --git a/core/src/zeit/connector/converter.py b/core/src/zeit/connector/converter.py index b8142101eb..4d96170407 100644 --- a/core/src/zeit/connector/converter.py +++ b/core/src/zeit/connector/converter.py @@ -1,5 +1,3 @@ -import datetime - from sqlalchemy.dialects.postgresql import JSONB import grokcore.component as grok import sqlalchemy @@ -28,30 +26,30 @@ def deserialize(self, value): class BoolConverter(DefaultConverter): grok.context(sqlalchemy.Boolean) - def serialize(self, value: bool) -> str: + def serialize(self, value): return zeit.cms.content.dav.BoolProperty._toProperty(value) - def deserialize(self, value: str) -> bool: + def deserialize(self, value): return zeit.cms.content.dav.BoolProperty._fromProperty(value) class IntConverter(DefaultConverter): grok.context(sqlalchemy.Integer) - def serialize(self, value: int) -> str: + def serialize(self, value): return str(value) - def deserialize(self, value: str) -> int: + def deserialize(self, value): return int(value) class DatetimeConverter(DefaultConverter): grok.context(sqlalchemy.TIMESTAMP) - def serialize(self, value: datetime.datetime) -> str: + def serialize(self, value): return zeit.cms.content.dav.DatetimeProperty._toProperty(value) - def deserialize(self, value: str) -> datetime.datetime: + def deserialize(self, value): return zeit.cms.content.dav.DatetimeProperty._fromProperty(value) diff --git a/core/src/zeit/connector/filesystem.py b/core/src/zeit/connector/filesystem.py index ce5a6d1b36..031af1a473 100644 --- a/core/src/zeit/connector/filesystem.py +++ b/core/src/zeit/connector/filesystem.py @@ -13,7 +13,7 @@ from zeit.cms.content.sources import FEATURE_TOGGLES from zeit.connector.interfaces import ID_NAMESPACE, CannonicalId -from zeit.connector.models import ContentWithMetadataColumns as Content +from zeit.connector.models import Content import zeit.cms.config import zeit.cms.content.dav import zeit.connector.interfaces diff --git a/core/src/zeit/connector/models.py b/core/src/zeit/connector/models.py index bd60db7f2f..ec906f7fbf 100644 --- a/core/src/zeit/connector/models.py +++ b/core/src/zeit/connector/models.py @@ -16,7 +16,6 @@ from sqlalchemy.orm import declared_attr, mapped_column, relationship import pytz import sqlalchemy -import zope.component from zeit.cms.content.sources import FEATURE_TOGGLES from zeit.connector.interfaces import INTERNAL_PROPERTY, DeleteProperty, LockStatus @@ -177,17 +176,6 @@ def lock_status(self): NS = 'http://namespaces.zeit.de/CMS/' - @staticmethod - def converter(column): - if 'converter' in column.info: - return zope.component.queryAdapter( - column.type, - zeit.connector.interfaces.IConverter, - column.info['converter'], - ) - else: - return zeit.connector.interfaces.IConverter(column) - def to_webdav(self): if self.unsorted is None: return {} @@ -205,9 +193,7 @@ def to_webdav(self): if FEATURE_TOGGLES.find('read_metadata_columns'): for column in self._columns_with_name(): namespace, name = column.info['namespace'], column.info['name'] - value = getattr(self, column.name) - converter = self.converter(column) - props[(name, self.NS + namespace)] = converter.serialize(value) + props[(name, self.NS + namespace)] = getattr(self, column.name) if self.lock: props[('lock_principal', INTERNAL_PROPERTY)] = self.lock.principal @@ -235,8 +221,7 @@ def from_webdav(self, props): namespace, name = column.info['namespace'], column.info['name'] value = props.get((name, self.NS + namespace), self) if value is not self: - converter = self.converter(column) - setattr(self, column.name, converter.deserialize(value)) + setattr(self, column.name, value) unsorted = collections.defaultdict(dict) for (k, ns), v in props.items(): From 2e23b75c9e3ff2b0a06ebe85e63a3f37ab05fc83 Mon Sep 17 00:00:00 2001 From: Martin Stolle Date: Tue, 24 Sep 2024 11:33:21 +0200 Subject: [PATCH 4/9] WCM-285: replace overscrolling model field with existing date_created --- core/src/zeit/connector/models.py | 9 +- .../src/zeit/connector/tests/test_contract.py | 20 ++-- .../zeit/connector/tests/test_postgresql.py | 94 ------------------- 3 files changed, 15 insertions(+), 108 deletions(-) diff --git a/core/src/zeit/connector/models.py b/core/src/zeit/connector/models.py index ec906f7fbf..ade1844e3b 100644 --- a/core/src/zeit/connector/models.py +++ b/core/src/zeit/connector/models.py @@ -90,12 +90,6 @@ class DevelopmentCommonMetadata: access = mapped_column(Unicode, index=True, info={'namespace': 'document', 'name': 'access'}) -class DevelopmentZeitWeb: - overscrolling_enabled = mapped_column( - Boolean, info={'namespace': 'document', 'name': 'overscrolling'} - ) - - class ContentBase: __abstract__ = True __tablename__ = 'properties' @@ -229,6 +223,8 @@ def from_webdav(self, props): continue if ns == INTERNAL_PROPERTY: continue + if isinstance(v, datetime): + continue unsorted[ns.replace(self.NS, '', 1)][k] = v self.unsorted = unsorted @@ -299,7 +295,6 @@ class DevelopmentContent( PublishInfo, SemanticChange, DevelopmentCommonMetadata, - DevelopmentZeitWeb, ): lock_class = 'LockWithMetadataColumns' diff --git a/core/src/zeit/connector/tests/test_contract.py b/core/src/zeit/connector/tests/test_contract.py index fa752efacd..9056229b50 100644 --- a/core/src/zeit/connector/tests/test_contract.py +++ b/core/src/zeit/connector/tests/test_contract.py @@ -889,23 +889,29 @@ class ContractZopeSQL( class ContractProperties: def setUp(self): super().setUp() - FEATURE_TOGGLES.set('read_metadata_columns', 'True') - FEATURE_TOGGLES.set('write_metadata_columns', 'True') + FEATURE_TOGGLES.set('read_metadata_columns', True) + FEATURE_TOGGLES.set('write_metadata_columns', True) self.repository['testcontent'] = ExampleContentType() def test_converts_scalar_types_on_read(self): + example_date = datetime(2010, 1, 1, 0, 0, tzinfo=pytz.UTC) self.repository.connector.changeProperties( 'http://xml.zeit.de/testcontent', - {('overscrolling', 'http://namespaces.zeit.de/CMS/document'): True}, + {('date_created', 'http://namespaces.zeit.de/CMS/document'): example_date}, + ) + self.assertEqual( + example_date, + zeit.cms.workflow.interfaces.IModified(self.repository['testcontent']).date_created, ) - self.assertIs(True, self.repository['testcontent'].overscrolling) def test_converts_scalar_types_on_write(self): + example_date = datetime(2010, 1, 1, 0, 0, tzinfo=pytz.UTC) with checked_out(self.repository['testcontent']) as co: - co.overscrolling = True + zeit.cms.workflow.interfaces.IModified(co).date_created = example_date resource = self.repository.connector['http://xml.zeit.de/testcontent'] - self.assertIs( - True, resource.properties[('overscrolling', 'http://namespaces.zeit.de/CMS/document')] + self.assertEqual( + example_date, + resource.properties[('date_created', 'http://namespaces.zeit.de/CMS/document')], ) diff --git a/core/src/zeit/connector/tests/test_postgresql.py b/core/src/zeit/connector/tests/test_postgresql.py index 7d305be418..a7bf35dfa1 100644 --- a/core/src/zeit/connector/tests/test_postgresql.py +++ b/core/src/zeit/connector/tests/test_postgresql.py @@ -356,100 +356,6 @@ def test_search_looks_in_columns_or_unsorted_depending_on_toggle(self): self.assertEqual(res.id, unique_id) -class ChannelsColumnTest(zeit.connector.testing.SQLTest): - layer = zeit.connector.testing.SQL_CONTENT_LAYER - - def setUp(self): - super().setUp() - FEATURE_TOGGLES.set('read_metadata_columns', True) - FEATURE_TOGGLES.set('write_metadata_columns', True) - - def _make_resource(self, channels): - res = self.add_resource('foo', properties={('channels', DOCUMENT_SCHEMA_NS): channels}) - return self.connector._get_content(res.id) - - def assert_channels(self, channels, expected_channels, expected_dav_channels): - self.assertEqual(channels, expected_channels) - res = self.connector['http://xml.zeit.de/testing/foo'] - self.assertEqual(expected_dav_channels, res.properties[('channels', DOCUMENT_SCHEMA_NS)]) - - def test_modify_channels(self): - content = self._make_resource('channel1;channel2 sub1 sub2') - self.assertEqual(content.channels, {'channel1': [], 'channel2': ['sub1', 'sub2']}) - self.connector.changeProperties( - content.uniqueid, {('channels', DOCUMENT_SCHEMA_NS): 'channel2'} - ) - content = self.connector._get_content(content.uniqueid) - self.assertEqual(content.channels, {'channel2': []}) - - def test_empty_input(self): - channels = '' - content = self._make_resource(channels) - self.assert_channels(content.channels, {}, channels) - - def test_single_channel(self): - channels = 'channel1' - content = self._make_resource(channels) - self.assert_channels(content.channels, {'channel1': []}, channels) - - def test_multiple_channels(self): - channels = 'channel1;channel2;channel3' - content = self._make_resource(channels) - self.assert_channels( - content.channels, {'channel1': [], 'channel2': [], 'channel3': []}, channels - ) - - def test_single_channel_with_subchannels(self): - channels = 'channel1 sub1 sub2' - content = self._make_resource(channels) - self.assert_channels(content.channels, {'channel1': ['sub1', 'sub2']}, channels) - - def test_same_channel_with_subchannels(self): - channels = 'channel1 sub1;channel1 sub2 sub3;channel1 sub4' - content = self._make_resource(channels) - self.assert_channels( - content.channels, - {'channel1': ['sub1', 'sub2', 'sub3', 'sub4']}, - 'channel1 sub1 sub2 sub3 sub4', - ) - - def test_multiple_channels_with_subchannels(self): - channels = 'channel1 sub1;channel2 sub2 sub3;channel3 sub4' - content = self._make_resource(channels) - self.assert_channels( - content.channels, - {'channel1': ['sub1'], 'channel2': ['sub2', 'sub3'], 'channel3': ['sub4']}, - channels, - ) - - def test_whitespace_handling(self): - channels = ' channel1 sub1 ; channel2 sub2 sub3 ; channel3 sub4 ' - content = self._make_resource(channels) - self.assert_channels( - content.channels, - {'channel1': ['sub1'], 'channel2': ['sub2', 'sub3'], 'channel3': ['sub4']}, - 'channel1 sub1;channel2 sub2 sub3;channel3 sub4', - ) - - def test_trailing_semicolon(self): - channels = 'channel1;channel2;' - content = self._make_resource(channels) - self.assert_channels(content.channels, {'channel1': [], 'channel2': []}, channels[:-1]) - - def test_leading_semicolon(self): - channels = ';channel1;channel2' - content = self._make_resource(channels) - self.assert_channels(content.channels, {'channel1': [], 'channel2': []}, channels[1:]) - - def test_multiple_semicolons(self): - content = self._make_resource('channel1;;channel2;;;channel3') - self.assert_channels( - content.channels, - {'channel1': [], 'channel2': [], 'channel3': []}, - 'channel1;channel2;channel3', - ) - - class WorkflowColumnsTest(zeit.connector.testing.SQLTest): layer = zeit.connector.testing.SQL_CONTENT_LAYER From ab8b347330c80e57c7fd65698831b78d20122d57 Mon Sep 17 00:00:00 2001 From: Martin Stolle Date: Tue, 24 Sep 2024 14:16:10 +0200 Subject: [PATCH 5/9] WCM-285: channels to work with dict --- core/src/zeit/cms/content/dav.py | 5 +++ core/src/zeit/connector/converter.py | 35 ++++++------------- core/src/zeit/connector/mock.py | 2 +- core/src/zeit/connector/models.py | 2 +- .../src/zeit/connector/tests/test_contract.py | 21 +++++++++++ 5 files changed, 38 insertions(+), 27 deletions(-) diff --git a/core/src/zeit/cms/content/dav.py b/core/src/zeit/cms/content/dav.py index b49b1060c9..5a73d5a920 100644 --- a/core/src/zeit/cms/content/dav.py +++ b/core/src/zeit/cms/content/dav.py @@ -407,6 +407,7 @@ class CollectionTextLineProperty: SPLIT_PATTERN = re.compile(r'(?!\\);') def __init__(self, context, value_type, properties, propertykey): + self.has_sql_type = ConnectorModel.column_by_name(*propertykey) is not None self.context = context self.value_type = value_type self.properties = properties @@ -417,6 +418,8 @@ def __init__(self, context, value_type, properties, propertykey): self._type = self._type[0] def fromProperty(self, value): + if self.has_sql_type and FEATURE_TOGGLES.find('read_metadata_columns'): + return value typ = zope.component.getMultiAdapter( (self.value_type, self.properties, self.propertykey), zeit.cms.content.interfaces.IDAVPropertyConverter, @@ -441,6 +444,8 @@ def fromProperty(self, value): return self._type(result) def toProperty(self, value): + if self.has_sql_type and FEATURE_TOGGLES.find('write_metadata_columns'): + return value typ = zope.component.getMultiAdapter( (self.value_type, self.properties, self.propertykey), zeit.cms.content.interfaces.IDAVPropertyConverter, diff --git a/core/src/zeit/connector/converter.py b/core/src/zeit/connector/converter.py index 4d96170407..49c26f6901 100644 --- a/core/src/zeit/connector/converter.py +++ b/core/src/zeit/connector/converter.py @@ -53,31 +53,16 @@ def deserialize(self, value): return zeit.cms.content.dav.DatetimeProperty._fromProperty(value) -class ChannelsConverter(DefaultConverter): +@grok.implementer(zeit.connector.interfaces.IConverter) +class DictConverter(grok.Adapter): grok.context(JSONB) - grok.name('channels') - def serialize(self, value: dict) -> str: + def serialize(self, value): if not value: - return '' - elements = [] - for channel, subchannels in value.items(): - if subchannels: - elements.append(f"{channel} {' '.join(subchannels)}") - else: - elements.append(channel) - return ';'.join(elements) - - def deserialize(self, value: str) -> dict: - """channels are separated by semicolon, subchannels are separated by space""" - channels = {} - if value: - elements = [i.split() for i in value.split(';') if i.strip()] - for element in elements: - channel = element[0] - subchannels = element[1:] if len(element) > 1 else [] - if channel in channels: - channels[channel].extend(subchannels) - else: - channels[channel] = subchannels - return channels + return {} + return value + + def deserialize(self, value): + if not value: + return {} + return value diff --git a/core/src/zeit/connector/mock.py b/core/src/zeit/connector/mock.py index 2815b9875b..a12125152b 100644 --- a/core/src/zeit/connector/mock.py +++ b/core/src/zeit/connector/mock.py @@ -26,7 +26,7 @@ MoveError, ) from zeit.connector.lock import lock_is_foreign -from zeit.connector.models import DevelopmentContent as Content +from zeit.connector.models import Content import zeit.cms.config import zeit.cms.repository.interfaces import zeit.connector.cache diff --git a/core/src/zeit/connector/models.py b/core/src/zeit/connector/models.py index ade1844e3b..0669e3afbd 100644 --- a/core/src/zeit/connector/models.py +++ b/core/src/zeit/connector/models.py @@ -39,7 +39,7 @@ def table_args(tablename): channels = mapped_column( JSONB, nullable=True, - info={'namespace': 'document', 'name': 'channels', 'converter': 'channels'}, + info={'namespace': 'document', 'name': 'channels'}, ) diff --git a/core/src/zeit/connector/tests/test_contract.py b/core/src/zeit/connector/tests/test_contract.py index 9056229b50..f33184dde4 100644 --- a/core/src/zeit/connector/tests/test_contract.py +++ b/core/src/zeit/connector/tests/test_contract.py @@ -914,6 +914,27 @@ def test_converts_scalar_types_on_write(self): resource.properties[('date_created', 'http://namespaces.zeit.de/CMS/document')], ) + def test_converts_aggregate_types_on_read(self): + example_channels = {'channel1': [], 'channel2': ['sub1', 'sub2']} + self.repository.connector.changeProperties( + 'http://xml.zeit.de/testcontent', + {('channels', 'http://namespaces.zeit.de/CMS/document'): example_channels}, + ) + self.assertEqual( + example_channels, + self.repository['testcontent'].channels, + ) + + def test_converts_aggregate_types_on_write(self): + example_channels = {'channel1': [], 'channel2': ['sub1', 'sub2']} + with checked_out(self.repository['testcontent']) as co: + co.channels = example_channels + resource = self.repository.connector['http://xml.zeit.de/testcontent'] + self.assertEqual( + example_channels, + resource.properties[('channels', 'http://namespaces.zeit.de/CMS/document')], + ) + class PropertiesSQL(ContractProperties, zeit.cms.testing.FunctionalTestCase): layer = zeit.connector.testing.SQL_CONTENT_LAYER From a13629976af8b4aaa2ff0f63840d1359613532e0 Mon Sep 17 00:00:00 2001 From: Martin Stolle Date: Tue, 24 Sep 2024 14:31:37 +0200 Subject: [PATCH 6/9] WCM-285: add changelog --- core/docs/changelog/WCM-285.change | 1 + 1 file changed, 1 insertion(+) create mode 100644 core/docs/changelog/WCM-285.change diff --git a/core/docs/changelog/WCM-285.change b/core/docs/changelog/WCM-285.change new file mode 100644 index 0000000000..21d5438682 --- /dev/null +++ b/core/docs/changelog/WCM-285.change @@ -0,0 +1 @@ +WCM-285: write channels as read and do not convert back to DAV \ No newline at end of file From 4f03f35bdc4edbc9802b0e7367f69572f44f88d3 Mon Sep 17 00:00:00 2001 From: Martin Stolle Date: Wed, 25 Sep 2024 08:15:55 +0200 Subject: [PATCH 7/9] WCM-285: move Converter back to filesystem --- core/src/zeit/connector/converter.py | 68 ----------------------- core/src/zeit/connector/filesystem.py | 77 ++++++++++++++++++++++++++- core/src/zeit/connector/interfaces.py | 10 ---- core/src/zeit/connector/mock.py | 4 +- core/src/zeit/connector/models.py | 2 - 5 files changed, 78 insertions(+), 83 deletions(-) delete mode 100644 core/src/zeit/connector/converter.py diff --git a/core/src/zeit/connector/converter.py b/core/src/zeit/connector/converter.py deleted file mode 100644 index 49c26f6901..0000000000 --- a/core/src/zeit/connector/converter.py +++ /dev/null @@ -1,68 +0,0 @@ -from sqlalchemy.dialects.postgresql import JSONB -import grokcore.component as grok -import sqlalchemy -import zope.interface - -import zeit.connector.interfaces - - -@grok.implementer(zeit.connector.interfaces.IConverter) -@grok.adapter(sqlalchemy.Column) -def converter_from_column_type(column): - return zeit.connector.interfaces.IConverter(column.type) - - -@grok.implementer(zeit.connector.interfaces.IConverter) -class DefaultConverter(grok.Adapter): - grok.context(zope.interface.Interface) - - def serialize(self, value): - return value - - def deserialize(self, value): - return value - - -class BoolConverter(DefaultConverter): - grok.context(sqlalchemy.Boolean) - - def serialize(self, value): - return zeit.cms.content.dav.BoolProperty._toProperty(value) - - def deserialize(self, value): - return zeit.cms.content.dav.BoolProperty._fromProperty(value) - - -class IntConverter(DefaultConverter): - grok.context(sqlalchemy.Integer) - - def serialize(self, value): - return str(value) - - def deserialize(self, value): - return int(value) - - -class DatetimeConverter(DefaultConverter): - grok.context(sqlalchemy.TIMESTAMP) - - def serialize(self, value): - return zeit.cms.content.dav.DatetimeProperty._toProperty(value) - - def deserialize(self, value): - return zeit.cms.content.dav.DatetimeProperty._fromProperty(value) - - -@grok.implementer(zeit.connector.interfaces.IConverter) -class DictConverter(grok.Adapter): - grok.context(JSONB) - - def serialize(self, value): - if not value: - return {} - return value - - def deserialize(self, value): - if not value: - return {} - return value diff --git a/core/src/zeit/connector/filesystem.py b/core/src/zeit/connector/filesystem.py index 031af1a473..bc2cfb95a4 100644 --- a/core/src/zeit/connector/filesystem.py +++ b/core/src/zeit/connector/filesystem.py @@ -6,8 +6,11 @@ import os import os.path +from sqlalchemy.dialects.postgresql import JSONB import gocept.cache.property +import grokcore.component as grok import lxml.etree +import sqlalchemy import zope.app.file.image import zope.interface @@ -268,7 +271,7 @@ def _convert_sql_types(self, properties): column = Content.column_by_name(*key) if column is None: continue - converter = zeit.connector.interfaces.IConverter(column) + converter = IConverter(column) properties[key] = converter.deserialize(value) def _guess_type(self, id): @@ -316,3 +319,75 @@ def parse_properties(xml): value += '' properties[('keywords', 'http://namespaces.zeit.de/CMS/tagging')] = value return properties + + +class IConverter(zope.interface.Interface): + """Converts webdav values to and from the postgresql database.""" + + def serialize(value): + pass + + def deserialize(value): + pass + + +@grok.implementer(IConverter) +@grok.adapter(sqlalchemy.Column) +def converter_from_column_type(column): + return IConverter(column.type) + + +@grok.implementer(IConverter) +class DefaultConverter(grok.Adapter): + grok.context(zope.interface.Interface) + + def serialize(self, value): + return value + + def deserialize(self, value): + return value + + +class BoolConverter(DefaultConverter): + grok.context(sqlalchemy.Boolean) + + def serialize(self, value): + return zeit.cms.content.dav.BoolProperty._toProperty(value) + + def deserialize(self, value): + return zeit.cms.content.dav.BoolProperty._fromProperty(value) + + +class IntConverter(DefaultConverter): + grok.context(sqlalchemy.Integer) + + def serialize(self, value): + return str(value) + + def deserialize(self, value): + return int(value) + + +class DatetimeConverter(DefaultConverter): + grok.context(sqlalchemy.TIMESTAMP) + + def serialize(self, value): + return zeit.cms.content.dav.DatetimeProperty._toProperty(value) + + def deserialize(self, value): + return zeit.cms.content.dav.DatetimeProperty._fromProperty(value) + + +@grok.implementer(IConverter) +class DictConverter(grok.Adapter): + grok.context(JSONB) + + def serialize(self, value): + if not value: + return {} + return value + + def deserialize(self, value): + if not value: + return {} + return value diff --git a/core/src/zeit/connector/interfaces.py b/core/src/zeit/connector/interfaces.py index 2bc264fd47..251cef6a70 100644 --- a/core/src/zeit/connector/interfaces.py +++ b/core/src/zeit/connector/interfaces.py @@ -354,16 +354,6 @@ class IResourceInvalidatedEvent(zope.interface.Interface): id = zope.interface.Attribute('Unique id of resource') -class IConverter(zope.interface.Interface): - """Converts webdav values to and from the postgresql database.""" - - def serialize(value): - pass - - def deserialize(value): - pass - - @zope.interface.implementer(IResourceInvalidatedEvent) class ResourceInvalidatedEvent: def __init__(self, id): diff --git a/core/src/zeit/connector/mock.py b/core/src/zeit/connector/mock.py index a12125152b..eddef24d38 100644 --- a/core/src/zeit/connector/mock.py +++ b/core/src/zeit/connector/mock.py @@ -15,7 +15,7 @@ import zope.event from zeit.cms.content.sources import FEATURE_TOGGLES -from zeit.connector.converter import DefaultConverter +from zeit.connector.filesystem import DefaultConverter from zeit.connector.interfaces import ( ID_NAMESPACE, UUID_PROPERTY, @@ -388,7 +388,7 @@ def _set_properties(self, id, properties): if FEATURE_TOGGLES.find('write_metadata_columns'): column = Content.column_by_name(name, namespace) - converter = zeit.connector.interfaces.IConverter(column) + converter = zeit.connector.filesystem.IConverter(column) value = converter.serialize(value) else: converter = DefaultConverter(None) diff --git a/core/src/zeit/connector/models.py b/core/src/zeit/connector/models.py index 0669e3afbd..83b8ae6b6d 100644 --- a/core/src/zeit/connector/models.py +++ b/core/src/zeit/connector/models.py @@ -223,8 +223,6 @@ def from_webdav(self, props): continue if ns == INTERNAL_PROPERTY: continue - if isinstance(v, datetime): - continue unsorted[ns.replace(self.NS, '', 1)][k] = v self.unsorted = unsorted From 380d8191dcdb2099fdc33a413ff6881cf50b78e1 Mon Sep 17 00:00:00 2001 From: Martin Stolle Date: Wed, 25 Sep 2024 09:37:33 +0200 Subject: [PATCH 8/9] WCM-285: if you can write into a column, just write the column and not into the unsorted field --- core/src/zeit/connector/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/core/src/zeit/connector/models.py b/core/src/zeit/connector/models.py index 83b8ae6b6d..e3e4eb38ea 100644 --- a/core/src/zeit/connector/models.py +++ b/core/src/zeit/connector/models.py @@ -216,6 +216,7 @@ def from_webdav(self, props): value = props.get((name, self.NS + namespace), self) if value is not self: setattr(self, column.name, value) + props.pop((name, self.NS + namespace), None) unsorted = collections.defaultdict(dict) for (k, ns), v in props.items(): From 2f9db370283290239f339a9116249c827781a6b3 Mon Sep 17 00:00:00 2001 From: Martin Stolle Date: Wed, 25 Sep 2024 13:58:29 +0200 Subject: [PATCH 9/9] WCM-285: remove channels, to unblock the revert of the converter changes --- core/docs/changelog/WCM-285.change | 2 +- core/src/zeit/connector/filesystem.py | 16 -------------- .../src/zeit/connector/tests/test_contract.py | 21 ------------------- 3 files changed, 1 insertion(+), 38 deletions(-) diff --git a/core/docs/changelog/WCM-285.change b/core/docs/changelog/WCM-285.change index 21d5438682..a916c3114b 100644 --- a/core/docs/changelog/WCM-285.change +++ b/core/docs/changelog/WCM-285.change @@ -1 +1 @@ -WCM-285: write channels as read and do not convert back to DAV \ No newline at end of file +WCM-285: bring back the old behavior for the connector and write data as read if enabled \ No newline at end of file diff --git a/core/src/zeit/connector/filesystem.py b/core/src/zeit/connector/filesystem.py index bc2cfb95a4..024cd1fc7b 100644 --- a/core/src/zeit/connector/filesystem.py +++ b/core/src/zeit/connector/filesystem.py @@ -6,7 +6,6 @@ import os import os.path -from sqlalchemy.dialects.postgresql import JSONB import gocept.cache.property import grokcore.component as grok import lxml.etree @@ -376,18 +375,3 @@ def serialize(self, value): def deserialize(self, value): return zeit.cms.content.dav.DatetimeProperty._fromProperty(value) - - -@grok.implementer(IConverter) -class DictConverter(grok.Adapter): - grok.context(JSONB) - - def serialize(self, value): - if not value: - return {} - return value - - def deserialize(self, value): - if not value: - return {} - return value diff --git a/core/src/zeit/connector/tests/test_contract.py b/core/src/zeit/connector/tests/test_contract.py index f33184dde4..9056229b50 100644 --- a/core/src/zeit/connector/tests/test_contract.py +++ b/core/src/zeit/connector/tests/test_contract.py @@ -914,27 +914,6 @@ def test_converts_scalar_types_on_write(self): resource.properties[('date_created', 'http://namespaces.zeit.de/CMS/document')], ) - def test_converts_aggregate_types_on_read(self): - example_channels = {'channel1': [], 'channel2': ['sub1', 'sub2']} - self.repository.connector.changeProperties( - 'http://xml.zeit.de/testcontent', - {('channels', 'http://namespaces.zeit.de/CMS/document'): example_channels}, - ) - self.assertEqual( - example_channels, - self.repository['testcontent'].channels, - ) - - def test_converts_aggregate_types_on_write(self): - example_channels = {'channel1': [], 'channel2': ['sub1', 'sub2']} - with checked_out(self.repository['testcontent']) as co: - co.channels = example_channels - resource = self.repository.connector['http://xml.zeit.de/testcontent'] - self.assertEqual( - example_channels, - resource.properties[('channels', 'http://namespaces.zeit.de/CMS/document')], - ) - class PropertiesSQL(ContractProperties, zeit.cms.testing.FunctionalTestCase): layer = zeit.connector.testing.SQL_CONTENT_LAYER