From a783df5da2e68e78ad083437b3311d56906affab Mon Sep 17 00:00:00 2001 From: Peter Mathis Date: Thu, 7 Mar 2024 11:55:28 +0100 Subject: [PATCH 1/5] Implement `pat-contentbrowser` widget --- plone/app/z3cform/converters.py | 30 ++- plone/app/z3cform/converters.zcml | 8 +- plone/app/z3cform/interfaces.py | 4 + .../templates/contentbrowser_display.pt | 44 ++++ .../z3cform/templates/relateditems_display.pt | 80 +++--- plone/app/z3cform/widgets.zcml | 17 +- plone/app/z3cform/widgets/contentbrowser.py | 231 ++++++++++++++++++ 7 files changed, 356 insertions(+), 58 deletions(-) create mode 100644 plone/app/z3cform/templates/contentbrowser_display.pt create mode 100644 plone/app/z3cform/widgets/contentbrowser.py diff --git a/plone/app/z3cform/converters.py b/plone/app/z3cform/converters.py index 1e824628..1a8efedc 100644 --- a/plone/app/z3cform/converters.py +++ b/plone/app/z3cform/converters.py @@ -3,11 +3,11 @@ from datetime import time from plone.app.z3cform import utils from plone.app.z3cform.interfaces import IAjaxSelectWidget +from plone.app.z3cform.interfaces import IContentBrowserWidget from plone.app.z3cform.interfaces import IDatetimeWidget from plone.app.z3cform.interfaces import IDateWidget from plone.app.z3cform.interfaces import ILinkWidget from plone.app.z3cform.interfaces import IQueryStringWidget -from plone.app.z3cform.interfaces import IRelatedItemsWidget from plone.app.z3cform.interfaces import ISelectWidget from plone.app.z3cform.interfaces import ISingleCheckBoxBoolWidget from plone.app.z3cform.interfaces import ITimeWidget @@ -304,9 +304,9 @@ def toFieldValue(self, value): return collectionType(untokenized_value) -@adapter(IRelation, IRelatedItemsWidget) -class RelationChoiceRelatedItemsWidgetConverter(BaseDataConverter): - """Data converter for RelationChoice fields using the RelatedItemsWidget.""" +@adapter(IRelation, IContentBrowserWidget) +class RelationChoiceContentBrowserWidgetConverter(BaseDataConverter): + """Data converter for RelationChoice fields using the ContentBrowserWidget.""" def toWidgetValue(self, value): if not value: @@ -328,8 +328,15 @@ def toFieldValue(self, value): return self.field.missing_value +# BBB +class RelationChoiceRelatedItemsWidgetConverter( + RelationChoiceContentBrowserWidgetConverter +): + """backwards compatibility""" + + @adapter(IRelation, ISequenceWidget) -class RelationChoiceSelectWidgetConverter(RelationChoiceRelatedItemsWidgetConverter): +class RelationChoiceSelectWidgetConverter(RelationChoiceContentBrowserWidgetConverter): """Data converter for RelationChoice fields using with SequenceWidgets, which expect sequence values. """ @@ -341,9 +348,9 @@ def toWidgetValue(self, value): return [IUUID(value)] -@adapter(ICollection, IRelatedItemsWidget) -class RelatedItemsDataConverter(BaseDataConverter): - """Data converter for ICollection fields using the RelatedItemsWidget.""" +@adapter(ICollection, IContentBrowserWidget) +class ContentBrowserDataConverter(BaseDataConverter): + """Data converter for ICollection fields using the ContentBrowserWidget.""" def toWidgetValue(self, value): """Converts from field value to widget. @@ -405,8 +412,13 @@ def toFieldValue(self, value): return collectionType(valueType(v) for v in value) +# BBB +class RelatedItemsDataConverter(ContentBrowserDataConverter): + """backwards compatibility""" + + @adapter(IRelationList, ISequenceWidget) -class RelationListSelectWidgetDataConverter(RelatedItemsDataConverter): +class RelationListSelectWidgetDataConverter(ContentBrowserDataConverter): """Data converter for RelationChoice fields using with SequenceWidgets, which expect sequence values. """ diff --git a/plone/app/z3cform/converters.zcml b/plone/app/z3cform/converters.zcml index 5aca1fc2..eabfba73 100644 --- a/plone/app/z3cform/converters.zcml +++ b/plone/app/z3cform/converters.zcml @@ -8,16 +8,16 @@ - + - + diff --git a/plone/app/z3cform/interfaces.py b/plone/app/z3cform/interfaces.py index 0c22c77a..9b4b065c 100644 --- a/plone/app/z3cform/interfaces.py +++ b/plone/app/z3cform/interfaces.py @@ -91,6 +91,10 @@ class IRelatedItemsWidget(ITextWidget): """Marker interface for the RelatedItemsWidget.""" +class IContentBrowserWidget(ITextWidget): + """Marker interface for the RelatedItemsWidget.""" + + class IRichTextWidget(patextfield_IRichTextWidget): """Marker interface for the TinyMCEWidget.""" diff --git a/plone/app/z3cform/templates/contentbrowser_display.pt b/plone/app/z3cform/templates/contentbrowser_display.pt new file mode 100644 index 00000000..972a9702 --- /dev/null +++ b/plone/app/z3cform/templates/contentbrowser_display.pt @@ -0,0 +1,44 @@ + diff --git a/plone/app/z3cform/templates/relateditems_display.pt b/plone/app/z3cform/templates/relateditems_display.pt index b6203c2b..a3b19ea7 100644 --- a/plone/app/z3cform/templates/relateditems_display.pt +++ b/plone/app/z3cform/templates/relateditems_display.pt @@ -1,46 +1,44 @@ - - - + + + + + + diff --git a/plone/app/z3cform/widgets.zcml b/plone/app/z3cform/widgets.zcml index 5fd76ff2..fe65f2ca 100644 --- a/plone/app/z3cform/widgets.zcml +++ b/plone/app/z3cform/widgets.zcml @@ -223,27 +223,36 @@ + + + + Date: Wed, 22 May 2024 12:08:20 +0200 Subject: [PATCH 2/5] changenote --- news/197.feature | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 news/197.feature diff --git a/news/197.feature b/news/197.feature new file mode 100644 index 00000000..abc8225c --- /dev/null +++ b/news/197.feature @@ -0,0 +1,9 @@ +Implement new `ContentBrowserWidget` for `pat-contentbrowser` pattern. + +The deprecated `RelatedItemsWidget` and `pat-relateditems` pattern is still available +and imports should not break. But the default widget and converter adapter registration for +z3c.relationfield is changed to the new widget. + +Since `plone.app.relationfield` defines the widget with `plone.autoform` schema +hints nothing changes until the package is updated to the new widget. +[petschki] From 3e03ac9a0c3a91a390e7592217a4a60dddf1129e Mon Sep 17 00:00:00 2001 From: Peter Mathis Date: Wed, 22 May 2024 12:26:39 +0200 Subject: [PATCH 3/5] update tests --- plone/app/z3cform/tests/test_widgets.py | 170 +++++++++++++++++++++++- 1 file changed, 166 insertions(+), 4 deletions(-) diff --git a/plone/app/z3cform/tests/test_widgets.py b/plone/app/z3cform/tests/test_widgets.py index 77438a0b..f453ba14 100644 --- a/plone/app/z3cform/tests/test_widgets.py +++ b/plone/app/z3cform/tests/test_widgets.py @@ -8,8 +8,8 @@ from plone.app.testing import TEST_USER_ID from plone.app.z3cform.tests.layer import PAZ3CForm_INTEGRATION_TESTING from plone.app.z3cform.widgets.base import PatternFormElement +from plone.app.z3cform.widgets.contentbrowser import ContentBrowserWidget from plone.app.z3cform.widgets.datetime import DateWidget -from plone.app.z3cform.widgets.relateditems import RelatedItemsWidget from plone.app.z3cform.widgets.text import TextFieldWidget from plone.autoform.directives import widget from plone.autoform.form import AutoExtensibleForm @@ -1407,7 +1407,7 @@ class IRelationsType(Interface): multiple = RelationList(title="Multiple (Relations field)", required=False) -class RelatedItemsWidgetTemplateIntegrationTests(unittest.TestCase): +class ContentBrowserWidgetTemplateIntegrationTests(unittest.TestCase): layer = PAZ3CForm_INTEGRATION_TESTING def setUp(self): @@ -1444,7 +1444,7 @@ def test_related_items_widget_display_template(self): default_view.update() single = default_view.w["single"] - self.assertIsInstance(single, RelatedItemsWidget) + self.assertIsInstance(single, ContentBrowserWidget) self.assertTrue(single.value, target.UID()) items = single.items() self.assertIsInstance(items, ContentListing) @@ -1463,7 +1463,7 @@ def test_related_items_widget_display_template(self): ) multiple = default_view.w["multiple"] - self.assertIsInstance(multiple, RelatedItemsWidget) + self.assertIsInstance(multiple, ContentBrowserWidget) self.assertTrue(multiple.value, ";".join([target.UID(), doc.UID()])) items = multiple.items() self.assertIsInstance(items, ContentListing) @@ -1649,6 +1649,168 @@ def test_fieldwidget(self): self.assertIs(widget.request, request) +class ContentBrowserWidgetTests(unittest.TestCase): + layer = PAZ3CForm_INTEGRATION_TESTING + + def setUp(self): + self.portal = self.layer["portal"] + self.request = self.layer["request"] + setRoles(self.portal, TEST_USER_ID, ["Manager"]) + + def test_single_selection(self): + """The pattern_options value for maximumSelectionSize should + be 1 when the field only allows a single selection.""" + from plone.app.z3cform.widgets.contentbrowser import ContentBrowserFieldWidget + + field = Choice( + __name__="selectfield", + values=["one", "two", "three"], + ) + widget = ContentBrowserFieldWidget(field, self.request) + widget.context = self.portal + widget.update() + pattern_options = widget.get_pattern_options() + self.assertEqual(pattern_options.get("maximumSelectionSize", 0), 1) + + def test_multiple_selection(self): + """The pattern_options key maximumSelectionSize shouldn't be + set when the field allows multiple selections""" + from plone.app.z3cform.widgets.contentbrowser import ContentBrowserFieldWidget + from Zope2.App.schema import Zope2VocabularyRegistry + from zope.schema.interfaces import ISource + + field = List( + __name__="selectfield", + value_type=Choice(vocabulary="foobar"), + ) + widget = ContentBrowserFieldWidget(field, self.request) + widget.context = self.portal + + vocab = Mock() + alsoProvides(vocab, ISource) + with mock.patch.object(Zope2VocabularyRegistry, "get", return_value=vocab): + widget.update() + patterns_options = widget.get_pattern_options() + self.assertFalse("maximumSelectionSize" in patterns_options) + self.assertEqual( + patterns_options["vocabularyUrl"], + "http://nohost/plone/@@getVocabulary?name=foobar&field=selectfield", + ) + + def test_converter_RelationChoice(self): + from plone.app.z3cform.converters import ( + RelationChoiceContentBrowserWidgetConverter, + ) + + brain = Mock(getObject=Mock(return_value="obj")) + portal_catalog = Mock(return_value=[brain]) + widget = Mock() + converter = RelationChoiceContentBrowserWidgetConverter( + TextLine(), + widget, + ) + + with mock.patch( + "plone.app.z3cform.converters.IUUID", + return_value="id", + ): + self.assertEqual(converter.toWidgetValue("obj"), "id") + self.assertEqual(converter.toWidgetValue(None), None) + + with mock.patch( + "plone.app.z3cform.converters.getToolByName", + return_value=portal_catalog, + ): + self.assertEqual(converter.toFieldValue("id"), "obj") + self.assertEqual(converter.toFieldValue(None), None) + + def test_converter_RelationList(self): + from plone.app.z3cform.converters import ContentBrowserDataConverter + from z3c.relationfield.interfaces import IRelationList + + field = List() + alsoProvides(field, IRelationList) + brain1 = Mock(getObject=Mock(return_value="obj1"), UID="id1") + brain2 = Mock(getObject=Mock(return_value="obj2"), UID="id2") + portal_catalog = Mock(return_value=[brain1, brain2]) + widget = Mock(separator=";") + converter = ContentBrowserDataConverter(field, widget) + + self.assertEqual(converter.toWidgetValue(None), None) + with mock.patch( + "plone.app.z3cform.converters.IUUID", + side_effect=["id1", "id2"], + ): + self.assertEqual( + converter.toWidgetValue(["obj1", "obj2"]), + "id1;id2", + ) + + self.assertEqual(converter.toFieldValue(None), None) + with mock.patch( + "plone.app.z3cform.converters.getToolByName", + return_value=portal_catalog, + ): + self.assertEqual( + converter.toFieldValue("id1;id2"), + ["obj1", "obj2"], + ) + + def test_converter_List_of_Choice(self): + from plone.app.z3cform.converters import ContentBrowserDataConverter + + fields = ( + List(), + List(value_type=TextLine()), + List(value_type=BytesLine()), + List(value_type=Choice(values=["one", "two", "three"])), + ) + for field in fields: + expected_value_type = getattr( + field.value_type, + "_type", + str, + ) + if expected_value_type is None: + expected_value_type = str + widget = Mock(separator=";") + converter = ContentBrowserDataConverter(field, widget) + + self.assertEqual(converter.toWidgetValue(None), None) + self.assertEqual( + converter.toWidgetValue(["id1", "id2"]), + "id1;id2", + ) + + self.assertEqual(converter.toFieldValue(None), None) + if expected_value_type == bytes: + expected = [b"id1", b"id2"] + else: + expected = ["id1", "id2"] + self.assertEqual( + converter.toFieldValue("id1;id2"), + expected, + ) + + self.assertEqual(converter.toFieldValue(None), None) + self.assertEqual( + type(converter.toFieldValue("id1;id2")[0]), + expected_value_type, + ) + + def test_fieldwidget(self): + from plone.app.z3cform.widgets.contentbrowser import ContentBrowserFieldWidget + from plone.app.z3cform.widgets.contentbrowser import ContentBrowserWidget + + field = Mock(__name__="field", title="", required=True) + vocabulary = Mock() + request = Mock() + widget = ContentBrowserFieldWidget(field, vocabulary, request) + self.assertTrue(isinstance(widget, ContentBrowserWidget)) + self.assertIs(widget.field, field) + self.assertIs(widget.request, request) + + class RichTextWidgetTests(unittest.TestCase): layer = PAZ3CForm_INTEGRATION_TESTING From 54ce09d3791396fa089c0e200eb1d0ce57bb0de7 Mon Sep 17 00:00:00 2001 From: Peter Mathis Date: Fri, 13 Sep 2024 08:24:45 +0200 Subject: [PATCH 4/5] switch `LinkWidget` to contentbrowser --- plone/app/z3cform/templates/link_input.pt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plone/app/z3cform/templates/link_input.pt b/plone/app/z3cform/templates/link_input.pt index ff3e0f39..853760f0 100644 --- a/plone/app/z3cform/templates/link_input.pt +++ b/plone/app/z3cform/templates/link_input.pt @@ -25,11 +25,11 @@
-
From bc626b59d07421997c1dd968cdefd080eaa70e2d Mon Sep 17 00:00:00 2001 From: Peter Mathis Date: Mon, 16 Sep 2024 14:59:42 +0200 Subject: [PATCH 5/5] Add BBB adapters --- plone/app/z3cform/converters.py | 3 +++ plone/app/z3cform/converters.zcml | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/plone/app/z3cform/converters.py b/plone/app/z3cform/converters.py index 1a8efedc..fb586c65 100644 --- a/plone/app/z3cform/converters.py +++ b/plone/app/z3cform/converters.py @@ -8,6 +8,7 @@ from plone.app.z3cform.interfaces import IDateWidget from plone.app.z3cform.interfaces import ILinkWidget from plone.app.z3cform.interfaces import IQueryStringWidget +from plone.app.z3cform.interfaces import IRelatedItemsWidget from plone.app.z3cform.interfaces import ISelectWidget from plone.app.z3cform.interfaces import ISingleCheckBoxBoolWidget from plone.app.z3cform.interfaces import ITimeWidget @@ -329,6 +330,7 @@ def toFieldValue(self, value): # BBB +@adapter(IRelation, IRelatedItemsWidget) class RelationChoiceRelatedItemsWidgetConverter( RelationChoiceContentBrowserWidgetConverter ): @@ -413,6 +415,7 @@ def toFieldValue(self, value): # BBB +@adapter(ICollection, IRelatedItemsWidget) class RelatedItemsDataConverter(ContentBrowserDataConverter): """backwards compatibility""" diff --git a/plone/app/z3cform/converters.zcml b/plone/app/z3cform/converters.zcml index eabfba73..09668922 100644 --- a/plone/app/z3cform/converters.zcml +++ b/plone/app/z3cform/converters.zcml @@ -23,4 +23,8 @@ /> + + + +