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,
cache_factory='erp5_ui_long'))
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):
""" Method overload - properties are now defined on the ptype """
......@@ -854,7 +858,11 @@ class Base( CopyContainer,
Test purpose
"""
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
initializePortalTypeDynamicProperties(self, klass, ptype, aq_key, \
self.getPortalObject())
......@@ -866,7 +874,11 @@ class Base( CopyContainer,
# and default properties can be associated per portal type
# and per class. Other uses are possible (ex. WebSection).
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
# If this is a portal_type property and everything is already defined
......@@ -898,15 +910,6 @@ class Base( CopyContainer,
Base.aq_method_generating.append(aq_key)
try:
# 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
initializeClassDynamicProperties(self, klass)
......
......@@ -29,7 +29,7 @@ import Products
from Products.CMFCore.TypesTool import FactoryTypeInformation
from Products.CMFCore.Expression import Expression
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.Base import getClassPropertyList
from Products.ERP5Type.UnrestrictedMethod import UnrestrictedMethod
......@@ -304,48 +304,24 @@ class ERP5TypeInformation(XMLObject,
"""
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')
def isConstructionAllowed(self, 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')
def constructInstance(self, container, id, created_by_builder=0,
......@@ -356,10 +332,37 @@ class ERP5TypeInformation(XMLObject,
Call the init_script for the portal_type.
Returns the object.
"""
m = self._queryFactoryMethod(container, temp_object)
if isinstance(m, Exception):
raise m
ob = m(id, **kw)
if not temp_object and not self.isConstructionAllowed(container):
raise AccessControl_Unauthorized('Cannot create %s' % self.getId())
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
# in order to initialize aq_dynamic
......@@ -375,7 +378,7 @@ class ERP5TypeInformation(XMLObject,
# notify workflow after generating local roles, in order to prevent
# 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:
for workflow in workflow_tool.getWorkflowsFor(ob):
workflow.notifyCreated(ob)
......
......@@ -506,72 +506,6 @@ from Products.ERP5Type.Globals import InitializeClass
from Accessor.Base import func_code
from Products.CMFCore.utils import manage_addContentForm, manage_addContent
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$')
......@@ -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):
"""Imports a document class and registers it in ERP5Type Document
repository ( Products.ERP5Type.Document )
......@@ -949,103 +920,72 @@ def importLocalDocument(class_id, document_path = None):
import Products.ERP5Type.Document
import Permissions
if document_path is None:
instance_home = getConfiguration().instancehome
path = os.path.join(instance_home, "Document")
else:
path = document_path
path = os.path.join(path, "%s.py" % class_id)
from Products.ERP5Type import document_class_registry
module_path = 'Products.ERP5Type.Document.' + class_id
document_module = sys.modules.get(module_path)
# Import Document Class and Initialize it
f = open(path)
try:
document_module = imp.load_source(module_path, path, f)
document_class = getattr(document_module, class_id)
document_constructor = DocumentConstructor(document_class)
document_constructor_name = "add%s" % class_id
document_constructor.__name__ = document_constructor_name
except Exception:
f.close()
if document_module is not None:
sys.modules[module_path] = document_module
raise
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:
instance_home = getConfiguration().instancehome
path = os.path.join(instance_home, "Document")
else:
path = document_path
path = os.path.join(path, "%s.py" % class_id)
module_path = "erp5.document"
classpath = "%s.%s" % (module_path, class_id)
try:
module = imp.load_source(classpath, path)
except:
raise AttributeError("document was not registered: %s, %s" % (class_id, document_path))
document_class_registry[class_id] = classpath
else:
f.close()
setattr(Products.ERP5Type.Document, class_id, document_module)
setattr(Products.ERP5Type.Document, document_constructor_name,
document_constructor)
setDefaultClassProperties(document_class)
ModuleSecurityInfo('Products.ERP5Type.Document').declareProtected(
Permissions.AddPortalContent, document_constructor_name,)
InitializeClass(document_class)
# 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)
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)
### newTempFoo
from Products.ERP5Type.ERP5Type import ERP5TypeInformation
klass = getattr(module, class_id)
temp_type = ERP5TypeInformation(klass.portal_type)
temp_document_constructor = temp_type.constructTempInstance
temp_document_constructor_name = "newTemp%s" % class_id
temp_document_constructor.__name__ = temp_document_constructor_name
setattr(Products.ERP5Type.Document,
temp_document_constructor_name,
temp_document_constructor)
ModuleSecurityInfo('Products.ERP5Type.Document').declarePublic(
temp_document_constructor_name,) # XXX Probably bad security
# Update Meta Types
new_meta_types = []
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
# XXX really?
return klass, tuple()
return document_class, constructors
def initializeLocalRegistry(directory_name, import_local_method,
path_arg_name='path'):
......@@ -1132,26 +1072,12 @@ def initializeProduct( context,
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
contentFactoryTypeInformations = []
for content in content_classes:
if hasattr(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:
......@@ -1242,31 +1168,6 @@ def createConstraintList(property_holder, constraint_definition):
# 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):
"""
Return a context used for evaluating a TALES expression.
......
......@@ -98,6 +98,10 @@ def initialize( context ):
portal_tools = portal_tools,
content_constructors = content_constructors,
content_classes = content_classes)
from Dynamic import portaltypeclass
portaltypeclass.initializeDynamicModules()
# Register our Workflow factories directly (if on CMF 2)
Products.ERP5Type.Workflow.registerAllWorkflowFactories(context)
# We should register local constraints at some point
......
......@@ -4,7 +4,6 @@ import unittest
import transaction
from Products.ERP5Type.tests.ERP5TypeTestCase import ERP5TypeTestCase
from Products.ERP5Type.tests.backportUnittest import skip
class TestNewStyleClasses(ERP5TypeTestCase):
......@@ -127,8 +126,6 @@ class TestNewStyleClasses(ERP5TypeTestCase):
# reset the type
person_type.setTypeClass('Person')
TestNewStyleClasses = skip("portal type classes code is not yet committed")(TestNewStyleClasses)
def test_suite():
suite = unittest.TestSuite()
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