Commit 13c22d1a authored by Arnaud Fontaine's avatar Arnaud Fontaine

Accessors generation is now performed in StandardProperty, AcquiredProperty,

CategoryProperty and DynamicCategoryProperty rather than setDefaultProperties
from Utils.

erp5.accessor_holder has also been split up into two additional modules, namely 
erp5.accessor_holder.property_sheet, containing accessor holders for ZODB 
Property Sheets, and erp5.accessor_holder.portal_type, containing accessor 
holders specific to the Portal Types (only being used by PreferenceTool and egov 
for now). erp5.accessor_holder only contains accessor holders common to both 
Portal Types and Property Sheets (such as BaseAccessorHolder).

This commit also enables code committed in r43886.


git-svn-id: https://svn.erp5.org/repos/public/erp5/trunk@43892 20353a03-c40f-0410-a6d1-a30d3c3de9de
parent f38a05c2
......@@ -325,92 +325,21 @@ class PropertyHolder(object):
return [x for x in self.__dict__.items() if x[0] not in
PropertyHolder.RESERVED_PROPERTY_SET]
# Accessor generation
def createAccessor(self, id):
"""
Invokes appropriate factory and create an accessor
"""
fake_accessor = getattr(self, id)
ptype = getattr(self, 'portal_type', None)
if ptype is None:
ptype = self._portal_type
if fake_accessor is PropertyHolder.WORKFLOW_METHOD_MARKER:
# Case 1 : a workflow method only
accessor = Base._doNothing
else:
# Case 2 : a workflow method over an accessor
(accessor_class, accessor_args, key) = fake_accessor
accessor = accessor_class(id, key, *accessor_args)
for wf_id, tr_id, once in self.workflow_method_registry.get(id, ()):
if not isinstance(accessor, WorkflowMethod):
accessor = WorkflowMethod(accessor)
if once:
accessor.registerTransitionOncePerTransaction(ptype, wf_id, tr_id)
else:
accessor.registerTransitionAlways(ptype, wf_id, tr_id)
else:
if once:
accessor.registerTransitionOncePerTransaction(ptype, wf_id, tr_id)
else:
accessor.registerTransitionAlways(ptype, wf_id, tr_id)
setattr(self, id, accessor)
return accessor
def registerAccessor(self, id, key, accessor_class, accessor_args):
"""
Saves in a dictionary all parameters required to create an accessor
The goal here is to minimize memory occupation. We have found the following:
- the size of a tuple with simple types and the size
of None are the same (a pointer)
- the size of a pointer to a class is the same as the
size of None
- the python caching system for tuples is efficient for tuples
which contain simple types (string, int, etc.) but innefficient
for tuples which contain a pointer
- as a result, it is better to create separate dicts if
values contain pointers and single dict if value is
a tuple of simple types
Parameters:
id -- The id the accessor (ex. getFirstName)
def registerWorkflowMethod(self, id, wf_id, tr_id, once_per_transaction=0):
portal_type = self.portal_type
key -- The id of the property (ex. first_name) or the id of the
method for Alias accessors
"""
#LOG('registerAccessor', 0, "%s %s %s" % (id , self._portal_type, accessor_args))
# First we try to compress the information required
# to build a new accessor in such way that
# if the same information is provided twice, we
# shall keep it once only
new_accessor_args = []
for arg in accessor_args:
if type(arg) is types.ListType:
new_accessor_args.append(tuple(arg))
workflow_method = getattr(self, id, None)
if workflow_method is None:
workflow_method = WorkflowMethod(Base._doNothing)
setattr(self, id, workflow_method)
if once_per_transaction:
workflow_method.registerTransitionOncePerTransaction(portal_type,
wf_id,
tr_id)
else:
new_accessor_args.append(arg)
accessor_args = tuple(new_accessor_args)
original_registration_tuple = (accessor_class, accessor_args, key)
registration_tuple = method_registration_cache.get(original_registration_tuple)
if registration_tuple is None:
registration_tuple = original_registration_tuple
method_registration_cache[registration_tuple] = registration_tuple
# Use the cached tuple (same value, different pointer)
setattr(self, id, registration_tuple)
def registerWorkflowMethod(self, id, wf_id, tr_id, once_per_transaction=0):
#LOG('registerWorkflowMethod', 0, "%s %s %s %s %s" % (self._portal_type, id, wf_id, tr_id, once_per_transaction))
signature = (wf_id, tr_id, once_per_transaction)
signature_list = self.workflow_method_registry.get(id, ())
if signature not in signature_list:
self.workflow_method_registry[id] = signature_list + (signature,)
if getattr(self, id, None) is None:
setattr(self, id, PropertyHolder.WORKFLOW_METHOD_MARKER)
self.createAccessor(id)
workflow_method.registerTransitionAlways(portal_type,
wf_id,
tr_id)
def declareProtected(self, permission, accessor_name):
"""
......@@ -520,23 +449,6 @@ def getClassPropertyList(klass):
if p not in ps_list])
return ps_list
def initializeClassDynamicProperties(self, klass):
if klass not in Base.aq_method_generated:
# Recurse to superclasses
for super_klass in klass.__bases__:
if getattr(super_klass, 'isRADContent', 0):
initializeClassDynamicProperties(self, super_klass)
# Initialize default properties
from Utils import setDefaultClassProperties
if not getattr(klass, 'isPortalContent', None):
if getattr(klass, 'isRADContent', 0):
setDefaultClassProperties(klass)
# Mark as generated
Base.aq_method_generated.add(klass)
def initializePortalTypeDynamicProperties(self, klass, ptype, aq_key, portal):
raise ValueError("No reason to go through this no more with portal type classes")
def initializePortalTypeDynamicWorkflowMethods(ptype_klass, portal_workflow):
"""We should now make sure workflow methods are defined
and also make sure simulation state is defined."""
......@@ -567,10 +479,8 @@ def initializePortalTypeDynamicWorkflowMethods(ptype_klass, portal_workflow):
if not hasattr(ptype_klass, method_id):
method = getter(method_id, wf_id)
# Attach to portal_type
setattr(ptype_klass, method_id, method)
ptype_klass.security.declareProtected(
Permissions.AccessContentsInformation,
method_id )
ptype_klass.registerAccessor(method,
Permissions.AccessContentsInformation)
storage = dc_workflow_dict
transitions = wf.transitions
......@@ -806,12 +716,6 @@ class Base( CopyContainer,
self._setDescription(value)
self.reindexObject()
security.declareProtected( Permissions.AccessContentsInformation, 'test_dyn' )
def test_dyn(self):
"""
"""
initializeClassDynamicProperties(self, self.__class__)
security.declarePublic('provides')
def provides(cls, interface_name):
"""
......@@ -848,18 +752,6 @@ class Base( CopyContainer,
pformat(rev1.__dict__),
pformat(rev2.__dict__)))
def initializePortalTypeDynamicProperties(self):
"""
Test purpose
"""
ptype = self.portal_type
klass = self.__class__
aq_key = self._aq_key()
initializePortalTypeDynamicProperties(self, klass, ptype, aq_key, \
self.getPortalObject())
from Products.ERP5Form.PreferenceTool import createPreferenceToolAccessorList
createPreferenceToolAccessorList(self.getPortalObject())
def _aq_dynamic(self, id):
# ahah! disabled, thanks to portal type classes
return None
......
......@@ -48,6 +48,7 @@ class PropertySheet(Folder):
security = ClassSecurityInfo()
security.declareObjectProtected(Permissions.AccessContentsInformation)
# TODO: REMOVE
security.declareProtected(Permissions.AccessContentsInformation,
'exportToFilesystemDefinition')
def exportToFilesystemDefinition(self):
......@@ -86,22 +87,15 @@ class PropertySheet(Folder):
return (properties, categories, constraints)
security.declarePrivate('createAccessorHolder')
def createAccessorHolder(self):
def createAccessorHolder(self, expression_context, portal):
"""
Create a new accessor holder from the Property Sheet (the
accessors are created through a Property Holder)
Create a new accessor holder from the Property Sheet
"""
property_holder = PropertyHolder(self.getId())
accessor_holder = AccessorHolderType(self.getId())
# Prepare the Property Holder
property_holder._properties, \
property_holder._categories, \
property_holder._constraints = self.exportToFilesystemDefinition()
self.applyOnAccessorHolder(accessor_holder, expression_context, portal)
return AccessorHolderType.fromPropertyHolder(
property_holder,
self.getPortalObject(),
'erp5.accessor_holder')
return accessor_holder
@staticmethod
def _guessFilesystemPropertyPortalType(attribute_dict):
......
This diff is collapsed.
......@@ -7,8 +7,8 @@ from Products.ERP5Type.Accessor.Constant import Getter as ConstantGetter
from Products.ERP5Type.Globals import InitializeClass
from Products.ERP5Type.Base import Base as ERP5Base
from Products.ERP5Type.Base import PropertyHolder, initializePortalTypeDynamicWorkflowMethods
from Products.ERP5Type.Utils import createAllCategoryAccessors, \
createExpressionContext, UpperCase, setDefaultProperties
from Products.ERP5Type.Utils import UpperCase
from Products.ERP5Type.Core.CategoryProperty import CategoryProperty
from ExtensionClass import ExtensionClass, pmc_init_of
from zope.interface import classImplements
......@@ -130,11 +130,11 @@ class PortalTypeMetaClass(GhostBaseMetaClass, PropertyHolder):
cls.security = ClassSecurityInfo()
@classmethod
def getSubclassList(metacls, cls):
def getSubclassList(meta_class, cls):
"""
Returns classes deriving from cls
"""
return metacls.subclass_register.get(cls, [])
return meta_class.subclass_register.get(cls, [])
def getAccessorHolderPropertyList(cls):
"""
......@@ -145,10 +145,12 @@ class PortalTypeMetaClass(GhostBaseMetaClass, PropertyHolder):
"""
cls.loadClass()
property_dict = {}
for klass in cls.mro():
if klass.__module__ == 'erp5.accessor_holder':
if klass.__module__.startswith('erp5.accessor_holder'):
for property in klass._properties:
property_dict.setdefault(property['id'], property)
return property_dict.values()
def resetAcquisition(cls):
......@@ -210,36 +212,12 @@ class PortalTypeMetaClass(GhostBaseMetaClass, PropertyHolder):
raise AttributeError
def generatePortalTypeAccessors(cls, site, portal_type_category_list):
createAllCategoryAccessors(site,
cls,
portal_type_category_list,
createExpressionContext(site, site))
# Properties defined on the portal type itself are generated in
# erp5.portal_type directly, but this is unusual case (only
# PDFTypeInformation seems to use it)
portal_type_property_list = getattr(cls, '_properties', None)
if portal_type_property_list:
setDefaultProperties(cls)
# make sure that category accessors from the portal type definition
# are generated, no matter what
# XXX this code is duplicated here, in PropertySheetTool, and in Base
# and anyway is ugly, as tuple-like registration does not help
for id, fake_accessor in cls._getPropertyHolderItemList():
if not isinstance(fake_accessor, tuple):
continue
if fake_accessor is PropertyHolder.WORKFLOW_METHOD_MARKER:
# Case 1 : a workflow method only
accessor = ERP5Base._doNothing
else:
# Case 2 : a workflow method over an accessor
(accessor_class, accessor_args, key) = fake_accessor
accessor = accessor_class(id, key, *accessor_args)
# Add the accessor to the accessor holder
setattr(cls, id, accessor)
category_tool = getattr(site, 'portal_categories', None)
for category_id in portal_type_category_list:
# we need to generate only categories defined on portal type
CategoryProperty.applyDefinitionOnAccessorHolder(cls,
category_id,
category_tool)
portal_workflow = getattr(site, 'portal_workflow', None)
if portal_workflow is None:
......@@ -259,9 +237,8 @@ class PortalTypeMetaClass(GhostBaseMetaClass, PropertyHolder):
for group in ERP5TypeInformation.defined_group_list:
value = cls.__name__ in site._getPortalGroupedTypeSet(group)
accessor_name = 'is' + UpperCase(group) + 'Type'
setattr(cls, accessor_name, ConstantGetter(accessor_name, group, value))
cls.declareProtected(Permissions.AccessContentsInformation,
accessor_name)
method = ConstantGetter(accessor_name, group, value)
cls.registerAccessor(method, Permissions.AccessContentsInformation)
from Products.ERP5Type.Cache import initializePortalCachingProperties
initializePortalCachingProperties(site)
......@@ -274,7 +251,7 @@ class PortalTypeMetaClass(GhostBaseMetaClass, PropertyHolder):
cls.loadClass()
result = PropertyHolder._getPropertyHolderItemList(cls)
for parent in cls.mro():
if parent.__module__ == 'erp5.accessor_holder':
if parent.__module__.startswith('erp5.accessor_holder'):
for x in parent.__dict__.items():
if x[0] not in PropertyHolder.RESERVED_PROPERTY_SET:
result.append(x)
......
......@@ -32,16 +32,14 @@ import os
import inspect
from types import ModuleType
from dynamic_module import registerDynamicModule
from accessor_holder import _generateBaseAccessorHolder, \
_generatePreferenceToolAccessorHolder
from Products.ERP5Type.dynamic.dynamic_module import registerDynamicModule
from Products.ERP5Type.mixin.temporary import TemporaryDocumentMixin
from Products.ERP5Type.Base import Base, resetRegisteredWorkflowMethod
from Products.ERP5Type.Globals import InitializeClass
from Products.ERP5Type.Utils import setDefaultClassProperties
from Products.ERP5Type import document_class_registry, mixin_class_registry
from Products.ERP5Type.Accessor.Constant import PropertyGetter as ConstantGetter
from Products.ERP5Type.dynamic.accessor_holder import AccessorHolderModuleType, \
createAllAccessorHolderList
from zLOG import LOG, ERROR, INFO, WARNING
......@@ -59,71 +57,6 @@ def _importClass(classpath):
except StandardError:
raise ImportError('Could not import document class %s' % classpath)
def _createAccessorHolderList(site,
portal_type_name,
property_sheet_name_set):
"""
Create the accessor holder list with the given ZODB Property Sheets
"""
from erp5 import accessor_holder
getPropertySheet = site.portal_property_sheets._getOb
accessor_holder_list = []
if "Base" in property_sheet_name_set:
# useless if Base Category is not yet here or if we're currently
# generating accessors for Base Categories
accessor_holder_class = _generateBaseAccessorHolder(site, accessor_holder)
if accessor_holder_class is not None:
accessor_holder_list.append(accessor_holder_class)
for property_sheet_name in property_sheet_name_set:
# LOG("ERP5Type.dynamic", INFO,
# "Getting accessor holder for " + property_sheet_name)
try:
# Get the already generated accessor holder
accessor_holder_list.append(getattr(accessor_holder, property_sheet_name))
except AttributeError:
try:
property_sheet = getPropertySheet(property_sheet_name)
except KeyError:
LOG("ERP5Type.dynamic", WARNING,
"Ignoring missing Property Sheet " + property_sheet_name)
continue
# Generate the accessor holder as it has not been done yet
try:
accessor_holder_class = property_sheet.createAccessorHolder()
except Exception:
LOG("ERP5Type.dynamic", ERROR,
"Invalid Property Sheet " + property_sheet_name)
raise
accessor_holder_list.append(accessor_holder_class)
setattr(accessor_holder, property_sheet_name, accessor_holder_class)
# LOG("ERP5Type.dynamic", INFO,
# "Created accessor holder for %s" % property_sheet_name)
# XXX a hook to add per-portal type accessor holders maybe?
if portal_type_name == "Preference Tool":
accessor_holder_class = \
_generatePreferenceToolAccessorHolder(site,
accessor_holder_list,
accessor_holder)
accessor_holder_list.insert(0, accessor_holder_class)
# LOG("ERP5Type.dynamic", INFO,
# "Got accessor holder for %s: %s" % (property_sheet_name, accessor_holder_list))
return accessor_holder_list
# Loading Cache Factory portal type would generate the accessor holder
# for Cache Factory, itself defined with Standard Property thus
# loading the portal type Standard Property, itself defined with
......@@ -177,7 +110,6 @@ def generatePortalTypeClass(site, portal_type_name):
portal_type_category_list = []
attribute_dict = dict(portal_type=portal_type_name,
_properties=[],
_categories=[],
constraints=[])
......@@ -269,73 +201,11 @@ def generatePortalTypeClass(site, portal_type_name):
property_sheet_generating_portal_type_set.add(portal_type_name)
property_sheet_tool = getattr(site, 'portal_property_sheets', None)
property_sheet_name_set = set()
# The Property Sheet Tool may be None if the code is updated but
# the BT has not been upgraded yet with portal_property_sheets
if property_sheet_tool is None:
if not getattr(site, '_v_bootstrapping', False):
LOG("ERP5Type.dynamic", WARNING,
"Property Sheet Tool was not found. Please update erp5_core "
"Business Template")
zodb_property_sheet_name_set = set()
else:
zodb_property_sheet_name_set = set(property_sheet_tool.objectIds())
if portal_type is not None:
# Get the Property Sheets defined on the portal_type and use the
# ZODB Property Sheet rather than the filesystem
for property_sheet in portal_type.getTypePropertySheetList():
if property_sheet in zodb_property_sheet_name_set:
property_sheet_name_set.add(property_sheet)
# PDFTypeInformation document class, for example, defines a
# method which generates dynamically properties and this is
# heavily used by egov
update_definition_dict = getattr(portal_type,
'updatePropertySheetDefinitionDict',
None)
if update_definition_dict is not None and not \
update_definition_dict.__module__.startswith('Products.ERP5Type.ERP5Type'):
try:
update_definition_dict(attribute_dict)
except AttributeError:
pass
# Only kept for backward-compatibility as Preference and System
# Preference have Preference Type as portal type, which define
# getTypePropertySheetList properly and, likewise, Preference Tool
# has Preference Tool Type as its portal type
if portal_type_name in ("Preference Tool",
"Preference",
"System Preference"):
if portal_type is None or \
not portal_type.getPortalType().startswith(portal_type_name):
for property_sheet in zodb_property_sheet_name_set:
if property_sheet.endswith('Preference'):
property_sheet_name_set.add(property_sheet)
# Get the Property Sheets defined on the document and its bases
# recursively
from Products.ERP5Type.Base import getClassPropertyList
for property_sheet in getClassPropertyList(klass):
# If the Property Sheet is a string, then this is a ZODB
# Property Sheet
#
# NOTE: The Property Sheets of a document should be given as a
# string from now on
if not isinstance(property_sheet, basestring):
property_sheet = property_sheet.__name__
if property_sheet in zodb_property_sheet_name_set:
property_sheet_name_set.add(property_sheet)
if property_sheet_name_set:
# Initialize ZODB Property Sheets accessor holders
accessor_holder_list = _createAccessorHolderList(site,
accessor_holder_list = createAllAccessorHolderList(site,
portal_type_name,
property_sheet_name_set)
portal_type,
klass)
base_category_set = set(attribute_dict['_categories'])
for accessor_holder in accessor_holder_list:
......@@ -388,15 +258,29 @@ def initializeDynamicModules():
for example classes created through ClassTool that are in
$INSTANCE_HOME/Document
erp5.accessor_holder
holds accessors of ZODB Property Sheets
holds accessor holders common to ZODB Property Sheets and Portal Types
erp5.accessor_holder.property_sheet
holds accessor holders of ZODB Property Sheets
erp5.accessor_holder.portal_type
holds accessors holders of Portal Types
"""
erp5 = ModuleType("erp5")
sys.modules["erp5"] = erp5
erp5.document = ModuleType("erp5.document")
sys.modules["erp5.document"] = erp5.document
erp5.accessor_holder = ModuleType("erp5.accessor_holder")
erp5.accessor_holder = AccessorHolderModuleType("erp5.accessor_holder")
sys.modules["erp5.accessor_holder"] = erp5.accessor_holder
erp5.accessor_holder.property_sheet = \
AccessorHolderModuleType("erp5.accessor_holder.property_sheet")
sys.modules["erp5.accessor_holder.property_sheet"] = \
erp5.accessor_holder.property_sheet
erp5.accessor_holder.portal_type = registerDynamicModule(
'erp5.accessor_holder.portal_type',
AccessorHolderModuleType)
portal_type_container = registerDynamicModule('erp5.portal_type',
generateLazyPortalTypeClass)
......@@ -502,10 +386,14 @@ def synchronizeDynamicModules(context, force=False):
inspect.isclass):
klass.restoreGhostState()
# Clear accessor holders of ZODB Property Sheets
for property_sheet_id in erp5.accessor_holder.__dict__.keys():
if not property_sheet_id.startswith('__'):
delattr(erp5.accessor_holder, property_sheet_id)
# Clear accessor holders of ZODB Property Sheets and Portal Types
erp5.accessor_holder.clear()
erp5.accessor_holder.property_sheet.clear()
for name in erp5.accessor_holder.portal_type.__dict__.keys():
if name[0] != '_':
delattr(erp5.accessor_holder.portal_type, name)
finally:
Base.aq_method_lock.release()
......
......@@ -595,14 +595,16 @@ class TestZodbPropertySheet(ERP5TypeTestCase):
# The accessor holder will be generated once the new Person will
# be created as Person type has test Property Sheet
self.failIfHasAttribute(erp5.accessor_holder, 'TestMigration')
self.failIfHasAttribute(erp5.accessor_holder.property_sheet,
'TestMigration')
new_person = portal.person_module.newContent(
id='testAssignZodbPropertySheet', portal_type='Person')
self.assertHasAttribute(erp5.accessor_holder, 'TestMigration')
self.assertHasAttribute(erp5.accessor_holder.property_sheet,
'TestMigration')
self.assertTrue(erp5.accessor_holder.TestMigration in \
self.assertTrue(erp5.accessor_holder.property_sheet.TestMigration in \
erp5.portal_type.Person.mro())
# Check that the accessors have been properly created for all
......@@ -677,7 +679,7 @@ class TestZodbPropertySheet(ERP5TypeTestCase):
new_person = portal.person_module.newContent(
id='testAssignZodbPropertySheet', portal_type='Person')
self.failIfHasAttribute(erp5.accessor_holder, 'TestMigration')
self.failIfHasAttribute(erp5.accessor_holder.property_sheet, 'TestMigration')
self.failIfHasAttribute(new_person, 'getTestStandardPropertyAssign')
finally:
......@@ -687,15 +689,18 @@ class TestZodbPropertySheet(ERP5TypeTestCase):
def _checkAddPropertyToZodbPropertySheet(self,
new_property_function,
added_accessor_name):
import erp5.accessor_holder
import erp5.accessor_holder.property_sheet
self.failIfHasAttribute(erp5.accessor_holder, 'TestMigration')
self.failIfHasAttribute(erp5.accessor_holder.property_sheet,
'TestMigration')
new_property_function('add')
self._forceTestAccessorHolderGeneration()
self.assertHasAttribute(erp5.accessor_holder, 'TestMigration')
self.assertHasAttribute(erp5.accessor_holder.TestMigration,
self.assertHasAttribute(erp5.accessor_holder.property_sheet,
'TestMigration')
self.assertHasAttribute(erp5.accessor_holder.property_sheet.TestMigration,
added_accessor_name)
def testAddStandardPropertyToZodbPropertySheet(self):
......@@ -738,15 +743,18 @@ class TestZodbPropertySheet(ERP5TypeTestCase):
change_setter_func,
new_value,
changed_accessor_name):
import erp5.accessor_holder
import erp5.accessor_holder.property_sheet
self.failIfHasAttribute(erp5.accessor_holder, 'TestMigration')
self.failIfHasAttribute(erp5.accessor_holder.property_sheet,
'TestMigration')
change_setter_func(new_value)
self._forceTestAccessorHolderGeneration()
self.assertHasAttribute(erp5.accessor_holder, 'TestMigration')
self.assertHasAttribute(erp5.accessor_holder.TestMigration,
self.assertHasAttribute(erp5.accessor_holder.property_sheet,
'TestMigration')
self.assertHasAttribute(erp5.accessor_holder.property_sheet.TestMigration,
changed_accessor_name)
def testChangeStandardPropertyOfZodbPropertySheet(self):
......@@ -798,7 +806,7 @@ class TestZodbPropertySheet(ERP5TypeTestCase):
Delete the given property from the test Property Sheet and check
whether its corresponding accessor is not there anymore
"""
import erp5.accessor_holder
import erp5.accessor_holder.property_sheet
self.failIfHasAttribute(erp5.accessor_holder, 'TestMigration')
......@@ -807,8 +815,8 @@ class TestZodbPropertySheet(ERP5TypeTestCase):
self.test_property_sheet.deleteContent(property_id)
self._forceTestAccessorHolderGeneration()
self.assertHasAttribute(erp5.accessor_holder, 'TestMigration')
self.failIfHasAttribute(erp5.accessor_holder.TestMigration,
self.assertHasAttribute(erp5.accessor_holder.property_sheet, 'TestMigration')
self.failIfHasAttribute(erp5.accessor_holder.property_sheet.TestMigration,
accessor_name)
def testDeleteStandardPropertyFromZodbPropertySheet(self):
......
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