Base.py 135 KB
Newer Older
1
# -*- coding: utf-8 -*-
Jean-Paul Smets committed
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
##############################################################################
#
# Copyright (c) 2002-2003 Nexedi SARL and Contributors. All Rights Reserved.
#                    Jean-Paul Smets-Solanes <jp@nexedi.com>
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsability of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# garantees and support are strongly adviced to contract a Free Software
# Service Company
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#
##############################################################################

30
from struct import unpack
31
from copy import copy
32
import warnings
33
import types
34
import thread, threading
35

36
from Products.ERP5Type.Globals import InitializeClass, DTMLFile
37
from AccessControl import ClassSecurityInfo
38
from AccessControl.Permission import pname, Permission
39
from AccessControl.PermissionRole import rolesForPermissionOn
Aurel committed
40 41
from AccessControl.SecurityManagement import getSecurityManager
from AccessControl.ZopeGuards import guarded_getattr
42
from Acquisition import aq_base, aq_inner, aq_acquire, aq_chain
43
from DateTime import DateTime
44
import OFS.History
45 46
from OFS.SimpleItem import SimpleItem
from OFS.PropertyManager import PropertyManager
47
from persistent.TimeStamp import TimeStamp
48
from zExceptions import NotFound, Unauthorized
49

50 51
from ZopePatch import ERP5PropertyManager

52
from Products.CMFCore.PortalContent import PortalContent
Yoshinori Okuji committed
53
from Products.CMFCore.Expression import Expression
54
from Products.CMFCore.utils import getToolByName, _checkConditionalGET, _setCacheHeaders, _ViewEmulator
55
from Products.CMFCore.WorkflowCore import ObjectDeleted, ObjectMoved
56
from Products.CMFCore.CMFCatalogAware import CMFCatalogAware
57

58
from Products.DCWorkflow.Transitions import TRIGGER_WORKFLOW_METHOD, TRIGGER_USER_ACTION
Jean-Paul Smets committed
59 60 61

from Products.ERP5Type import _dtmldir
from Products.ERP5Type import PropertySheet
62
from Products.ERP5Type import interfaces
Jean-Paul Smets committed
63
from Products.ERP5Type import Permissions
64
from Products.ERP5Type.patches.CMFCoreSkinnable import SKINDATA, skinResolve
Jean-Paul Smets committed
65
from Products.ERP5Type.Utils import UpperCase
66
from Products.ERP5Type.Utils import convertToUpperCase, convertToMixedCase
67
from Products.ERP5Type.Utils import createExpressionContext, simple_decorator
68
from Products.ERP5Type.Accessor.Accessor import Accessor
69
from Products.ERP5Type.Accessor.Constant import PropertyGetter as ConstantGetter
Jean-Paul Smets committed
70
from Products.ERP5Type.Accessor.TypeDefinition import list_types
Yoshinori Okuji committed
71
from Products.ERP5Type.Accessor import Base as BaseAccessor
72
from Products.ERP5Type.mixin.property_translatable import PropertyTranslatableBuiltInDictMixIn
73
from Products.ERP5Type.XMLExportImport import Base_asXML
74
from Products.ERP5Type.Cache import CachingMethod, clearCache, getReadOnlyTransactionCache
75
from Accessor import WorkflowState
76
from Products.ERP5Type.Log import log as unrestrictedLog
77
from Products.ERP5Type.TransactionalVariable import getTransactionalVariable
78
from Products.ERP5Type.Accessor.TypeDefinition import type_definition
Jean-Paul Smets committed
79

80 81
from CopySupport import CopyContainer, CopyError,\
    tryMethodCallWithTemporaryPermission
82
from Errors import DeferredCatalogError, UnsupportedWorkflowMethod
83
from Products.CMFActivity.ActiveObject import ActiveObject
84
from Products.ERP5Type.Accessor.Accessor import Accessor as Method
85
from Products.ERP5Type.Accessor.TypeDefinition import asDate
86
from Products.ERP5Type.Message import Message
87
from Products.ERP5Type.ConsistencyMessage import ConsistencyMessage
88
from Products.ERP5Type.UnrestrictedMethod import UnrestrictedMethod, super_user
Jean-Paul Smets committed
89

90 91
from zope.interface import classImplementsOnly, implementedBy

92
import sys, re
Jean-Paul Smets committed
93 94

from cStringIO import StringIO
95 96
from socket import gethostname, gethostbyaddr
import random
Jean-Paul Smets committed
97

98 99 100
import inspect
from pprint import pformat

101 102
import zope.interface

103
from ZODB.POSException import ConflictError
104
from zLOG import LOG, INFO, ERROR, WARNING
Jean-Paul Smets committed
105

106
_MARKER = []
107

108
global registered_workflow_method_set
109
wildcard_interaction_method_id_match = re.compile(r'[[.?*+{(\\]').search
110 111 112 113 114 115 116 117
workflow_method_registry = [] # XXX A set() would be better but would require a hash in WorkflowMethod class

def resetRegisteredWorkflowMethod(portal_type=None):
  """
    TODO: unwrap workflow methos which were standard methods initially
  """
  for method in workflow_method_registry:
    method.reset(portal_type=portal_type)
118

119 120
  del workflow_method_registry[:]

121 122
class WorkflowMethod(Method):

123 124 125 126 127
  def __init__(self, method, id=None, reindex=1):
    """
      method - a callable object or a method

      id - the workflow transition id. This is useful
Fabien Morin committed
128
           to emulate "old" CMF behaviour but is
129 130 131 132 133 134 135 136
           somehow inconsistent with the new registration based
           approach implemented here.

           We store id as _transition_id and use it
           to register the transition for each portal
           type and each workflow for which it is
           applicable.
    """
Romain Courteaud committed
137 138
    self._m = method
    if id is None:
139 140 141
      self._transition_id = method.__name__
    else:
      self._transition_id = id
142 143
    # Only publishable methods can be published as interactions
    # A pure private method (ex. _doNothing) can not be published
Fabien Morin committed
144
    # This is intentional to prevent methods such as submit, share to
145 146 147
    # be called from a URL. If someone can show that this way
    # is wrong (ex. for remote operation of a site), let us know.
    if not method.__name__.startswith('_'):
148
      self.__name__ = method.__name__
149 150 151 152 153 154
      for func_id in ['func_code', 'func_defaults', 'func_dict', 'func_doc', 'func_globals', 'func_name']:
        setattr(self, func_id, getattr(method, func_id, None))
    self._invoke_once = {}
    self._invoke_always = {} # Store in a dict all workflow IDs which require to
                                # invoke wrapWorkflowMethod at every call
                                # during the same transaction
155

156 157
  def getTransitionId(self):
    return self._transition_id
158

Romain Courteaud committed
159
  def __call__(self, instance, *args, **kw):
160
    """
Romain Courteaud committed
161 162
      Invoke the wrapped method, and deal with the results.
    """
163
    try:
Nicolas Dumazet committed
164
      wf = getattr(instance.getPortalObject(), 'portal_workflow')
165
    except AttributeError:
166 167 168 169
      # XXX instance is unwrapped(no acquisition)
      # XXX I must think that what is a correct behavior.(Yusei)
      return self._m(instance, *args, **kw)

170 171 172
    # Build a list of transitions which may need to be invoked
    instance_path = instance.getPhysicalPath()
    portal_type = instance.portal_type
173
    transactional_variable = getTransactionalVariable()
Nicolas Dumazet committed
174
    invoke_once_dict = self._invoke_once.get(portal_type, {})
175 176
    valid_invoke_once_item_list = []
    # Only keep those transitions which were never invoked
177
    once_transition_dict = {}
Nicolas Dumazet committed
178
    for wf_id, transition_list in invoke_once_dict.iteritems():
179 180 181 182
      valid_transition_list = []
      for transition_id in transition_list:
        once_transition_key = ('Products.ERP5Type.Base.WorkflowMethod.__call__',
                                wf_id, transition_id, instance_path)
183 184
        once_transition_dict[(wf_id, transition_id)] = once_transition_key
        if once_transition_key not in transactional_variable:
185 186 187 188
          valid_transition_list.append(transition_id)
      if valid_transition_list:
        valid_invoke_once_item_list.append((wf_id, valid_transition_list))
    candidate_transition_item_list = valid_invoke_once_item_list + \
Jean-Paul Smets committed
189 190
                           self._invoke_always.get(portal_type, {}).items()

191
    #LOG('candidate_transition_item_list %s' % self.__name__, 0, str(candidate_transition_item_list))
192 193 194

    # Try to return immediately if there are no transition to invoke
    if not candidate_transition_item_list:
195
      return self.__dict__['_m'](instance, *args, **kw)
196

197 198 199 200 201
    # Prepare a list of transitions which should be invoked.
    # This list is based on the results of isWorkflowMethodSupported.
    # An interaction is ignored if the guard prevents execution.
    # Otherwise, an exception is raised if the workflow transition does not
    # exist from the current state, or if the guard rejects it.
202 203 204
    valid_transition_item_list = []
    for wf_id, transition_list in candidate_transition_item_list:
      candidate_workflow = wf[wf_id]
205 206 207 208
      valid_list = []
      for transition_id in transition_list:
        if candidate_workflow.isWorkflowMethodSupported(instance, transition_id):
          valid_list.append(transition_id)
209 210 211 212
          once_transition_key = once_transition_dict.get((wf_id, transition_id))
          if once_transition_key:
            # a run-once transition, prevent it from running again in
            # the same transaction
213
            transactional_variable[once_transition_key] = 1
214
        elif candidate_workflow.__class__.__name__ == 'DCWorkflowDefinition':
215 216 217
          raise UnsupportedWorkflowMethod(instance, wf_id, transition_id)
          # XXX Keep the log for projects that needs to comment out
          #     the previous line.
218 219 220 221
          LOG("WorkflowMethod.__call__", ERROR,
              "Transition %s/%s on %r is ignored. Current state is %r."
              % (wf_id, transition_id, instance,
                 candidate_workflow._getWorkflowStateOf(instance, id_only=1)))
222 223
      if valid_list:
        valid_transition_item_list.append((wf_id, valid_list))
224

225
    #LOG('valid_transition_item_list %s' % self.__name__, 0, str(valid_transition_item_list))
Jean-Paul Smets committed
226

227
    # Call whatever must be called before changing states
228
    for wf_id, transition_list in valid_transition_item_list:
229
       wf[wf_id].notifyBefore(instance, transition_list, args=args, kw=kw)
230 231

    # Compute expected result
232
    result = self.__dict__['_m'](instance, *args, **kw)
233 234

    # Change the state of statefull workflows
235
    for wf_id, transition_list in valid_transition_item_list:
236
      try:
237
        wf[wf_id].notifyWorkflowMethod(instance, transition_list, args=args, kw=kw)
238 239 240 241 242 243
      except ObjectDeleted:
        # Re-raise with a different result.
        raise ObjectDeleted(result)
      except ObjectMoved, ex:
        # Re-raise with a different result.
        raise ObjectMoved(ex.getNewObject(), result)
244

245
    # Call whatever must be called after changing states
246
    for wf_id, transition_list in valid_transition_item_list:
247
      wf[wf_id].notifySuccess(instance, transition_list, result, args=args, kw=kw)
248

249 250 251
    # Return result finally
    return result

252 253 254 255 256
  # Interactions should not be disabled during normal operation. Only in very
  # rare and specific cases like data migration. That's why it is implemented
  # with temporary monkey-patching, instead of slowing down __call__ with yet
  # another condition.

257 258 259
  _do_interaction = __call__
  _no_interaction_lock = threading.Lock()
  _no_interaction_log = None
260
  _no_interaction_thread_id = None
261 262

  def _no_interaction(self, *args, **kw):
263 264
    if WorkflowMethod._no_interaction_thread_id != thread.get_ident():
      return self._do_interaction(*args, **kw)
265 266 267 268 269 270 271 272 273 274
    log = "skip interactions for %r" % args[0]
    if WorkflowMethod._no_interaction_log != log:
      WorkflowMethod._no_interaction_log = log
      LOG("WorkflowMethod", INFO, log)
    return self.__dict__['_m'](*args, **kw)

  @staticmethod
  @simple_decorator
  def disable(func):
    def wrapper(*args, **kw):
275 276 277
      thread_id = thread.get_ident()
      if WorkflowMethod._no_interaction_thread_id == thread_id:
        return func(*args, **kw)
278 279
      WorkflowMethod._no_interaction_lock.acquire()
      try:
280
        WorkflowMethod._no_interaction_thread_id = thread_id
281 282 283 284
        WorkflowMethod.__call__ = WorkflowMethod.__dict__['_no_interaction']
        return func(*args, **kw)
      finally:
        WorkflowMethod.__call__ = WorkflowMethod.__dict__['_do_interaction']
285
        WorkflowMethod._no_interaction_thread_id = None
286 287 288 289 290 291 292
        WorkflowMethod._no_interaction_lock.release()
    return wrapper

  @staticmethod
  def disabled():
    return WorkflowMethod._no_interaction_lock.locked()

293
  def registerTransitionAlways(self, portal_type, workflow_id, transition_id):
294 295 296
    """
      Transitions registered as always will be invoked always
    """
297 298
    transition_list = self._invoke_always.setdefault(portal_type, {}).setdefault(workflow_id, [])
    if transition_id not in transition_list: transition_list.append(transition_id)
299
    self.register()
300

301
  def registerTransitionOncePerTransaction(self, portal_type, workflow_id, transition_id):
302
    """
Fabien Morin committed
303
      Transitions registered as one per transactions will be invoked
304 305
      only once per transaction
    """
306 307
    transition_list = self._invoke_once.setdefault(portal_type, {}).setdefault(workflow_id, [])
    if transition_id not in transition_list: transition_list.append(transition_id)
308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325
    self.register()

  def register(self):
    """
      Registers the method so that _aq_reset may later reset it
    """
    workflow_method_registry.append(self)

  def reset(self, portal_type=None):
    """
      Reset the list of registered interactions or transitions
    """
    if portal_type:
      self._invoke_once[portal_type] = {}
      self._invoke_always[portal_type] = {}
    else:
      self._invoke_once = {}
      self._invoke_always = {}
326

327
def _aq_reset():
328 329 330 331 332 333 334 335 336
  warnings.warn("_aq_reset is deprecated in favor of "\
                "portal_types.resetDynamicDocumentsOnceAtTransactionBoundary, "\
                "calling this method affects greatly performances",
                DeprecationWarning, stacklevel=2)

  # Callers expect to re-generates accessors right now, so call
  # resetDynamicDocuments to maintain backward-compatibility
  from Products.ERP5.ERP5Site import getSite
  getSite().portal_types.resetDynamicDocuments()
337

338
class PropertyHolder(object):
339
  isRADContent = 1
340
  WORKFLOW_METHOD_MARKER = ('Base._doNothing',)
341 342 343
  RESERVED_PROPERTY_SET = {'_constraints', '_properties', '_categories',
                           '__implements__', 'property_sheets',
                           '__ac_permissions__', '_erp5_properties'}
344

345 346
  def __init__(self, name='PropertyHolder'):
    self.__name__ = name
347 348
    self.security = ClassSecurityInfo() # We create a new security info object
    self.workflow_method_registry = {}
349

350 351 352 353 354
    self._categories = []
    self._properties = []
    self._constraints = []
    self.constraints = []

355
  def _getPropertyHolderItemList(self):
356
    return [x for x in self.__dict__.items() if x[0] not in
357
        PropertyHolder.RESERVED_PROPERTY_SET]
358

359
  def registerWorkflowMethod(self, id, wf_id, tr_id, once_per_transaction=0):
360 361 362 363
    portal_type = self.portal_type

    workflow_method = getattr(self, id, None)
    if workflow_method is None:
364
      # XXX: We should pass 'tr_id' as second parameter.
365 366 367 368 369 370 371 372 373 374
      workflow_method = WorkflowMethod(Base._doNothing)
      setattr(self, id, workflow_method)
    if once_per_transaction:
      workflow_method.registerTransitionOncePerTransaction(portal_type,
                                                           wf_id,
                                                           tr_id)
    else:
      workflow_method.registerTransitionAlways(portal_type,
                                               wf_id,
                                               tr_id)
375 376 377 378 379 380

  def declareProtected(self, permission, accessor_name):
    """
      It is possible to gain 30% of accessor RAM footprint
      by postponing security declaration.

Fabien Morin committed
381
      WARNING: we optimize size by not setting security if
382
      it is the same as the default. This may be a bit
383 384 385
      dangerous if classes use another default
      security.
    """
386
    if permission not in (Permissions.AccessContentsInformation, Permissions.ModifyPortalContent):
387 388 389
      self.security.declareProtected(permission, accessor_name)

  # Inspection methods
390 391 392 393
  def getAccessorMethodItemList(self):
    """
    Return a list of tuple (id, method) for every accessor
    """
394
    accessor_method_item_list = []
395
    accessor_method_item_list_append = accessor_method_item_list.append
396
    for x, y in self._getPropertyHolderItemList():
397
      if isinstance(y, tuple):
398
        if y is PropertyHolder.WORKFLOW_METHOD_MARKER or x == '__ac_permissions__':
399
          continue
400
        if len(y) == 0:
401
          continue
402 403 404 405
        if not issubclass(y[0], Accessor):
          continue
      elif not isinstance(y, Accessor):
        continue
406
      accessor_method_item_list_append((x, y))
407
    return accessor_method_item_list
408 409 410 411 412

  def getAccessorMethodIdList(self):
    """
    Return the list of accessor IDs
    """
413
    return [ x[0] for x in self.getAccessorMethodItemList() ]
414 415 416 417 418

  def getWorkflowMethodItemList(self):
    """
    Return a list of tuple (id, method) for every workflow method
    """
419
    return [x for x in self._getPropertyHolderItemList() if isinstance(x[1], WorkflowMethod)
420
        or (isinstance(x[1], types.TupleType)
421
            and x[1] is PropertyHolder.WORKFLOW_METHOD_MARKER)]
422 423 424 425 426

  def getWorkflowMethodIdList(self):
    """
    Return the list of workflow method IDs
    """
427
    return [x[0] for x in self.getWorkflowMethodItemList()]
428 429 430 431 432 433 434

  def _getClassDict(self, klass, inherited=1, local=1):
    """
    Return a dict for every property of a class
    """
    result = {}
    if inherited:
435 436
      for parent in reversed(klass.mro()):
        result.update(parent.__dict__)
437 438 439 440 441 442 443 444 445 446 447 448
    if local:
      result.update(klass.__dict__)
    return result

  def _getClassItemList(self, klass, inherited=1, local=1):
    """
    Return a list of tuple (id, method) for every property of a class
    """
    return self._getClassDict(klass, inherited=inherited, local=local).items()

  def getClassMethodItemList(self, klass, inherited=1, local=1):
    """
449
    Return a list of tuple (id, method, module) for every class method
450
    """
451
    return [x for x in self._getClassItemList(klass, inherited=inherited,
452
      local=local) if callable(x[1])]
453 454 455 456 457

  def getClassMethodIdList(self, klass, inherited=1, local=1):
    """
    Return the list of class method IDs
    """
458 459
    return [x[0] for x in self.getClassMethodItemList(klass,
      inherited=inherited, local=local)]
460 461 462 463 464

  def getClassPropertyItemList(self, klass, inherited=1, local=1):
    """
    Return a list of tuple (id, method) for every class method
    """
465 466
    return [x for x in self._getClassItemList(klass, inherited=inherited,
      local=local) if not callable(x[1])]
467 468 469 470 471

  def getClassPropertyIdList(self, klass, inherited=1, local=1):
    """
    Return the list of class method IDs
    """
472 473
    return [x[0] for x in self.getClassPropertyItemList(klass,
      inherited=inherited, local=local)]
474

475 476 477 478
def getClassPropertyList(klass):
  ps_list = getattr(klass, 'property_sheets', ())
  ps_list = tuple(ps_list)
  for super_klass in klass.__bases__:
479 480 481
    if getattr(super_klass, 'isRADContent', 0):
      ps_list = ps_list + tuple([p for p in getClassPropertyList(super_klass)
        if p not in ps_list])
482
  return ps_list
483

484
def initializePortalTypeDynamicWorkflowMethods(ptype_klass, portal_workflow):
485 486 487 488 489 490
  """We should now make sure workflow methods are defined
  and also make sure simulation state is defined."""
  # aq_inner is required to prevent extra name lookups from happening
  # infinitely. For instance, if a workflow is missing, and the acquisition
  # wrapper contains an object with _aq_dynamic defined, the workflow id
  # is looked up with _aq_dynamic, thus causes infinite recursions.
491

492 493
  portal_workflow = aq_inner(portal_workflow)
  portal_type = ptype_klass.__name__
Nicolas Dumazet committed
494

495 496
  dc_workflow_dict = {}
  interaction_workflow_dict = {}
497
  for wf in portal_workflow.getWorkflowsFor(portal_type):
Nicolas Dumazet committed
498 499 500
    wf_id = wf.id
    wf_type = wf.__class__.__name__
    if wf_type == "DCWorkflowDefinition":
501 502 503 504 505 506
      # Create state var accessor
      # and generate methods that support the translation of workflow states
      state_var = wf.variables.getStateVar()
      for method_id, getter in (
          ('get%s' % UpperCase(state_var), WorkflowState.Getter),
          ('get%sTitle' % UpperCase(state_var), WorkflowState.TitleGetter),
507
          ('getTranslated%s' % UpperCase(state_var),
508
                                     WorkflowState.TranslatedGetter),
509
          ('getTranslated%sTitle' % UpperCase(state_var),
510 511 512
                                     WorkflowState.TranslatedTitleGetter),
          ('serialize%s' % UpperCase(state_var), WorkflowState.SerializeGetter),
          ):
513
        if not hasattr(ptype_klass, method_id):
514 515
          method = getter(method_id, wf_id)
          # Attach to portal_type
516 517
          ptype_klass.registerAccessor(method,
                                       Permissions.AccessContentsInformation)
Nicolas Dumazet committed
518 519 520 521 522 523 524 525 526 527 528

      storage = dc_workflow_dict
      transitions = wf.transitions
    elif wf_type == "InteractionWorkflowDefinition":
      storage = interaction_workflow_dict
      transitions = wf.interactions
    else:
      continue

    # extract Trigger transitions from workflow definitions for later
    transition_id_set = set(transitions.objectIds())
529
    trigger_dict = {}
Nicolas Dumazet committed
530
    for tr_id in transition_id_set:
531
      tdef = transitions[tr_id]
Nicolas Dumazet committed
532 533 534 535 536 537 538 539 540
      if tdef.trigger_type == TRIGGER_WORKFLOW_METHOD:
        trigger_dict[tr_id] = tdef

    storage[wf_id] = (transition_id_set, trigger_dict)

  for wf_id, v in dc_workflow_dict.iteritems():
    transition_id_set, trigger_dict = v
    for tr_id, tdef in trigger_dict.iteritems():
      method_id = convertToMixedCase(tr_id)
541 542 543
      try:
        method = getattr(ptype_klass, method_id)
      except AttributeError:
544
        ptype_klass.security.declareProtected(Permissions.AccessContentsInformation,
Nicolas Dumazet committed
545
                                              method_id)
546
        ptype_klass.registerWorkflowMethod(method_id, wf_id, tr_id)
Nicolas Dumazet committed
547 548 549 550 551 552
        continue

      # Wrap method
      if not callable(method):
        LOG('initializePortalTypeDynamicWorkflowMethods', 100,
            'WARNING! Can not initialize %s on %s' % \
553
              (method_id, portal_type))
Nicolas Dumazet committed
554 555 556 557
        continue

      if not isinstance(method, WorkflowMethod):
        method = WorkflowMethod(method)
558
        setattr(ptype_klass, method_id, method)
Nicolas Dumazet committed
559 560 561 562 563 564
      else:
        # We must be sure that we
        # are going to register class defined
        # workflow methods to the appropriate transition
        transition_id = method.getTransitionId()
        if transition_id in transition_id_set:
565 566
          method.registerTransitionAlways(portal_type, wf_id, transition_id)
      method.registerTransitionAlways(portal_type, wf_id, tr_id)
Nicolas Dumazet committed
567

Nicolas Dumazet committed
568 569 570
  if not interaction_workflow_dict:
    return

571 572 573
  # all methods in mro of portal type class: that contains all
  # workflow methods and accessors you could possibly ever need
  class_method_id_list = ptype_klass.getClassMethodIdList(ptype_klass)
Nicolas Dumazet committed
574

575
  interaction_queue = []
576
  # XXX This part is (more or less...) a copy and paste
577 578
  for wf_id, v in interaction_workflow_dict.iteritems():
    transition_id_set, trigger_dict = v
Nicolas Dumazet committed
579
    for tr_id, tdef in trigger_dict.iteritems():
580
      # Check portal type filter
581
      if (tdef.portal_type_filter is not None and \
582
          portal_type not in tdef.portal_type_filter):
583
        continue
584 585 586 587 588 589 590 591

      # Check portal type group filter
      if tdef.portal_type_group_filter is not None:
        getPortalGroupedTypeSet = portal_workflow.getPortalObject()._getPortalGroupedTypeSet
        if not any(portal_type in getPortalGroupedTypeSet(portal_type_group) for
                   portal_type_group in tdef.portal_type_group_filter):
          continue

Nicolas Dumazet committed
592 593 594
      for imethod_id in tdef.method_id:
        if wildcard_interaction_method_id_match(imethod_id):
          # Interactions workflows can use regexp based wildcard methods
595 596 597 598
          # XXX What happens if exception ?
          method_id_matcher = re.compile(imethod_id).match

          # queue transitions using regexps for later examination
599 600 601 602 603
          interaction_queue.append((wf_id,
                                    tr_id,
                                    transition_id_set,
                                    tdef.once_per_transaction,
                                    method_id_matcher))
604

Nicolas Dumazet committed
605
          # XXX - class stuff is missing here
606
          method_id_list = filter(method_id_matcher, class_method_id_list)
Nicolas Dumazet committed
607 608 609 610 611 612
        else:
          # Single method
          # XXX What if the method does not exist ?
          #     It's not consistent with regexp based filters.
          method_id_list = [imethod_id]
        for method_id in method_id_list:
613
          method = getattr(ptype_klass, method_id, _MARKER)
Nicolas Dumazet committed
614
          if method is _MARKER:
Nicolas Dumazet committed
615 616
            # set a default security, if this method is not already
            # protected.
617 618
            if method_id not in ptype_klass.security.names:
              ptype_klass.security.declareProtected(
Nicolas Dumazet committed
619
                  Permissions.AccessContentsInformation, method_id)
620
            ptype_klass.registerWorkflowMethod(method_id, wf_id, tr_id,
621
                                               tdef.once_per_transaction)
Nicolas Dumazet committed
622 623 624
            continue

          # Wrap method
625 626 627 628 629
          if not callable(method) or method_id in (
              # To prevent infinite recursion in case of mistake in a worflow,
              # methods that are always used by WorkflowMethod.__call__
              # must be excluded.
              'getPortalObject', 'getPhysicalPath', 'getId'):
Nicolas Dumazet committed
630 631
            LOG('initializePortalTypeDynamicWorkflowMethods', 100,
                'WARNING! Can not initialize %s on %s' % \
632
                  (method_id, portal_type))
Nicolas Dumazet committed
633 634 635
            continue
          if not isinstance(method, WorkflowMethod):
            method = WorkflowMethod(method)
636
            setattr(ptype_klass, method_id, method)
Nicolas Dumazet committed
637 638 639 640 641 642
          else:
            # We must be sure that we
            # are going to register class defined
            # workflow methods to the appropriate transition
            transition_id = method.getTransitionId()
            if transition_id in transition_id_set:
643
              method.registerTransitionAlways(portal_type, wf_id, transition_id)
Nicolas Dumazet committed
644
          if tdef.once_per_transaction:
645
            method.registerTransitionOncePerTransaction(portal_type, wf_id, tr_id)
Nicolas Dumazet committed
646
          else:
647
            method.registerTransitionAlways(portal_type, wf_id, tr_id)
648

649 650 651
  if not interaction_queue:
    return

652 653 654 655 656 657
  # the only methods that could have appeared since last check are
  # workflow methods
  # TODO we could just queue the ids of methods that are attached to the
  # portal type class in the previous loop, to improve performance
  new_method_set = set(ptype_klass.getWorkflowMethodIdList())
  added_method_set = new_method_set.difference(class_method_id_list)
658 659 660
  # We need to run this part twice in order to handle interactions of interactions
  # ex. an interaction workflow creates a workflow method which matches
  # the regexp of another interaction workflow
661
  for wf_id, tr_id, transition_id_set, once, method_id_matcher in interaction_queue:
662 663
    for method_id in filter(method_id_matcher, added_method_set):
      # method must already exist and be a workflow method
664
      method = getattr(ptype_klass, method_id)
665 666
      transition_id = method.getTransitionId()
      if transition_id in transition_id_set:
667
        method.registerTransitionAlways(portal_type, wf_id, transition_id)
668
      if once:
669
        method.registerTransitionOncePerTransaction(portal_type, wf_id, tr_id)
670
      else:
671
        method.registerTransitionAlways(portal_type, wf_id, tr_id)
672

673 674 675 676
class Base( CopyContainer,
            PortalContent,
            ActiveObject,
            OFS.History.Historical,
677 678 679
            ERP5PropertyManager,
            PropertyTranslatableBuiltInDictMixIn
            ):
Jean-Paul Smets committed
680 681 682 683 684 685 686 687
  """
    This is the base class for all ERP5 Zope objects.
    It defines object attributes which are necessary to implement
    relations and data synchronisation

    id  --  the standard object id
    rid --  the standard object id in the master ODB the object was
        subsribed from
688
    uid --  a global object id which is unique
Jean-Paul Smets committed
689 690 691 692 693 694 695 696 697 698 699 700
    sid --  the id of the subscribtion/syncrhonisation object which
        this object was generated from

    sync_status -- The status of this document in the synchronization
             process (NONE, SENT, ACKNOWLEDGED, SYNCHRONIZED)
             could work as a workflow but CPU expensive

  """
  meta_type = 'ERP5 Base Object'
  portal_type = 'Base Object'
  #_local_properties = () # no need since getattr
  isRADContent = 1    #
701 702 703 704 705 706 707 708 709 710 711 712 713
  isPortalContent = ConstantGetter('isPortalContent', value=True)
  isCapacity = ConstantGetter('isCapacity', value=False)
  isCategory = ConstantGetter('isCategory', value=False)
  isBaseCategory = ConstantGetter('isBaseCategory', value=False)
  isInventoryMovement = ConstantGetter('isInventoryMovement', value=False)
  isDelivery = ConstantGetter('isDelivery', value=False)
  isInventory = ConstantGetter('isInventory', value=False)
  # If set to 0, reindexing will not happen (useful for optimization)
  isIndexable = ConstantGetter('isIndexable', value=True)
  isPredicate = ConstantGetter('isPredicate', value=False)
  isTemplate = ConstantGetter('isTemplate', value=False)
  isDocument = ConstantGetter('isDocument', value=False)
  isTempDocument = ConstantGetter('isTempDocument', value=False)
714

715
  # Dynamic method acquisition system (code generation)
716
  aq_method_generated = set()
717
  aq_method_generating = []
718
  aq_portal_type = {}
719
  aq_related_generated = 0
720

721 722 723
  # Declarative security - in ERP5 we use AccessContentsInformation to
  # define the right of accessing content properties as opposed
  # to view which is the right to view the object with a form
Jean-Paul Smets committed
724
  security = ClassSecurityInfo()
725
  security.declareObjectProtected(Permissions.AccessContentsInformation)
Jean-Paul Smets committed
726 727

  # Declarative properties
Kevin Deldycke committed
728
  property_sheets = ( PropertySheet.Base, )
Jean-Paul Smets committed
729

730 731 732 733 734
  # Declarative interfaces
  zope.interface.implements(interfaces.ICategoryAccessProvider,
                            interfaces.IValueAccessProvider,
                            )

Jean-Paul Smets committed
735
  # We want to use a default property view
736
  manage_main = manage_propertiesForm = DTMLFile( 'properties', _dtmldir )
737
  manage_main._setName('manage_main')
738 739 740 741 742 743

  manage_options = ( PropertyManager.manage_options +
                     SimpleItem.manage_options +
                     OFS.History.Historical.manage_options +
                     CMFCatalogAware.manage_options
                   )
Jean-Paul Smets committed
744

745 746 747 748 749
  # Place for all is... method
  security.declareProtected(Permissions.AccessContentsInformation, 'isMovement')
  def isMovement(self):
    return 0

750
  security.declareProtected( Permissions.ModifyPortalContent, 'setTitle' )
751
  def setTitle(self, value):
752
    """ sets the title. (and then reindexObject)"""
753 754
    self._setTitle(value)
    self.reindexObject()
755

756 757
  security.declarePublic('provides')
  def provides(cls, interface_name):
758
    """
759 760
    Check if the current class provides a particular interface from ERP5Type's
    interfaces registry
761
    """
762 763 764
    interface = getattr(interfaces, interface_name, None)
    if interface is not None:
      return interface.implementedBy(cls)
765 766 767
    return False
  provides = classmethod(CachingMethod(provides, 'Base.provides',
                                       cache_factory='erp5_ui_long'))
768

769
  def _aq_key(self):
770
    return (self.portal_type, self.__class__)
771

772
  def _propertyMap(self, local_properties=False):
773
    """ Method overload - properties are now defined on the ptype """
774 775
    property_list = []
    # Get all the accessor holders for this portal type
776
    if not local_properties:
777
      property_list += self.__class__.getAccessorHolderPropertyList()
778

779 780
    property_list += getattr(self, '_local_properties', [])
    return tuple(property_list)
Aurel committed
781

782 783 784 785 786
  def manage_historyCompare(self, rev1, rev2, REQUEST,
                            historyComparisonResults=''):
    return Base.inheritedAttribute('manage_historyCompare')(
          self, rev1, rev2, REQUEST,
          historyComparisonResults=OFS.History.html_diff(
787 788
              pformat(rev1.__dict__),
              pformat(rev2.__dict__)))
789

790
  def _aq_dynamic(self, id):
791 792 793
    # ahah! disabled, thanks to portal type classes
    return None

Jean-Paul Smets committed
794 795 796 797 798 799 800
  # Constructor
  def __init__(self, id, uid=None, rid=None, sid=None, **kw):
    self.id = id
    if uid is not None :
      self.uid = uid # Else it will be generated when we need it
    self.sid = sid

Yoshinori Okuji committed
801 802
  # XXX This is necessary to override getId which is also defined in SimpleItem.
  security.declareProtected( Permissions.AccessContentsInformation, 'getId' )
Arnaud Fontaine committed
803
  getId = BaseAccessor.Getter('getId', 'id', 'string')
Yoshinori Okuji committed
804

Jean-Paul Smets committed
805
  # Debug
806 807
  security.declareProtected(Permissions.AccessContentsInformation,
                            'getOid')
Jean-Paul Smets committed
808 809 810 811 812 813
  def getOid(self):
    """
      Return ODB oid
    """
    return self._p_oid

814 815
  security.declareProtected(Permissions.AccessContentsInformation,
                            'getOidRepr')
816 817 818 819 820 821
  def getOidRepr(self):
    """
      Return ODB oid, in an 'human' readable form.
    """
    from ZODB.utils import oid_repr
    return oid_repr(self._p_oid)
Aurel committed
822

823 824
  security.declareProtected(Permissions.AccessContentsInformation,
                            'getSerial')
825 826 827
  def getSerial(self):
    """Return ODB Serial."""
    return self._p_serial
Aurel committed
828

829 830
  security.declareProtected(Permissions.AccessContentsInformation,
                            'getHistorySerial')
831 832 833
  def getHistorySerial(self):
    """Return ODB Serial, in the same format used for history keys"""
    return '.'.join([str(x) for x in unpack('>HHHH', self._p_serial)])
834

Jean-Paul Smets committed
835 836 837
  # Utils
  def _getCategoryTool(self):
    return aq_inner(self.getPortalObject().portal_categories)
838

839 840
  def _getTypesTool(self):
    return aq_inner(self.getPortalObject().portal_types)
Jean-Paul Smets committed
841

842
  def _doNothing(self, *args, **kw):
843 844
    # A method which does nothing (and can be used to build WorkflowMethods which trigger worklow transitions)
    pass
845

Jean-Paul Smets committed
846
  # Generic accessor
847
  def _getDefaultAcquiredProperty(self, key, default_value, null_value,
848
        acquisition_object_id=None, base_category=None, portal_type=None,
849
        copy_value=0, mask_value=0, accessor_id=None, depends=None,
850 851
        storage_id=None, alt_accessor_id=None, is_list_type=0, is_tales_type=0,
        checked_permission=None):
852
    """
Jean-Paul Smets committed
853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876
      This method implements programmable acquisition of values in ERP5.

      The principle is that some object attributes should be looked up,
      copied or synchronized from the values of another object which relates
      to the first thereof.

      The parameters which define value acquisition are:

      base_category --    a single base category or a list of base categories
                          to look up related objects

      portal_type   --    a single portal type or a list of portal types to filter the
                          list of related objects

      copy_value    --    if set to 1, the looked up value will be copied
                          as an attribute of self

      mask_value    --    if set to 1, the value of the attribute of self
                          has priority on the looked up value

      accessor_id   --    the id of the accessor to call on the related filtered objects

      depends       --    a list of parameters to propagate in the look up process

877 878
      acquisition_object_id -- List of object Ids where look up properties
                               before looking up on acquired objects
879
      The purpose of copy_value / mask_value is to solve issues
Jean-Paul Smets committed
880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903
      related to relations and synchronisation of data. copy_value determines
      if a value should be copied as an attribute of self. Copying a value is
      useful for example when we do invoices and want to remember the price at
      a given point of time. mask_value allows to give priority to the value
      holded by self, rather than to the lookup through related objects.
      This is for example useful for invoices (again) for which we want the value
      not to change in time.

      Another case is the case of independent modules on multiple Zope. If for example
      a sales opportunity modules runs on a Zope No1 and an Organisation module runs
      on a Zope No 2. We want to enter peoples's names on the Zope No1. They will be entered
      as strings and stored as such in attributes. When such opportunities are synchronized
      on the Zope No 2, we want to be able to augment content locally by adding some
      category information (in order to say for example that M. Lawno is client/person/23)
      and eventually want M. Lawno to be displayed as "James Lawno". So, we want to give
      priority to the looked up attribute rather than to the attribute. However,
      we may want Zope No 1 to still display "James Lawno" as "M. Lawno". This means
      that we do not want to synchronize back this attribute.

      Other case : we add relation after entering information...

      Other case : we want to change the phone number of a related object without
      going to edit the related object
    """
904
    # Push context to prevent loop
905
    tv = getTransactionalVariable()
906
    if isinstance(portal_type, list):
907
      portal_type = tuple(portal_type)
908 909
    elif portal_type is None:
      portal_type = ()
910 911
    acquisition_key = ('_getDefaultAcquiredProperty', self.getPath(), key,
                       acquisition_object_id, base_category, portal_type,
912
                       copy_value, mask_value, accessor_id, depends,
913
                       storage_id, alt_accessor_id, is_list_type, is_tales_type,
914
                       checked_permission)
915
    if acquisition_key in tv:
916
      return null_value[0]
917 918 919 920 921 922 923 924 925 926 927 928

    tv[acquisition_key] = 1

    try:
      if storage_id is None: storage_id=key
      #LOG("Get Acquired Property storage_id",0,str(storage_id))
      # Test presence of attribute without acquisition
      # if present, get it in its context, thus we keep acquisition if
      # returned value is an object
      d = getattr(aq_base(self), storage_id, _MARKER)
      if d is not _MARKER:
        value = getattr(self, storage_id, None)
Jean-Paul Smets committed
929
      else:
930
        value = None
931
      local_value = value
932
      # If we hold an attribute and mask_value is set, return the attribute
933
      if mask_value and value not in null_value:
934 935 936 937 938
        # Pop context
        if is_tales_type:
          expression = Expression(value)
          econtext = createExpressionContext(self)
          return expression(econtext)
Jean-Paul Smets committed
939
        else:
940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955
          if is_list_type:
            # We must provide the first element of the acquired list
            if value in null_value:
              result = None
            else:
              if isinstance(value, (list, tuple)):
                if len(value) is 0:
                  result = None
                else:
                  result = value[0]
              else:
                result = value
          else:
            # Value is a simple type
            result = value
          return result
956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993

      #Look at acquisition object before call acquisition
      if acquisition_object_id is not None:
        value = None
        if isinstance(acquisition_object_id, str):
          acquisition_object_id = tuple(acquisition_object_id)
        for object_id in acquisition_object_id:
          try:
            value = self[object_id]
            if value not in null_value:
              break
          except (KeyError, AttributeError):
            pass
        if copy_value:
          if getattr(self, storage_id, None) is None:
            # Copy the value if it does not already exist as an attribute of self
            # Like in the case of orders / invoices
            setattr(self, storage_id, value)
        if is_list_type:
          # We must provide the first element of the acquired list
          if value in null_value:
            result = None
          else:
            if isinstance(value, (list, tuple)):
              if len(value) is 0:
                result = None
              else:
                result = value[0]
            else:
              result = value
        else:
          # Value is a simple type
          result = value
      else:
        result = None
      if result not in null_value:
        return result

994 995 996 997
      # Retrieve the list of related objects
      #LOG("Get Acquired Property self",0,str(self))
      #LOG("Get Acquired Property portal_type",0,str(portal_type))
      #LOG("Get Acquired Property base_category",0,str(base_category))
998 999
      #super_list = self.getValueList(base_category, portal_type=portal_type) # We only do a single jump
      super_list = self.getAcquiredValueList(base_category, portal_type=portal_type,
1000
                                              checked_permission=checked_permission) # Full acquisition
1001
      super_list = [o for o in super_list if o.getPhysicalPath() != self.getPhysicalPath()] # Make sure we do not create stupid loop here
1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019
      #LOG("Get Acquired Property super_list",0,str(super_list))
      #LOG("Get Acquired Property accessor_id",0,str(accessor_id))
      if len(super_list) > 0:
        super = super_list[0]
        # Retrieve the value
        if accessor_id is None:
          value = super.getProperty(key)
        else:
          method = getattr(super, accessor_id)
          value = method() # We should add depends here XXXXXX
                          # There is also a strong risk here of infinite loop
        if copy_value:
          if getattr(self, storage_id, None) is None:
            # Copy the value if it does not already exist as an attribute of self
            # Like in the case of orders / invoices
            setattr(self, storage_id, value)
        if is_list_type:
          # We must provide the first element of the acquired list
1020
          if value in null_value:
1021
            result = None
Jean-Paul Smets committed
1022
          else:
1023 1024 1025 1026 1027 1028 1029 1030 1031 1032
            if isinstance(value, (list, tuple)):
              if len(value) is 0:
                result = None
              else:
                result = value[0]
            else:
              result = value
        else:
          # Value is a simple type
          result = value
Jean-Paul Smets committed
1033
      else:
1034
        result = None
1035
      if result not in null_value:
1036
        return result
1037 1038 1039 1040 1041
      elif local_value not in null_value:
        # Nothing has been found by looking up
        # through acquisition documents, fallback by returning
        # at least local_value
        return local_value
1042 1043 1044 1045 1046 1047 1048
      else:
        #LOG("alt_accessor_id",0,str(alt_accessor_id))
        if alt_accessor_id is not None:
          for id in alt_accessor_id:
            #LOG("method",0,str(id))
            method = getattr(self, id, None)
            if callable(method):
1049 1050 1051 1052
              try:
                result = method(checked_permission=checked_permission)
              except TypeError:
                result = method()
1053
              if result not in null_value:
1054 1055 1056 1057 1058 1059 1060
                if is_list_type:
                  if isinstance(result, (list, tuple)):
                    # We must provide the first element of the alternate result
                    if len(result) > 0:
                      return result[0]
                  else:
                    return result
Jean-Paul Smets committed
1061
                else:
1062
                  # Result is a simple type
Jean-Paul Smets committed
1063 1064
                  return result

1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075
        if copy_value:
          return getattr(self,storage_id, default_value)
        else:
          # Return the default value defined at the class level XXXXXXXXXXXXXXX
          return default_value
    finally:
      # Pop the acquisition context.
      try:
        del tv[acquisition_key]
      except KeyError:
        pass
Jean-Paul Smets committed
1076

1077
  def _getAcquiredPropertyList(self, key, default_value, null_value,
1078
     base_category, portal_type=None, copy_value=0, mask_value=0, append_value=0,
Jean-Paul Smets committed
1079
     accessor_id=None, depends=None, storage_id=None, alt_accessor_id=None,
1080
     acquisition_object_id=None,
1081
     is_list_type=0, is_tales_type=0, checked_permission=None):
Jean-Paul Smets committed
1082 1083 1084 1085 1086 1087 1088
    """
      Default accessor. Implements the default
      attribute accessor.

      portal_type
      copy_value
      depends
1089

Jean-Paul Smets committed
1090
    """
1091
    # Push context to prevent loop
1092
    tv = getTransactionalVariable()
1093 1094
    if isinstance(portal_type, list):
      portal_type = tuple(portal_type)
1095 1096
    elif portal_type is None:
      portal_type = ()
1097
    acquisition_key = ('_getAcquiredPropertyList', self.getPath(), key, base_category,
1098 1099
                       portal_type, copy_value, mask_value, accessor_id,
                       depends, storage_id, alt_accessor_id,
1100
                       acquisition_object_id, is_list_type, is_tales_type,
1101
                       checked_permission)
1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114
    if acquisition_key in tv:
      return null_value

    tv[acquisition_key] = 1

    try:
      if storage_id is None: storage_id=key
      value = getattr(self, storage_id, None)
      if mask_value and value is not None:
        if is_tales_type:
          expression = Expression(value)
          econtext = createExpressionContext(self)
          return expression(econtext)
Jean-Paul Smets committed
1115
        else:
1116
          return value
1117 1118 1119 1120 1121 1122 1123 1124 1125 1126
      super_list = []
      if acquisition_object_id is not None:
        if isinstance(acquisition_object_id, str):
          acquisition_object_id = tuple(acquisition_object_id)
        for object_id in acquisition_object_id:
          try:
            acquisition_object = self[object_id]
            super_list.append(acquisition_object)
          except (KeyError, AttributeError):
            pass
1127 1128 1129 1130 1131
      super_list += self.getAcquiredValueList(
        base_category,
        portal_type=portal_type,
        checked_permission=checked_permission,
        ) # Full acquisition
1132
      super_list = [o for o in super_list if o.getPhysicalPath() != self.getPhysicalPath()] # Make sure we do not create stupid loop here
1133 1134 1135 1136 1137 1138 1139 1140 1141 1142
      if len(super_list) > 0:
        value = []
        for super in super_list:
          if accessor_id is None:
            if is_list_type:
              result = super.getPropertyList(key)
              if isinstance(result, (list, tuple)):
                value += result
              else:
                value += [result]
Jean-Paul Smets committed
1143
            else:
1144
              value += [super.getProperty(key)]
Jean-Paul Smets committed
1145
          else:
1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158
            method = getattr(super, accessor_id)
            if is_list_type:
              result = method() # We should add depends here
              if isinstance(result, (list, tuple)):
                value += result
              else:
                value += [result]
            else:
              value += [method()] # We should add depends here
        if copy_value:
          if not hasattr(self, storage_id):
            setattr(self, storage_id, value)
        return value
Jean-Paul Smets committed
1159
      else:
1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170
        # ?????
        if copy_value:
          return getattr(self,storage_id, default_value)
        else:
          return default_value
    finally:
      # Pop the acquisition context.
      try:
        del tv[acquisition_key]
      except KeyError:
        pass
Jean-Paul Smets committed
1171 1172

  security.declareProtected( Permissions.AccessContentsInformation, 'getProperty' )
1173 1174 1175 1176 1177
  def getProperty(self, key, d=_MARKER, **kw):
    """getProperty is the generic accessor to all properties and categories
    defined on this object.
    If an accessor exists for this property, the accessor will be called,
    default value will be passed to the accessor as first positional argument.
Jean-Paul Smets committed
1178
    """
1179
    __traceback_info__ = (key,)
Jean-Paul Smets committed
1180 1181
    accessor_name = 'get' + UpperCase(key)
    aq_self = aq_base(self)
1182
    if getattr(aq_self, accessor_name, None) is not None:
Jean-Paul Smets committed
1183
      method = getattr(self, accessor_name)
1184 1185 1186 1187 1188 1189 1190 1191
      if d is not _MARKER:
        try:
          # here method is a method defined on the class, we don't know if the
          # method supports default argument or not, so we'll try and if the
          # method doesn't accepts it, we ignore default argument.
          return method(d, **kw)
        except TypeError:
          pass
Yoshinori Okuji committed
1192
      return method(**kw)
1193 1194 1195 1196
    # Try a mono valued accessor if it is available
    # and return it as a list
    if accessor_name.endswith('List'):
      mono_valued_accessor_name = accessor_name[:-4]
1197
      method = getattr(self.__class__, mono_valued_accessor_name, None)
1198
      if method is not None:
1199 1200
        # We have a monovalued property
        if d is _MARKER:
1201
          result = method(self, **kw)
1202 1203
        else:
          try:
1204
            result = method(self, d, **kw)
1205
          except TypeError: