Commit 5fea47d1 authored by Arnaud Fontaine's avatar Arnaud Fontaine

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).
parent 08b48c6f
...@@ -6380,6 +6380,45 @@ class TestBusinessTemplate(BusinessTemplateMixin): ...@@ -6380,6 +6380,45 @@ class TestBusinessTemplate(BusinessTemplateMixin):
self.assertEqual("FooBar", getattr(foo_in_portal, "title")) self.assertEqual("FooBar", getattr(foo_in_portal, "title"))
self.uninstallBusinessTemplate('test_168_CheckPortalTypeAndPathInSameBusinessTemplate') 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): def test_169_CheckPortalTypeAndPathInSameBusinessTemplateAndBrokenObjectModification(self):
""" """
Make sure we have possibility to change broken Make sure we have possibility to change broken
......
...@@ -56,6 +56,7 @@ ...@@ -56,6 +56,7 @@
<key> <string>method_id</string> </key> <key> <string>method_id</string> </key>
<value> <value>
<list> <list>
<string>_setSourceReference</string>
<string>validate</string> <string>validate</string>
<string>invalidate</string> <string>invalidate</string>
</list> </list>
......
<?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>
<?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>
<?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>
<?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>
<?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>
<?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>
<?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>
A business templates with documents exported with their __class__ from Products.ERP5Type.Document
person_module/test_person
person_module/test_person/**
\ No newline at end of file
BusinessTemplate_test_legacy_products_erp5_document_compatibility
\ No newline at end of file
...@@ -153,6 +153,7 @@ class ComponentTool(BaseTool): ...@@ -153,6 +153,7 @@ class ComponentTool(BaseTool):
package.reset() package.reset()
component_package_list.append(package.__name__) component_package_list.append(package.__name__)
erp5.component.filesystem_import_dict = None
erp5.component.ref_manager.gc() erp5.component.ref_manager.gc()
# Clear pylint cache # Clear pylint cache
......
...@@ -35,6 +35,7 @@ import imp ...@@ -35,6 +35,7 @@ import imp
import collections import collections
from Products.ERP5.ERP5Site import getSite from Products.ERP5.ERP5Site import getSite
from Products.ERP5Type import product_path as ERP5Type_product_path
from . import aq_method_lock from . import aq_method_lock
from types import ModuleType from types import ModuleType
from zLOG import LOG, BLATHER, WARNING from zLOG import LOG, BLATHER, WARNING
...@@ -109,11 +110,21 @@ class ComponentDynamicPackage(ModuleType): ...@@ -109,11 +110,21 @@ class ComponentDynamicPackage(ModuleType):
perhaps because the Finder of another Component Package could do it or perhaps because the Finder of another Component Package could do it or
because this is a filesystem module... because this is a filesystem module...
""" """
# Ignore imports with a path which are filesystem-only and any import erp5.component
# absolute imports which does not start with this package prefix,
# None there means that "normal" sys.path will be used # ZODB Components
if path or not fullname.startswith(self._namespace_prefix): if not path:
return None 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 import_lock_held = True
try: try:
...@@ -124,6 +135,26 @@ class ComponentDynamicPackage(ModuleType): ...@@ -124,6 +135,26 @@ class ComponentDynamicPackage(ModuleType):
try: try:
site = getSite() 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 # __import__ will first try a relative import, for example
# erp5.component.XXX.YYY.ZZZ where erp5.component.XXX.YYY is the current # erp5.component.XXX.YYY.ZZZ where erp5.component.XXX.YYY is the current
# Component where an import is done # Component where an import is done
...@@ -139,13 +170,7 @@ class ComponentDynamicPackage(ModuleType): ...@@ -139,13 +170,7 @@ class ComponentDynamicPackage(ModuleType):
id_ = "%s.%s.%s" % (self._id_prefix, version, name) id_ = "%s.%s.%s" % (self._id_prefix, version, name)
# aq_base() because this should not go up to ERP5Site and trigger # aq_base() because this should not go up to ERP5Site and trigger
# side-effects, after all this only check for existence... # side-effects, after all this only check for existence...
try: component = getattr(aq_base(site.portal_components), id_, None)
component_tool = aq_base(site.portal_components)
except AttributeError:
# For old sites, just use FS Documents...
return None
component = getattr(component_tool, id_, None)
if component is None or component.getValidationState() not in ('modified', if component is None or component.getValidationState() not in ('modified',
'validated'): 'validated'):
return None return None
...@@ -161,12 +186,7 @@ class ComponentDynamicPackage(ModuleType): ...@@ -161,12 +186,7 @@ class ComponentDynamicPackage(ModuleType):
# name=REFERENCE # name=REFERENCE
else: else:
try: component_tool = aq_base(site.portal_components)
component_tool = aq_base(site.portal_components)
except AttributeError:
# For old sites, just use FS Documents...
return None
for version in site.getVersionPriorityNameList(): for version in site.getVersionPriorityNameList():
id_ = "%s.%s.%s" % (self._id_prefix, version, name) id_ = "%s.%s.%s" % (self._id_prefix, version, name)
component = getattr(component_tool, id_, None) component = getattr(component_tool, id_, None)
...@@ -219,6 +239,14 @@ class ComponentDynamicPackage(ModuleType): ...@@ -219,6 +239,14 @@ class ComponentDynamicPackage(ModuleType):
module for any reason... module for any reason...
""" """
site = getSite() 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):] name = fullname[len(self._namespace_prefix):]
# if only Version package (erp5.component.XXX.VERSION_version) is # if only Version package (erp5.component.XXX.VERSION_version) is
...@@ -268,6 +296,9 @@ class ComponentDynamicPackage(ModuleType): ...@@ -268,6 +296,9 @@ class ComponentDynamicPackage(ModuleType):
setattr(self, name, module) setattr(self, name, module)
sys.modules[module_fullname_alias] = module sys.modules[module_fullname_alias] = module
MNAME_MAP[module_fullname_alias] = module.__name__ 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 return module
component = getattr(site.portal_components, component_id) component = getattr(site.portal_components, component_id)
...@@ -288,6 +319,8 @@ class ComponentDynamicPackage(ModuleType): ...@@ -288,6 +319,8 @@ class ComponentDynamicPackage(ModuleType):
sys.modules[module_fullname] = module sys.modules[module_fullname] = module
if module_fullname_alias: if module_fullname_alias:
sys.modules[module_fullname_alias] = module 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) # This must be set for imports at least (see PEP 302)
module.__file__ = '<' + relative_url + '>' module.__file__ = '<' + relative_url + '>'
...@@ -308,6 +341,8 @@ class ComponentDynamicPackage(ModuleType): ...@@ -308,6 +341,8 @@ class ComponentDynamicPackage(ModuleType):
del sys.modules[module_fullname] del sys.modules[module_fullname]
if module_fullname_alias: if module_fullname_alias:
del sys.modules[module_fullname_alias] del sys.modules[module_fullname_alias]
if module_fullname_filesystem:
del sys.modules[module_fullname_filesystem]
raise ImportError( raise ImportError(
"%s: cannot load Component %s (%s)" % (fullname, name, error)), \ "%s: cannot load Component %s (%s)" % (fullname, name, error)), \
...@@ -319,6 +354,8 @@ class ComponentDynamicPackage(ModuleType): ...@@ -319,6 +354,8 @@ class ComponentDynamicPackage(ModuleType):
if module_fullname_alias: if module_fullname_alias:
setattr(self, name, module) setattr(self, name, module)
MNAME_MAP[module_fullname_alias] = module_fullname MNAME_MAP[module_fullname_alias] = module_fullname
if module_fullname_filesystem:
MNAME_MAP[module_fullname_filesystem] = module.__name__
import erp5.component import erp5.component
erp5.component.ref_manager.add_module(module) erp5.component.ref_manager.add_module(module)
...@@ -408,10 +445,14 @@ class ComponentDynamicPackage(ModuleType): ...@@ -408,10 +445,14 @@ class ComponentDynamicPackage(ModuleType):
for k in modsec_dict.keys(): for k in modsec_dict.keys():
if k.startswith(self._namespace): if k.startswith(self._namespace):
del modsec_dict[k] del modsec_dict[k]
for k in MNAME_MAP.keys(): for k, v in MNAME_MAP.items():
if k.startswith(self._namespace): if v.startswith(self._namespace):
del MNAME_MAP[k] 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(): for name, module in package.__dict__.items():
if name[0] == '_' or not isinstance(module, ModuleType): if name[0] == '_' or not isinstance(module, ModuleType):
continue continue
......
...@@ -120,6 +120,9 @@ class ComponentPackageType(PackageType): ...@@ -120,6 +120,9 @@ class ComponentPackageType(PackageType):
the top level, otherwise a module being relied upon may have a the top level, otherwise a module being relied upon may have a
different API after reset, thus it may fail... 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): def __init__(self, *args, **kwargs):
super(ComponentPackageType, self).__init__(*args, **kwargs) super(ComponentPackageType, self).__init__(*args, **kwargs)
self.ref_manager = RefManager() self.ref_manager = RefManager()
......
...@@ -178,7 +178,11 @@ class ComponentMixin(PropertyRecordableMixin, Base): ...@@ -178,7 +178,11 @@ class ComponentMixin(PropertyRecordableMixin, Base):
'description': BaseAccessor.Getter('getDescription', 'description': BaseAccessor.Getter('getDescription',
'description', 'description',
'string', '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'" _message_invalid_id = "ID is invalid, should be '${id_prefix}.VERSION.REFERENCE'"
......
...@@ -2655,25 +2655,11 @@ def foobar(self, a, b="portal_type"): ...@@ -2655,25 +2655,11 @@ def foobar(self, a, b="portal_type"):
from Products.ERP5Type.Core.DocumentComponent import DocumentComponent from Products.ERP5Type.Core.DocumentComponent import DocumentComponent
class TestZodbDocumentComponent(_TestZodbComponent): class _TestZodbDocumentComponentMixin(_TestZodbComponent):
""" """
Tests specific to ZODB Document Component. This is only for Document Common to all Component class inheriting from Document Component (so
previously defined in bt5 and installed on the filesystem in Interface and Mixin)
$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 testAtLeastOneClassNamedAfterReference(self): def testAtLeastOneClassNamedAfterReference(self):
component = self._newComponent( component = self._newComponent(
self._generateReference('TestClassNamedAfterReference')) self._generateReference('TestClassNamedAfterReference'))
...@@ -2921,6 +2907,124 @@ class TestGC(XMLObject): ...@@ -2921,6 +2907,124 @@ class TestGC(XMLObject):
'gc: collectable <Implements 0x%x>\n' % Implements_id], 'gc: collectable <Implements 0x%x>\n' % Implements_id],
sorted(found_line_list)) 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 from Products.ERP5Type.Core.TestComponent import TestComponent
class TestZodbTestComponent(_TestZodbComponent): class TestZodbTestComponent(_TestZodbComponent):
...@@ -3091,7 +3195,7 @@ class Test(ERP5TypeTestCase): ...@@ -3091,7 +3195,7 @@ class Test(ERP5TypeTestCase):
self.commit() self.commit()
from Products.ERP5Type.Core.InterfaceComponent import InterfaceComponent from Products.ERP5Type.Core.InterfaceComponent import InterfaceComponent
class TestZodbInterfaceComponent(TestZodbDocumentComponent): class TestZodbInterfaceComponent(_TestZodbDocumentComponentMixin):
""" """
Tests specific to ZODB Interface Component. Tests specific to ZODB Interface Component.
""" """
......
...@@ -33,6 +33,7 @@ of Portal Type as Classes and ZODB Components ...@@ -33,6 +33,7 @@ of Portal Type as Classes and ZODB Components
import unittest import unittest
from Products.ERP5Type.tests.ERP5TypeTestCase import ERP5TypeTestCase from Products.ERP5Type.tests.ERP5TypeTestCase import ERP5TypeTestCase
from AccessControl.ZopeGuards import guarded_import
from Products.ERP5Type.tests.utils import LogInterceptor from Products.ERP5Type.tests.utils import LogInterceptor
class TestERP5Type(ERP5TypeTestCase, LogInterceptor): class TestERP5Type(ERP5TypeTestCase, LogInterceptor):
...@@ -205,6 +206,41 @@ class TestERP5Type(ERP5TypeTestCase, LogInterceptor): ...@@ -205,6 +206,41 @@ class TestERP5Type(ERP5TypeTestCase, LogInterceptor):
subdocument_record = sql_catalog.getRecordForUid(subdocument.uid) subdocument_record = sql_catalog.getRecordForUid(subdocument.uid)
self.assertEqual(subdocument.getPath(), subdocument_record.path) 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(): def test_suite():
suite = unittest.TestSuite() suite = unittest.TestSuite()
suite.addTest(unittest.makeSuite(TestERP5Type)) suite.addTest(unittest.makeSuite(TestERP5Type))
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment