[Erp5-report] r44780 jm - in /erp5/trunk/products: ERP5/ ERP5/Document/ ERP5Type/ ERP5Type/...

nobody at svn.erp5.org nobody at svn.erp5.org
Wed Mar 30 11:36:27 CEST 2011


Author: jm
Date: Wed Mar 30 11:36:27 2011
New Revision: 44780

URL: http://svn.erp5.org?rev=44780&view=rev
Log:
Reimplement migration of persistent objects with obsolete classes

This fixes several issues:
- Some classes like XMLObject were outside Products.ERP5Type.Document
  and there were not migrated.
- Persistent migration using _delOb/_setOb does not work with mount points.

A new Base.migrateToPortalTypeClass method is also provided to migrate objects
persistently. Note however that migration of HBTrees requires additional work,
using PickleUpdater method.

Added:
    erp5/trunk/products/ERP5Type/dynamic/persistent_migration.py
Modified:
    erp5/trunk/products/ERP5/Document/BusinessTemplate.py
    erp5/trunk/products/ERP5/ERP5Site.py
    erp5/trunk/products/ERP5Type/Base.py
    erp5/trunk/products/ERP5Type/ERP5Type.py
    erp5/trunk/products/ERP5Type/Tool/BaseTool.py
    erp5/trunk/products/ERP5Type/Tool/TypesTool.py
    erp5/trunk/products/ERP5Type/Utils.py
    erp5/trunk/products/ERP5Type/dynamic/lazy_class.py
    erp5/trunk/products/ERP5Type/dynamic/portal_type_class.py
    erp5/trunk/products/ERP5Type/tests/testDynamicClassGeneration.py

Modified: erp5/trunk/products/ERP5/Document/BusinessTemplate.py
URL: http://svn.erp5.org/erp5/trunk/products/ERP5/Document/BusinessTemplate.py?rev=44780&r1=44779&r2=44780&view=diff
==============================================================================
--- erp5/trunk/products/ERP5/Document/BusinessTemplate.py [utf8] (original)
+++ erp5/trunk/products/ERP5/Document/BusinessTemplate.py [utf8] Wed Mar 30 11:36:27 2011
@@ -58,7 +58,7 @@ from Products.ERP5Type.Utils import read
 from Products.ERP5Type.Utils import readLocalTest, \
                                     writeLocalTest, \
                                     removeLocalTest
-from Products.ERP5Type.Utils import convertToUpperCase, PersistentMigrationMixin
+from Products.ERP5Type.Utils import convertToUpperCase
 from Products.ERP5Type import Permissions, PropertySheet, interfaces
 from Products.ERP5Type.XMLObject import XMLObject
 from Products.ERP5Type.dynamic.portal_type_class import synchronizeDynamicModules
@@ -546,23 +546,6 @@ class BaseTemplateItem(Implicit, Persist
   def importFile(self, bta, **kw):
     bta.importFiles(item=self)
 
-  def migrateToPortalTypeClass(self, obj):
-    klass = obj.__class__
-    if klass.__module__ == 'erp5.portal_type':
-      return obj
-    portal_type = getattr(aq_base(obj), 'portal_type', None)
-    if portal_type is None:
-      portal_type = getattr(klass, 'portal_type', None)
-
-    if portal_type is None:
-      # ugh?
-      return obj
-    import erp5.portal_type
-    newklass = getattr(erp5.portal_type, portal_type)
-    assert klass != newklass
-    obj.__class__ = newklass
-    return obj
-
   def removeProperties(self, obj, export, keep_workflow_history=False):
     """
     Remove unneeded properties for export
@@ -590,8 +573,6 @@ class BaseTemplateItem(Implicit, Persist
         attr_set.update(('last_max_id_dict', 'last_id_dict'))
       elif classname == 'Types Tool' and klass.__module__ == 'erp5.portal_type':
         attr_set.add('type_provider_list')
-    else:
-      obj = self.migrateToPortalTypeClass(obj)
 
     for attr in obj.__dict__.keys():
       if attr in attr_set or attr.startswith('_cache_cookie_'):
@@ -840,12 +821,6 @@ class ObjectTemplateItem(BaseTemplateIte
         else: # new object
           modified_object_list[path] = 'New', type_name
 
-        # if that's an old style class, use a portal type class instead
-        migrateme = getattr(obj, '_migrateToPortalTypeClass', None)
-        if migrateme is not None:
-          migrateme()
-        self._objects[path] = obj
-
       # update _p_jar property of objects cleaned by removeProperties
       transaction.savepoint(optimistic=True)
       for path, old_object in upgrade_list:
@@ -1054,14 +1029,6 @@ class ObjectTemplateItem(BaseTemplateIte
 
           # install object
           obj = self._objects[path]
-          if isinstance(self, PortalTypeTemplateItem):
-            # if that's an old style class, use a portal type class instead
-            # XXX PortalTypeTemplateItem-specific
-            migrateme = getattr(obj, '_migrateToPortalTypeClass', None)
-            if migrateme is not None:
-              migrateme()
-              self._objects[path] = obj
-
           # XXX Following code make Python Scripts compile twice, because
           #     _getCopy returns a copy without the result of the compilation.
           #     A solution could be to add a specific _getCopy method to
@@ -1924,9 +1891,8 @@ class PortalTypeTemplateItem(ObjectTempl
       if score is None:
         obj = self._objects[path]
         klass = obj.__class__
-        if klass.__module__.startswith('Products.ERP5Type.Document.'):
+        if klass.__module__ != 'erp5.portal_type':
           portal_type = obj.portal_type
-          obj._p_deactivate()
         else:
           portal_type = klass.__name__
         depend = path_dict.get(portal_type)
@@ -1938,11 +1904,7 @@ class PortalTypeTemplateItem(ObjectTempl
           return 0, path
         cache[path] = score = depend and 1 + solveDependency(depend)[0] or 0
       return score, path
-    PersistentMigrationMixin._no_migration += 1
-    try:
-      object_key_list.sort(key=solveDependency)
-    finally:
-      PersistentMigrationMixin._no_migration -= 1
+    object_key_list.sort(key=solveDependency)
     return object_key_list
 
   # XXX : this method is kept temporarily, but can be removed once all bt5 are
@@ -2858,12 +2820,6 @@ class ActionTemplateItem(ObjectTemplateI
         for name, obj in action_dict.iteritems():
           imported_action = container._importOldAction(obj).aq_base
 
-          # if that's an old style class, use a portal type class instead
-          # XXX PortalTypeTemplateItem-specific
-          migrateme = getattr(imported_action, '_migrateToPortalTypeClass', None)
-          if migrateme is not None:
-            migrateme()
-
     else:
       BaseTemplateItem.install(self, context, trashbin, **kw)
       p = context.getPortalObject()

Modified: erp5/trunk/products/ERP5/ERP5Site.py
URL: http://svn.erp5.org/erp5/trunk/products/ERP5/ERP5Site.py?rev=44780&r1=44779&r2=44780&view=diff
==============================================================================
--- erp5/trunk/products/ERP5/ERP5Site.py [utf8] (original)
+++ erp5/trunk/products/ERP5/ERP5Site.py [utf8] Wed Mar 30 11:36:27 2011
@@ -34,7 +34,6 @@ from Products.ERP5Type.Accessor.Constant
 from Products.ERP5Type.Cache import caching_instance_method
 from Products.ERP5Type.Cache import CachingMethod, CacheCookieMixin
 from Products.ERP5Type.ERP5Type import ERP5TypeInformation
-from Products.ERP5.Document.BusinessTemplate import BusinessTemplate
 from Products.ERP5Type.Log import log as unrestrictedLog
 from Products.CMFActivity.Errors import ActivityPendingError
 import ERP5Defaults
@@ -1433,44 +1432,24 @@ class ERP5Site(FolderMixIn, CMFSite, Cac
     if self.getERP5SiteGlobalId() in [None, '']:
       self.erp5_site_global_id = global_id
 
-  security.declareProtected(Permissions.ManagePortal, 'migrateToPortalTypeClass')
-  def migrateToPortalTypeClass(self, REQUEST=None):
-    """Compatibility code that allows migrating a site to portal type classes.
-    
-    We consider that a Site is migrated if its Types Tool is migrated
-    (it will always be migrated last)"""
-    types_tool = getattr(self, 'portal_types', None)
-    if types_tool is None:
-      # empty site
-      return
-    if types_tool.__class__.__module__ == 'erp5.portal_type':
-      # nothing to do, already migrated
-      if REQUEST is not None:
-        return REQUEST.RESPONSE.redirect(
-          '%s?portal_status_message=' \
-          'Nothing to do, already migrated.' % \
-          self.absolute_url())
-      return
-
-    # note that the site itself is not migrated (ERP5Site is not a portal type)
-    # only the tools and top level modules are.
-    # Normally, PersistentMigrationMixin should take care of the rest.
-    id_list = self.objectIds()
-
-    # make sure that Types Tool is migrated last
-    id_list.remove('portal_types')
-    id_list.append('portal_types')
-    for id in id_list:
-      method = getattr(self[id], '_migrateToPortalTypeClass', None)
-      if method is None:
-        continue
-      method()
-
-    if REQUEST is not None:
-      return REQUEST.RESPONSE.redirect(
-        '%s?portal_status_message=' \
-        'Successfully migrated tools and types to portal type classes.' % \
-        self.absolute_url())
+  security.declareProtected(Permissions.ManagePortal,
+                            'migrateToPortalTypeClass')
+  def migrateToPortalTypeClass(self):
+    from Products.ERP5Type.dynamic.persistent_migration import PickleUpdater
+    from Products.ERP5Type.Tool.BaseTool import BaseTool
+    PickleUpdater(self)
+    for tool in self.objectValues():
+      if isinstance(tool, BaseTool):
+        tool_id = tool.id
+        if tool_id != 'portal_property_sheets':
+          if tool_id in ('portal_categories', ):
+            tool = tool.activate()
+          tool.migrateToPortalTypeClass(tool_id not in (
+            'portal_activities', 'portal_simulation', 'portal_templates',
+            'portal_trash'))
+          if tool_id in ('portal_trash',):
+            for obj in tool.objectValues():
+              obj.migrateToPortalTypeClass()
 
 Globals.InitializeClass(ERP5Site)
 

Modified: erp5/trunk/products/ERP5Type/Base.py
URL: http://svn.erp5.org/erp5/trunk/products/ERP5Type/Base.py?rev=44780&r1=44779&r2=44780&view=diff
==============================================================================
--- erp5/trunk/products/ERP5Type/Base.py [utf8] (original)
+++ erp5/trunk/products/ERP5Type/Base.py [utf8] Wed Mar 30 11:36:27 2011
@@ -3578,25 +3578,6 @@ class Base( CopyContainer,
   def isItem(self):
     return self.portal_type in self.getPortalItemTypeList()
 
-  def _migrateToPortalTypeClass(self):
-    klass = self.__class__
-    portal_type = self.getPortalType()
-    if klass.__module__ not in ('erp5.portal_type', 'erp5.temp_portal_type'):
-      import erp5.portal_type
-      newklass = getattr(erp5.portal_type, portal_type)
-      assert klass != newklass
-      self.__class__ = newklass
-      self._p_changed = True
-      # this might look useless, but it is necessary to explicitely record
-      # the change in the parent container, because the class has changed
-      try:
-        parent = self.getParentValue()
-      except AttributeError:
-        return
-      id = self.getId()
-      parent._delOb(id)
-      parent._setOb(id, self)
-
   security.declareProtected(Permissions.DeletePortalContent,
                             'migratePortalType')
   def migratePortalType(self, portal_type):

Modified: erp5/trunk/products/ERP5Type/ERP5Type.py
URL: http://svn.erp5.org/erp5/trunk/products/ERP5Type/ERP5Type.py?rev=44780&r1=44779&r2=44780&view=diff
==============================================================================
--- erp5/trunk/products/ERP5Type/ERP5Type.py [utf8] (original)
+++ erp5/trunk/products/ERP5Type/ERP5Type.py [utf8] Wed Mar 30 11:36:27 2011
@@ -723,7 +723,8 @@ class ERP5TypeInformation(XMLObject,
 
       This is used to update an existing site or to import a BT.
       """
-      from Products.ERP5Type.Document.ActionInformation import ActionInformation
+      import erp5.portal_type
+      ActionInformation = getattr(erp5.portal_type, 'Action Information')
       old_action = old_action.__getstate__()
       action_type = old_action.pop('category', None)
       action = ActionInformation(self.generateNewId())

Modified: erp5/trunk/products/ERP5Type/Tool/BaseTool.py
URL: http://svn.erp5.org/erp5/trunk/products/ERP5Type/Tool/BaseTool.py?rev=44780&r1=44779&r2=44780&view=diff
==============================================================================
--- erp5/trunk/products/ERP5Type/Tool/BaseTool.py [utf8] (original)
+++ erp5/trunk/products/ERP5Type/Tool/BaseTool.py [utf8] Wed Mar 30 11:36:27 2011
@@ -92,35 +92,23 @@ class BaseTool (UniqueObject, Folder):
                 meta_types.append(meta_type)
         return meta_types
 
-    def _migrateToPortalTypeClass(self):
-      portal_type = self.getPortalType()
+    def _fixPortalTypeBeforeMigration(self, portal_type):
       # Tools are causing problems: they used to have no type_class, or wrong
       # type_class, or sometimes have no type definitions at all.
-      # Check that everything is alright before trying
-      # to migrate the tool:
+      # Fix type definition if possible before any migration.
       from Products.ERP5.ERP5Site import getSite
       types_tool = getSite().portal_types
       type_definition = getattr(types_tool, portal_type, None)
-      if type_definition is None:
-        LOG('BaseTool._migrateToPortalTypeClass', WARNING,
-            "No portal type definition was found for Tool '%s'"
-            " (class %s, portal_type '%s')"
-            % (self.getId(), self.__class__.__name__, portal_type))
-        return
-
-      type_class = type_definition.getTypeClass()
-      if type_class in ('Folder', None):
+      if type_definition is not None and \
+         type_definition.getTypeClass() in ('Folder', None):
         # wrong type_class, fix it manually:
         from Products.ERP5Type import document_class_registry
-        document_class_name = portal_type.replace(' ', '')
-        if document_class_name in document_class_registry:
-          type_definition.type_class = document_class_name
-        else:
-          LOG('BaseTool._migrateToPortalTypeClass', WARNING,
-              'No document class could be found for portal type %s'
+        try:
+          type_definition.type_class = document_class_registry[
+            portal_type.replace(' ', '')]
+        except KeyError:
+          LOG('BaseTool._migratePortalType', WARNING,
+              'No document class could be found for portal type %r'
               % portal_type)
-          return
-
-      return super(BaseTool, self)._migrateToPortalTypeClass()
 
 InitializeClass(BaseTool)

Modified: erp5/trunk/products/ERP5Type/Tool/TypesTool.py
URL: http://svn.erp5.org/erp5/trunk/products/ERP5Type/Tool/TypesTool.py?rev=44780&r1=44779&r2=44780&view=diff
==============================================================================
--- erp5/trunk/products/ERP5Type/Tool/TypesTool.py [utf8] (original)
+++ erp5/trunk/products/ERP5Type/Tool/TypesTool.py [utf8] Wed Mar 30 11:36:27 2011
@@ -128,7 +128,7 @@ class TypesTool(TypeProvider):
       'Standard Property',
       'Acquired Property',
       'Dummy Class Tool',
-      # the following ones are required by '_migrateToPortalTypeClass'
+      # XXX the following ones are required by '_migrateToPortalTypeClass'
       'Types Tool',
       'Property Sheet Tool',
       # the following ones are required to upgrade an existing site
@@ -387,11 +387,6 @@ class TypesTool(TypeProvider):
       trashbin = UnrestrictedMethod(trash_tool.newTrashBin)(self.id)
       trashbin._setOb(old_types_tool.id, old_types_tool)
 
-  def _migrateToPortalTypeClass(self):
-    for type_definition in self.objectValues():
-      type_definition._migrateToPortalTypeClass()
-    return super(TypesTool, self)._migrateToPortalTypeClass()
-
 # Compatibility code to access old "ERP5 Role Information" objects.
 OldRoleInformation = imp.new_module('Products.ERP5Type.RoleInformation')
 sys.modules[OldRoleInformation.__name__] = OldRoleInformation

Modified: erp5/trunk/products/ERP5Type/Utils.py
URL: http://svn.erp5.org/erp5/trunk/products/ERP5Type/Utils.py?rev=44780&r1=44779&r2=44780&view=diff
==============================================================================
--- erp5/trunk/products/ERP5Type/Utils.py [utf8] (original)
+++ erp5/trunk/products/ERP5Type/Utils.py [utf8] Wed Mar 30 11:36:27 2011
@@ -893,44 +893,6 @@ def setDefaultClassProperties(property_h
         )
       }
 
-class PersistentMigrationMixin(object):
-  """
-  All classes issued from ERP5Type.Document.XXX submodules
-  will gain with mixin as a base class.
-
-  It allows us to migrate ERP5Type.Document.XXX.YYY classes to
-  erp5.portal_type.ZZZ namespace
-
-  Note that migration can be disabled by setting the '_no_migration'
-  class attribute to a nonzero value, as all old objects in the system
-  should inherit from this mixin
-  """
-  _no_migration = 0
-
-  def __setstate__(self, value):
-    klass = self.__class__
-    if PersistentMigrationMixin._no_migration \
-        or klass.__module__ in ('erp5.portal_type', 'erp5.temp_portal_type'):
-      super(PersistentMigrationMixin, self).__setstate__(value)
-      return
-
-    portal_type = value.get('portal_type')
-    if portal_type is None:
-      portal_type = getattr(klass, 'portal_type', None)
-    if portal_type is None:
-      LOG('ERP5Type', PROBLEM,
-          "no portal type was found for %s (class %s)" \
-               % (self, klass))
-      super(PersistentMigrationMixin, self).__setstate__(value)
-    else:
-      # proceed with migration
-      import erp5.portal_type
-      newklass = getattr(erp5.portal_type, portal_type)
-      assert self.__class__ != newklass
-      self.__class__ = newklass
-      self.__setstate__(value)
-      LOG('ERP5Type', TRACE, "Migration for object %s" % self)
-
 from Globals import Persistent, PersistentMapping
 
 def importLocalDocument(class_id, path=None, class_path=None):
@@ -968,30 +930,8 @@ def importLocalDocument(class_id, path=N
 
   ### Migration
   module_name = "Products.ERP5Type.Document.%s" % class_id
-
-  # Most of Document modules define a single class
-  # (ERP5Type.Document.Person.Person)
-  # but some (eek) need to act as module to find other documents,
-  # e.g. ERP5Type.Document.BusinessTemplate.SkinTemplateItem
-  #
-  def migrate_me_document_loader(document_name):
-    klass = getattr(module, document_name)
-    if issubclass(klass, (Persistent, PersistentMapping)):
-      setDefaultClassProperties(klass)
-      InitializeClass(klass)
-
-      class MigrateMe(PersistentMigrationMixin, klass):
-        pass
-      MigrateMe.__name__ = document_name
-      MigrateMe.__module__ = module_name
-      return MigrateMe
-    else:
-      return klass
-  from dynamic.dynamic_module import registerDynamicModule
-  document_module = registerDynamicModule(module_name,
-                                          migrate_me_document_loader)
-
-  setattr(Products.ERP5Type.Document, class_id, document_module)
+  sys.modules[module_name] = module
+  setattr(Products.ERP5Type.Document, class_id, module)
 
   ### newTempFoo
   from Products.ERP5Type.ERP5Type import ERP5TypeInformation

Modified: erp5/trunk/products/ERP5Type/dynamic/lazy_class.py
URL: http://svn.erp5.org/erp5/trunk/products/ERP5Type/dynamic/lazy_class.py?rev=44780&r1=44779&r2=44780&view=diff
==============================================================================
--- erp5/trunk/products/ERP5Type/dynamic/lazy_class.py [utf8] (original)
+++ erp5/trunk/products/ERP5Type/dynamic/lazy_class.py [utf8] Wed Mar 30 11:36:27 2011
@@ -18,6 +18,7 @@ from zLOG import LOG, WARNING, BLATHER
 
 from portal_type_class import generatePortalTypeClass
 from accessor_holder import AccessorHolderType
+import persistent_migration
 
 # PersistentBroken can't be reused directly
 # because its « layout differs from 'GhostPortalType' »
@@ -183,6 +184,7 @@ class PortalTypeMetaClass(GhostBaseMetaC
       for attr in cls.__dict__.keys():
         if attr not in ('__module__',
                         '__doc__',
+                        '__setstate__',
                         'workflow_method_registry',
                         '__isghost__',
                         'portal_type'):
@@ -306,6 +308,11 @@ class PortalTypeMetaClass(GhostBaseMetaC
         for key, value in attribute_dict.iteritems():
           setattr(klass, key, value)
 
+        if getattr(klass.__setstate__, 'im_func', None) is \
+           persistent_migration.__setstate__:
+          # optimization to reduce overhead of compatibility code
+          klass.__setstate__ = persistent_migration.Base__setstate__
+
         for interface in interface_list:
           classImplements(klass, interface)
 

Added: erp5/trunk/products/ERP5Type/dynamic/persistent_migration.py
URL: http://svn.erp5.org/erp5/trunk/products/ERP5Type/dynamic/persistent_migration.py?rev=44780&view=auto
==============================================================================
--- erp5/trunk/products/ERP5Type/dynamic/persistent_migration.py (added)
+++ erp5/trunk/products/ERP5Type/dynamic/persistent_migration.py [utf8] Wed Mar 30 11:36:27 2011
@@ -0,0 +1,209 @@
+##############################################################################
+#
+# Copyright (c) 2011 Nexedi SARL and Contributors. All Rights Reserved.
+#                    Julien Muchembled <jm at nexedi.com>
+#
+# WARNING: This program as such is intended to be used by professional
+# programmers who take the whole responsibility of assessing all potential
+# consequences resulting from its eventual inadequacies and bugs
+# End users who are looking for a ready-to-use solution with commercial
+# guarantees and support are strongly adviced to contract a Free Software
+# Service Company
+#
+# This program is Free Software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+##############################################################################
+
+# The class of an object is first fixed non-persistently by __setstate__:
+# - It can't be done before because its portal_type maybe different from
+#   the one specified on the old class.
+# - If done later, some methods may be wrong or missing.
+# By default, objects are not migrated persistently, mainly because the old
+# class may be copied in the pickle of the container, and we can't access it
+# from __setstate__.
+
+import re
+from AccessControl import ClassSecurityInfo
+from Acquisition import aq_base
+from OFS.Folder import Folder as OFS_Folder
+from persistent import Persistent, wref
+from zLOG import LOG, PROBLEM, DEBUG, TRACE
+from ZODB.serialize import ObjectWriter, ObjectReader
+from Products.ERP5Type import Permissions
+from Products.ERP5Type.Base import Base, WorkflowMethod
+
+isOldBTree = re.compile(r'BTrees\._(..)BTree\.(\1)BTree$').match
+
+class Ghost(object):
+
+  def __init__(self, oid):
+    self._p_oid = oid
+
+class LazyPersistent(object):
+
+  def __call__(self, oid):
+    return Ghost(oid)
+
+class LazyBTree(LazyPersistent):
+  """Fake class to prevent loading too many objects while migrating BTrees
+
+  When we don't migrate recursively, we don't want to migrate values of BTrees,
+  and for performance reasons, we don't even want to load them.
+  So the only remaining way to know if a BTree contains BTrees/Buckets or values
+  is to look at how the state is structured.
+  """
+
+  def getOidList(self, state):
+    if state and len(state) > 1:
+      # return oid of first/next bucket
+      return state[1]._p_oid,
+    return ()
+
+class PickleUpdater(ObjectReader, ObjectWriter, object):
+  """Function-like class to update obsolete references in pickle"""
+
+  def __new__(cls, obj, recursive=False):
+    assert cls.get, "Persistent migration of pickle requires ZODB >= 3.5"
+    self = object.__new__(cls)
+    obj = aq_base(obj)
+    connection = obj._p_jar
+    ObjectReader.__init__(self, connection, connection._cache,
+                                connection._db.classFactory)
+    ObjectWriter.__init__(self, obj)
+    migrated_oid_set = set()
+    oid_set = set((obj._p_oid,))
+    while oid_set:
+      oid = oid_set.pop()
+      obj = self.get(oid)
+      obj._p_activate()
+      klass = obj.__class__
+      self.lazy = None
+      if not recursive:
+        _setOb = getattr(klass, '_setOb', None)
+        if _setOb:
+          if isinstance(_setOb, WorkflowMethod):
+            _setOb = _setOb._m
+          if _setOb.im_func is OFS_Folder._setOb.im_func:
+            self.lazy = Ghost
+        elif klass.__module__[:7] == 'BTrees.' and klass.__name__ != 'Length':
+          self.lazy = LazyBTree()
+      self.oid_dict = {}
+      self.oid_set = set()
+      p, serial = self._conn._storage.load(oid, '')
+      unpickler = self._get_unpickler(p)
+      def find_global(*args):
+        self.do_migrate = args != (klass.__module__, klass.__name__) and \
+                          not isOldBTree('%s.%s' % args)
+        unpickler.find_global = self._get_class
+        return self._get_class(*args)
+      unpickler.find_global = find_global
+      unpickler.load() # class
+      state = unpickler.load()
+      if isinstance(self.lazy, LazyPersistent):
+        self.oid_set.update(self.lazy.getOidList(state))
+      migrated_oid_set.add(oid)
+      oid_set |= self.oid_set - migrated_oid_set
+      self.oid_set = None
+      if self.do_migrate:
+        LOG('PickleUpdater', DEBUG, 'migrate %r (%r)' % (obj, klass))
+        self.setGhostState(obj, self.serialize(obj))
+        obj._p_changed = 1
+
+  get = getattr(ObjectReader, 'load_oid', None)
+
+  def getOid(self, obj):
+    if isinstance(obj, (Persistent, type, wref.WeakRef)):
+      return getattr(obj, '_p_oid', None)
+
+  def load_oid(self, oid):
+    if self.oid_set is not None:
+      if self.lazy:
+        return self.lazy(oid)
+      self.oid_set.add(oid)
+    return self.get(oid)
+
+  def load_persistent(self, oid, klass):
+    obj = ObjectReader.load_persistent(self, oid, klass)
+    if self.oid_set is not None:
+      if not self.lazy:
+        self.oid_set.add(oid)
+      obj._p_activate()
+      self.oid_dict[oid] = oid_klass = ObjectWriter.persistent_id(self, obj)
+      if oid_klass != (oid, klass):
+        self.do_migrate = True
+    return obj
+
+  def persistent_id(self, obj):
+    assert type(obj) is not Ghost
+    oid = self.getOid(obj)
+    if type(oid) is str:
+      try:
+        return self.oid_dict[oid]
+      except KeyError:
+        obj._p_activate()
+    return ObjectWriter.persistent_id(self, obj)
+
+if 1:
+  from Products.ERP5Type.Core.Folder import Folder
+  from Products.ERP5.Tool.CategoryTool import CategoryTool
+
+  Base__setstate__ = Base.__setstate__
+
+  def __setstate__(self, value):
+    klass = self.__class__
+
+    if klass.__module__ in ('erp5.portal_type', 'erp5.temp_portal_type'):
+      return Base__setstate__(self, value)
+    try:
+      portal_type = value.get('portal_type') or klass.portal_type
+    except AttributeError:
+      LOG('ERP5Type', PROBLEM,
+          "no portal type was found for %r (class %s)" % (self, klass))
+      return Base__setstate__(self, value)
+    if portal_type == 'Dummy Class Tool':
+      return Base__setstate__(self, value)
+    # proceed with migration
+    self._fixPortalTypeBeforeMigration(portal_type)
+    import erp5.portal_type
+    newklass = getattr(erp5.portal_type, portal_type)
+    assert self.__class__ is not newklass
+    self.__class__ = newklass
+    self.__setstate__(value)
+    LOG('Base.__setstate__', TRACE, "migrate %r" % self)
+
+  def migrateToPortalTypeClass(self, recursive=False):
+    """Migrate persistently all referenced classes
+
+    When 'recursive' is False, subobjects (read objectValues) are not migrated.
+    So a typical migration of a big folder using activities would be:
+
+      folder.migrateToPortalTypeClass()
+      for obj in folder.objectValues():
+        obj.activate().migrateToPortalTypeClass(True)
+
+    Note however this pattern does not work for HBTrees, because sub-btrees are
+    treated like subobjects for PickleUpdater.
+    """
+    PickleUpdater(self, recursive)
+
+  Base.__setstate__ = __setstate__
+  Folder.__setstate__ = CategoryTool.__setstate__ = __setstate__
+  Base._fixPortalTypeBeforeMigration = lambda self, portal_type: None
+  Base.migrateToPortalTypeClass = migrateToPortalTypeClass
+  Base.security.declareProtected(Permissions.ManagePortal,
+                                 'migrateToPortalTypeClass')
+
+else:
+  __setstate__ = None

Modified: erp5/trunk/products/ERP5Type/dynamic/portal_type_class.py
URL: http://svn.erp5.org/erp5/trunk/products/ERP5Type/dynamic/portal_type_class.py?rev=44780&r1=44779&r2=44780&view=diff
==============================================================================
--- erp5/trunk/products/ERP5Type/dynamic/portal_type_class.py [utf8] (original)
+++ erp5/trunk/products/ERP5Type/dynamic/portal_type_class.py [utf8] Wed Mar 30 11:36:27 2011
@@ -183,11 +183,14 @@ def generatePortalTypeClass(site, portal
     raise AttributeError('Document class is not defined on Portal Type %s' \
             % portal_type_name)
 
-  type_class_path = document_class_registry.get(type_class)
-  if type_class_path is None:
-    raise AttributeError('Document class %s has not been registered:' \
-                         ' cannot import it as base of Portal Type %s' \
-                         % (type_class, portal_type_name))
+  if '.' in type_class:
+    type_class_path = type_class
+  else:
+    type_class_path = document_class_registry.get(type_class)
+    if type_class_path is None:
+      raise AttributeError('Document class %s has not been registered:'
+                           ' cannot import it as base of Portal Type %s'
+                           % (type_class, portal_type_name))
 
   klass = _importClass(type_class_path)
 
@@ -338,6 +341,7 @@ def synchronizeDynamicModules(context, f
       bootstrap = None
       from Products.ERP5Type.Tool.PropertySheetTool import PropertySheetTool
       from Products.ERP5Type.Tool.TypesTool import TypesTool
+      import erp5.portal_type
       for tool_class in TypesTool, PropertySheetTool:
         # if the instance has no property sheet tool, or incomplete
         # property sheets, we need to import some data to bootstrap
@@ -345,10 +349,6 @@ def synchronizeDynamicModules(context, f
         tool_id = tool_class.id
         tool = getattr(portal, tool_id, None)
         if tool is None:
-          # Create a "non-migrated" (types) tool, so that
-          # ERP5Site.migrateToPortalTypeClass doesn't think there nothing to do.
-          # On the other hand, we must make sure TypesTool._bootstrap installs
-          # the needed portal types in order to migrate this bootstrap tool.
           tool = tool_class()
           try:
             portal._setObject(tool_id, tool, set_owner=False, suppress_events=True)
@@ -366,18 +366,16 @@ def synchronizeDynamicModules(context, f
         try:
           os.chdir(bootstrap)
           tool._bootstrap()
+          tool.__class__ = getattr(erp5.portal_type, tool.portal_type)
         finally:
           os.chdir(cwd)
 
-      if bootstrap:
-        if not getattr(portal, '_v_bootstrapping', False):
-          LOG('ERP5Site', INFO, 'Transition successful, please update your'
-              ' business templates')
-        # XXX: if some portal types are missing, for instance
-        # if some Tools have no portal types, this is likely to fail with an
-        # error. On the other hand, we can't proceed without this change,
-        # and if we dont import the xml, the instance wont start.
-        portal.migrateToPortalTypeClass()
+      if bootstrap and not getattr(portal, '_v_bootstrapping', False):
+        from Products.ERP5Type.dynamic.persistent_migration import PickleUpdater
+        if PickleUpdater.get:
+          portal.migrateToPortalTypeClass()
+        LOG('ERP5Site', INFO, 'Transition successful, please update your'
+            ' business templates')
 
       _bootstrapped.add(portal.id)
 

Modified: erp5/trunk/products/ERP5Type/tests/testDynamicClassGeneration.py
URL: http://svn.erp5.org/erp5/trunk/products/ERP5Type/tests/testDynamicClassGeneration.py?rev=44780&r1=44779&r2=44780&view=diff
==============================================================================
--- erp5/trunk/products/ERP5Type/tests/testDynamicClassGeneration.py [utf8] (original)
+++ erp5/trunk/products/ERP5Type/tests/testDynamicClassGeneration.py [utf8] Wed Mar 30 11:36:27 2011
@@ -28,9 +28,11 @@
 #
 ##############################################################################
 
+import gc
 import unittest
 import transaction
 
+from persistent import Persistent
 from Products.ERP5Type.dynamic.portal_type_class import synchronizeDynamicModules
 from Products.ERP5Type.tests.ERP5TypeTestCase import ERP5TypeTestCase
 from Products.ERP5Type.tests.backportUnittest import expectedFailure, skip
@@ -42,75 +44,101 @@ class TestPortalTypeClass(ERP5TypeTestCa
   def getBusinessTemplateList(self):
     return 'erp5_base',
 
-  def testImportNonMigratedPerson(self):
+  def testMigrateOldObject(self):
     """
-    Import a .xml containing a Person created with an old
-    Products.ERP5Type.Document.Person.Person type
-    """
-    person_module = self.portal.person_module
-    self.importObjectFromFile(person_module, 'non_migrated_person.xml')
-    transaction.commit()
-
-    non_migrated_person = person_module.non_migrated_person
-    # check that object unpickling instanciated a new style object
-    person_class = self.portal.portal_types.getPortalTypeClass('Person')
-    self.assertEquals(non_migrated_person.__class__, person_class)
-
-  @expectedFailure
-  def testImportNonMigratedDocumentUsingContentClass(self):
-    """
-    Import a .xml containing a Base Type with old Document path
-    Products.ERP5Type.ERP5Type.ERP5TypeInformation
-
-    This Document class is different because it's a content_class,
-    i.e. it was not in Products.ERP5Type.Document.** but was
-    imported directly as such.
-    """
-    self.importObjectFromFile(self.portal, 'Category.xml')
-    transaction.commit()
-
-    non_migrated_type = self.portal.Category
-    # check that object unpickling instanciated a new style object
-    base_type_class = self.portal.portal_types.getPortalTypeClass('Base Type')
-    self.assertEquals(non_migrated_type.__class__, base_type_class)
-
-  def testMigrateOldObjectFromZODB(self):
-    """
-    Load an object with ERP5Type.Document.Person.Person from the ZODB
-    and check that migration works well
+    Check migration of persistent objects with old classes
+    like Products.ERP5(Type).Document.Person.Person
     """
     from Products.ERP5Type.Document.Person import Person
+    person_module = self.portal.person_module
+    connection = person_module._p_jar
+    newId = self.portal.person_module.generateNewId
 
-    # remove temporarily the migration
-    from Products.ERP5Type.Utils import PersistentMigrationMixin
-    PersistentMigrationMixin.migrate = 0
-
-    person_module = self.getPortal().person_module
-    obj_id = "this_object_is_old"
-    old_object = Person(obj_id)
-    person_module._setObject(obj_id, old_object)
-    old_object = person_module._getOb(obj_id)
+    def unload(id):
+      oid = person_module._tree[id]._p_oid
+      person_module._tree._p_deactivate()
+      connection._cache.invalidate(oid)
+      gc.collect()
+      # make sure we manage to remove the object from memory
+      assert connection._cache.get(oid, None) is None
+      return oid
+
+    def check(migrated):
+      klass = old_object.__class__
+      self.assertEqual(klass.__module__,
+        migrated and 'erp5.portal_type' or 'Products.ERP5.Document.Person')
+      self.assertEqual(klass.__name__, 'Person')
+      self.assertEqual(klass.__setstate__ is Persistent.__setstate__, migrated)
 
+    # Import a .xml containing a Person created with an old
+    # Products.ERP5Type.Document.Person.Person type
+    self.importObjectFromFile(person_module, 'non_migrated_person.xml')
     transaction.commit()
-    self.assertEquals(old_object.__class__.__module__, 'Products.ERP5Type.Document.Person')
-    self.assertEquals(old_object.__class__.__name__, 'Person')
-
-    self.assertTrue(hasattr(old_object.__class__, '__setstate__'))
-
-    # unload/deactivate the object
-    old_object._p_invalidate()
+    unload('non_migrated_person')
+    old_object = person_module.non_migrated_person
+    # object unpickling should have instanciated a new style object directly
+    check(1)
 
+    obj_id = newId()
+    person_module._setObject(obj_id, Person(obj_id))
+    transaction.commit()
+    unload(obj_id)
+    old_object = person_module[obj_id]
     # From now on, everything happens as if the object was a old, non-migrated
-    # object with an old Products.ERP5Type.Document.Person.Person
-
-    # now turn on migration
-    PersistentMigrationMixin.migrate = 1
-
+    # object with an old Products.ERP5(Type).Document.Person.Person
+    check(0)
     # reload the object
     old_object._p_activate()
+    check(1)
+    # automatic migration is not persistent
+    old_object = None
+    # (note we get back the object directly from its oid to make sure we test
+    # the class its pickle and not the one in its container)
+    old_object = connection.get(unload(obj_id))
+    check(0)
+
+    try:
+      from ZODB import __version__
+
+    except ImportError: # recent ZODB
+      # Test persistent migration
+      old_object.migrateToPortalTypeClass()
+      old_object = None
+      transaction.commit()
+      old_object = connection.get(unload(obj_id))
+      check(1)
+      # but the container still have the old class
+      old_object = None
+      unload(obj_id)
+      old_object = person_module[obj_id]
+      check(0)
+
+      # Test persistent migration of containers
+      obj_id = newId()
+      person_module._setObject(obj_id, Person(obj_id))
+      transaction.commit()
+      unload(obj_id)
+      person_module.migrateToPortalTypeClass()
+      transaction.commit()
+      unload(obj_id)
+      old_object = person_module[obj_id]
+      check(1)
+      # not recursive by default
+      old_object = None
+      old_object = connection.get(unload(obj_id))
+      check(0)
+
+      # Test recursive migration
+      old_object = None
+      unload(obj_id)
+      person_module.migrateToPortalTypeClass(True)
+      transaction.commit()
+      old_object = connection.get(unload(obj_id))
+      check(1)
 
-    self.assertEquals(old_object.__class__.__module__, 'erp5.portal_type')
-    self.assertEquals(old_object.__class__.__name__, 'Person')
+    else: # Zope 2.8
+      # compatibility code not implemented
+      self.assertRaises(AssertionError, old_object.migrateToPortalTypeClass)
 
   def testChangeMixin(self):
     """



More information about the Erp5-report mailing list