Commit a94f18cd authored by Nicolas Dumazet's avatar Nicolas Dumazet

Portal type classes.

- All ERP5 objects now become instances of erp5.portal_type.**
  Being an instance of a portal type does no longer only mean
  "having a portal_type attribute", it now also means deriving from
  a specific, ad-hoc Python class for this portal type.

- erp5.portal_type module is built dynamically and its objects
  are classes subclassing the physical Document classes on disk.

- ERP5Type.Document fate:
  + classes previously stored here are gone
  + newTempXXX methods stay, and will work correctly. But a call
    to such a method will require an BaseType object in
    portal_types module.
  + other stuff is gone

- Temporary documents will be instances of erp5.temp_portal_type.*
  All classes in this submodule subclass the respective
  erp5.portal_type.* persistent class

- Documents that were created dynamically without a product path
  (for instance, those created with ClassTool) are now stored
  in a specific module, erp5.document.*


Migration after this revision should be handled automatically,
but updating beyond this point should nonetheless not be done
carelessly.

Expected changes in XML for business templates:
 - Classpath of documents:
      ERP5Type.Document.XXX -> erp5.portal_type.XXX
 - new "type_class" attribute on Portal Type Objects (BaseType Documents)



git-svn-id: https://svn.erp5.org/repos/public/erp5/trunk@39371 20353a03-c40f-0410-a6d1-a30d3c3de9de
parent 007078b4
...@@ -830,7 +830,11 @@ class Base( CopyContainer, ...@@ -830,7 +830,11 @@ class Base( CopyContainer,
cache_factory='erp5_ui_long')) cache_factory='erp5_ui_long'))
def _aq_key(self): def _aq_key(self):
return (self.portal_type, self.__class__) klass_list = self.__class__.__mro__
i = 0
while klass_list[i].__module__ in ('erp5.portal_type', 'erp5.temp_portal_type'):
i += 1
return (self.portal_type, klass_list[i])
def _propertyMap(self): def _propertyMap(self):
""" Method overload - properties are now defined on the ptype """ """ Method overload - properties are now defined on the ptype """
...@@ -854,7 +858,11 @@ class Base( CopyContainer, ...@@ -854,7 +858,11 @@ class Base( CopyContainer,
Test purpose Test purpose
""" """
ptype = self.portal_type ptype = self.portal_type
klass = self.__class__ klass_list = self.__class__.__mro__
i = 0
while klass_list[i].__module__ in ('erp5.portal_type', 'erp5.temp_portal_type'):
i += 1
klass = klass_list[i]
aq_key = (ptype, klass) # We do not use _aq_key() here for speed aq_key = (ptype, klass) # We do not use _aq_key() here for speed
initializePortalTypeDynamicProperties(self, klass, ptype, aq_key, \ initializePortalTypeDynamicProperties(self, klass, ptype, aq_key, \
self.getPortalObject()) self.getPortalObject())
...@@ -866,7 +874,11 @@ class Base( CopyContainer, ...@@ -866,7 +874,11 @@ class Base( CopyContainer,
# and default properties can be associated per portal type # and default properties can be associated per portal type
# and per class. Other uses are possible (ex. WebSection). # and per class. Other uses are possible (ex. WebSection).
ptype = self.portal_type ptype = self.portal_type
klass = self.__class__ klass_list = self.__class__.__mro__
i = 0
while klass_list[i].__module__ in ('erp5.portal_type', 'erp5.temp_portal_type'):
i += 1
klass = klass_list[i]
aq_key = (ptype, klass) # We do not use _aq_key() here for speed aq_key = (ptype, klass) # We do not use _aq_key() here for speed
# If this is a portal_type property and everything is already defined # If this is a portal_type property and everything is already defined
...@@ -898,15 +910,6 @@ class Base( CopyContainer, ...@@ -898,15 +910,6 @@ class Base( CopyContainer,
Base.aq_method_generating.append(aq_key) Base.aq_method_generating.append(aq_key)
try: try:
# Proceed with property generation # Proceed with property generation
if self.isTempObject() and len(klass.__bases__) == 1:
# If self is a simple temporary object (e.g. not a composed one),
# generate methods for the base document class rather than for the
# temporary document class.
# Otherwise, instances of the base document class would fail
# in calling such methods, because they are not instances of
# the temporary document class.
klass = klass.__bases__[0]
# Generate class methods # Generate class methods
initializeClassDynamicProperties(self, klass) initializeClassDynamicProperties(self, klass)
......
...@@ -29,7 +29,7 @@ import Products ...@@ -29,7 +29,7 @@ import Products
from Products.CMFCore.TypesTool import FactoryTypeInformation from Products.CMFCore.TypesTool import FactoryTypeInformation
from Products.CMFCore.Expression import Expression from Products.CMFCore.Expression import Expression
from Products.CMFCore.exceptions import AccessControl_Unauthorized from Products.CMFCore.exceptions import AccessControl_Unauthorized
from Products.CMFCore.utils import _checkPermission, getToolByName from Products.CMFCore.utils import getToolByName
from Products.ERP5Type import interfaces, Constraint, Permissions, PropertySheet from Products.ERP5Type import interfaces, Constraint, Permissions, PropertySheet
from Products.ERP5Type.Base import getClassPropertyList from Products.ERP5Type.Base import getClassPropertyList
from Products.ERP5Type.UnrestrictedMethod import UnrestrictedMethod from Products.ERP5Type.UnrestrictedMethod import UnrestrictedMethod
...@@ -304,48 +304,24 @@ class ERP5TypeInformation(XMLObject, ...@@ -304,48 +304,24 @@ class ERP5TypeInformation(XMLObject,
""" """
return default return default
# The following 2 methods should not be used.
_getFactoryMethod = deprecated(FactoryTypeInformation._getFactoryMethod)
_constructInstance = deprecated(FactoryTypeInformation._constructInstance)
def _queryFactoryMethod(self, container, temp_object=0):
product = self.product
factory = self.factory
if not product or not factory:
return ValueError('Product factory for %s was undefined'
% self.getId())
try:
p = container.manage_addProduct[product]
except AttributeError:
pass
else:
if temp_object:
factory = factory[:3] == 'add' and 'newTemp' + factory[3:] or ''
m = getattr(p, factory, None)
if m is None:
return ValueError('Product factory for %s was invalid'
% self.getId())
if temp_object:
return m
permission = self.permission
if permission:
if _checkPermission(permission, container):
return m
else:
try:
# validate() can either raise Unauthorized or return 0 to
# mean unauthorized.
if getSecurityManager().validate(p, p, factory, m):
return m
except zExceptions_Unauthorized, e:
return e
return AccessControl_Unauthorized('Cannot create %s' % self.getId())
security.declarePublic('isConstructionAllowed') security.declarePublic('isConstructionAllowed')
def isConstructionAllowed(self, container): def isConstructionAllowed(self, container):
"""Test if user is allowed to create an instance in the given container """Test if user is allowed to create an instance in the given container
""" """
return not isinstance(self._queryFactoryMethod(container), Exception) permission = self.permission or 'Add portal content'
return getSecurityManager().checkPermission(permission, container)
security.declarePublic('constructTempInstance')
def constructTempInstance(self, container, id, *args, **kw ):
"""
All ERP5Type.Document.newTempXXXX are constructTempInstance methods
"""
# you should not pass temp_object to constructTempInstance
ob = self.constructInstance(container, id, temp_object=1, *args, **kw)
if container.isTempObject():
container._setObject(id, ob.aq_base)
return ob
security.declarePublic('constructInstance') security.declarePublic('constructInstance')
def constructInstance(self, container, id, created_by_builder=0, def constructInstance(self, container, id, created_by_builder=0,
...@@ -356,10 +332,37 @@ class ERP5TypeInformation(XMLObject, ...@@ -356,10 +332,37 @@ class ERP5TypeInformation(XMLObject,
Call the init_script for the portal_type. Call the init_script for the portal_type.
Returns the object. Returns the object.
""" """
m = self._queryFactoryMethod(container, temp_object) if not temp_object and not self.isConstructionAllowed(container):
if isinstance(m, Exception): raise AccessControl_Unauthorized('Cannot create %s' % self.getId())
raise m
ob = m(id, **kw) portal = container.getPortalObject()
klass = portal.portal_types.getPortalTypeClass(
self.getId(),
temp=temp_object)
ob = klass(id)
if temp_object:
ob = ob.__of__(container)
for ignore in ('activate_kw', 'is_indexable', 'reindex_kw'):
kw.pop(ignore, None)
else:
activate_kw = kw.pop('activate_kw', None)
if activate_kw is not None:
ob.__of__(container).setDefaultActivateParameters(**activate_kw)
reindex_kw = kw.pop('reindex_kw', None)
if reindex_kw is not None:
ob.__of__(container).setDefaultReindexParameters(**reindex_kw)
is_indexable = kw.pop('is_indexable', None)
if is_indexable is not None:
ob.isIndexable = is_indexable
container._setObject(id, ob)
ob = container._getOb(id)
# if no activity tool, the object has already an uid
if getattr(aq_base(ob), 'uid', None) is None:
ob.uid = portal.portal_catalog.newUid()
if kw:
ob._edit(force_update=1, **kw)
# Portal type has to be set before setting other attributes # Portal type has to be set before setting other attributes
# in order to initialize aq_dynamic # in order to initialize aq_dynamic
...@@ -375,7 +378,7 @@ class ERP5TypeInformation(XMLObject, ...@@ -375,7 +378,7 @@ class ERP5TypeInformation(XMLObject,
# notify workflow after generating local roles, in order to prevent # notify workflow after generating local roles, in order to prevent
# Unauthorized error on transition's condition # Unauthorized error on transition's condition
workflow_tool = getToolByName(self.getPortalObject(), 'portal_workflow', None) workflow_tool = getToolByName(portal, 'portal_workflow', None)
if workflow_tool is not None: if workflow_tool is not None:
for workflow in workflow_tool.getWorkflowsFor(ob): for workflow in workflow_tool.getWorkflowsFor(ob):
workflow.notifyCreated(ob) workflow.notifyCreated(ob)
......
...@@ -506,72 +506,6 @@ from Products.ERP5Type.Globals import InitializeClass ...@@ -506,72 +506,6 @@ from Products.ERP5Type.Globals import InitializeClass
from Accessor.Base import func_code from Accessor.Base import func_code
from Products.CMFCore.utils import manage_addContentForm, manage_addContent from Products.CMFCore.utils import manage_addContentForm, manage_addContent
from AccessControl.PermissionRole import PermissionRole from AccessControl.PermissionRole import PermissionRole
from MethodObject import Method
class DocumentConstructor(Method):
func_code = func_code()
func_code.co_varnames = ('folder', 'id', 'REQUEST', 'kw')
func_code.co_argcount = 2
func_defaults = (None,)
def __init__(self, klass):
self.klass = klass
def __call__(self, folder, id, REQUEST=None,
activate_kw=None, is_indexable=None, reindex_kw=None, **kw):
o = self.klass(id)
if activate_kw is not None:
o.__of__(folder).setDefaultActivateParameters(**activate_kw)
if reindex_kw is not None:
o.__of__(folder).setDefaultReindexParameters(**reindex_kw)
if is_indexable is not None:
o.isIndexable = is_indexable
folder._setObject(id, o)
o = folder._getOb(id)
# if no activity tool, the object has already an uid
if getattr(aq_base(o), 'uid', None) is None:
o.uid = folder.portal_catalog.newUid()
if kw: o._edit(force_update=1, **kw)
if REQUEST is not None:
REQUEST['RESPONSE'].redirect( 'manage_main' )
return o
class TempDocumentConstructor(DocumentConstructor):
def __init__(self, klass):
# Create a new class to set permissions specific to temporary objects.
class TempDocument(klass):
isTempDocument = PropertyConstantGetter('isTempDocument', value=True)
__roles__ = None
# Replace some attributes.
for name in ('isIndexable', 'reindexObject', 'recursiveReindexObject',
'activate', 'setUid', 'setTitle', 'getTitle', 'getUid'):
setattr(TempDocument, name, getattr(klass, '_temp_%s' % name))
# Make some methods public.
for method_id in ('reindexObject', 'recursiveReindexObject',
'activate', 'setUid', 'setTitle', 'getTitle',
'edit', 'setProperty', 'getUid', 'setCriterion',
'setCriterionPropertyList'):
setattr(TempDocument, '%s__roles__' % method_id, None)
self.klass = TempDocument
def __call__(self, folder, id, REQUEST=None,
activate_kw=None, is_indexable=None, reindex_kw=None, **kw):
o = self.klass(id)
# Use the real container instead of the factory dispatcher.
#
# XXX some code use this constructor directly instead of
# through the factory system.
if getattr(aq_base(folder), 'Destination', None) is not None:
folder = folder.Destination()
o = o.__of__(folder)
if kw:
o._edit(force_update=1, **kw)
return o
python_file_parser = re.compile('^(.*)\.py$') python_file_parser = re.compile('^(.*)\.py$')
...@@ -942,6 +876,43 @@ def setDefaultClassProperties(property_holder): ...@@ -942,6 +876,43 @@ def setDefaultClassProperties(property_holder):
) )
} }
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 migrate
class attribute to 0/False, as all old objects in the system
should inherit from this mixin
"""
migrate = 1
def __setstate__(self, value):
if not PersistentMigrationMixin.migrate:
super(PersistentMigrationMixin, self).__setstate__(value)
return
portal_type = value.get('portal_type')
if portal_type is None:
portal_type = getattr(self.__class__, 'portal_type', None)
if portal_type is None:
LOG('ERP5Type', PROBLEM,
"no portal type was found for %s (class %s)" \
% (self, self.__class__))
super(PersistentMigrationMixin, self).__setstate__(value)
else:
# proceed with migration
import erp5.portal_type
klass = getattr(erp5.portal_type, portal_type)
self.__class__ = klass
self.__setstate__(value)
LOG('ERP5Type', INFO, "Migration for object %s" % self)
from Globals import Persistent, PersistentMapping
def importLocalDocument(class_id, document_path = None): def importLocalDocument(class_id, document_path = None):
"""Imports a document class and registers it in ERP5Type Document """Imports a document class and registers it in ERP5Type Document
repository ( Products.ERP5Type.Document ) repository ( Products.ERP5Type.Document )
...@@ -949,103 +920,72 @@ def importLocalDocument(class_id, document_path = None): ...@@ -949,103 +920,72 @@ def importLocalDocument(class_id, document_path = None):
import Products.ERP5Type.Document import Products.ERP5Type.Document
import Permissions import Permissions
from Products.ERP5Type import document_class_registry
classpath = document_class_registry.get(class_id)
if classpath is None:
# if the document was not registered before, it means that it is
# a local document in INSTANCE_HOME/Document/
# (created by ClassTool?)
if document_path is None: if document_path is None:
instance_home = getConfiguration().instancehome instance_home = getConfiguration().instancehome
path = os.path.join(instance_home, "Document") path = os.path.join(instance_home, "Document")
else: else:
path = document_path path = document_path
path = os.path.join(path, "%s.py" % class_id) path = os.path.join(path, "%s.py" % class_id)
module_path = "erp5.document"
module_path = 'Products.ERP5Type.Document.' + class_id classpath = "%s.%s" % (module_path, class_id)
document_module = sys.modules.get(module_path)
# Import Document Class and Initialize it
f = open(path)
try: try:
document_module = imp.load_source(module_path, path, f) module = imp.load_source(classpath, path)
document_class = getattr(document_module, class_id) except:
document_constructor = DocumentConstructor(document_class) raise AttributeError("document was not registered: %s, %s" % (class_id, document_path))
document_constructor_name = "add%s" % class_id document_class_registry[class_id] = classpath
document_constructor.__name__ = document_constructor_name
except Exception:
f.close()
if document_module is not None:
sys.modules[module_path] = document_module
raise
else: else:
f.close() module_path = classpath.rsplit('.', 1)[0]
module = __import__(module_path, {}, {}, (module_path,))
### 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.dynamicmodule import dynamicmodule
document_module = dynamicmodule(module_name, migrate_me_document_loader)
setattr(Products.ERP5Type.Document, class_id, document_module) setattr(Products.ERP5Type.Document, class_id, document_module)
setattr(Products.ERP5Type.Document, document_constructor_name,
document_constructor) ### newTempFoo
setDefaultClassProperties(document_class) from Products.ERP5Type.ERP5Type import ERP5TypeInformation
ModuleSecurityInfo('Products.ERP5Type.Document').declareProtected( klass = getattr(module, class_id)
Permissions.AddPortalContent, document_constructor_name,) temp_type = ERP5TypeInformation(klass.portal_type)
InitializeClass(document_class) temp_document_constructor = temp_type.constructTempInstance
# Temp documents are created as standard classes with a different constructor
# which patches some methods are the instance level to prevent reindexing
temp_document_constructor = TempDocumentConstructor(document_class)
temp_document_constructor_name = "newTemp%s" % class_id temp_document_constructor_name = "newTemp%s" % class_id
temp_document_constructor.__name__ = temp_document_constructor_name
setattr(Products.ERP5Type.Document, setattr(Products.ERP5Type.Document,
temp_document_constructor_name, temp_document_constructor_name,
temp_document_constructor) temp_document_constructor)
ModuleSecurityInfo('Products.ERP5Type.Document').declarePublic( ModuleSecurityInfo('Products.ERP5Type.Document').declarePublic(
temp_document_constructor_name,) # XXX Probably bad security temp_document_constructor_name,) # XXX Probably bad security
# Update Meta Types # XXX really?
new_meta_types = [] return klass, tuple()
for meta_type in Products.meta_types:
if meta_type['name'] != document_class.meta_type:
new_meta_types.append(meta_type)
else:
# Update new_meta_types
instance_class = None
new_meta_types.append(
{ 'name': document_class.meta_type,
'action': ('manage_addProduct/%s/%s' % (
'ERP5Type', document_constructor_name)),
'product': 'ERP5Type',
'permission': document_class.add_permission,
'visibility': 'Global',
'interfaces': document_class.__implements__,
'instance': instance_class,
'container_filter': None
},)
Products.meta_types = tuple(new_meta_types)
# Update Constructors
m = Products.ERP5Type._m
if hasattr(document_class, 'factory_type_information'):
constructors = ( manage_addContentForm
, manage_addContent
, document_constructor
, temp_document_constructor
, ('factory_type_information',
document_class.factory_type_information) )
else:
constructors = ( manage_addContentForm
, manage_addContent
, document_constructor
, temp_document_constructor )
initial = constructors[0]
m[initial.__name__]=manage_addContentForm
default_permission = ('Manager',)
pr=PermissionRole(document_class.add_permission, default_permission)
m[initial.__name__+'__roles__']=pr
for method in constructors[1:]:
if isinstance(method, tuple):
name, method = method
else:
name=os.path.split(method.__name__)[-1]
if name != 'factory_type_information':
# Add constructor to product dispatcher
m[name]=method
else:
# Append fti to product dispatcher
if not m.has_key(name): m[name] = []
m[name].append(method)
m[name+'__roles__']=pr
return document_class, constructors
def initializeLocalRegistry(directory_name, import_local_method, def initializeLocalRegistry(directory_name, import_local_method,
path_arg_name='path'): path_arg_name='path'):
...@@ -1132,26 +1072,12 @@ def initializeProduct( context, ...@@ -1132,26 +1072,12 @@ def initializeProduct( context,
product_name = module_name.split('.')[-1] product_name = module_name.split('.')[-1]
# Define content constructors for Document content classes (RAD)
initializeDefaultConstructors(content_classes)
extra_content_constructors = []
for content_class in content_classes:
if hasattr(content_class, 'add' + content_class.__name__):
extra_content_constructors += [
getattr(content_class, 'add' + content_class.__name__)]
if hasattr(content_class, 'newTemp' + content_class.__name__):
extra_content_constructors += [
getattr(content_class, 'newTemp' + content_class.__name__)]
# Define FactoryTypeInformations for all content classes # Define FactoryTypeInformations for all content classes
contentFactoryTypeInformations = [] contentFactoryTypeInformations = []
for content in content_classes: for content in content_classes:
if hasattr(content, 'factory_type_information'): if hasattr(content, 'factory_type_information'):
contentFactoryTypeInformations.append(content.factory_type_information) contentFactoryTypeInformations.append(content.factory_type_information)
# Aggregate
content_constructors = list(content_constructors) + list(extra_content_constructors)
# Try to make some standard directories available # Try to make some standard directories available
try: try:
...@@ -1242,31 +1168,6 @@ def createConstraintList(property_holder, constraint_definition): ...@@ -1242,31 +1168,6 @@ def createConstraintList(property_holder, constraint_definition):
# Constructor initialization # Constructor initialization
##################################################### #####################################################
def initializeDefaultConstructors(klasses):
for klass in klasses:
if getattr(klass, 'isRADContent', 0) and hasattr(klass, 'security'):
setDefaultConstructor(klass)
klass.security.declareProtected(Permissions.AddPortalContent,
'add' + klass.__name__)
def setDefaultConstructor(klass):
"""
Create the default content creation method
"""
document_constructor_name = 'add' + klass.__name__
if not hasattr(klass, document_constructor_name):
document_constructor = DocumentConstructor(klass)
setattr(klass, document_constructor_name, document_constructor)
document_constructor.__name__ = document_constructor_name
temp_document_constructor_name = 'newTemp' + klass.__name__
if not hasattr(klass, temp_document_constructor_name):
temp_document_constructor = TempDocumentConstructor(klass)
setattr(klass, temp_document_constructor_name, temp_document_constructor)
temp_document_constructor.__name__ = temp_document_constructor_name
klass.security.declarePublic(temp_document_constructor_name)
def createExpressionContext(object, portal=None): def createExpressionContext(object, portal=None):
""" """
Return a context used for evaluating a TALES expression. Return a context used for evaluating a TALES expression.
......
...@@ -98,6 +98,10 @@ def initialize( context ): ...@@ -98,6 +98,10 @@ def initialize( context ):
portal_tools = portal_tools, portal_tools = portal_tools,
content_constructors = content_constructors, content_constructors = content_constructors,
content_classes = content_classes) content_classes = content_classes)
from Dynamic import portaltypeclass
portaltypeclass.initializeDynamicModules()
# Register our Workflow factories directly (if on CMF 2) # Register our Workflow factories directly (if on CMF 2)
Products.ERP5Type.Workflow.registerAllWorkflowFactories(context) Products.ERP5Type.Workflow.registerAllWorkflowFactories(context)
# We should register local constraints at some point # We should register local constraints at some point
......
...@@ -4,7 +4,6 @@ import unittest ...@@ -4,7 +4,6 @@ import unittest
import transaction import transaction
from Products.ERP5Type.tests.ERP5TypeTestCase import ERP5TypeTestCase from Products.ERP5Type.tests.ERP5TypeTestCase import ERP5TypeTestCase
from Products.ERP5Type.tests.backportUnittest import skip
class TestNewStyleClasses(ERP5TypeTestCase): class TestNewStyleClasses(ERP5TypeTestCase):
...@@ -127,8 +126,6 @@ class TestNewStyleClasses(ERP5TypeTestCase): ...@@ -127,8 +126,6 @@ class TestNewStyleClasses(ERP5TypeTestCase):
# reset the type # reset the type
person_type.setTypeClass('Person') person_type.setTypeClass('Person')
TestNewStyleClasses = skip("portal type classes code is not yet committed")(TestNewStyleClasses)
def test_suite(): def test_suite():
suite = unittest.TestSuite() suite = unittest.TestSuite()
suite.addTest(unittest.makeSuite(TestNewStyleClasses)) suite.addTest(unittest.makeSuite(TestNewStyleClasses))
......
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