ERP5Type.py 30.6 KB
Newer Older
1
# -*- coding: utf-8 -*-
Jean-Paul Smets's avatar
Jean-Paul Smets committed
2 3
##############################################################################
#
4 5
# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
# Copyright (c) 2002-2004 Nexedi SARL and Contributors. All Rights Reserved.
Jean-Paul Smets's avatar
Jean-Paul Smets committed
6
#                    Jean-Paul Smets-Solanes <jp@nexedi.com>
Jean-Paul Smets's avatar
Jean-Paul Smets committed
7 8 9 10 11 12 13 14
#
# 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
#
15 16 17 18 19 20
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE
Jean-Paul Smets's avatar
Jean-Paul Smets committed
21 22 23
#
##############################################################################

Julien Muchembled's avatar
Julien Muchembled committed
24
import zope.interface
25
from Products.ERP5Type.Globals import InitializeClass
26
from AccessControl import ClassSecurityInfo, getSecurityManager
27
from Acquisition import aq_base, aq_inner, aq_parent
28
import Products
29
from Products.CMFCore.TypesTool import FactoryTypeInformation
30
from Products.CMFCore.Expression import Expression
31
from Products.CMFCore.exceptions import AccessControl_Unauthorized
Nicolas Dumazet's avatar
Nicolas Dumazet committed
32
from Products.CMFCore.utils import getToolByName
33
from Products.ERP5Type import interfaces, Constraint, Permissions, PropertySheet
34
from Products.ERP5Type.Base import getClassPropertyList
35
from Products.ERP5Type.UnrestrictedMethod import UnrestrictedMethod
36
from Products.ERP5Type.Utils import deprecated, createExpressionContext
37
from Products.ERP5Type.XMLObject import XMLObject
38
from Products.ERP5Type.Cache import CachingMethod
39 40
from Products.ERP5Type.dynamic.accessor_holder import getPropertySheetValueList, \
    getAccessorHolderList
41
from Products.ERP5Type.TransactionalVariable import getTransactionalVariable
Jean-Paul Smets's avatar
Jean-Paul Smets committed
42

Julien Muchembled's avatar
Julien Muchembled committed
43 44
ERP5TYPE_SECURITY_GROUP_ID_GENERATION_SCRIPT = 'ERP5Type_asSecurityGroupId'

45 46
from TranslationProviderBase import TranslationProviderBase

47 48 49
from sys import exc_info
from zLOG import LOG, ERROR
from Products.CMFCore.exceptions import zExceptions_Unauthorized
50

51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
def getCurrentUserIdOrAnonymousToken():
  """Return connected user_id or simple token for
  Anonymous users in scope of transaction.
  """
  tv = getTransactionalVariable()
  USER_ID_KEY = '_user_id'
  ANONYMOUS_OWNER_ROLE_VALUE = 'Anonymous Owner'
  try:
    return tv[USER_ID_KEY]
  except KeyError:
    user = getSecurityManager().getUser()
    if user is not None:
      user_id = user.getId()
    else:
      user_id = ANONYMOUS_OWNER_ROLE_VALUE
    tv[USER_ID_KEY] = user_id
    return user_id
Julien Muchembled's avatar
Julien Muchembled committed
68 69

class LocalRoleAssignorMixIn(object):
Julien Muchembled's avatar
Julien Muchembled committed
70 71
    """Mixin class used by type informations to compute and update local roles
    """
Julien Muchembled's avatar
Julien Muchembled committed
72 73 74 75 76 77
    security = ClassSecurityInfo()
    security.declareObjectProtected(Permissions.AccessContentsInformation)

    zope.interface.implements(interfaces.ILocalRoleAssignor)

    security.declarePrivate('updateLocalRolesOnObject')
78 79
    @UnrestrictedMethod
    def updateLocalRolesOnDocument(self, ob, user_name=None, reindex=True):
Julien Muchembled's avatar
Julien Muchembled committed
80 81 82 83 84 85
      """
        Assign Local Roles to Groups on object 'ob', based on Portal Type Role
        Definitions and "ERP5 Role Definition" objects contained inside 'ob'.
      """
      if user_name is None:
        # First try to guess from the owner
86 87 88 89
        owner = ob.getOwnerTuple()
        if owner:
          user_name = owner[1]
        else:
90
          user_name = getSecurityManager().getUser().getId()
Julien Muchembled's avatar
Julien Muchembled committed
91 92 93

      group_id_role_dict = self.getLocalRolesFor(ob, user_name)

94 95
      ## Update role assignments to groups
      # Save the owner
96
      for group, role_list in (ob.__ac_local_roles__ or {}).iteritems():
97 98 99
        if 'Owner' in role_list:
          group_id_role_dict.setdefault(group, set()).add('Owner')
      # Assign new roles
100
      ob.__ac_local_roles__ = ac_local_roles = {}
101 102
      for group, role_list in group_id_role_dict.iteritems():
        if role_list:
103
          ac_local_roles[group] = list(role_list)
104
      ## Make sure that the object is reindexed
Julien Muchembled's avatar
Julien Muchembled committed
105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124
      if reindex:
        ob.reindexObjectSecurity()

    security.declarePrivate("getLocalRolesFor")
    def getLocalRolesFor(self, ob, user_name=None):
      """Compute the security that should be applied on an object

      Returned value is a dict: {groud_id: role_name_set, ...}
      """
      group_id_role_dict = {}
      # Merge results from applicable roles
      for role in self.getFilteredRoleListFor(ob):
        for group_id, role_list \
        in role.getLocalRolesFor(ob, user_name).iteritems():
          group_id_role_dict.setdefault(group_id, set()).update(role_list)
      return group_id_role_dict

    security.declarePrivate('getFilteredRoleListFor')
    def getFilteredRoleListFor(self, ob=None):
      """Return all role generators applicable to the object."""
125
      ec = None # createExpressionContext is slow so we call it only if needed
Julien Muchembled's avatar
Julien Muchembled committed
126
      for role in self.getRoleInformationList():
127 128
        if ec is None:
          ec = createExpressionContext(ob)
Julien Muchembled's avatar
Julien Muchembled committed
129 130 131 132 133 134
        if role.testCondition(ec):
          yield role

      # Return also explicit local roles defined as subobjects of the document
      if getattr(aq_base(ob), 'isPrincipiaFolderish', 0) and \
         self.allowType('Role Definition'):
135
        for role in ob.objectValues(spec='ERP5 Role Definition'):
Julien Muchembled's avatar
Julien Muchembled committed
136 137 138 139 140 141 142
          if role.getRoleName():
            yield role

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getRoleInformationList')
    def getRoleInformationList(self):
      """Return all Role Information objects stored on this portal type"""
143
      return self.objectValues(meta_type='ERP5 Role Information')
Julien Muchembled's avatar
Julien Muchembled committed
144

145 146
    security.declareProtected(Permissions.ModifyPortalContent,
                              'updateRoleMapping')
Julien Muchembled's avatar
Julien Muchembled committed
147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175
    def updateRoleMapping(self, REQUEST=None, form_id=''):
      """Update the local roles in existing objects.
         XXX This should be implemented the same way as
             ERP5Site_checkCatalogTable (cf erp5_administration).
      """
      portal = self.getPortalObject()
      update_role_tag = self.__class__.__name__ + ".updateRoleMapping"

      object_list = [x.path for x in
                     portal.portal_catalog(portal_type=self.id, limit=None)]
      object_list_len = len(object_list)
      # We need to use activities in order to make sure it will
      # work for an important number of objects
      activate = portal.portal_activities.activate
      for i in xrange(0, object_list_len, 100):
        current_path_list = object_list[i:i+100]
        activate(activity='SQLQueue', priority=3, tag=update_role_tag) \
        .callMethodOnObjectList(current_path_list,
                                'updateLocalRolesOnSecurityGroups',
                                reindex=False)
        activate(activity='SQLQueue', priority=3, after_tag=update_role_tag) \
        .callMethodOnObjectList(current_path_list,
                                'reindexObjectSecurity')

      if REQUEST is not None:
        message = '%d objects updated' % object_list_len
        return REQUEST.RESPONSE.redirect('%s/%s?portal_status_message=%s'
          % (self.absolute_url_path(), form_id, message))

Julien Muchembled's avatar
Julien Muchembled committed
176 177
    def _importRole(self, role_property_dict):
      """Import a role from a BT or from an old portal type"""
178 179 180
      import erp5
      RoleInformation = getattr(erp5.portal_type, 'Role Information')
      role = RoleInformation(self.generateNewId())
Julien Muchembled's avatar
Julien Muchembled committed
181 182 183 184 185 186 187 188 189 190 191 192
      for k, v in role_property_dict.iteritems():
        if k == 'condition':
          if isinstance(v, Expression):
            v = v.text
          if not v:
            continue
          v = Expression(v)
        elif k == 'priority':
          continue
        elif k == 'id':
          k, v = 'role_name', tuple(x.strip() for x in v.split(';'))
        elif k in ('base_category', 'category'):
193
          k, v = 'role_' + k, tuple(y for y in (x.strip() for x in v) if y)
Julien Muchembled's avatar
Julien Muchembled committed
194 195 196 197
        elif k == 'base_category_script':
          k = 'role_base_category_script_id'
        setattr(role, k, v)
      role.uid = None
198
      return self[self._setObject(role.id, role, set_owner=0)]
Julien Muchembled's avatar
Julien Muchembled committed
199

200 201
class ERP5TypeInformation(XMLObject,
                          FactoryTypeInformation,
Julien Muchembled's avatar
Julien Muchembled committed
202
                          LocalRoleAssignorMixIn,
203
                          TranslationProviderBase):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
204
    """
205
    ERP5 Types are based on FactoryTypeInformation
Jean-Paul Smets's avatar
Jean-Paul Smets committed
206 207

    The most important feature of ERP5Types is programmable acquisition which
208
    allows defining attributes which are acquired through categories.
Jean-Paul Smets's avatar
Jean-Paul Smets committed
209 210

    Another feature is to define the way attributes are stored (localy,
211
    database, etc.). This allows combining multiple attribute sources
212 213
    in a single object. This feature will be in reality implemented
    through PropertySheet classes (TALES expressions)
214 215 216 217 218

    TODO:
    - add a warning for legacy groups which are no longer used
    - move groups of portal types to categories
      (now that we have portal types of portal types)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
219 220
    """

221 222
    portal_type = 'Base Type'
    meta_type = 'ERP5 Base Type'
223

Jean-Paul Smets's avatar
Jean-Paul Smets committed
224
    security = ClassSecurityInfo()
225
    security.declareObjectProtected(Permissions.AccessContentsInformation)
226

227
    zope.interface.implements(interfaces.IActionContainer)
228

229
    # Declarative properties
Julien Muchembled's avatar
Julien Muchembled committed
230
    property_sheets = ( PropertySheet.BaseType, )
231

232
    acquire_local_roles = False
233 234 235 236
    property_sheet_list = ()
    base_category_list = ()
    init_script = ''
    product = 'ERP5Type'
237
    hidden_content_type_list = ()
238
    permission = ''
239

240 241
    def __init__(self, id, **kw):
      XMLObject.__init__(self, id)
242 243 244 245
      if 'meta_type' in kw:
        kw.setdefault('content_meta_type', kw.pop('meta_type'))
      if 'icon' in kw:
        kw.setdefault('content_icon', kw.pop('icon'))
246
      self.__dict__.update(kw)
247

Yoshinori Okuji's avatar
Yoshinori Okuji committed
248
    # Groups are used to classify portal types (e.g. resource).
249 250 251 252 253 254 255 256 257 258 259
    # IMPLEMENTATION NOTE
    # The current implementation is practical but not modular at all
    # Providing extensible document groups will be desirable at some point
    # Implementation could consist for example in allowing to define
    # the list of groups at the level of ERP5Site either
    # with a default value or with a user specified value. Accessors
    # to portal type groups should then be generated dynamically through
    # _aq_dynamic. This would provide limitless group definition.
    # The main issue in providing too much flexibility at this level
    # is to reduce standardisation. New groups should therefore be handled
    # with great care.
Yoshinori Okuji's avatar
Yoshinori Okuji committed
260
    defined_group_list = (
261
      # Framework
262
      'alarm', 'rule', 'constraint',
263
      # ERP5 UBM (5 Classes)
264
      'resource', 'node', 'item',
265 266 267 268
      'path', # movement is generated from all *_movement group above.
      # Documents need to have portal types associated to them
      # just to be able to spawn temporary objects with the same behavior
      'abstract',
269
      # Trade
270
      'discount', 'payment_condition', 'payment_node',
271
      'supply', 'supply_path', 'inventory_movement',
272 273
      'delivery', 'delivery_movement',
      'order', 'order_movement',
274
      'open_order',
275
      'container', 'container_line',
Sebastien Robin's avatar
Sebastien Robin committed
276
      'inventory',
277
      # Different Aspects of Supplier-Customer relation
278
      'sale', 'purchase', 'internal',
279 280
      # PDM
      'transformation', 'variation', 'sub_variation',
281
      'product', 'service', 'model_path',
282 283 284 285 286 287
      # Accounting
      'accounting_transaction', 'accounting_movement',
      'invoice', 'invoice_movement', 'balance_transaction_line',
      # CRM
      'event', 'ticket',
      # DMS
288
      'document', 'web_document', 'file_document', 'embedded_document',
289
      'recent_document', 'my_document', 'template_document',
290
      'crawler_index',
291
      # Solvers and simulation
Sebastien Robin's avatar
Sebastien Robin committed
292
      'divergence_tester', 'target_solver', 'delivery_solver',
293
      'amount_generator',  'amount_generator_line', 'amount_generator_cell',
294
      # Business Processes
295
      'trade_model_path', 'business_link', 'business_process',
296 297 298
      # Movement Group
      'movement_group',
      # Calendar
299
      'calendar_period',
300 301
      # Project
      'project',
302 303
      # budget
      'budget_variation',
304 305
      # Module
      'module',
306 307
      # Base
      'entity',
308 309
      # LEGACY - needs a warning - XXX-JPS
      'tax_movement',
Yoshinori Okuji's avatar
Yoshinori Okuji committed
310 311 312
    )
    group_list = ()

313 314 315 316 317 318 319
    security.declarePublic('allowType')
    def allowType(self, contentType):
      """Test if objects of 'self' can contain objects of 'contentType'
      """
      return (not self.getTypeFilterContentType()
              or contentType in self.getTypeAllowedContentTypeList())

Jean-Paul Smets's avatar
Jean-Paul Smets committed
320 321 322 323
    #
    #   Acquisition editing interface
    #

324 325 326 327 328 329
    security.declarePrivate('_guessMethodAliases')
    def _guessMethodAliases(self):
        """ Override this method to disable Method Aliases in ERP5.
        """
        self.setMethodAliases({})
        return 1
330

331 332 333 334 335 336 337 338
    # security is declared by superclass
    def queryMethodID(self, alias, default=None, context=None):
        """ Query method ID by alias.
        
        In ERP5 we don't do aliases.
        """
        return default

339 340 341 342
    security.declarePublic('isConstructionAllowed')
    def isConstructionAllowed(self, container):
      """Test if user is allowed to create an instance in the given container
      """
Nicolas Dumazet's avatar
Nicolas Dumazet committed
343 344 345 346 347 348 349 350
      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
      """
351
      return self.constructInstance(container, id, temp_object=1, *args, **kw)
352

353
    security.declarePublic('constructInstance')
354
    def constructInstance(self, container, id, created_by_builder=0,
Nicolas Delaby's avatar
Nicolas Delaby committed
355
                          temp_object=0, compute_local_role=None,
356 357
                          notify_workflow=True, is_indexable=None,
                          activate_kw=None, reindex_kw=None, **kw):
358 359 360 361 362 363
      """
      Build a "bare" instance of the appropriate type in
      'container', using 'id' as its id.
      Call the init_script for the portal_type.
      Returns the object.
      """
Nicolas Delaby's avatar
Nicolas Delaby committed
364
      if compute_local_role is None:
365
        # If temp object, set to False
Nicolas Delaby's avatar
Nicolas Delaby committed
366
        compute_local_role = not temp_object
Nicolas Dumazet's avatar
Nicolas Dumazet committed
367 368 369 370 371 372 373 374 375 376 377
      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)
378 379 380 381
        # Setup only Owner local role on Document like
        # container._setObject(set_owner=True) does.
        user_id = getCurrentUserIdOrAnonymousToken()
        ob.manage_setLocalRoles(user_id, ['Owner'])
Nicolas Dumazet's avatar
Nicolas Dumazet committed
382 383 384 385 386 387 388 389 390 391 392 393 394
      else:
        if activate_kw is not None:
          ob.__of__(container).setDefaultActivateParameters(**activate_kw)
        if reindex_kw is not None:
          ob.__of__(container).setDefaultReindexParameters(**reindex_kw)
        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()

395 396
      # Portal type has to be set before setting other attributes
      # in order to initialize aq_dynamic
397
      ob.portal_type = self.getId()
398

399
      if compute_local_role:
400
        # Do not reindex object because it's already done by manage_afterAdd
401
        self.updateLocalRolesOnDocument(ob, reindex=False)
402

403
      if notify_workflow:
404 405
        # notify workflow after generating local roles, in order to prevent
        # Unauthorized error on transition's condition
Nicolas Dumazet's avatar
Nicolas Dumazet committed
406
        workflow_tool = getToolByName(portal, 'portal_workflow', None)
407 408 409
        if workflow_tool is not None:
          for workflow in workflow_tool.getWorkflowsFor(ob):
            workflow.notifyCreated(ob)
410

411
      if not temp_object:
412 413 414
        init_script = self.getTypeInitScriptId()
        if init_script:
          # Acquire the init script in the context of this object
415 416 417 418 419
          getattr(ob, init_script)(created_by_builder=created_by_builder,
                                   edit_kw=kw)

      if kw:
        ob._edit(force_update=1, **kw)
420

421 422 423
      return ob

    def _getPropertyHolder(self):
424 425
      import erp5.portal_type as module
      return getattr(module, self.getId())
426

427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447
    security.declarePrivate('updatePropertySheetDefinitionDict')
    def updatePropertySheetDefinitionDict(self, definition_dict):
      for property_sheet_name in self.getTypePropertySheetList():
        base = getattr(PropertySheet, property_sheet_name, None)
        if base is not None:
          for list_name, property_list in definition_dict.items():
            try:
              property_list += getattr(base, list_name, ())
            except TypeError:
              raise ValueError("%s is not a list for %s" % (list_name, base))
      if '_categories' in definition_dict:
        definition_dict['_categories'] += self.getTypeBaseCategoryList()

    # The following 2 methods are needed before there are generated.

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getTypePropertySheetList')
    def getTypePropertySheetList(self):
      """Getter for 'type_property_sheet' property"""
      return list(self.property_sheet_list)

448 449 450 451 452 453
    security.declareProtected(Permissions.AccessContentsInformation,
                              'getTypeBaseCategoryList')
    def getTypeBaseCategoryList(self):
      """Getter for 'type_base_category' property"""
      return list(self.base_category_list)

454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470
    def getTypePropertySheetValueList(self):
      type_property_sheet_list = self.getTypePropertySheetList()
      if not type_property_sheet_list:
        return []

      return getPropertySheetValueList(self.getPortalObject(),
                                       type_property_sheet_list)

    def getAccessorHolderList(self):
      type_property_sheet_value_list = self.getTypePropertySheetValueList()
      if not type_property_sheet_value_list:
        return []

      return getAccessorHolderList(self.getPortalObject(),
                                   self.getPortalType(),
                                   type_property_sheet_value_list)

471 472 473 474 475 476
    # XXX these methods, _baseGetTypeClass, getTypeMixinList, and
    # getTypeInterfaceList, are required for a bootstrap issue that
    # the portal type class Base Type is required for _aq_dynamic on
    # Base Type. So surpress calling _aq_dynamic when obtaining information
    # required for generating a portal type class by declaring these methods
    # explicitly.
477 478 479
    def _baseGetTypeClass(self):
      return getattr(aq_base(self), 'type_class', None)

480 481 482 483 484
    security.declareProtected(Permissions.AccessContentsInformation,
                              'getTypeFactoryMethodId')
    def getTypeFactoryMethodId(self):
      return getattr(aq_base(self), 'factory', ())

485 486 487 488 489 490 491 492 493 494
    security.declareProtected(Permissions.AccessContentsInformation,
                              'getTypeMixinList')
    def getTypeMixinList(self):
      return getattr(aq_base(self), 'type_mixin', ())

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getTypeInterfaceList')
    def getTypeInterfaceList(self):
      return getattr(aq_base(self), 'type_interface', ())

495 496 497 498 499 500 501 502 503 504 505
    security.declareProtected(Permissions.AccessContentsInformation,
                              'getTypeClass')
    def getTypeClass(self):
      """Getter for type_class"""
      base = self._baseGetTypeClass()
      if base is None:
        # backwards compatibility: if the object has no
        # new-style type class, use the oldstyle factory attribute
        init_script = self.getTypeFactoryMethodId()
        if init_script and init_script.startswith('add'):
          base = init_script[3:]
506 507 508
          # and of course migrate the property,
          # avoiding any useless interaction/reindexation
          self.type_class = base
509 510
      return base

511
    security.declareProtected(Permissions.AccessContentsInformation,
512 513 514
                              'getInstanceBaseCategoryList')
    def getInstanceBaseCategoryList(self):
      """ Return all base categories of the portal type """
515
      return list(self._getPropertyHolder()._categories)
516

517 518 519 520
    security.declareProtected(Permissions.AccessContentsInformation,
                              'getInstancePropertyAndBaseCategoryList')
    def getInstancePropertyAndBaseCategoryList(self):
      """Return all the properties and base categories of the portal type. """
521
      # PropertHolder._properties doesn't contain 'content' properties.
522
      ob = self.constructTempInstance(self, self.getId())
523 524 525
      property_list = list(getattr(ob.__class__, '_properties', []))
      self.updatePropertySheetDefinitionDict({'_properties': property_list})
      for property_sheet in getClassPropertyList(ob.__class__):
526
        property_list += getattr(property_sheet, '_properties', () )
527

528 529 530 531 532 533 534 535
      return_set = set()
      for property in property_list:
        if property['type'] == 'content':
          for suffix in property['acquired_property_id']:
            return_set.add(property['id'] + '_' + suffix)
        else:
          return_set.add(property['id'])
      for category in ob.getBaseCategoryList():
536 537
        return_set.add(category)
        return_set.add(category + '_free_text')
538 539 540 541

      # XXX Can't return set to restricted code in Zope 2.8.
      return list(return_set)

542
    security.declareProtected(Permissions.AccessContentsInformation,
543
                              'getInstancePropertyMap')
544 545 546 547
    def getInstancePropertyMap(self):
      """
      Returns the list of properties which are specific to the portal type.
      """
548
      return self.__class__.propertyMap()
549

550 551
    security.declareProtected(Permissions.AccessContentsInformation,
                              'PrincipiaSearchSource')
552
    def PrincipiaSearchSource(self):
Julien Muchembled's avatar
Julien Muchembled committed
553
      """Return keywords for "Find" tab in ZMI"""
554 555 556 557
      search_source_list = [self.getId(),
                            self.getTypeFactoryMethodId(),
                            self.getTypeAddPermission(),
                            self.getTypeInitScriptId()]
558 559
      search_source_list += self.getTypePropertySheetList()
      search_source_list += self.getTypeBaseCategoryList()
560
      return ' '.join(filter(None, search_source_list))
561

562 563
    security.declarePrivate('getDefaultViewFor')
    def getDefaultViewFor(self, ob, view='view'):
564 565
      """Return the object that renders the default view for the given object
      """
566
      ec = createExpressionContext(ob)
567
      other_action = None
568
      for action in self.getActionList():
569 570
        if action['id'] == view or (action['category'] is not None and
                                    action['category'].endswith('_' + view)):
571 572 573
          if action.test(ec):
            break
        elif other_action is None:
574 575
          # In case that "view" (or "list") action is not present or not allowed,
          # find something that's allowed (of the same category, if possible).
576 577
          if action.test(ec):
            other_action = action
578
      else:
579 580 581 582 583
        action = other_action
        if action is None:
          raise AccessControl_Unauthorized(
            'No accessible views available for %r' % ob.getPath())

584
      target = action.cook(ec)['url'].strip().split(ec.vars['object_url'])[-1]
585 586 587 588 589
      if target.startswith('/'):
          target = target[1:]
      __traceback_info__ = self.getId(), target
      return ob.restrictedTraverse(target)

590 591 592 593 594 595 596 597 598
    security.declarePrivate('getCachedActionList')
    def getCacheableActionList(self):
      """Return a cacheable list of enabled actions"""
      return [action.getCacheableAction()
              for action in self.getActionInformationList()
              if action.isVisible()]

    def _getActionList(self):
      action_list = self.getCacheableActionList()
599 600 601 602 603
      # This sort is a duplicate of calculation with what is done
      # on portal_actions.listFilteredActionsFor . But getDefaultViewFor
      # needs the sort here. This needs to be reviewed, because it is possible
      # to define in portal_actions some actions that will have higher
      # priorities than actions defined on portal types
604 605 606 607
      action_list.sort(key=lambda x:x['priority'])
      return action_list
    _getActionList = CachingMethod(_getActionList,
      id='getActionList',
608
      cache_factory='erp5_content_long',
609
      cache_id_generator=lambda method_id, *args: method_id)
610

611 612 613 614
    security.declarePrivate('getActionList')
    def getActionList(self):
      """Return the list of enabled actions from cache, sorted by priority"""
      return self._getActionList(self, scope=self.id)
615 616

    security.declareProtected(Permissions.ModifyPortalContent,
617 618
                              'clearGetActionListCache')
    def clearGetActionListCache(self):
619
      """Clear a cache of _getRawActionInformationList."""
620
      self._getActionList.delete(scope=self.id)
621

622 623
    security.declareProtected(Permissions.AccessContentsInformation,
                              'getActionInformationList')
624
    def getActionInformationList(self):
625
      """Return all Action Information objects stored on this portal type"""
626
      return self.objectValues(meta_type='ERP5 Action Information')
627

628
    def getIcon(self):
629 630 631 632 633 634
      try:
        return self.getTypeIcon()
      except AttributeError:
        # do not fail if the property is missing: getTypeIcon is used in the ZMI
        # and we always want to display the ZMI no matter what
        return ''
635 636

    def getTypeInfo(self, *args):
637
      if args:
638
        return self.getParentValue().getTypeInfo(*args)
639
      return XMLObject.getTypeInfo(self)
640 641

    security.declareProtected(Permissions.AccessContentsInformation,
642 643
                              'getAvailablePropertySheetList')
    def getAvailablePropertySheetList(self):
644 645 646 647 648 649 650
      property_sheet_set = set([k for k in PropertySheet.__dict__
                                if not k.startswith('__')])

      property_sheet_tool = self.getPortalObject().portal_property_sheets
      property_sheet_set.update(property_sheet_tool.objectIds())

      return sorted(property_sheet_set)
651 652

    security.declareProtected(Permissions.AccessContentsInformation,
653 654
                              'getAvailableConstraintList')
    def getAvailableConstraintList(self):
655
      return sorted(k for k in Constraint.__dict__
656
                      if k != 'Constraint' and not k.startswith('__'))
657 658

    security.declareProtected(Permissions.AccessContentsInformation,
659 660 661
                              'getAvailableGroupList')
    def getAvailableGroupList(self):
      return sorted(self.defined_group_list)
662 663

    security.declareProtected(Permissions.AccessContentsInformation,
664 665 666
                              'getAvailableBaseCategoryList')
    def getAvailableBaseCategoryList(self):
        return sorted(self._getCategoryTool().getBaseCategoryList())
667

668
    #
669
    # XXX CMF compatibility
670
    #
671

672 673
    security.declareProtected(Permissions.ManagePortal,
                              'setPropertySheetList')
674
    @deprecated
675 676 677 678 679
    def setPropertySheetList(self, property_sheet_list):
      self._setTypePropertySheetList(property_sheet_list)

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getHiddenContentTypeList')
680
    @deprecated
681 682 683 684 685
    def getHiddenContentTypeList(self):
      return self.getTypeHiddenContentTypeList(())

    # Compatibitility code for actions

686
    security.declareProtected(Permissions.AddPortalContent, 'addAction')
687
    @deprecated
688 689 690 691 692
    def addAction(self, id, name, action, condition, permission, category,
                  icon=None, visible=1, priority=1.0, REQUEST=None,
                  description=None):
      if isinstance(permission, basestring):
        permission = permission,
693 694
      if isinstance(action, str) and action[:7] not in ('string:', 'python:'):
        value = 'string:${object_url}/' + value
695 696 697 698 699 700 701 702 703 704 705 706 707
      self.newContent(portal_type='Action Information',
                      reference=id,
                      title=name,
                      action=action,
                      condition=condition,
                      permission_list=permission,
                      action_type=category,
                      icon=icon,
                      visible=visible,
                      float_index=priority,
                      description=description)

    security.declareProtected(Permissions.ModifyPortalContent, 'deleteActions')
708
    @deprecated
709 710 711 712
    def deleteActions(self, selections=(), REQUEST=None):
      action_list = self.listActions()
      self.manage_delObjects([action_list[x].id for x in selections])

713
    security.declarePrivate('listActions')
714
    @deprecated
715 716
    def listActions(self, info=None, object=None):
      """ List all the actions defined by a provider."""
717
      return sorted(self.getActionInformationList(),
718
                    key=lambda x: (x.getFloatIndex(), x.getId()))
719 720

    def _importOldAction(self, old_action):
Julien Muchembled's avatar
Julien Muchembled committed
721 722 723 724
      """Convert a CMF action to an ERP5 action

      This is used to update an existing site or to import a BT.
      """
725 726
      import erp5.portal_type
      ActionInformation = getattr(erp5.portal_type, 'Action Information')
727 728
      old_action = old_action.__getstate__()
      action_type = old_action.pop('category', None)
729
      action = ActionInformation(self.generateNewId())
730 731
      for k, v in old_action.iteritems():
        if k in ('action', 'condition', 'icon'):
732
          if not v:
733 734
            continue
          v = v.__class__(v.text)
735 736
        setattr(action, {'id': 'reference',
                         'priority': 'float_index',
737
                         'permissions': 'action_permission',
738
                        }.get(k, k), v)
739 740 741 742 743 744
      action.uid = None
      action = self[self._setObject(action.id, action, set_owner=0)]
      if action_type:
        action._setCategoryMembership('action_type', action_type)
      return action

745
    def _exportOldAction(self, action):
Julien Muchembled's avatar
Julien Muchembled committed
746 747 748 749
      """Convert an ERP5 action to a CMF action

      This is used to export a BT.
      """
750
      from Products.CMFCore.ActionInformation import ActionInformation
751 752
      old_action = ActionInformation(action.reference,
        category=action.getActionType(),
753 754 755 756
        priority=action.getFloatIndex(),
        permissions=tuple(action.getActionPermissionList()))
      for k, v in action.__dict__.iteritems():
        if k in ('action', 'condition', 'icon'):
757 758
          if not v:
            continue
759
          v = v.__class__(v.text)
760
        elif k in ('id', 'float_index', 'action_permission', 'reference'):
761 762 763 764
          continue
        setattr(old_action, k, v)
      return old_action

Jean-Paul Smets's avatar
Jean-Paul Smets committed
765
InitializeClass( ERP5TypeInformation )