From 6f74bf1b127d29f91ad09b89c0e2c75d49c5d06a Mon Sep 17 00:00:00 2001 From: sunew Date: Wed, 27 Jun 2018 11:42:13 +0200 Subject: [PATCH] [fc] Repository: plone.app.portlets Branch: refs/heads/master Date: 2018-06-27T01:51:30+02:00 Author: Sune Broendum Woeller (sunew) Commit: https://github.com/plone/plone.app.portlets/commit/661a8a2034faabefe47434c3804eb53545fbba01 Portlet add and edit forms AutoExtensibleForm. But some portlet addforms fail on creating the Assignment, if there is a FormExtender for the form, and the addform uses "Assignment(**data)" for creation and not "Assignment(key=data.get(key, None))". Fix this by filtering away data values that does not come from the 'core' schema. Files changed: A plone/app/portlets/tests/test_formextender.py M CHANGES.rst M plone/app/portlets/browser/formhelper.py Repository: plone.app.portlets Branch: refs/heads/master Date: 2018-06-27T01:51:51+02:00 Author: Sune Broendum Woeller (sunew) Commit: https://github.com/plone/plone.app.portlets/commit/a7d16da24cf5973c05da3188b2694972e5d168bf isort config Files changed: M setup.cfg Repository: plone.app.portlets Branch: refs/heads/master Date: 2018-06-27T11:42:13+02:00 Author: Sune Broendum Woeller (sunew) Commit: https://github.com/plone/plone.app.portlets/commit/943d2c61c897e62adb1ca39c39a053001738559a Merge pull request #115 from plone/fix-formextender Fix for making the already existing FormExtender feature work Files changed: A plone/app/portlets/tests/test_formextender.py M CHANGES.rst M plone/app/portlets/browser/formhelper.py M setup.cfg --- last_commit.txt | 55 ++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 47 insertions(+), 8 deletions(-) diff --git a/last_commit.txt b/last_commit.txt index ef69645d7d..db8166a615 100644 --- a/last_commit.txt +++ b/last_commit.txt @@ -1,15 +1,54 @@ -Repository: plone.app.workflow +Repository: plone.app.portlets -Branch: refs/heads/python3 -Date: 2018-06-27T00:29:05+02:00 -Author: Jens W. Klein (jensens) -Commit: https://github.com/plone/plone.app.workflow/commit/5f92c38fc13e67c7b470fe7b5c0816183b25ef97 +Branch: refs/heads/master +Date: 2018-06-27T01:51:30+02:00 +Author: Sune Broendum Woeller (sunew) +Commit: https://github.com/plone/plone.app.portlets/commit/661a8a2034faabefe47434c3804eb53545fbba01 -setDefaultRoles is deprecated. addPermission from AccessControl.Permission is used. +Portlet add and edit forms AutoExtensibleForm. But some portlet addforms fail on creating the Assignment, if there is a FormExtender for the form, +and the addform uses "Assignment(**data)" for creation and not "Assignment(key=data.get(key, None))". +Fix this by filtering away data values that does not come from the 'core' schema. Files changed: -M plone/app/workflow/permissions.py +A plone/app/portlets/tests/test_formextender.py +M CHANGES.rst +M plone/app/portlets/browser/formhelper.py -b'diff --git a/plone/app/workflow/permissions.py b/plone/app/workflow/permissions.py\nindex cba2874..6b489d4 100644\n--- a/plone/app/workflow/permissions.py\n+++ b/plone/app/workflow/permissions.py\n@@ -1,26 +1,42 @@\n-from Products.CMFCore.permissions import setDefaultRoles\n+# -*- coding: utf-8 -*-\n from AccessControl import ModuleSecurityInfo\n+from AccessControl.Permission import addPermission\n \n security = ModuleSecurityInfo("plone.app.workflow.permissions")\n \n # Controls access to the "sharing" page\n security.declarePublic("DelegateRoles")\n DelegateRoles = "Sharing page: Delegate roles"\n-setDefaultRoles(DelegateRoles, (\'Manager\', \'Site Administrator\', \'Owner\', \'Editor\', \'Reviewer\', ))\n+addPermission(\n+ DelegateRoles,\n+ (\'Manager\', \'Site Administrator\', \'Owner\', \'Editor\', \'Reviewer\', ),\n+)\n \n # Control the individual roles\n security.declarePublic("DelegateReaderRole")\n DelegateReaderRole = "Sharing page: Delegate Reader role"\n-setDefaultRoles(DelegateReaderRole, (\'Manager\', \'Site Administrator\', \'Owner\', \'Editor\', \'Reviewer\'))\n+addPermission(\n+ DelegateReaderRole,\n+ (\'Manager\', \'Site Administrator\', \'Owner\', \'Editor\', \'Reviewer\'),\n+)\n \n security.declarePublic("DelegateEditorRole")\n DelegateEditorRole = "Sharing page: Delegate Editor role"\n-setDefaultRoles(DelegateEditorRole, (\'Manager\', \'Site Administrator\', \'Owner\', \'Editor\'))\n+addPermission(\n+ DelegateEditorRole,\n+ (\'Manager\', \'Site Administrator\', \'Owner\', \'Editor\'),\n+)\n \n security.declarePublic("DelegateContributorRole")\n DelegateContributorRole = "Sharing page: Delegate Contributor role"\n-setDefaultRoles(DelegateContributorRole, (\'Manager\', \'Site Administrator\', \'Owner\',))\n+addPermission(\n+ DelegateContributorRole,\n+ (\'Manager\', \'Site Administrator\', \'Owner\',),\n+)\n \n security.declarePublic("DelegateReviewerRole")\n DelegateReviewerRole = "Sharing page: Delegate Reviewer role"\n-setDefaultRoles(DelegateReviewerRole, (\'Manager\', \'Site Administrator\', \'Reviewer\',))\n+addPermission(\n+ DelegateReviewerRole,\n+ (\'Manager\', \'Site Administrator\', \'Reviewer\',),\n+)\n' +b'diff --git a/CHANGES.rst b/CHANGES.rst\nindex 4b6a1c4..578db5a 100644\n--- a/CHANGES.rst\n+++ b/CHANGES.rst\n@@ -34,6 +34,14 @@ Bug fixes:\n - Test against plone.app.contenttypes instead of ATContentTypes.\n [davisagli]\n \n+- Portlet add and edit forms already extend AutoExtensibleForm from\n+ plone.autoform. But some portlet\n+ addforms fail on creating the Assignment, if there is a FormExtender\n+ for the form, and the addform uses `Assignment(**data)` for creation\n+ instead of explicit parameters. Fix this by filtering\n+ away data values that does not come from the \'core\' schema.\n+ [sunew]\n+\n \n 4.3.1 (2017-08-07)\n ------------------\ndiff --git a/plone/app/portlets/browser/formhelper.py b/plone/app/portlets/browser/formhelper.py\nindex 5ec6829..62d10c4 100644\n--- a/plone/app/portlets/browser/formhelper.py\n+++ b/plone/app/portlets/browser/formhelper.py\n@@ -1,5 +1,6 @@\n # -*- coding: utf-8 -*-\n from z3c.form import button\n+from z3c.form import field\n from z3c.form import form\n from zope.component import getMultiAdapter\n from zope.interface import implementer\n@@ -60,7 +61,17 @@ def __call__(self):\n return super(AddForm, self).__call__()\n \n def createAndAdd(self, data):\n- obj = self.create(data)\n+ # Filter away data values that does not come from the \'core\' schema.\n+ # Additional values can come from AutoExtensibleForm/FormExtender\n+ # schemas,and the portlet Assignment creation will fail if the\n+ # portlet AddForm create() method is using "Assignment(**data)"\n+ # instead of explicit parameters.\n+ # Extender values are set by form.applyChanges below, via the usual\n+ # z3cform adapter lookups.\n+ schema_keys = field.Fields(self.schema).keys()\n+ unextended_data = {key: data[key]\n+ for key in schema_keys if data.has_key(key)}\n+ obj = self.create(unextended_data)\n \n # Acquisition wrap temporarily to satisfy things like vocabularies\n # depending on tools\ndiff --git a/plone/app/portlets/tests/test_formextender.py b/plone/app/portlets/tests/test_formextender.py\nnew file mode 100644\nindex 0000000..86c74aa\n--- /dev/null\n+++ b/plone/app/portlets/tests/test_formextender.py\n@@ -0,0 +1,274 @@\n+# -*- coding: utf-8 -*-\n+\n+from plone.app.portlets.browser.interfaces import IPortletAddForm\n+from plone.app.portlets.browser.interfaces import IPortletEditForm\n+from plone.app.portlets.portlets import news\n+from plone.app.portlets.storage import PortletAssignmentMapping\n+from plone.app.portlets.tests.base import PortletsTestCase\n+from plone.portlets.interfaces import IPortletAssignment\n+from plone.portlets.interfaces import IPortletAssignmentSettings\n+from plone.portlets.interfaces import IPortletManager\n+from plone.portlets.interfaces import IPortletRenderer\n+from plone.portlets.interfaces import IPortletType\n+from plone.z3cform.fieldsets.extensible import FormExtender\n+from plone.z3cform.fieldsets.interfaces import IFormExtender\n+from z3c.form import field\n+from zope import schema\n+from zope.component import adapter\n+from zope.component import getGlobalSiteManager\n+from zope.component import getMultiAdapter\n+from zope.component import getUtility\n+from zope.interface import implementer\n+from zope.interface import Interface\n+from zope.publisher.interfaces.browser import IDefaultBrowserLayer\n+\n+\n+# A sample schemaextender:\n+\n+\n+EXTENDER_PREFIX = \'portletcssclass\'\n+\n+\n+class IPortletCssClass(Interface):\n+ """ Schema for portlet css class """\n+\n+ # css_class is just an example.\n+ # In real life a css_class implementation would be a\n+ # Choice field with a vocabulary, editable in a controlpanel.\n+ css_class = schema.TextLine(\n+ title=u\'Portlet appearance\',\n+ required=False\n+ )\n+\n+\n+class PortletCssClassFormExtender(FormExtender):\n+\n+ def update(self):\n+ self.add(IPortletCssClass, prefix=EXTENDER_PREFIX)\n+\n+\n+@adapter(IPortletAssignment)\n+@implementer(IPortletCssClass)\n+class PortletCssClassAdapter(object):\n+ def __init__(self, context):\n+ # avoid recursion\n+ self.__dict__[\'context\'] = context\n+\n+ def __setattr__(self, attr, value):\n+ settings = IPortletAssignmentSettings(self.context)\n+ settings[attr] = value\n+\n+ def __getattr__(self, attr):\n+ settings = IPortletAssignmentSettings(self.context)\n+ return settings.get(attr, None)\n+\n+\n+class TestSchemaExtender(PortletsTestCase):\n+\n+ def test_addform_fields(self):\n+ schema_field_names = field.Fields(news.INewsPortlet).keys()\n+\n+ # We use the news portlet as a random example of a portlet\n+ portlet = getUtility(IPortletType, name=\'portlets.News\')\n+\n+ mapping = self.portal.restrictedTraverse(\n+ \'++contextportlets++plone.leftcolumn\')\n+ addview = mapping.restrictedTraverse(\'+/\' + portlet.addview)\n+ addview.update()\n+ addview_field_names = addview.fields.keys()\n+\n+ # Our addview schema before we register our extender:\n+ self.assertEqual(addview_field_names, schema_field_names)\n+\n+ # Register our schemaextender\n+ gsm = getGlobalSiteManager()\n+ gsm.registerAdapter(PortletCssClassAdapter,\n+ (IPortletAssignment,))\n+ gsm.registerAdapter(PortletCssClassFormExtender,\n+ (Interface,\n+ IDefaultBrowserLayer,\n+ IPortletAddForm),\n+ IFormExtender,\n+ \'portletcssclass.extender\')\n+\n+ mapping = self.portal.restrictedTraverse(\n+ \'++contextportlets++plone.leftcolumn\')\n+ addview = mapping.restrictedTraverse(\'+/\' + portlet.addview)\n+ addview.update()\n+ addview_field_names = addview.fields.keys()\n+\n+ gsm.unregisterAdapter(PortletCssClassFormExtender,\n+ (Interface,\n+ IDefaultBrowserLayer,\n+ IPortletAddForm),\n+ IFormExtender,\n+ \'portletcssclass.extender\')\n+ gsm.unregisterAdapter(PortletCssClassAdapter,\n+ (IPortletAssignment,))\n+\n+ # Our addview schema now includes our extended schema:\n+ self.assertEqual(addview_field_names,\n+ schema_field_names + [EXTENDER_PREFIX+\'.css_class\'])\n+\n+ def test_invoke_add_form(self):\n+ portlet = getUtility(IPortletType, name=\'portlets.News\')\n+ mapping = self.portal.restrictedTraverse(\n+ \'++contextportlets++plone.leftcolumn\')\n+ for m in mapping.keys():\n+ del mapping[m]\n+ addview = mapping.restrictedTraverse(\'+/\' + portlet.addview)\n+ addview.update()\n+ addview.createAndAdd(data={\'count\': 5,\n+ EXTENDER_PREFIX+\'.css_class\': \'my-class\'})\n+ portlet_assignment = mapping.values()[0]\n+ settings = IPortletAssignmentSettings(portlet_assignment)\n+\n+ self.assertEqual(portlet_assignment.count, 5)\n+ # We have not extended our storage adapter, so nothing gets saved:\n+ self.assertIsNone(settings.get(\'css_class\', None))\n+\n+ # Register our schemaextender\n+ gsm = getGlobalSiteManager()\n+ gsm.registerAdapter(PortletCssClassAdapter,\n+ (IPortletAssignment,))\n+ gsm.registerAdapter(PortletCssClassFormExtender,\n+ (Interface,\n+ IDefaultBrowserLayer,\n+ IPortletAddForm),\n+ IFormExtender,\n+ \'portletcssclass.extender\')\n+ for m in mapping.keys():\n+ del mapping[m]\n+ addview = mapping.restrictedTraverse(\'+/\' + portlet.addview)\n+ addview.update()\n+ addview.createAndAdd(data={\'count\': 5,\n+ EXTENDER_PREFIX+\'.css_class\': \'my-class\'})\n+ portlet_assignment = mapping.values()[0]\n+ settings = IPortletAssignmentSettings(portlet_assignment)\n+\n+ gsm.unregisterAdapter(PortletCssClassFormExtender,\n+ (Interface,\n+ IDefaultBrowserLayer,\n+ IPortletAddForm),\n+ IFormExtender,\n+ \'portletcssclass.extender\')\n+ gsm.unregisterAdapter(PortletCssClassAdapter,\n+ (IPortletAssignment,))\n+\n+ self.assertEqual(portlet_assignment.count, 5)\n+ # The prefix is used for the form field, not for the stored data:\n+ self.assertEqual(settings.get(\'css_class\'), \'my-class\')\n+\n+ def test_editform_fields(self):\n+\n+ schema_field_names = field.Fields(news.INewsPortlet).keys()\n+\n+ mapping = PortletAssignmentMapping()\n+ request = self.folder.REQUEST\n+ mapping[\'foo\'] = news.Assignment(count=5)\n+ editview = getMultiAdapter((mapping[\'foo\'], request), name=\'edit\')\n+ editview.update()\n+ editview_field_names = editview.fields.keys()\n+\n+ # Our editview schema before we register our extender:\n+ self.assertEqual(editview_field_names, schema_field_names)\n+\n+ # Register our schemaextender\n+ gsm = getGlobalSiteManager()\n+ gsm.registerAdapter(PortletCssClassAdapter,\n+ (IPortletAssignment,))\n+ gsm.registerAdapter(PortletCssClassFormExtender,\n+ (Interface,\n+ IDefaultBrowserLayer,\n+ IPortletEditForm),\n+ IFormExtender,\n+ \'portletcssclass.extender\')\n+\n+ mapping = PortletAssignmentMapping()\n+ request = self.folder.REQUEST\n+ mapping[\'foo\'] = news.Assignment(count=5)\n+ editview = getMultiAdapter((mapping[\'foo\'], request), name=\'edit\')\n+ editview.update()\n+ editview_field_names = editview.fields.keys()\n+\n+ gsm.unregisterAdapter(PortletCssClassFormExtender,\n+ (Interface,\n+ IDefaultBrowserLayer,\n+ IPortletEditForm),\n+ IFormExtender,\n+ \'portletcssclass.extender\')\n+ gsm.unregisterAdapter(PortletCssClassAdapter,\n+ (IPortletAssignment,))\n+\n+ # Our editview schema now includes our extended schema:\n+ self.assertEqual(editview_field_names,\n+ schema_field_names + [EXTENDER_PREFIX+\'.css_class\'])\n+\n+ def test_invoke_edit_form(self):\n+ mapping = PortletAssignmentMapping()\n+ request = self.folder.REQUEST\n+\n+ mapping[\'foo\'] = news.Assignment(count=5)\n+ editview = getMultiAdapter((mapping[\'foo\'], request), name=\'edit\')\n+ editview.update()\n+ editview.applyChanges(data={\'count\': 6,\n+ EXTENDER_PREFIX+\'.css_class\': \'my-class\'})\n+ portlet_assignment = mapping.values()[0]\n+ settings = IPortletAssignmentSettings(portlet_assignment)\n+\n+ self.assertEqual(portlet_assignment.count, 6)\n+ # We have not extended our storage adapter, so nothing gets saved:\n+ self.assertIsNone(settings.get(\'css_class\', None))\n+\n+ # Register our schemaextender\n+ gsm = getGlobalSiteManager()\n+ gsm.registerAdapter(PortletCssClassAdapter,\n+ (IPortletAssignment,))\n+ gsm.registerAdapter(PortletCssClassFormExtender,\n+ (Interface,\n+ IDefaultBrowserLayer,\n+ IPortletEditForm),\n+ IFormExtender,\n+ \'portletcssclass.extender\')\n+ mapping = PortletAssignmentMapping()\n+ request = self.folder.REQUEST\n+\n+ mapping[\'foo\'] = news.Assignment(count=5)\n+ editview = getMultiAdapter((mapping[\'foo\'], request), name=\'edit\')\n+ editview.update()\n+ editview.applyChanges(data={\'count\': 6,\n+ EXTENDER_PREFIX+\'.css_class\': \'my-class\'})\n+ portlet_assignment = mapping.values()[0]\n+ settings = IPortletAssignmentSettings(portlet_assignment)\n+\n+ gsm.unregisterAdapter(PortletCssClassFormExtender,\n+ (Interface,\n+ IDefaultBrowserLayer,\n+ IPortletEditForm),\n+ IFormExtender,\n+ \'portletcssclass.extender\')\n+ gsm.unregisterAdapter(PortletCssClassAdapter,\n+ (IPortletAssignment,))\n+\n+ self.assertEqual(portlet_assignment.count, 6)\n+ # The prefix is used for the form field, not for the stored data:\n+ self.assertEqual(settings.get(\'css_class\'), \'my-class\')\n+\n+ def test_renderer(self):\n+ context = self.folder\n+ request = self.folder.REQUEST\n+ view = self.folder.restrictedTraverse(\'@@plone\')\n+ manager = getUtility(\n+ IPortletManager, name=\'plone.leftcolumn\', context=self.portal)\n+ assignment = news.Assignment(count=5)\n+\n+ renderer = getMultiAdapter(\n+ (context, request, view, manager, assignment), IPortletRenderer)\n+ self.assertTrue(isinstance(renderer, news.Renderer))\n+\n+\n+def test_suite():\n+ from unittest import TestSuite, makeSuite\n+ suite = TestSuite()\n+ suite.addTest(makeSuite(TestSchemaExtender))\n+ return suite\n' + +Repository: plone.app.portlets + + +Branch: refs/heads/master +Date: 2018-06-27T01:51:51+02:00 +Author: Sune Broendum Woeller (sunew) +Commit: https://github.com/plone/plone.app.portlets/commit/a7d16da24cf5973c05da3188b2694972e5d168bf + +isort config + +Files changed: +M setup.cfg + +b'diff --git a/setup.cfg b/setup.cfg\nindex d80c466..27e2916 100644\n--- a/setup.cfg\n+++ b/setup.cfg\n@@ -4,3 +4,13 @@ create-wheel = yes\n # When Python 2-3 compatible:\n # [bdist_wheel]\n # universal = 1\n+\n+[isort]\n+# for details see\n+# http://docs.plone.org/develop/styleguide/python.html#grouping-and-sorting\n+force_alphabetical_sort = True\n+force_single_line = True\n+lines_after_imports = 2\n+line_length = 200\n+not_skip =\n+ __init__.py\n' + +Repository: plone.app.portlets + + +Branch: refs/heads/master +Date: 2018-06-27T11:42:13+02:00 +Author: Sune Broendum Woeller (sunew) +Commit: https://github.com/plone/plone.app.portlets/commit/943d2c61c897e62adb1ca39c39a053001738559a + +Merge pull request #115 from plone/fix-formextender + +Fix for making the already existing FormExtender feature work + +Files changed: +A plone/app/portlets/tests/test_formextender.py +M CHANGES.rst +M plone/app/portlets/browser/formhelper.py +M setup.cfg + +b'diff --git a/CHANGES.rst b/CHANGES.rst\nindex 4b6a1c4..578db5a 100644\n--- a/CHANGES.rst\n+++ b/CHANGES.rst\n@@ -34,6 +34,14 @@ Bug fixes:\n - Test against plone.app.contenttypes instead of ATContentTypes.\n [davisagli]\n \n+- Portlet add and edit forms already extend AutoExtensibleForm from\n+ plone.autoform. But some portlet\n+ addforms fail on creating the Assignment, if there is a FormExtender\n+ for the form, and the addform uses `Assignment(**data)` for creation\n+ instead of explicit parameters. Fix this by filtering\n+ away data values that does not come from the \'core\' schema.\n+ [sunew]\n+\n \n 4.3.1 (2017-08-07)\n ------------------\ndiff --git a/plone/app/portlets/browser/formhelper.py b/plone/app/portlets/browser/formhelper.py\nindex 5ec6829..62d10c4 100644\n--- a/plone/app/portlets/browser/formhelper.py\n+++ b/plone/app/portlets/browser/formhelper.py\n@@ -1,5 +1,6 @@\n # -*- coding: utf-8 -*-\n from z3c.form import button\n+from z3c.form import field\n from z3c.form import form\n from zope.component import getMultiAdapter\n from zope.interface import implementer\n@@ -60,7 +61,17 @@ def __call__(self):\n return super(AddForm, self).__call__()\n \n def createAndAdd(self, data):\n- obj = self.create(data)\n+ # Filter away data values that does not come from the \'core\' schema.\n+ # Additional values can come from AutoExtensibleForm/FormExtender\n+ # schemas,and the portlet Assignment creation will fail if the\n+ # portlet AddForm create() method is using "Assignment(**data)"\n+ # instead of explicit parameters.\n+ # Extender values are set by form.applyChanges below, via the usual\n+ # z3cform adapter lookups.\n+ schema_keys = field.Fields(self.schema).keys()\n+ unextended_data = {key: data[key]\n+ for key in schema_keys if data.has_key(key)}\n+ obj = self.create(unextended_data)\n \n # Acquisition wrap temporarily to satisfy things like vocabularies\n # depending on tools\ndiff --git a/plone/app/portlets/tests/test_formextender.py b/plone/app/portlets/tests/test_formextender.py\nnew file mode 100644\nindex 0000000..86c74aa\n--- /dev/null\n+++ b/plone/app/portlets/tests/test_formextender.py\n@@ -0,0 +1,274 @@\n+# -*- coding: utf-8 -*-\n+\n+from plone.app.portlets.browser.interfaces import IPortletAddForm\n+from plone.app.portlets.browser.interfaces import IPortletEditForm\n+from plone.app.portlets.portlets import news\n+from plone.app.portlets.storage import PortletAssignmentMapping\n+from plone.app.portlets.tests.base import PortletsTestCase\n+from plone.portlets.interfaces import IPortletAssignment\n+from plone.portlets.interfaces import IPortletAssignmentSettings\n+from plone.portlets.interfaces import IPortletManager\n+from plone.portlets.interfaces import IPortletRenderer\n+from plone.portlets.interfaces import IPortletType\n+from plone.z3cform.fieldsets.extensible import FormExtender\n+from plone.z3cform.fieldsets.interfaces import IFormExtender\n+from z3c.form import field\n+from zope import schema\n+from zope.component import adapter\n+from zope.component import getGlobalSiteManager\n+from zope.component import getMultiAdapter\n+from zope.component import getUtility\n+from zope.interface import implementer\n+from zope.interface import Interface\n+from zope.publisher.interfaces.browser import IDefaultBrowserLayer\n+\n+\n+# A sample schemaextender:\n+\n+\n+EXTENDER_PREFIX = \'portletcssclass\'\n+\n+\n+class IPortletCssClass(Interface):\n+ """ Schema for portlet css class """\n+\n+ # css_class is just an example.\n+ # In real life a css_class implementation would be a\n+ # Choice field with a vocabulary, editable in a controlpanel.\n+ css_class = schema.TextLine(\n+ title=u\'Portlet appearance\',\n+ required=False\n+ )\n+\n+\n+class PortletCssClassFormExtender(FormExtender):\n+\n+ def update(self):\n+ self.add(IPortletCssClass, prefix=EXTENDER_PREFIX)\n+\n+\n+@adapter(IPortletAssignment)\n+@implementer(IPortletCssClass)\n+class PortletCssClassAdapter(object):\n+ def __init__(self, context):\n+ # avoid recursion\n+ self.__dict__[\'context\'] = context\n+\n+ def __setattr__(self, attr, value):\n+ settings = IPortletAssignmentSettings(self.context)\n+ settings[attr] = value\n+\n+ def __getattr__(self, attr):\n+ settings = IPortletAssignmentSettings(self.context)\n+ return settings.get(attr, None)\n+\n+\n+class TestSchemaExtender(PortletsTestCase):\n+\n+ def test_addform_fields(self):\n+ schema_field_names = field.Fields(news.INewsPortlet).keys()\n+\n+ # We use the news portlet as a random example of a portlet\n+ portlet = getUtility(IPortletType, name=\'portlets.News\')\n+\n+ mapping = self.portal.restrictedTraverse(\n+ \'++contextportlets++plone.leftcolumn\')\n+ addview = mapping.restrictedTraverse(\'+/\' + portlet.addview)\n+ addview.update()\n+ addview_field_names = addview.fields.keys()\n+\n+ # Our addview schema before we register our extender:\n+ self.assertEqual(addview_field_names, schema_field_names)\n+\n+ # Register our schemaextender\n+ gsm = getGlobalSiteManager()\n+ gsm.registerAdapter(PortletCssClassAdapter,\n+ (IPortletAssignment,))\n+ gsm.registerAdapter(PortletCssClassFormExtender,\n+ (Interface,\n+ IDefaultBrowserLayer,\n+ IPortletAddForm),\n+ IFormExtender,\n+ \'portletcssclass.extender\')\n+\n+ mapping = self.portal.restrictedTraverse(\n+ \'++contextportlets++plone.leftcolumn\')\n+ addview = mapping.restrictedTraverse(\'+/\' + portlet.addview)\n+ addview.update()\n+ addview_field_names = addview.fields.keys()\n+\n+ gsm.unregisterAdapter(PortletCssClassFormExtender,\n+ (Interface,\n+ IDefaultBrowserLayer,\n+ IPortletAddForm),\n+ IFormExtender,\n+ \'portletcssclass.extender\')\n+ gsm.unregisterAdapter(PortletCssClassAdapter,\n+ (IPortletAssignment,))\n+\n+ # Our addview schema now includes our extended schema:\n+ self.assertEqual(addview_field_names,\n+ schema_field_names + [EXTENDER_PREFIX+\'.css_class\'])\n+\n+ def test_invoke_add_form(self):\n+ portlet = getUtility(IPortletType, name=\'portlets.News\')\n+ mapping = self.portal.restrictedTraverse(\n+ \'++contextportlets++plone.leftcolumn\')\n+ for m in mapping.keys():\n+ del mapping[m]\n+ addview = mapping.restrictedTraverse(\'+/\' + portlet.addview)\n+ addview.update()\n+ addview.createAndAdd(data={\'count\': 5,\n+ EXTENDER_PREFIX+\'.css_class\': \'my-class\'})\n+ portlet_assignment = mapping.values()[0]\n+ settings = IPortletAssignmentSettings(portlet_assignment)\n+\n+ self.assertEqual(portlet_assignment.count, 5)\n+ # We have not extended our storage adapter, so nothing gets saved:\n+ self.assertIsNone(settings.get(\'css_class\', None))\n+\n+ # Register our schemaextender\n+ gsm = getGlobalSiteManager()\n+ gsm.registerAdapter(PortletCssClassAdapter,\n+ (IPortletAssignment,))\n+ gsm.registerAdapter(PortletCssClassFormExtender,\n+ (Interface,\n+ IDefaultBrowserLayer,\n+ IPortletAddForm),\n+ IFormExtender,\n+ \'portletcssclass.extender\')\n+ for m in mapping.keys():\n+ del mapping[m]\n+ addview = mapping.restrictedTraverse(\'+/\' + portlet.addview)\n+ addview.update()\n+ addview.createAndAdd(data={\'count\': 5,\n+ EXTENDER_PREFIX+\'.css_class\': \'my-class\'})\n+ portlet_assignment = mapping.values()[0]\n+ settings = IPortletAssignmentSettings(portlet_assignment)\n+\n+ gsm.unregisterAdapter(PortletCssClassFormExtender,\n+ (Interface,\n+ IDefaultBrowserLayer,\n+ IPortletAddForm),\n+ IFormExtender,\n+ \'portletcssclass.extender\')\n+ gsm.unregisterAdapter(PortletCssClassAdapter,\n+ (IPortletAssignment,))\n+\n+ self.assertEqual(portlet_assignment.count, 5)\n+ # The prefix is used for the form field, not for the stored data:\n+ self.assertEqual(settings.get(\'css_class\'), \'my-class\')\n+\n+ def test_editform_fields(self):\n+\n+ schema_field_names = field.Fields(news.INewsPortlet).keys()\n+\n+ mapping = PortletAssignmentMapping()\n+ request = self.folder.REQUEST\n+ mapping[\'foo\'] = news.Assignment(count=5)\n+ editview = getMultiAdapter((mapping[\'foo\'], request), name=\'edit\')\n+ editview.update()\n+ editview_field_names = editview.fields.keys()\n+\n+ # Our editview schema before we register our extender:\n+ self.assertEqual(editview_field_names, schema_field_names)\n+\n+ # Register our schemaextender\n+ gsm = getGlobalSiteManager()\n+ gsm.registerAdapter(PortletCssClassAdapter,\n+ (IPortletAssignment,))\n+ gsm.registerAdapter(PortletCssClassFormExtender,\n+ (Interface,\n+ IDefaultBrowserLayer,\n+ IPortletEditForm),\n+ IFormExtender,\n+ \'portletcssclass.extender\')\n+\n+ mapping = PortletAssignmentMapping()\n+ request = self.folder.REQUEST\n+ mapping[\'foo\'] = news.Assignment(count=5)\n+ editview = getMultiAdapter((mapping[\'foo\'], request), name=\'edit\')\n+ editview.update()\n+ editview_field_names = editview.fields.keys()\n+\n+ gsm.unregisterAdapter(PortletCssClassFormExtender,\n+ (Interface,\n+ IDefaultBrowserLayer,\n+ IPortletEditForm),\n+ IFormExtender,\n+ \'portletcssclass.extender\')\n+ gsm.unregisterAdapter(PortletCssClassAdapter,\n+ (IPortletAssignment,))\n+\n+ # Our editview schema now includes our extended schema:\n+ self.assertEqual(editview_field_names,\n+ schema_field_names + [EXTENDER_PREFIX+\'.css_class\'])\n+\n+ def test_invoke_edit_form(self):\n+ mapping = PortletAssignmentMapping()\n+ request = self.folder.REQUEST\n+\n+ mapping[\'foo\'] = news.Assignment(count=5)\n+ editview = getMultiAdapter((mapping[\'foo\'], request), name=\'edit\')\n+ editview.update()\n+ editview.applyChanges(data={\'count\': 6,\n+ EXTENDER_PREFIX+\'.css_class\': \'my-class\'})\n+ portlet_assignment = mapping.values()[0]\n+ settings = IPortletAssignmentSettings(portlet_assignment)\n+\n+ self.assertEqual(portlet_assignment.count, 6)\n+ # We have not extended our storage adapter, so nothing gets saved:\n+ self.assertIsNone(settings.get(\'css_class\', None))\n+\n+ # Register our schemaextender\n+ gsm = getGlobalSiteManager()\n+ gsm.registerAdapter(PortletCssClassAdapter,\n+ (IPortletAssignment,))\n+ gsm.registerAdapter(PortletCssClassFormExtender,\n+ (Interface,\n+ IDefaultBrowserLayer,\n+ IPortletEditForm),\n+ IFormExtender,\n+ \'portletcssclass.extender\')\n+ mapping = PortletAssignmentMapping()\n+ request = self.folder.REQUEST\n+\n+ mapping[\'foo\'] = news.Assignment(count=5)\n+ editview = getMultiAdapter((mapping[\'foo\'], request), name=\'edit\')\n+ editview.update()\n+ editview.applyChanges(data={\'count\': 6,\n+ EXTENDER_PREFIX+\'.css_class\': \'my-class\'})\n+ portlet_assignment = mapping.values()[0]\n+ settings = IPortletAssignmentSettings(portlet_assignment)\n+\n+ gsm.unregisterAdapter(PortletCssClassFormExtender,\n+ (Interface,\n+ IDefaultBrowserLayer,\n+ IPortletEditForm),\n+ IFormExtender,\n+ \'portletcssclass.extender\')\n+ gsm.unregisterAdapter(PortletCssClassAdapter,\n+ (IPortletAssignment,))\n+\n+ self.assertEqual(portlet_assignment.count, 6)\n+ # The prefix is used for the form field, not for the stored data:\n+ self.assertEqual(settings.get(\'css_class\'), \'my-class\')\n+\n+ def test_renderer(self):\n+ context = self.folder\n+ request = self.folder.REQUEST\n+ view = self.folder.restrictedTraverse(\'@@plone\')\n+ manager = getUtility(\n+ IPortletManager, name=\'plone.leftcolumn\', context=self.portal)\n+ assignment = news.Assignment(count=5)\n+\n+ renderer = getMultiAdapter(\n+ (context, request, view, manager, assignment), IPortletRenderer)\n+ self.assertTrue(isinstance(renderer, news.Renderer))\n+\n+\n+def test_suite():\n+ from unittest import TestSuite, makeSuite\n+ suite = TestSuite()\n+ suite.addTest(makeSuite(TestSchemaExtender))\n+ return suite\ndiff --git a/setup.cfg b/setup.cfg\nindex d80c466..27e2916 100644\n--- a/setup.cfg\n+++ b/setup.cfg\n@@ -4,3 +4,13 @@ create-wheel = yes\n # When Python 2-3 compatible:\n # [bdist_wheel]\n # universal = 1\n+\n+[isort]\n+# for details see\n+# http://docs.plone.org/develop/styleguide/python.html#grouping-and-sorting\n+force_alphabetical_sort = True\n+force_single_line = True\n+lines_after_imports = 2\n+line_length = 200\n+not_skip =\n+ __init__.py\n'