[Erp5-report] r39371 nicolas.dumazet - in /erp5/trunk/products/ERP5Type: ./ tests/

nobody at svn.erp5.org nobody at svn.erp5.org
Wed Oct 20 08:43:40 CEST 2010


Author: nicolas.dumazet
Date: Wed Oct 20 08:43:40 2010
New Revision: 39371

URL: http://svn.erp5.org?rev=39371&view=rev
Log:
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)


Modified:
    erp5/trunk/products/ERP5Type/Base.py
    erp5/trunk/products/ERP5Type/ERP5Type.py
    erp5/trunk/products/ERP5Type/Utils.py
    erp5/trunk/products/ERP5Type/__init__.py
    erp5/trunk/products/ERP5Type/tests/testMigration.py

Modified: erp5/trunk/products/ERP5Type/Base.py
URL: http://svn.erp5.org/erp5/trunk/products/ERP5Type/Base.py?rev=39371&r1=39370&r2=39371&view=diff
==============================================================================
--- erp5/trunk/products/ERP5Type/Base.py [utf8] (original)
+++ erp5/trunk/products/ERP5Type/Base.py [utf8] Wed Oct 20 08:43:40 2010
@@ -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)
 

Modified: erp5/trunk/products/ERP5Type/ERP5Type.py
URL: http://svn.erp5.org/erp5/trunk/products/ERP5Type/ERP5Type.py?rev=39371&r1=39370&r2=39371&view=diff
==============================================================================
--- erp5/trunk/products/ERP5Type/ERP5Type.py [utf8] (original)
+++ erp5/trunk/products/ERP5Type/ERP5Type.py [utf8] Wed Oct 20 08:43:40 2010
@@ -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)

Modified: erp5/trunk/products/ERP5Type/Utils.py
URL: http://svn.erp5.org/erp5/trunk/products/ERP5Type/Utils.py?rev=39371&r1=39370&r2=39371&view=diff
==============================================================================
--- erp5/trunk/products/ERP5Type/Utils.py [utf8] (original)
+++ erp5/trunk/products/ERP5Type/Utils.py [utf8] Wed Oct 20 08:43:40 2010
@@ -506,72 +506,6 @@ from Products.ERP5Type.Globals import In
 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_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 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, docume
   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
 # 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.

Modified: erp5/trunk/products/ERP5Type/__init__.py
URL: http://svn.erp5.org/erp5/trunk/products/ERP5Type/__init__.py?rev=39371&r1=39370&r2=39371&view=diff
==============================================================================
--- erp5/trunk/products/ERP5Type/__init__.py [utf8] (original)
+++ erp5/trunk/products/ERP5Type/__init__.py [utf8] Wed Oct 20 08:43:40 2010
@@ -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

Modified: erp5/trunk/products/ERP5Type/tests/testMigration.py
URL: http://svn.erp5.org/erp5/trunk/products/ERP5Type/tests/testMigration.py?rev=39371&r1=39370&r2=39371&view=diff
==============================================================================
--- erp5/trunk/products/ERP5Type/tests/testMigration.py [utf8] (original)
+++ erp5/trunk/products/ERP5Type/tests/testMigration.py [utf8] Wed Oct 20 08:43:40 2010
@@ -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(ERP5TypeTestCa
       # 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))




More information about the Erp5-report mailing list