From 7a0542c265ffd35ec84577ef20b37a0ebd07bdf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9rome=20Perrin?= <jerome@nexedi.com> Date: Fri, 7 May 2010 12:36:04 +0000 Subject: [PATCH] When exporting a type information, export it in the context in its type provider. When installing a type information, register its provider if it's not already registered. git-svn-id: https://svn.erp5.org/repos/public/erp5/trunk@35110 20353a03-c40f-0410-a6d1-a30d3c3de9de --- product/ERP5/Document/BusinessTemplate.py | 82 +++++++++----- product/ERP5/tests/testBusinessTemplate.py | 125 +++++++++++++++++++++ 2 files changed, 179 insertions(+), 28 deletions(-) diff --git a/product/ERP5/Document/BusinessTemplate.py b/product/ERP5/Document/BusinessTemplate.py index d37877ac1a..6ca83cdfdd 100644 --- a/product/ERP5/Document/BusinessTemplate.py +++ b/product/ERP5/Document/BusinessTemplate.py @@ -31,7 +31,7 @@ import fnmatch, gc, imp, os, re, shutil, sys from Shared.DC.ZRDB.Connection import Connection as RDBConnection from Products.ERP5Type.DiffUtils import DiffFile from Products.ERP5Type.Globals import Persistent, PersistentMapping -from Acquisition import Implicit, aq_base +from Acquisition import Implicit, aq_base, aq_inner, aq_parent from AccessControl import ClassSecurityInfo from Products.CMFCore.utils import getToolByName from Products.ERP5Type.Accessor.Constant import PropertyGetter as ConstantGetter @@ -854,11 +854,12 @@ class ObjectTemplateItem(BaseTemplateItem): """ pass - def onNewObject(self): + def onNewObject(self, obj): """ Installation hook. Called when installation process determined that object to install is new on current site (it's not replacing an existing object). + `obj` parameter is the newly created object in its acquisition context. Can be overridden by subclasses. """ pass @@ -947,9 +948,10 @@ class ObjectTemplateItem(BaseTemplateItem): saved_uid_dict = {} subobjects_dict = {} portal_type_dict = {} - # Object already exists old_obj = container._getOb(object_id, None) + object_existed = old_obj is not None if old_obj is not None: + # Object already exists recurse(saveHook, old_obj) if getattr(aq_base(old_obj), 'groups', None) is not None: # we must keep original order groups @@ -969,8 +971,7 @@ class ObjectTemplateItem(BaseTemplateItem): portal_type_dict['workflow_chain'] = \ getChainByType(context)[1].get('chain_' + object_id, '') container.manage_delObjects([object_id]) - else: - self.onNewObject() + # install object obj = self._objects[path] if getattr(obj, 'meta_type', None) == 'Script (Python)': @@ -997,6 +998,11 @@ class ObjectTemplateItem(BaseTemplateItem): LOG("BT, install", 0, object_id) raise obj = container._getOb(object_id) + + if not object_existed: + # A new object was added, call the hook + self.onNewObject(obj) + # mark a business template installation so in 'PortalType_afterClone' scripts # we can implement logical for reseting or not attributes (i.e reference). self.REQUEST.set('is_business_template_installation', 1) @@ -1414,7 +1420,7 @@ class CategoryTemplateItem(ObjectTemplateItem): def beforeInstall(self): self._installed_new_category = False - def onNewObject(self): + def onNewObject(self, obj): self._installed_new_category = True def afterInstall(self): @@ -1753,6 +1759,10 @@ class PortalTypeTemplateItem(ObjectTemplateItem): p = context.getPortalObject() for relative_url in self._archive.keys(): obj = p.unrestrictedTraverse(relative_url) + # normalize relative_url, not all type informations are stored in + # "portal_types" + relative_url = '%s/%s' % (obj.getPhysicalPath()[-2:]) + obj = obj._getCopy(context) # obj is in ghost state and an attribute must be accessed # so that obj.__dict__ does not return an empty dict @@ -1768,6 +1778,17 @@ class PortalTypeTemplateItem(ObjectTemplateItem): delattr(obj, attr) self._objects[relative_url] = obj obj.wl_clearLocks() + + def onNewObject(self, obj): + """ When we install a type which is contained in a type provider not + registered on types tool, register the type provider. + """ + portal = obj.getPortalObject() + types_tool = portal.portal_types + type_container_id = obj.getParentId() + if type_container_id not in types_tool.type_provider_list: + types_tool.type_provider_list = tuple(types_tool.type_provider_list) + ( + type_container_id,) # XXX : this method is kept temporarily, but can be removed once all bt5 are # re-exported with separated workflow-chain information @@ -1938,7 +1959,7 @@ class PortalTypeWorkflowChainTemplateItem(BaseTemplateItem): (wf_id, portal_type) chain_dict[chain_key] = self._objects[path] else: - if portal_type not in context.portal_types.objectIds(): + if context.portal_types.getTypeInfo(portal_type) is None: raise ValueError('Cannot chain workflow %r to non existing ' 'portal type %r' % (self._chain_string_separator\ .join(self._objects[path]) @@ -2043,16 +2064,15 @@ class PortalTypeAllowedContentTypeTemplateItem(BaseTemplateItem): def build(self, context, **kw): types_tool = getToolByName(self.getPortalObject(), 'portal_types') - types_list = list(types_tool.objectIds()) for key in self._archive.keys(): try: portal_type, allowed_type = key.split(' | ') except ValueError: raise ValueError('Invalid item %r in %s' % (key, self.name)) + ob = types_tool.getTypeInfo(portal_type) # check properties corresponds to what is defined in site - if not portal_type in types_list: + if ob is None: raise ValueError, "Portal Type %s not found in site" %(portal_type,) - ob = types_tool._getOb(portal_type) prop_value = getattr(ob, self.class_property, ()) if not allowed_type in prop_value and not self.is_bt_for_diff: raise ValueError, "%s %s not found in portal type %s" % ( @@ -2151,15 +2171,14 @@ class PortalTypeAllowedContentTypeTemplateItem(BaseTemplateItem): action = update_dict[key] if action == 'nothing': continue - try: - portal_id = key.split('/')[-1] - portal_type = types_tool._getOb(portal_id) - except (AttributeError, KeyError): + portal_id = key.split('/')[-1] + type_information = types_tool.getTypeInfo(portal_id) + if type_information is None: raise AttributeError, "Portal type '%s' not found while " \ "installing %s" % (portal_id, self.getTitle()) property_list = self._objects.get(key, []) old_property_list = old_objects.get(key, ()) - object_property_list = getattr(portal_type, self.class_property, ()) + object_property_list = getattr(type_information, self.class_property, ()) # merge differences between portal types properties # for example: # * current value : [A,B,C] @@ -2169,7 +2188,7 @@ class PortalTypeAllowedContentTypeTemplateItem(BaseTemplateItem): for id in object_property_list: if id not in property_list and id not in old_property_list: property_list.append(id) - setattr(portal_type, self.class_property, tuple(property_list)) + setattr(type_information, self.class_property, tuple(property_list)) def uninstall(self, context, **kw): object_path = kw.get('object_path', None) @@ -2180,19 +2199,19 @@ class PortalTypeAllowedContentTypeTemplateItem(BaseTemplateItem): else: object_key_list = self._objects.keys() for key in object_key_list: - try: - portal_id = key.split('/')[-1] - portal_type = types_tool._getOb(portal_id) - except (AttributeError, KeyError): - LOG("portal types not found : ", 100, portal_id) + portal_id = key.split('/')[-1] + type_information = types_tool.getTypeInfo(portal_id) + if type_information is None: + LOG("BusinessTemplate", WARNING, + "Portal type %r not found while uninstalling" % (portal_id,)) continue property_list = self._objects[key] - original_property_list = list(getattr(portal_type, + original_property_list = list(getattr(type_information, self.class_property, ())) for id in property_list: if id in original_property_list: original_property_list.remove(id) - setattr(portal_type, self.class_property, tuple(original_property_list)) + setattr(type_information, self.class_property, tuple(original_property_list)) class PortalTypeHiddenContentTypeTemplateItem(PortalTypeAllowedContentTypeTemplateItem): @@ -2588,7 +2607,7 @@ class ActionTemplateItem(ObjectTemplateItem): Gets action copy from action provider given the action id or reference """ # Several tools still use CMF actions - if obj.getParentId() == 'portal_types': + if interfaces.ITypeProvider.providedBy(obj.getParentValue()): return self._getPortalTypeActionCopy(obj, value) else: return self._getPortalToolActionCopy(obj, context, value) @@ -2600,6 +2619,8 @@ class ActionTemplateItem(ObjectTemplateItem): url, value = id.split(' | ') url = posixpath.split(url) obj = p.unrestrictedTraverse(url) + # normalize url + url = obj.getPhysicalPath()[-2:] action = self._getActionCopy(obj, context, value) if action is None: if self.is_bt_for_diff: @@ -2625,7 +2646,7 @@ class ActionTemplateItem(ObjectTemplateItem): path, id = id.rsplit('/', 1) container = p.unrestrictedTraverse(path) - if container.getParentId() == 'portal_types': + if interfaces.ITypeProvider.providedBy(aq_parent(aq_inner(container))): # XXX future BT should use 'reference' instead of 'id' reference = getattr(obj, 'reference', None) or obj.id portal_type_dict.setdefault(path, {})[reference] = obj @@ -2742,7 +2763,7 @@ class ActionTemplateItem(ObjectTemplateItem): obj = p.unrestrictedTraverse(relative_url, None) # Several tools still use CMF actions if obj is not None: - is_new_action = obj.getParentId() == 'portal_types' + is_new_action = interfaces.ITypeProvider.providedBy(obj.getParentValue()) key = is_new_action and 'reference' or 'id' else: relative_url, key, value = self._splitPath(id) @@ -2753,8 +2774,8 @@ class ActionTemplateItem(ObjectTemplateItem): if getattr(action_list[index], key, None) == value: obj.deleteActions(selections=(index,)) break - LOG('BusinessTemplate', 100, - 'unable to uninstall action at %s, ignoring' % relative_url ) + LOG('BusinessTemplate', WARNING, + 'Unable to uninstall action at %s, ignoring' % relative_url ) BaseTemplateItem.uninstall(self, context, **kw) class PortalTypeRolesTemplateItem(BaseTemplateItem): @@ -2768,6 +2789,8 @@ class PortalTypeRolesTemplateItem(BaseTemplateItem): for relative_url in self._archive.keys(): obj = p.unrestrictedTraverse("portal_types/%s" % relative_url.split('/', 1)[1]) + # normalize url + relative_url = '%s/%s' % (obj.getPhysicalPath()[-2:]) self._objects[relative_url] = type_role_list = [] for role in obj.getRoleInformationList(): type_role_dict = {} @@ -2880,6 +2903,9 @@ class PortalTypeRolesTemplateItem(BaseTemplateItem): type_roles_list = self._objects[roles_path] or [] for role_property_dict in type_roles_list: obj._importRole(role_property_dict) + else: + raise AttributeError("Path '%r' not found while " + "installing roles" % (path, )) def uninstall(self, context, **kw): p = context.getPortalObject() diff --git a/product/ERP5/tests/testBusinessTemplate.py b/product/ERP5/tests/testBusinessTemplate.py index a4a0914b38..7e69e8229b 100644 --- a/product/ERP5/tests/testBusinessTemplate.py +++ b/product/ERP5/tests/testBusinessTemplate.py @@ -43,6 +43,7 @@ from urllib import pathname2url from Products.ERP5Type.Globals import PersistentMapping from Products.CMFCore.Expression import Expression from Products.ERP5Type.tests.utils import LogInterceptor +from Products.ERP5Type.Tool.TypesTool import TypeProvider from Products.ERP5Type.Workflow import addWorkflowByType from Products.ERP5Type.tests.backportUnittest import expectedFailure import shutil @@ -50,12 +51,18 @@ import os import gc import random import string +import tempfile +import glob from MethodObject import Method from Persistence import Persistent WORKFLOW_TYPE = 'erp5_workflow' +class DummyTypeProvider(TypeProvider): + id = 'dummy_type_provider' + + class TestBusinessTemplate(ERP5TypeTestCase, LogInterceptor): """ Test these operations: @@ -6378,6 +6385,124 @@ class TestBusinessTemplate(ERP5TypeTestCase, LogInterceptor): self.assertFalse(getattr(portal.some_file, 'isClassOverriden', False)) self.assertFalse(getattr(portal.another_file, 'isClassOverriden', False)) + def test_type_provider(self): + self.portal._setObject('dummy_type_provider', DummyTypeProvider()) + type_provider = self.portal.dummy_type_provider + types_tool = self.portal.portal_types + + registered_type_provider_list = types_tool.type_provider_list + # register this type provider + types_tool.type_provider_list = ( + 'dummy_type_provider',) + registered_type_provider_list + + dummy_type = type_provider.newContent( + portal_type='Base Type', + id='Dummy Type', + type_factory_method_id='addFolder', + type_property_sheet_list=('Reference',), + type_base_category_list=('source',), + type_allowed_content_type_list=('Dummy Type',), + type_hidden_content_type_list=('Dummy Type',) ) + + dummy_type.newContent(portal_type='Action Information', + reference='view', + title='View', ) + + dummy_type.newContent(portal_type='Role Information', + title='Dummy Role Definition', + role_name_list=('Assignee', )) + + pw = self.getWorkflowTool() + cbt = pw._chains_by_type.copy() + props = {} + for id, wf_ids in cbt.items(): + props['chain_%s' % id] = ','.join(wf_ids) + props['chain_Dummy Type'] = 'edit_workflow' + pw.manage_changeWorkflows('', props=props) + self.assertEquals(('edit_workflow', ), pw.getChainFor('Dummy Type')) + + bt = self.portal.portal_templates.newContent( + portal_type='Business Template', + title='test_bt', + template_tool_id_list=('dummy_type_provider', ), + template_portal_type_id_list=('Dummy Type',), + template_portal_type_role_list=('Dummy Type', ), + template_portal_type_workflow_chain_list=( + 'Dummy Type | edit_workflow',), + template_portal_type_allowed_content_type_list=( + 'Dummy Type | Dummy Type',), + template_portal_type_hidden_content_type_list=( + 'Dummy Type | Dummy Type',), + template_portal_type_property_sheet_list=( + 'Dummy Type | Reference',), + template_portal_type_base_category_list=( + 'Dummy Type | source',), + template_action_path_list=( + 'Dummy Type | view',),) + self.stepTic() + bt.build() + self.stepTic() + export_dir = tempfile.mkdtemp() + try: + bt.export(path=export_dir, local=True) + self.stepTic() + # portal type template item are exported in their physical location + for template_item in ('PortalTypeTemplateItem', + 'ActionTemplateItem',): + self.assertEquals(['dummy_type_provider'], + [os.path.basename(f) for f in + glob.glob('%s/%s/*' % (export_dir, template_item))]) + new_bt = self.portal.portal_templates.download( + url='file:/%s' % export_dir) + finally: + shutil.rmtree(export_dir) + + # uninstall the workflow chain + pw._chains_by_type = cbt + # unregister type provider + types_tool.type_provider_list = registered_type_provider_list + # uninstall the type provider (this will also uninstall the contained types) + self.portal.manage_delObjects(['dummy_type_provider']) + self.stepTic() + + new_bt.install() + + type_provider = self.portal._getOb('dummy_type_provider', None) + self.assertNotEqual(None, type_provider) + + # This type provider, will be automatically registered on types tool during + # business template installation, because it contains type information + self.assertTrue('dummy_type_provider' in types_tool.type_provider_list) + # The type is reinstalled + self.assertTrue('Dummy Type' in type_provider.objectIds()) + # is available from types tool + self.assertTrue('Dummy Type' in [ti.getId() for + ti in types_tool.listTypeInfo()]) + + dummy_type = types_tool.getTypeInfo('Dummy Type') + self.assertNotEquals(None, dummy_type) + # all the configuration from the type is still here + self.assertEquals(['Reference'], dummy_type.getTypePropertySheetList()) + self.assertEquals(['source'], dummy_type.getTypeBaseCategoryList()) + self.assertEquals(['Dummy Type'], dummy_type.getTypeAllowedContentTypeList()) + self.assertEquals(['Dummy Type'], dummy_type.getTypeHiddenContentTypeList()) + + action_list = dummy_type.contentValues(portal_type='Action Information') + self.assertEquals(['View'], [action.getTitle() for action in action_list]) + self.assertEquals(['view'], [action.getReference() for action in action_list]) + + role_list = dummy_type.contentValues(portal_type='Role Information') + self.assertEquals(['Dummy Role Definition'], + [role.getTitle() for role in role_list]) + + self.assertEquals(('edit_workflow',), pw.getChainFor('Dummy Type')) + + # and our type can be used + instance = self.portal.newContent(portal_type='Dummy Type', + id='test_document') + instance.setSourceReference('OK') + self.assertEquals('OK', instance.getSourceReference()) + def test_suite(): suite = unittest.TestSuite() -- 2.30.9