From 5fea47d1f80c0bda2a34f40d3871ad4c7d0c1ece Mon Sep 17 00:00:00 2001 From: Arnaud Fontaine <arnaud.fontaine@nexedi.com> Date: Thu, 21 May 2020 14:06:31 +0900 Subject: [PATCH] ZODB Components: Products import compatibility (MR !1271). This implements Products import compatibility based on Component.source_reference using existing import hooks so that Products.ERP5.Document.Person for example is importable (actually returning erp5.component.document.Person module). This only works with non-FS modules (IOW when the ZODB is accessible and portal.portal_components is available). --- .../test.erp5.testBusinessTemplate.py | 39 +++++ .../interactions/Component_reset.xml | 1 + .../person_module/test_person.xml | 138 +++++++++++++++++ .../test_person/default_address.xml | 38 +++++ .../test_person/default_career.xml | 61 ++++++++ .../test_person/default_email.xml | 30 ++++ .../person_module/test_person/default_fax.xml | 30 ++++ .../test_person/default_link.xml | 40 +++++ .../test_person/default_telephone.xml | 30 ++++ .../bt/description | 1 + .../bt/template_format_version | 1 + .../bt/template_path_list | 2 + .../bt/title | 1 + product/ERP5Type/Tool/ComponentTool.py | 1 + product/ERP5Type/dynamic/component_package.py | 81 +++++++--- product/ERP5Type/dynamic/dynamic_module.py | 3 + product/ERP5Type/mixin/component.py | 6 +- .../tests/testDynamicClassGeneration.py | 140 +++++++++++++++--- product/ERP5Type/tests/testERP5Type.py | 36 +++++ 19 files changed, 640 insertions(+), 39 deletions(-) create mode 100644 product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/PathTemplateItem/person_module/test_person.xml create mode 100644 product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/PathTemplateItem/person_module/test_person/default_address.xml create mode 100644 product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/PathTemplateItem/person_module/test_person/default_career.xml create mode 100644 product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/PathTemplateItem/person_module/test_person/default_email.xml create mode 100644 product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/PathTemplateItem/person_module/test_person/default_fax.xml create mode 100644 product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/PathTemplateItem/person_module/test_person/default_link.xml create mode 100644 product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/PathTemplateItem/person_module/test_person/default_telephone.xml create mode 100644 product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/bt/description create mode 100644 product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/bt/template_format_version create mode 100644 product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/bt/template_path_list create mode 100644 product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/bt/title diff --git a/bt5/erp5_core_test/TestTemplateItem/portal_components/test.erp5.testBusinessTemplate.py b/bt5/erp5_core_test/TestTemplateItem/portal_components/test.erp5.testBusinessTemplate.py index 379bc3c0d9..dd2e5f7fc8 100644 --- a/bt5/erp5_core_test/TestTemplateItem/portal_components/test.erp5.testBusinessTemplate.py +++ b/bt5/erp5_core_test/TestTemplateItem/portal_components/test.erp5.testBusinessTemplate.py @@ -6380,6 +6380,45 @@ class TestBusinessTemplate(BusinessTemplateMixin): self.assertEqual("FooBar", getattr(foo_in_portal, "title")) self.uninstallBusinessTemplate('test_168_CheckPortalTypeAndPathInSameBusinessTemplate') + def test_legacy_products_erp5_document_compatibility(self): + """Check we can import a business template referencing classes from + Products.ERP5Type.Document namespace and that the classes are migrated + """ + import Products.ERP5.tests + bt_path = os.path.join( + os.path.dirname(Products.ERP5.tests.__file__), + 'test_data', + 'BusinessTemplate_test_legacy_products_erp5_document_compatibility') + bt = self.portal.portal_templates.download(bt_path) + bt.install() + self.tic() + + # when loaded, the legacy classes have been updated to use + # erp5.portal_type namespace + self.assertEqual( + str(self.portal.person_module.test_person.__class__), + "<class 'erp5.portal_type.Person'>") + self.assertEqual( + str(self.portal.person_module.test_person.default_address.__class__), + "<class 'erp5.portal_type.Address'>") + self.assertEqual( + str(self.portal.person_module.test_person.default_career.__class__), + "<class 'erp5.portal_type.Career'>") + self.assertEqual( + str(self.portal.person_module.test_person.default_email.__class__), + "<class 'erp5.portal_type.Email'>") + self.assertEqual( + str(self.portal.person_module.test_person.default_fax.__class__), + "<class 'erp5.portal_type.Fax'>") + self.assertEqual( + str(self.portal.person_module.test_person.default_telephone.__class__), + "<class 'erp5.portal_type.Telephone'>") + self.assertEqual( + str(self.portal.person_module.test_person.default_link.__class__), + "<class 'erp5.portal_type.Link'>") + + self.uninstallBusinessTemplate('BusinessTemplate_test_legacy_products_erp5_document_compatibility') + def test_169_CheckPortalTypeAndPathInSameBusinessTemplateAndBrokenObjectModification(self): """ Make sure we have possibility to change broken diff --git a/product/ERP5/bootstrap/erp5_core/WorkflowTemplateItem/portal_workflow/dynamic_class_generation_interaction_workflow/interactions/Component_reset.xml b/product/ERP5/bootstrap/erp5_core/WorkflowTemplateItem/portal_workflow/dynamic_class_generation_interaction_workflow/interactions/Component_reset.xml index 0e0b1feef0..2091b19c1e 100644 --- a/product/ERP5/bootstrap/erp5_core/WorkflowTemplateItem/portal_workflow/dynamic_class_generation_interaction_workflow/interactions/Component_reset.xml +++ b/product/ERP5/bootstrap/erp5_core/WorkflowTemplateItem/portal_workflow/dynamic_class_generation_interaction_workflow/interactions/Component_reset.xml @@ -56,6 +56,7 @@ <key> <string>method_id</string> </key> <value> <list> + <string>_setSourceReference</string> <string>validate</string> <string>invalidate</string> </list> diff --git a/product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/PathTemplateItem/person_module/test_person.xml b/product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/PathTemplateItem/person_module/test_person.xml new file mode 100644 index 0000000000..71b59b1de0 --- /dev/null +++ b/product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/PathTemplateItem/person_module/test_person.xml @@ -0,0 +1,138 @@ +<?xml version="1.0"?> +<ZopeData> + <record id="1" aka="AAAAAAAAAAE="> + <pickle> + <global name="Person" module="Products.ERP5Type.Document.Person"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>_Access_contents_information_Permission</string> </key> + <value> + <tuple> + <string>Assignee</string> + <string>Assignor</string> + <string>Associate</string> + <string>Auditor</string> + <string>Author</string> + <string>Manager</string> + <string>Owner</string> + </tuple> + </value> + </item> + <item> + <key> <string>_Add_portal_content_Permission</string> </key> + <value> + <tuple> + <string>Assignee</string> + <string>Assignor</string> + <string>Associate</string> + <string>Author</string> + <string>Manager</string> + <string>Owner</string> + </tuple> + </value> + </item> + <item> + <key> <string>_Modify_portal_content_Permission</string> </key> + <value> + <tuple> + <string>Assignee</string> + <string>Assignor</string> + <string>Associate</string> + <string>Author</string> + <string>Manager</string> + <string>Owner</string> + </tuple> + </value> + </item> + <item> + <key> <string>_View_Permission</string> </key> + <value> + <tuple> + <string>Assignee</string> + <string>Assignor</string> + <string>Associate</string> + <string>Auditor</string> + <string>Author</string> + <string>Manager</string> + <string>Owner</string> + </tuple> + </value> + </item> + <item> + <key> <string>_count</string> </key> + <value> + <persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent> + </value> + </item> + <item> + <key> <string>_mt_index</string> </key> + <value> + <persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent> + </value> + </item> + <item> + <key> <string>_tree</string> </key> + <value> + <persistent> <string encoding="base64">AAAAAAAAAAQ=</string> </persistent> + </value> + </item> + <item> + <key> <string>description</string> </key> + <value> + <none/> + </value> + </item> + <item> + <key> <string>first_name</string> </key> + <value> <string>Test</string> </value> + </item> + <item> + <key> <string>id</string> </key> + <value> <string>test_person</string> </value> + </item> + <item> + <key> <string>language</string> </key> + <value> + <none/> + </value> + </item> + <item> + <key> <string>last_name</string> </key> + <value> <string>Person</string> </value> + </item> + <item> + <key> <string>portal_type</string> </key> + <value> <string>Person</string> </value> + </item> + <item> + <key> <string>user_id</string> </key> + <value> <string>P6</string> </value> + </item> + </dictionary> + </pickle> + </record> + <record id="2" aka="AAAAAAAAAAI="> + <pickle> + <global name="Length" module="BTrees.Length"/> + </pickle> + <pickle> <int>0</int> </pickle> + </record> + <record id="3" aka="AAAAAAAAAAM="> + <pickle> + <global name="OOBTree" module="BTrees.OOBTree"/> + </pickle> + <pickle> + <none/> + </pickle> + </record> + <record id="4" aka="AAAAAAAAAAQ="> + <pickle> + <global name="OOBTree" module="BTrees.OOBTree"/> + </pickle> + <pickle> + <none/> + </pickle> + </record> +</ZopeData> diff --git a/product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/PathTemplateItem/person_module/test_person/default_address.xml b/product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/PathTemplateItem/person_module/test_person/default_address.xml new file mode 100644 index 0000000000..f17f727d40 --- /dev/null +++ b/product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/PathTemplateItem/person_module/test_person/default_address.xml @@ -0,0 +1,38 @@ +<?xml version="1.0"?> +<ZopeData> + <record id="1" aka="AAAAAAAAAAE="> + <pickle> + <global name="GeographicAddress" module="Products.ERP5Type.Document.GeographicAddress"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>city</string> </key> + <value> <string>city</string> </value> + </item> + <item> + <key> <string>id</string> </key> + <value> <string>default_address</string> </value> + </item> + <item> + <key> <string>portal_type</string> </key> + <value> <string>Address</string> </value> + </item> + <item> + <key> <string>sid</string> </key> + <value> + <none/> + </value> + </item> + <item> + <key> <string>street_address</string> </key> + <value> <string>the address</string> </value> + </item> + <item> + <key> <string>zip_code</string> </key> + <value> <string>zip code</string> </value> + </item> + </dictionary> + </pickle> + </record> +</ZopeData> diff --git a/product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/PathTemplateItem/person_module/test_person/default_career.xml b/product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/PathTemplateItem/person_module/test_person/default_career.xml new file mode 100644 index 0000000000..85cb1e2089 --- /dev/null +++ b/product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/PathTemplateItem/person_module/test_person/default_career.xml @@ -0,0 +1,61 @@ +<?xml version="1.0"?> +<ZopeData> + <record id="1" aka="AAAAAAAAAAE="> + <pickle> + <global name="Career" module="Products.ERP5Type.Document.Career"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>_Access_contents_information_Permission</string> </key> + <value> + <tuple> + <string>Assignee</string> + <string>Assignor</string> + <string>Auditor</string> + <string>Manager</string> + <string>Owner</string> + </tuple> + </value> + </item> + <item> + <key> <string>_Modify_portal_content_Permission</string> </key> + <value> + <tuple> + <string>Assignee</string> + <string>Assignor</string> + <string>Manager</string> + <string>Owner</string> + </tuple> + </value> + </item> + <item> + <key> <string>_View_Permission</string> </key> + <value> + <tuple> + <string>Assignee</string> + <string>Assignor</string> + <string>Auditor</string> + <string>Manager</string> + <string>Owner</string> + </tuple> + </value> + </item> + <item> + <key> <string>categories</string> </key> + <value> + <tuple/> + </value> + </item> + <item> + <key> <string>id</string> </key> + <value> <string>default_career</string> </value> + </item> + <item> + <key> <string>portal_type</string> </key> + <value> <string>Career</string> </value> + </item> + </dictionary> + </pickle> + </record> +</ZopeData> diff --git a/product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/PathTemplateItem/person_module/test_person/default_email.xml b/product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/PathTemplateItem/person_module/test_person/default_email.xml new file mode 100644 index 0000000000..8eeb093759 --- /dev/null +++ b/product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/PathTemplateItem/person_module/test_person/default_email.xml @@ -0,0 +1,30 @@ +<?xml version="1.0"?> +<ZopeData> + <record id="1" aka="AAAAAAAAAAE="> + <pickle> + <global name="Url" module="Products.ERP5Type.Document.Url"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>coordinate_text</string> </key> + <value> <string>mail@example.com</string> </value> + </item> + <item> + <key> <string>id</string> </key> + <value> <string>default_email</string> </value> + </item> + <item> + <key> <string>portal_type</string> </key> + <value> <string>Email</string> </value> + </item> + <item> + <key> <string>sid</string> </key> + <value> + <none/> + </value> + </item> + </dictionary> + </pickle> + </record> +</ZopeData> diff --git a/product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/PathTemplateItem/person_module/test_person/default_fax.xml b/product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/PathTemplateItem/person_module/test_person/default_fax.xml new file mode 100644 index 0000000000..3ec49a2305 --- /dev/null +++ b/product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/PathTemplateItem/person_module/test_person/default_fax.xml @@ -0,0 +1,30 @@ +<?xml version="1.0"?> +<ZopeData> + <record id="1" aka="AAAAAAAAAAE="> + <pickle> + <global name="Telephone" module="Products.ERP5Type.Document.Telephone"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>coordinate_text</string> </key> + <value> <string>123456</string> </value> + </item> + <item> + <key> <string>id</string> </key> + <value> <string>default_fax</string> </value> + </item> + <item> + <key> <string>portal_type</string> </key> + <value> <string>Fax</string> </value> + </item> + <item> + <key> <string>sid</string> </key> + <value> + <none/> + </value> + </item> + </dictionary> + </pickle> + </record> +</ZopeData> diff --git a/product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/PathTemplateItem/person_module/test_person/default_link.xml b/product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/PathTemplateItem/person_module/test_person/default_link.xml new file mode 100644 index 0000000000..f89399b6db --- /dev/null +++ b/product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/PathTemplateItem/person_module/test_person/default_link.xml @@ -0,0 +1,40 @@ +<?xml version="1.0"?> +<ZopeData> + <record id="1" aka="AAAAAAAAAAE="> + <pickle> + <global name="Url" module="Products.ERP5Type.Document.Url"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>description</string> </key> + <value> + <none/> + </value> + </item> + <item> + <key> <string>id</string> </key> + <value> <string>default_link</string> </value> + </item> + <item> + <key> <string>portal_type</string> </key> + <value> <string>Link</string> </value> + </item> + <item> + <key> <string>sid</string> </key> + <value> + <none/> + </value> + </item> + <item> + <key> <string>title</string> </key> + <value> <string>1</string> </value> + </item> + <item> + <key> <string>url_string</string> </key> + <value> <string>https://example.com</string> </value> + </item> + </dictionary> + </pickle> + </record> +</ZopeData> diff --git a/product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/PathTemplateItem/person_module/test_person/default_telephone.xml b/product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/PathTemplateItem/person_module/test_person/default_telephone.xml new file mode 100644 index 0000000000..b4008c4332 --- /dev/null +++ b/product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/PathTemplateItem/person_module/test_person/default_telephone.xml @@ -0,0 +1,30 @@ +<?xml version="1.0"?> +<ZopeData> + <record id="1" aka="AAAAAAAAAAE="> + <pickle> + <global name="Telephone" module="Products.ERP5Type.Document.Telephone"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>coordinate_text</string> </key> + <value> <string>123456</string> </value> + </item> + <item> + <key> <string>id</string> </key> + <value> <string>default_telephone</string> </value> + </item> + <item> + <key> <string>portal_type</string> </key> + <value> <string>Telephone</string> </value> + </item> + <item> + <key> <string>sid</string> </key> + <value> + <none/> + </value> + </item> + </dictionary> + </pickle> + </record> +</ZopeData> diff --git a/product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/bt/description b/product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/bt/description new file mode 100644 index 0000000000..f79a6b7318 --- /dev/null +++ b/product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/bt/description @@ -0,0 +1 @@ +A business templates with documents exported with their __class__ from Products.ERP5Type.Document diff --git a/product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/bt/template_format_version b/product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/bt/template_format_version new file mode 100644 index 0000000000..56a6051ca2 --- /dev/null +++ b/product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/bt/template_format_version @@ -0,0 +1 @@ +1 \ No newline at end of file diff --git a/product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/bt/template_path_list b/product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/bt/template_path_list new file mode 100644 index 0000000000..eca988f49a --- /dev/null +++ b/product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/bt/template_path_list @@ -0,0 +1,2 @@ +person_module/test_person +person_module/test_person/** \ No newline at end of file diff --git a/product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/bt/title b/product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/bt/title new file mode 100644 index 0000000000..3a1a31ca46 --- /dev/null +++ b/product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/bt/title @@ -0,0 +1 @@ +BusinessTemplate_test_legacy_products_erp5_document_compatibility \ No newline at end of file diff --git a/product/ERP5Type/Tool/ComponentTool.py b/product/ERP5Type/Tool/ComponentTool.py index 2a4c6070a2..4e531344cb 100644 --- a/product/ERP5Type/Tool/ComponentTool.py +++ b/product/ERP5Type/Tool/ComponentTool.py @@ -153,6 +153,7 @@ class ComponentTool(BaseTool): package.reset() component_package_list.append(package.__name__) + erp5.component.filesystem_import_dict = None erp5.component.ref_manager.gc() # Clear pylint cache diff --git a/product/ERP5Type/dynamic/component_package.py b/product/ERP5Type/dynamic/component_package.py index 9352c71181..154edccd6d 100644 --- a/product/ERP5Type/dynamic/component_package.py +++ b/product/ERP5Type/dynamic/component_package.py @@ -35,6 +35,7 @@ import imp import collections from Products.ERP5.ERP5Site import getSite +from Products.ERP5Type import product_path as ERP5Type_product_path from . import aq_method_lock from types import ModuleType from zLOG import LOG, BLATHER, WARNING @@ -109,11 +110,21 @@ class ComponentDynamicPackage(ModuleType): perhaps because the Finder of another Component Package could do it or because this is a filesystem module... """ - # Ignore imports with a path which are filesystem-only and any - # absolute imports which does not start with this package prefix, - # None there means that "normal" sys.path will be used - if path or not fullname.startswith(self._namespace_prefix): - return None + import erp5.component + + # ZODB Components + if not path: + if not fullname.startswith(self._namespace_prefix): + return None + # FS import backward compatibility + else: + try: + fullname = erp5.component.filesystem_import_dict[fullname] + except (TypeError, KeyError): + return None + else: + if not fullname.startswith(self._namespace_prefix): + return None import_lock_held = True try: @@ -124,6 +135,26 @@ class ComponentDynamicPackage(ModuleType): try: site = getSite() + if erp5.component.filesystem_import_dict is None: + filesystem_import_dict = {} + try: + component_tool = aq_base(site.portal_components) + except AttributeError: + # For old sites, just use FS Documents... + return None + else: + for component in component_tool.objectValues(): + if component.getValidationState() == 'validated': + component_module_name = '%s.%s' % (component._getDynamicModuleNamespace(), + component.getReference()) + if component.getSourceReference() is not None: + filesystem_import_dict[component.getSourceReference()] = component_module_name + + if component.getPortalType() == 'Document Component': + filesystem_import_dict[('Products.ERP5Type.Document.' + + component.getReference())] = component_module_name + erp5.component.filesystem_import_dict = filesystem_import_dict + # __import__ will first try a relative import, for example # erp5.component.XXX.YYY.ZZZ where erp5.component.XXX.YYY is the current # Component where an import is done @@ -139,13 +170,7 @@ class ComponentDynamicPackage(ModuleType): id_ = "%s.%s.%s" % (self._id_prefix, version, name) # aq_base() because this should not go up to ERP5Site and trigger # side-effects, after all this only check for existence... - try: - component_tool = aq_base(site.portal_components) - except AttributeError: - # For old sites, just use FS Documents... - return None - - component = getattr(component_tool, id_, None) + component = getattr(aq_base(site.portal_components), id_, None) if component is None or component.getValidationState() not in ('modified', 'validated'): return None @@ -161,12 +186,7 @@ class ComponentDynamicPackage(ModuleType): # name=REFERENCE else: - try: - component_tool = aq_base(site.portal_components) - except AttributeError: - # For old sites, just use FS Documents... - return None - + component_tool = aq_base(site.portal_components) for version in site.getVersionPriorityNameList(): id_ = "%s.%s.%s" % (self._id_prefix, version, name) component = getattr(component_tool, id_, None) @@ -219,6 +239,14 @@ class ComponentDynamicPackage(ModuleType): module for any reason... """ site = getSite() + + if fullname.startswith('Products.'): + module_fullname_filesystem = fullname + import erp5.component + fullname = erp5.component.filesystem_import_dict[module_fullname_filesystem] + else: + module_fullname_filesystem = None + name = fullname[len(self._namespace_prefix):] # if only Version package (erp5.component.XXX.VERSION_version) is @@ -268,6 +296,9 @@ class ComponentDynamicPackage(ModuleType): setattr(self, name, module) sys.modules[module_fullname_alias] = module MNAME_MAP[module_fullname_alias] = module.__name__ + if module_fullname_filesystem: + sys.modules[module_fullname_filesystem] = module + MNAME_MAP[module_fullname_filesystem] = module.__name__ return module component = getattr(site.portal_components, component_id) @@ -288,6 +319,8 @@ class ComponentDynamicPackage(ModuleType): sys.modules[module_fullname] = module if module_fullname_alias: sys.modules[module_fullname_alias] = module + if module_fullname_filesystem: + sys.modules[module_fullname_filesystem] = module # This must be set for imports at least (see PEP 302) module.__file__ = '<' + relative_url + '>' @@ -308,6 +341,8 @@ class ComponentDynamicPackage(ModuleType): del sys.modules[module_fullname] if module_fullname_alias: del sys.modules[module_fullname_alias] + if module_fullname_filesystem: + del sys.modules[module_fullname_filesystem] raise ImportError( "%s: cannot load Component %s (%s)" % (fullname, name, error)), \ @@ -319,6 +354,8 @@ class ComponentDynamicPackage(ModuleType): if module_fullname_alias: setattr(self, name, module) MNAME_MAP[module_fullname_alias] = module_fullname + if module_fullname_filesystem: + MNAME_MAP[module_fullname_filesystem] = module.__name__ import erp5.component erp5.component.ref_manager.add_module(module) @@ -408,10 +445,14 @@ class ComponentDynamicPackage(ModuleType): for k in modsec_dict.keys(): if k.startswith(self._namespace): del modsec_dict[k] - for k in MNAME_MAP.keys(): - if k.startswith(self._namespace): + for k, v in MNAME_MAP.items(): + if v.startswith(self._namespace): del MNAME_MAP[k] + # Products import compatibility (module_fullname_filesystem) + if k.startswith('Products.'): + del sys.modules[k] + for name, module in package.__dict__.items(): if name[0] == '_' or not isinstance(module, ModuleType): continue diff --git a/product/ERP5Type/dynamic/dynamic_module.py b/product/ERP5Type/dynamic/dynamic_module.py index f6f32f082c..e145dbf68b 100644 --- a/product/ERP5Type/dynamic/dynamic_module.py +++ b/product/ERP5Type/dynamic/dynamic_module.py @@ -120,6 +120,9 @@ class ComponentPackageType(PackageType): the top level, otherwise a module being relied upon may have a different API after reset, thus it may fail... """ + # 'Products.ERP5.Document.Person' => 'erp5.component.document.Person' + filesystem_import_dict = None + def __init__(self, *args, **kwargs): super(ComponentPackageType, self).__init__(*args, **kwargs) self.ref_manager = RefManager() diff --git a/product/ERP5Type/mixin/component.py b/product/ERP5Type/mixin/component.py index fe67f7e215..084543be32 100644 --- a/product/ERP5Type/mixin/component.py +++ b/product/ERP5Type/mixin/component.py @@ -178,7 +178,11 @@ class ComponentMixin(PropertyRecordableMixin, Base): 'description': BaseAccessor.Getter('getDescription', 'description', 'string', - default='') + default=''), + 'source_reference': BaseAccessor.Getter('getSourceReference', + 'source_reference', + 'string', + storage_id='default_source_reference'), } _message_invalid_id = "ID is invalid, should be '${id_prefix}.VERSION.REFERENCE'" diff --git a/product/ERP5Type/tests/testDynamicClassGeneration.py b/product/ERP5Type/tests/testDynamicClassGeneration.py index 9387b37c75..26e0b93d68 100644 --- a/product/ERP5Type/tests/testDynamicClassGeneration.py +++ b/product/ERP5Type/tests/testDynamicClassGeneration.py @@ -2655,25 +2655,11 @@ def foobar(self, a, b="portal_type"): from Products.ERP5Type.Core.DocumentComponent import DocumentComponent -class TestZodbDocumentComponent(_TestZodbComponent): +class _TestZodbDocumentComponentMixin(_TestZodbComponent): """ - Tests specific to ZODB Document Component. This is only for Document - previously defined in bt5 and installed on the filesystem in - $INSTANCE_HOME/Document. Later on, Product Documents will also be migrated + Common to all Component class inheriting from Document Component (so + Interface and Mixin) """ - _portal_type = 'Document Component' - _document_class = DocumentComponent - - def _getValidSourceCode(self, class_name): - return '''from erp5.component.document.Person import Person - -class %sAnything: - pass - -class %s(Person): - pass -''' % (class_name, class_name) - def testAtLeastOneClassNamedAfterReference(self): component = self._newComponent( self._generateReference('TestClassNamedAfterReference')) @@ -2921,6 +2907,124 @@ class TestGC(XMLObject): 'gc: collectable <Implements 0x%x>\n' % Implements_id], sorted(found_line_list)) +class TestZodbDocumentComponent(_TestZodbDocumentComponentMixin): + """ + Tests specific to ZODB Document Component. This is only for Document + previously defined in bt5 and installed on the filesystem in + $INSTANCE_HOME/Document. Later on, Product Documents will also be migrated + """ + _portal_type = 'Document Component' + _document_class = DocumentComponent + + def _getValidSourceCode(self, class_name): + return '''from erp5.component.document.Person import Person + +class %sAnything: + pass + +class %s(Person): + pass +''' % (class_name, class_name) + + def testProductsERP5DocumentCompatibility(self): + """Check that document class also exist in its original namespace (source_reference) + + Document Component that were moved from file system Products/*/Document needs + to be still importable from their initial location, as there might be classes + in the database of these instances. + + There is no such test for Mixin/Interface/Tool because the code is the + same for all of them (component_package.py). + """ + self.failIfModuleImportable('TestProductsERP5DocumentCompatibility') + + test_component = self._newComponent( + 'TestProductsERP5DocumentCompatibility', + """\ +from Products.ERP5Type.Base import Base +class TestProductsERP5DocumentCompatibility(Base): + portal_type = 'Test ProductsERP5Document Compatibility' + test_attribute = 'TestProductsERP5DocumentCompatibility' +""" + ) + test_component.setSourceReference('Products.ERP5.Document.TestProductsERP5DocumentCompatibility') + test_component.validate() + self.tic() + + self.assertModuleImportable('TestProductsERP5DocumentCompatibility') + + from Products.ERP5.Document.TestProductsERP5DocumentCompatibility import TestProductsERP5DocumentCompatibility # pylint:disable=import-error,no-name-in-module + self.assertEqual(TestProductsERP5DocumentCompatibility.test_attribute, 'TestProductsERP5DocumentCompatibility') + + # this also exist in Products.ERP5Type.Document + from Products.ERP5Type.Document.TestProductsERP5DocumentCompatibility import TestProductsERP5DocumentCompatibility as TestProductsERP5DocumentCompatibility_from_ProductsERP5Type # pylint:disable=import-error,no-name-in-module + self.assertIs(TestProductsERP5DocumentCompatibility_from_ProductsERP5Type, TestProductsERP5DocumentCompatibility) + + # another component can also import the migrated component from its original name + test_component_importing = self._newComponent( + 'TestComponentImporting', + """\ +from Products.ERP5.Document.TestProductsERP5DocumentCompatibility import TestProductsERP5DocumentCompatibility +class TestComponentImporting(TestProductsERP5DocumentCompatibility): + pass +""" + ) + test_component_importing.validate() + self.tic() + + self.assertModuleImportable('TestComponentImporting') + from erp5.component.document.TestComponentImporting import TestComponentImporting # pylint:disable=import-error,no-name-in-module + + from Products.ERP5.Document.TestProductsERP5DocumentCompatibility import TestProductsERP5DocumentCompatibility # pylint:disable=import-error,no-name-in-module + self.assertTrue(issubclass(TestComponentImporting, TestProductsERP5DocumentCompatibility)) + + test_component.invalidate() + self.tic() + + # after invalidating the component, the legacy modules are no longer importable + with self.assertRaises(ImportError): + from Products.ERP5.Document.TestProductsERP5DocumentCompatibility import TestProductsERP5DocumentCompatibility # pylint:disable=import-error,no-name-in-module + with self.assertRaises(ImportError): + from Products.ERP5Type.Document.TestProductsERP5DocumentCompatibility import TestProductsERP5DocumentCompatibility # pylint:disable=import-error,no-name-in-module + + def testProductsERP5TypeDocumentCompatibility(self): + """Check that document class also exist in Products.ERP5Type.Document namespace + for compatibility. + + We also check that this module is properly reloaded when a document component + is modified. + """ + self.failIfModuleImportable('TestProductsERP5TypeDocumentCompatibility') + + test_component = self._newComponent( + 'TestProductsERP5TypeDocumentCompatibility', + """\ +from Products.ERP5Type.Base import Base +class TestProductsERP5TypeDocumentCompatibility(Base): + portal_type = 'Test ProductsERP5TypeDocument Compatibility' + generation = 1 +""" + ) + test_component.validate() + self.tic() + + self.assertModuleImportable('TestProductsERP5TypeDocumentCompatibility') + + from Products.ERP5Type.Document.TestProductsERP5TypeDocumentCompatibility import TestProductsERP5TypeDocumentCompatibility # pylint:disable=import-error,no-name-in-module + self.assertEqual(TestProductsERP5TypeDocumentCompatibility.generation, 1) + + test_component.setTextContent( + """\ +from Products.ERP5Type.Base import Base +class TestProductsERP5TypeDocumentCompatibility(Base): + portal_type = 'Test ProductsERP5TypeDocument Compatibility' + generation = 2 +""") + self.tic() + self.assertModuleImportable('TestProductsERP5TypeDocumentCompatibility') + from Products.ERP5Type.Document.TestProductsERP5TypeDocumentCompatibility import TestProductsERP5TypeDocumentCompatibility # pylint:disable=import-error,no-name-in-module + self.assertEqual(TestProductsERP5TypeDocumentCompatibility.generation, 2) + from Products.ERP5Type.Core.TestComponent import TestComponent class TestZodbTestComponent(_TestZodbComponent): @@ -3091,7 +3195,7 @@ class Test(ERP5TypeTestCase): self.commit() from Products.ERP5Type.Core.InterfaceComponent import InterfaceComponent -class TestZodbInterfaceComponent(TestZodbDocumentComponent): +class TestZodbInterfaceComponent(_TestZodbDocumentComponentMixin): """ Tests specific to ZODB Interface Component. """ diff --git a/product/ERP5Type/tests/testERP5Type.py b/product/ERP5Type/tests/testERP5Type.py index 4396ad66b1..081c2ef0f6 100644 --- a/product/ERP5Type/tests/testERP5Type.py +++ b/product/ERP5Type/tests/testERP5Type.py @@ -33,6 +33,7 @@ of Portal Type as Classes and ZODB Components import unittest from Products.ERP5Type.tests.ERP5TypeTestCase import ERP5TypeTestCase +from AccessControl.ZopeGuards import guarded_import from Products.ERP5Type.tests.utils import LogInterceptor class TestERP5Type(ERP5TypeTestCase, LogInterceptor): @@ -205,6 +206,41 @@ class TestERP5Type(ERP5TypeTestCase, LogInterceptor): subdocument_record = sql_catalog.getRecordForUid(subdocument.uid) self.assertEqual(subdocument.getPath(), subdocument_record.path) + def test_products_document_legacy(self): + """check document classes defined in Products/*/Document/*.py + """ + # note: this assertion below checks Alarm is really a legacy document class. + # if one day Alarm is moved to component, then this test needs to be updated + # with another module that lives on the file system. + import Products.ERP5.Document.Alarm + self.assertIn('product/ERP5/Document/Alarm.py', Products.ERP5.Document.Alarm.__file__) + + # document classes are also dynamically loaded in Products.ERP5Type.Document module + from Products.ERP5Type.Document.Alarm import Alarm as Alarm_from_ERP5Type # pylint:disable=import-error,no-name-in-module + self.assertIs(Alarm_from_ERP5Type, Products.ERP5.Document.Alarm.Alarm) + + # a new temp constructor is created + from Products.ERP5Type.Document import newTempAlarm # pylint:disable=import-error,no-name-in-module + self.assertIn(Alarm_from_ERP5Type, newTempAlarm(self.portal, '').__class__.mro()) + + # temp constructors are deprecated, they issue a warning when called + import mock + with mock.patch('Products.ERP5Type.Utils.warnings.warn') as warn: + newTempAlarm(self.portal, '') + warn.assert_called_with( + 'newTemp*(self, ID) will be removed, use self.newContent(temp_object=True, id=ID, portal_type=...)', + DeprecationWarning, 2) + + def test_03_NewTempObject(self): + # Products.ERP5Type.Document.newTempBase is another (not recommended) way + # of creating temp objects + import Products.ERP5Type.Document + o = Products.ERP5Type.Document.newTempBase(self.portal, 'id') + self.assertEqual(o.getId(), 'id') + self.assertEqual(o.getPortalType(), 'Base Object') + self.assertTrue(o.isTempObject()) + self.assertTrue(guarded_import("Products.ERP5Type.Document", fromlist=["newTempBase"])) + def test_suite(): suite = unittest.TestSuite() suite.addTest(unittest.makeSuite(TestERP5Type)) -- 2.30.9