BusinessManager.py 39.4 KB
Newer Older
1 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
# -*- coding: utf-8 -*-
##############################################################################
#
# Copyright (c) 2017 Nexedi SARL and Contributors. All Rights Reserved.
#                    Ayush-Tiwari <ayush.tiwari@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 31 32 33 34 35 36
import gc
import os
import posixpath
import transaction
import imghdr
import tarfile
import time
37 38 39
import hashlib
import fnmatch
import re
40
import threading
41
import pprint
42

43 44 45 46 47 48 49
from copy import deepcopy
from collections import defaultdict
from cStringIO import StringIO
from OFS.Image import Pdata
from lxml.etree import parse
from urllib import quote, unquote
from OFS import SimpleItem, XMLExportImport
50
from datetime import datetime
51 52
from itertools import chain
from operator import attrgetter
53 54 55 56 57
from persistent.list import PersistentList
from AccessControl import ClassSecurityInfo, Unauthorized, getSecurityManager
from Acquisition import Implicit, aq_base, aq_inner, aq_parent
from zLOG import LOG, INFO, WARNING

58
from Products.ERP5Type.XMLObject import XMLObject
59
from Products.ERP5Type.Core.Folder import Folder
60 61 62
from Products.CMFCore.utils import getToolByName
from Products.PythonScripts.PythonScript import PythonScript
from Products.ERP5Type.dynamic.lazy_class import ERP5BaseBroken
63
from Products.ERP5Type.Globals import Persistent, PersistentMapping
64 65
from Products.ERP5Type import Permissions, PropertySheet, interfaces
from Products.ERP5Type.Globals import InitializeClass
66
from Products.ERP5Type.TransactionalVariable import getTransactionalVariable
67
from Products.ERP5Type.patches.ppml import importXML
68
from Products.ERP5Type.Accessor.Constant import PropertyGetter as ConstantGetter
69

70 71 72 73 74 75 76 77 78 79 80 81 82 83
customImporters = {
    XMLExportImport.magic: importXML,
    }

CACHE_DATABASE_PATH = None
try:
  if int(os.getenv('ERP5_BT5_CACHE', 0)):
    from App.config import getConfiguration
    import gdbm
    instancehome = getConfiguration().instancehome
    CACHE_DATABASE_PATH = os.path.join(instancehome, 'bt5cache.db')
except TypeError:
  pass
cache_database = threading.local()
84 85
_MARKER = []

86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127
SEPARATELY_EXPORTED_PROPERTY_DICT = {
  # For objects whose class name is 'class_name', the 'property_name'
  # attribute is removed from the XML export, and the value is exported in a
  # separate file, with extension specified by 'extension'.
  # 'extension' must be None for auto-detection.
  #
  # class_name: (extension, unicode_data, property_name),
  "Document Component":  ("py",   0, "text_content"),
  "DTMLDocument":        (None,   0, "raw"),
  "DTMLMethod":          (None,   0, "raw"),
  "Extension Component": ("py",   0, "text_content"),
  "File":                (None,   0, "data"),
  "Image":               (None,   0, "data"),
  "OOoTemplate":         ("oot",  1, "_text"),
  "PDF":                 ("pdf",  0, "data"),
  "PDFForm":             ("pdf",  0, "data"),
  "Python Script":       ("py",   0, "_body"),
  "PythonScript":        ("py",   0, "_body"),
  "Spreadsheet":         (None,   0, "data"),
  "SQL":                 ("sql",  0, "src"),
  "SQL Method":          ("sql",  0, "src"),
  "Test Component":      ("py",   0, "text_content"),
  "Test Page":           (None,   0, "text_content"),
  "Web Page":            (None,   0, "text_content"),
  "Web Script":          (None,   0, "text_content"),
  "Web Style":           (None,   0, "text_content"),
  "ZopePageTemplate":    ("zpt",  1, "_text"),
}


def _delObjectWithoutHook(obj, id):
  """OFS.ObjectManager._delObject without calling manage_beforeDelete."""
  ob = obj._getOb(id)
  if obj._objects:
    obj._objects = tuple([i for i in obj._objects if i['id'] != id])
  obj._delOb(id)
  try:
    ob._v__object_deleted__ = 1
  except:
    pass


128 129 130 131 132 133 134
def _recursiveRemoveUid(obj):
  """Recusivly set uid to None, to prevent (un)indexing.
  This is used to prevent unindexing real objects when we delete subobjects on
  a copy of this object.
  """
  if getattr(aq_base(obj), 'uid', _MARKER) is not _MARKER:
    obj.uid = None
135 136 137 138
    # Make all objects and sub-object un-indexable
    # XXX: Should be moved into another function or change the name and desc
    # of this function
    obj.isIndexable = ConstantGetter('isIndexable', value=False)
139 140 141
  for subobj in obj.objectValues():
    _recursiveRemoveUid(subobj)

142

143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158
# New BusinessItem addition function
def manage_addBusinessItem(self, item_path='', item_sign=1, item_layer=0, *args, **kw):
  # Create BusinessItem object container
  c = BusinessItem(item_path, item_sign, item_layer)

  return c


# New BusinessPropertyItem addition function
def manage_addBusinessPropertyItem(self, item_path='', item_sign=1, item_layer=0, *args, **kw):
  # Create BusinessPathItem object container
  c = BusinessPropertyItem(item_path, item_sign, item_layer)

  return c


159
class BusinessManager(Folder):
160 161 162 163 164 165 166

  """Business Manager is responsible for saving objects and properties in
  an ERP5Site. Everything will be saved just via path"""

  meta_type = 'ERP5 Business Manager'
  portal_type = 'Business Manager'
  add_permission = Permissions.AddPortalContent
167
  allowed_types = ('Business Item', 'Business Property Item',)
168 169 170 171 172

  # Declarative security
  security = ClassSecurityInfo()
  security.declareObjectProtected(Permissions.AccessContentsInformation)

173
  template_format_version = 3
174 175
  status = 'uninstalled'

176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192
  # Factory Type Information
  factory_type_information = \
    {    'id'             : portal_type
       , 'meta_type'      : meta_type
       , 'icon'           : 'file_icon.gif'
       , 'product'        : 'ERP5Type'
       , 'factory'        : ''
       , 'type_class'     : 'BusinessManager'
       , 'immediate_view' : 'BusinessManager_view'
       , 'allow_discussion'     : 1
       , 'allowed_content_types': ('Business Item',
                                    'Business Property Item',
                                    )
       , 'filter_content_types' : 1
    }


193 194 195 196 197
  # Declarative security
  security = ClassSecurityInfo()
  security.declareObjectProtected(Permissions.AccessContentsInformation)

  # Declarative properties
198 199 200 201 202 203
  property_sheets = (
                      PropertySheet.Base,
                      PropertySheet.XMLObject,
                      PropertySheet.SimpleItem,
                      PropertySheet.CategoryCore,
                      PropertySheet.Version,
204 205
                    )

206
  def getShortRevision(self):
207
    return None
208

209 210 211 212 213 214 215 216 217 218
  def getStatus(self):
    """
    installed       :BI(s) are installed in OFS.
    uninstalled     :Values for BI(s) at the current version removed from OFS.
    reduced         :No two BI of same path exist at different layers.
    flatenned       :BI(s) should be at the zeroth layer.
    built           :BI(s) do have values from the OS DB.
    """
    return self.status

219 220 221
  def getInstallationState(self):
    return self.status

222 223 224 225 226 227 228 229
  def setStatus(self, status=None):
    if not status:
      raise ValueError, 'No status provided'
    else:
      self.status = status

  def applytoERP5(self, DB):
    """Apply the flattened/reduced Business Manager to the DB"""
230
    portal = self.getPortalObject()
231 232
    pass

233 234 235 236 237 238
  def _setTitle(self, value):
    """
    Override required due to bootstrap
    """
    self.title = value

239
  def getPathItemList(self):
240
    return self.objectValues()
241

242
  # XXX: Change into property
243
  security.declareProtected(Permissions.ManagePortal, 'getTemplateFormatVersion')
244 245 246
  def getTemplateFormatVersion(self):
    return self.template_format_version

247
  # XXX: Change into property
248 249 250 251 252 253 254
  def _setTemplateFormatVersion(self, value):
    self.template_format_version = int(value)

  def propertyMap(self):
    prop_map = super(BusinessManager, self).propertyMap()
    final_prop_map = prop_map+self._properties
    return final_prop_map
255

256
  def export(self, path=None, **kw):
257
    """
258
    Export the object as zexp file
259
    """
260
    if not self.getStatus() == 'built':
261
      raise ValueError, 'Manager not built properly'
262
    f = StringIO()
263

264
    self._p_jar.exportFile(self._p_oid, f)
265

266 267 268 269 270 271 272
    # XXX: Improve naming
    name = self.getTitle()
    name = posixpath.join(path, name)
    # XXX required due to overuse of os.path
    name = name.replace('\\', '/').replace(':', '/')
    name = quote(name + '.zexp')
    obj_path = name.replace('/', os.sep)
273

274 275
    f.seek(0)
    obj = f.read()
276

277 278 279 280 281 282 283 284
    object_path = os.path.join(path, obj_path)
    path = os.path.dirname(object_path)
    os.path.exists(path) or os.makedirs(path)
    f = open(object_path, 'wb')
    try:
      f.write(obj)
    finally:
      f.close()
285

286 287 288
  security.declareProtected(Permissions.ManagePortal, 'importFile')
  def importFile(self, path):
    """
289
      Import Business Manager object and all attribute to current BM itself
290
    """
291 292
    connection = self.aq_parent._p_jar
    file = open(path, 'rb')
293 294 295
    imported_manager = connection.importFile(file)
    self.title = imported_manager.title
    for obj in imported_manager.objectValues():
296
      delattr(obj, '__ac_local_roles__')
297
      new_id = self.generateNewId()
298 299 300 301 302
      # XXX: Donot merge this, needed just for migrated erp5_core
      try:
        self._setObject(new_id, aq_base(obj))
      except Exception:
        pass
303
      obj.isIndexable = ConstantGetter('isIndexable', value=False)
304

305
  def __add__(self, other):
306 307 308
    """
    Adds the Business Item objects for the given Business Manager objects
    """
309
    # XXX: Still to define
310 311 312
    return self

  __radd__ = __add__
313

314 315 316 317
  def __sub__(self, other):
    """
    Override subtract to find difference b/w the values in different cases.
    """
318
    # Create the sha list for all path item list available in current object
319
    sha_list = [item.sha for item in self._path_item_list]
320
    # Reverse the sign of Business Item objects for the old Business Manager
321
    # Trying comparing/subtracting ZODB with old installed object
322
    for path_item in other._path_item_list:
323
      if path_item.sha in sha_list:
324 325
        self._path_item_list = [item for item
                                in self._path_item_list
326
                                if item.sha != path_item.sha]
327
      else:
328
        path_item.sign = -1
329 330
        self._path_item_list.append(path_item)

331 332
    return self

333
  __rsub__ = __sub__
334

335
  security.declareProtected(Permissions.ManagePortal, 'storeTemplateData')
336
  def storeTemplateData(self, isBuild=False, **kw):
337
    """
338 339 340
    Store data for objects in the ERP5.
    Create Business Item sub-objects after resolving the paths. Also, add
    layers to all Business Item objects
341
    """
342
    portal = self.getPortalObject()
343
    LOG('Business Manager', INFO, 'Storing Manager Data')
344 345 346 347 348 349 350 351 352 353 354 355 356 357

    to_delete_id_list = []
    for item in self.objectValues():

      # Only try to resolve the Business Item objects
      if item.getPortalType() != 'Business Item':
        continue

      item_path = item.getProperty('item_path')
      # Resolve the path and update sub-objects lists
      path_list = self._resolvePath(portal, [], item_path.split('/'))

      if len(path_list) == 1 and path_list[0] == item_path:
        continue
358
      else:
359 360 361 362 363 364 365 366 367 368
        item_sign = item.getProperty('item_sign')
        item_layer = item.getProperty('item_layer')
        # Create new Business Item objects with path in path_list and sign and
        # layer same as that of the path used for resolving to new paths.
        for path in path_list:
          path_item = self.newContent(portal_type='Business Item')
          path_item.edit(
            item_path=path,
            item_sign=item_sign,
            item_layer=item_layer
369
            )
370 371 372
        # Add Id of BusinessItem to be deleted as we do already have resolved
        # path and new sub-objects based on resolved paths
        to_delete_id_list.append(item.getId())
373

374 375 376
    # Now, delete the original Business Item(s) sub-object as we do have
    # Business Item created from resolved paths
    self.manage_delObjects(ids=to_delete_id_list)
377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407

  def _resolvePath(self, folder, relative_url_list, id_list):
    """
      For Business Manager, we expect to resolve the path incase we face
      paths which expect to include sub-objects.
      For example: 'portal_catalog/erp5_mysql_innodb/**' should only consider
      the sub-objects of the object mentioned, and create separate BusinessItem
      objects for all of them.

      This method calls itself recursively.

      The folder is the current object which contains sub-objects.
      The list of ids are path components. If the list is empty,
      the current folder is valid.
    """
    if len(id_list) == 0:
      return ['/'.join(relative_url_list)]
    id = id_list[0]
    if re.search('[\*\?\[\]]', id) is None:
      # If the id has no meta character, do not have to check all objects.
      obj = folder._getOb(id, None)
      if obj is None:
        raise AttributeError, "Could not resolve '%s' during business template processing." % id
      return self._resolvePath(obj, relative_url_list + [id], id_list[1:])
    path_list = []
    for object_id in fnmatch.filter(folder.objectIds(), id):
      if object_id != "":
        path_list.extend(self._resolvePath(
            folder._getOb(object_id),
            relative_url_list + [object_id], id_list[1:]))
    return path_list
408

409 410
  def getPathList(self):
    path_list = []
411
    for item in self.objectValues():
412
      path_list.append(item.getProperty('item_path'))
413 414 415 416 417
    return path_list

  def getPathItemDict(self):
    path_item_dict = {}
    # TODO: Handle error for BM with multiple items at same path
418 419
    for item in self.objectValues():
      path_item_dict[item.getProperty('item_path')] = item
420 421 422 423 424 425 426 427 428
    return path_item_dict

  def getBusinessItemByPath(self, path):
    path_item_dict = self.getPathItemDict()
    try:
      return path_item_dict[path]
    except KeyError:
      return

429 430 431 432
  def build(self, no_action=False, **kw):
    """Creates new values for business item from the values from
    OFS Database"""
    LOG('Business Manager', INFO, 'Building Business Manager')
433 434
    removable_sub_object_path_list = kw.get('removable_sub_object_path', [])
    removable_property_dict = kw.get('removable_property', {})
435
    if not no_action:
436
      self.storeTemplateData(isBuild=True, **kw)
437
      for path_item in self.objectValues():
438 439 440 441 442
        kwargs = {}
        item_path = path_item.getProperty('item_path')
        kwargs['removable_property_list'] = removable_property_dict.get(item_path, [])
        kwargs['remove_sub_objects'] = item_path in removable_sub_object_path_list
        path_item.build(self, **kwargs)
443 444 445 446 447 448 449 450 451 452 453
      self.status = 'built'
    return self

  def flattenBusinessManager(self):
    """
    Flattening a reduced Business Manager with two path p1 and p2 where p1 <> p2:

    flatten([(p1, s1, l1, v1), (p2, s2, l2, v2)]) = [(p1, s1, 0, v1), (p2, s2, 0, v2)]
    A reduced Business Manager BT is said to be flattened if and only if:
    flatten(BT) = BT
    """
454
    pass
455

456 457 458
  def preinstall(self, check_dependencies=1, **kw):
    pass

459 460 461 462 463
  def reduceBusinessManager(self):
    """
    Reduction is a function that takes a Business Manager as input and returns
    a smaller Business Manager by taking out values with lower priority layers.

464 465 466
    After taking out BusinessItem(s) with lower priority layer, we also go
    through arithmetic in case there are multiple number of BI at the higher layer

467 468 469 470 471 472 473 474
    Two path on different layer are reduced as a single path with the highest layer:

    If l1 > l2,
    reduce([(p, s, l1, (a, b, c)), (p, s, l2, (d, e))]) = [(p, s, l1, merge(a, b, c))]

    A Business Manager BT is said to be reduced if and only if:
    reduce(BT) = BT
    """
475
    path_list = list(set([path_item.getBusinessPath() for path_item
476
                 in self.objectValues()]))
477

478
    reduced_path_item_list = []
479

480 481 482
    # We separate the path list in the ones which are repeated and the ones
    # which are unique for the installation
    seen_path_list = set()
483 484 485 486
    unique_path_list = [x for x
                        in path_list
                        if x not in seen_path_list
                        and not seen_path_list.add(x)]
487 488 489 490

    # Create an extra dict for values on path which are repeated in the path list
    seen_path_dict = {path: [] for path in seen_path_list}

491 492
    for path_item in self.objectValues():
      if path_item.getProperty('item_path') in seen_path_list:
493
        # In case the path is repeated keep the path_item in a separate dict
494
        # for further arithmetic
495
        seen_path_dict[path_item.getProperty('item_path')].append(path_item)
496
      else:
497 498
        # If the path is unique, add them in the list of reduced Business Item
        reduced_path_item_list.append(path_item)
499 500 501 502

    # Reduce the values and get the merged result out of it
    for path, path_item_list in seen_path_dict.items():

503
      # Create separate list of list items with highest priority
504
      higest_priority_layer = max(path_item_list, key=attrgetter('item_layer')).item_layer
505 506
      prioritized_path_item = [path_item for path_item
                               in path_item_list
507
                               if path_item.item_layer == higest_priority_layer]
508 509 510 511 512 513

      # Separate the positive and negative sign path_item
      if len(prioritized_path_item) > 1:

        path_item_list_add = [item for item
                              in prioritized_path_item
514
                              if item.getProperty('item_sign') > 0]
515 516

        path_item_list_subtract = [item for item
517
                                   in prioritized_path_item
518
                                   if item.getProperty('item_sign') < 0]
519 520 521 522

        combined_added_path_item = reduce(lambda x, y: x+y, path_item_list_add)
        combined_subtracted_path_item = reduce(lambda x, y: x+y, path_item_list_subtract)

523 524
        added_value = combined_added_path_item.objectValues()
        subtracted_value = combined_subtracted_path_item.objectValues()
525 526 527 528 529 530 531

        if added_value != subtracted_value:
          # Append the arithmetically combined path_item objects in the final
          # reduced list after removing the intersection
          added_value, subtracted_value = \
                  self._simplifyValueIntersection(added_value, subtracted_value)

532 533
          combined_added_path_item.value = added_value
          combined_subtracted_path_item.value = subtracted_value
534

535
          # Append the path_item to the final reduced path_item_list after
536 537 538
          # doing required arithmetic on it. Make sure to first append
          # subtracted item because while installation, we need to first
          # uninstall the old object and then install new object at same path
539
          reduced_path_item_list.append(combined_subtracted_path_item)
540
          reduced_path_item_list.append(combined_added_path_item)
541

542 543
      else:
        reduced_path_item_list.append(prioritized_path_item[0])
544

545 546 547
    self._path_item_list = reduced_path_item_list
    self.setStatus('reduced')

548 549 550 551 552 553 554 555 556 557 558
  def _simplifyValueIntersection(self, added_value, subtracted_value):
    """
    Returns values for the Business Item having same path and layer after
    removing the intersection of the values

    Parameters:
    added_value - Value for the Business Item having sign = +1
    subtracted_value - Value for Busienss Item having sign = -1
    """
    built_in_number_type = (int, long, float, complex)
    built_in_container_type = (tuple, list, dict, set)
559 560 561 562 563 564
    built_in_type_list = built_in_number_type + built_in_container_type

    # For ERP5 objects, we should return the added and subtracted values as it is
    if type(added_value).__name__ not in built_in_type_list and \
        type(subtracted_value).__name__ not in built_in_type_list:
      return added_value, subtracted_value
565

566
    # For all the values of container type, we remove the intersection
567 568
    added_value = [x for x in added_value if x not in subtracted_value]
    subtracted_value = [x for x in subtracted_value if x not in added_value]
569 570

    return added_value, subtracted_value
571

572
class BusinessItem(XMLObject):
573 574 575 576

  """Saves the path and values for objects, properties, etc, the
    attributes for a path configuration being:

577
    - item_path  (similar to an xpath expression)
578 579 580 581 582
        Examples of path :
          portal_type/Person
          portal_type/Person#title
          portal_type/Person#property_sheet?ancestor=DublinCore
          portal_type/Person#property_sheet?position=2
583 584 585
    - item_sign  (+1/-1)
    - item_layer (0, 1, 2, 3, etc.)
    - item_value (a set of pickable value in python)"""
586

587 588 589 590
  add_permission = Permissions.AddPortalContent
  # Declarative security
  security = ClassSecurityInfo()
  security.declareObjectProtected(Permissions.AccessContentsInformation)
591
  constructors = (manage_addBusinessItem,)
592 593
  portal_type = 'Business Item'
  meta_type = 'Business Item'
594
  icon = None
595
  isProperty = False
596
  isIndexable = False
597

598
  def _edit(self, item_path='', item_sign=1, item_layer=0, *args, **kw):
599 600 601
    """
    Overriden function so that we can update attributes for BusinessItem objects
    """
602
    return super(BusinessItem, self)._edit(item_path=item_path,
603 604 605
                                           item_sign=item_sign,
                                           item_layer=item_layer,
                                           **kw)
606 607 608 609 610 611 612 613

  def build(self, context, **kw):
    """
    Extract value for the given path from the OFS

    Three different situations to extract value:
    1. For paths which point directly to an object in OFS
    2. For paths which point to multiple objects inside a folder
614 615
    3. For paths which point to property of an object in OFS : In this case,
    we can have URL delimiters like ?, #, = in the path
616 617
    """
    LOG('Business Manager', INFO, 'Building Business Item')
618 619 620 621 622 623

    # Remove the old sub-objects if exisiting before building
    id_list = [l for l in self.objectIds()]
    if id_list:
      self.manage_delObjects(ids=id_list)

624
    p = context.getPortalObject()
625
    path = self.getProperty('item_path')
626 627 628 629 630 631 632 633 634 635
    try:
      # XXX: After we apply _resolve path list while storing Data for the
      # Business Manager, this should be of no use as there will be no path
      # where we are going to achieve something different for relative_path
      # from the result of _resolvePath on a given path.
      # TODO: Remove this after checking successfull implementation of
      # _resolve path in Business Manager in storeTemplateData
      for relative_url in self._resolvePath(p, [], path.split('/')):
        obj = p.unrestrictedTraverse(relative_url)
        obj = obj._getCopy(context)
636 637 638 639 640 641 642 643 644 645 646 647

        # We should remove the extra properties of object so that there
        # shouldn't be redundancy of the proeprties
        removable_property_list = kw.get('removable_property_list')

        # We should also add extra parameter to remove sub-objects by removing
        # `_tree` for any erp5 object. This way we can have control over adding
        # sub-objects as new Business Item objects
        remove_sub_objects = kw.get('remove_sub_objects')
        if remove_sub_objects:
          removable_property_list.append('_tree')
        obj = self.removeProperties(obj, 1, properties=removable_property_list)
648 649 650 651 652
        obj = obj.__of__(context)
        # XXX: '_recursiveRemoveUid' is not working as expected
        _recursiveRemoveUid(obj)
        obj = aq_base(obj)
        obj.isIndexable = ConstantGetter('isIndexable', value=False)
653 654
        new_id = self.generateNewId()
        self._setObject(new_id, obj, suppress_events=True)
655 656 657
    except AttributeError:
      # In case the object doesn't exist, just pass without raising error
      pass
658 659 660

  def _resolvePath(self, folder, relative_url_list, id_list):
    """
661
    We go through 3 types of paths:
662

663 664 665
    1. General path we find in erp5 for objects
    Ex: portal_type/Person
    In this case, we import/export the object on the path
666

667 668 669 670 671
    2. Path where we consider saving sub-objects also, in that case we create
    new BusinessItem for those objects
    Ex: portal_catalog/erp5_mysql_innodb/**
    This should create BI for the catalog methods sub-objects present in the
    erp5_catalog.
672

673
    This method calls itself recursively.
674

675 676 677
    The folder is the current object which contains sub-objects.
    The list of ids are path components. If the list is empty,
    the current folder is valid.
678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695
    """
    if len(id_list) == 0:
      return ['/'.join(relative_url_list)]
    id = id_list[0]
    if re.search('[\*\?\[\]]', id) is None:
      # If the id has no meta character, do not have to check all objects.
      obj = folder._getOb(id, None)
      if obj is None:
        raise AttributeError, "Could not resolve '%s' during BusinessItem processing." % id
      return self._resolvePath(obj, relative_url_list + [id], id_list[1:])
    path_list = []
    for object_id in fnmatch.filter(folder.objectIds(), id):
      if object_id != "":
        path_list.extend(self._resolvePath(
            folder._getOb(object_id),
            relative_url_list + [object_id], id_list[1:]))
    return path_list

696
  def install(self, context):
697 698 699 700 701
    """
    Set the value to the defined path.
    """
    # In case the path denotes property, we create separate object for
    # ObjectTemplateItem and handle the installation there.
702
    portal = context.getPortalObject()
703
    path = self.getProperty('item_path')
704 705
    path_list = path.split('/')
    container_path = path_list[:-1]
706

707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728
    object_id = path_list[-1]
    try:
      container = self.unrestrictedResolveValue(portal, container_path)
    except KeyError:
      # parent object can be set to nothing, in this case just go on
      container_url = '/'.join(container_path)
    old_obj = container._getOb(object_id, None)
    # delete the old object before installing a new object
    if old_obj:
      container._delObject(object_id)
    # Create a new object only if sign is +1
    # If sign is +1, set the new object on the container
    if int(self.getProperty('item_sign')) == 1:
      # install object
      obj = self.objectValues()[0]
      obj = obj._getCopy(container)
      # Before making `obj` a sub-object of `container`, we should the acquired
      # roles on obj
      obj.isIndexable = ConstantGetter('isIndexable', value=False)
      delattr(obj, '__ac_local_roles__')
      container._setObject(object_id, obj, suppress_events=True)
      obj = container._getOb(object_id)
729 730 731
      skin_tool = portal.portal_skins
      if obj.aq_parent.meta_type == 'CMF Skins Tool':
        registerSkinFolder(skin_tool, obj)
732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769

  def unrestrictedResolveValue(self, context=None, path='', default=_MARKER,
                               restricted=0):
    """
      Get the value without checking the security.
      This method does not acquire the parent.
    """
    if isinstance(path, basestring):
      stack = path.split('/')
    else:
      stack = list(path)
    stack.reverse()
    if stack:
      if context is None:
        portal = aq_inner(self.getPortalObject())
        container = portal
      else:
        container = context

      if restricted:
        validate = getSecurityManager().validate

      while stack:
        key = stack.pop()
        try:
          value = container[key]
        except KeyError:
          LOG('BusinessManager', WARNING,
              'Could not access object %s' % (path,))
          if default is _MARKER:
            raise
          return default

        if restricted:
          try:
            if not validate(container, container, key, value):
              raise Unauthorized('unauthorized access to element %s' % key)
          except Unauthorized:
770
            LOG('BusinessManager', WARNING,
771 772 773 774 775 776 777 778 779 780
                'access to %s is forbidden' % (path,))
          if default is _MARKER:
            raise
          return default

        container = value

      return value
    else:
      return context
781

782
  def __add__(self, other):
783 784 785
    """
    Add the values from the path when the path is same for 2 objects
    """
786
    if self.path != other.path:
787
      raise ValueError, "BusinessItem are incommensurable, have different path"
788
    elif self.sign != other.sign:
789
      raise ValueError, "BusinessItem are incommensurable, have different sign"
790
    else:
791
      self.value = self._mergeValue(value_list=[self.value, other.value])
792
      return self
793

794 795
  __radd__ = __add__

796
  def _mergeValue(self, value_list):
797
    """
798
    Merge value in value list
799 800 801 802 803 804 805 806

    merge(a, b, c) : A monotonic commutative function that depends on the
    type of a, b and c:

    if a, b and c are sets, merge = union
    if a, b and c are lists, merge = ordered concatenation
    if a, b and c are objects, merge = the object created the last
    else merge = MAX
807
    """
808
    builtin_number_type = (int, long, float, complex)
809 810

    # Now, consider the type of both values
811 812 813 814 815 816 817 818
    if all(isinstance(x, builtin_number_type) for x in value_list):
      merged_value = max(value_list)
    elif all(isinstance(x, set) for x in value_list):
      merged_value = set(chain.from_iterable(value_list))
    elif all(isinstance(x, list) for x in value_list):
      merged_value = list(chain.from_iterable(value_list))
    elif all(isinstance(x, tuple) for x in value_list):
      merged_value = tuple(chain.from_iterable(value_list))
819 820 821
    else:
      # In all other case, check if the values are objects and then take the
      # objects created last.
822

823
      # XXX: Should we go with creation date or modification_date ??
824 825
      # TODO:
      # 1. Add check that the values are ERP5 objects
826 827 828
      # 2. In case 2 maximum values are created at same time, prefer one with
      # higher priority layer
      merged_value = max([max(value, key=attrgetter('creation_date'))
829
                         for value in value_list],
830
                         key=attrgetter('creation_date'))
831 832 833

    return merged_value

834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877
  def removeProperties(self,
                       obj,
                       export,
                       properties=[],
                       keep_workflow_history=False,
                       keep_workflow_history_last_history_only=False):
    """
    Remove unneeded properties for export
    """
    obj._p_activate()
    klass = obj.__class__
    classname = klass.__name__
    attr_set = {'_dav_writelocks', '_filepath', '_owner', '_related_index',
                'last_id', 'uid',
                '__ac_local_roles__', '__ac_local_roles_group_id_dict__'}
    if properties:
      for prop in properties:
        if prop.endswith('_list'):
          prop = prop[:-5]
        attr_set.add(prop)
    if export:
      if keep_workflow_history_last_history_only:
        self._removeAllButLastWorkflowHistory(obj)
      elif not keep_workflow_history:
        attr_set.add('workflow_history')
      # PythonScript covers both Zope Python scripts
      # and ERP5 Python Scripts
      if isinstance(obj, PythonScript):
        attr_set.update(('func_code', 'func_defaults', '_code',
                         '_lazy_compilation', 'Python_magic'))
        for attr in 'errors', 'warnings', '_proxy_roles':
          if not obj.__dict__.get(attr, 1):
            delattr(obj, attr)
      elif classname in ('File', 'Image'):
        attr_set.update(('_EtagSupport__etag', 'size'))
      elif classname == 'SQL' and klass.__module__ == 'Products.ZSQLMethods.SQL':
        attr_set.update(('_arg', 'template'))
      elif interfaces.IIdGenerator.providedBy(obj):
        attr_set.update(('last_max_id_dict', 'last_id_dict'))
      elif classname == 'Types Tool' and klass.__module__ == 'erp5.portal_type':
        attr_set.add('type_provider_list')

    for attr in obj.__dict__.keys():
      if attr in attr_set or attr.startswith('_cache_cookie_'):
878 879 880 881 882 883 884
        try:
          delattr(obj, attr)
        except AttributeError:
          # XXX: Continue in cases where we want to delete some properties which
          # are not in attribute list
          # Raise an error
          continue
885 886 887 888 889 890

    if classname == 'PDFForm':
      if not obj.getProperty('business_template_include_content', 1):
        obj.deletePdfContent()
    return obj

891
  def getBusinessPath(self):
892
    return self.getProperty('item_path')
893 894

  def getBusinessPathSign(self):
895
    return self.getProperty('item_sign', 1)
896 897

  def getBusinessPathLayer(self):
898
    return self.getProperty('item_layer', 1)
899 900 901

  def getParentBusinessManager(self):
    return self.aq_parent
902 903 904 905 906 907 908 909 910 911 912 913 914

class BusinessPropertyItem(XMLObject):

  """Class to deal with path(s) which refer to property of an ERP5 object.
  Used to store property name, type and value for a given object and property"""

  add_permission = Permissions.AddPortalContent
  # Declarative security
  security = ClassSecurityInfo()
  security.declareObjectProtected(Permissions.AccessContentsInformation)

  portal_type = 'Business Property Item'
  meta_type = 'Business Property Item'
915
  icon = None
916 917
  isIndexable = False
  isProperty = True
918
  constructors = (manage_addBusinessPropertyItem,)
919 920 921 922 923

  def _edit(self, item_path='', item_sign=1, item_layer=0, *args, **kw):
    """
    Overriden function so that we can update attributes for BusinessItem objects
    """
924
    return super(BusinessPropertyItem, self)._edit(item_path=item_path,
925 926 927 928 929 930 931 932 933 934 935
                                                   item_sign=item_sign,
                                                   item_layer=item_layer,
                                                   **kw)

  def build(self, context, **kw):
    p = context.getPortalObject()
    path = self.getProperty('item_path')
    relative_url, property_id = path.split('#')
    obj = p.unrestrictedTraverse(relative_url)
    property_value = obj.getProperty(property_id)
    property_type = obj.getPropertyType(property_id)
936 937 938
    self.setProperty('item_property_name', property_id)
    self.setProperty('item_property_type', property_type)
    self.setProperty('item_property_value', property_value)
939 940 941 942 943 944

  def install(self, context):
    portal = context.getPortalObject()
    path = self.getProperty('item_path')
    relative_url, property_id = path.split('#')
    obj = portal.unrestrictedTraverse(relative_url)
945 946 947
    property_name = self.getProperty('item_property_name')
    property_type = self.getProperty('item_property_type')
    property_value = self.getProperty('item_property_value')
948 949
    # First remove the property from the existing path and keep the default
    # empty, and update only if the sign is +1
950 951 952 953 954
    obj._delPropValue(property_name)
    # Remove the '_list' from the end of property_name. This is required because
    # of the way _setProperty is defined where if the type is list_type, it
    # explicitly adds '_list' at the end of property_name before trying to call
    # the accessor
955 956
    if property_name.endswith('_list'):
      property_name = property_name[:-5]
957 958
    if int(self.getProperty('item_sign')) == 1:
      obj.setProperty(property_name, property_value, property_type)
959

960 961 962 963
  def getBusinessPath(self):
    return self.getProperty('item_path')

  def getBusinessPathSign(self):
964
    return self.getProperty('item_sign', 1)
965 966

  def getBusinessPathLayer(self):
967
    return self.getProperty('item_layer', 1)
968 969 970 971 972 973 974 975 976 977 978 979 980

  def getParentBusinessManager(self):
    return self.aq_parent

  def getBusinessItemPropertyName(self):
    return self.getProperty('item_property_name')

  def getBusinessItemPropertyType(self):
    return self.getProperty('item_property_type')

  def getBusinessItemPropertyValue(self):
    return self.getProperty('item_property_value')

981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076
def registerSkinFolder(skin_tool, skin_folder):
  request = skin_tool.REQUEST
  # XXX: Getting parameter from request instead of dialog is bad
  # XXX: This is even non consistent with rest of parameters selected by user
  #      (like update_translation or update_catalog)
  register_skin_selection = request.get('your_register_skin_selection', 1)
  reorder_skin_selection = request.get('your_reorder_skin_selection', 1)
  skin_layer_list = request.get('your_skin_layer_list',
                                skin_tool.getSkinSelections())

  skin_folder_id = skin_folder.getId()

  try:
    skin_selection_list = skin_folder.getProperty(
                 'business_template_registered_skin_selections',
                 skin_tool.getSkinSelections()
                 )
  except AttributeError:
    skin_selection_list = skin_tool.getSkinSelections()

  if isinstance(skin_selection_list, basestring):
    skin_selection_list = skin_selection_list.split()

  def skin_sort_key(skin_folder_id):
    obj = skin_tool._getOb(skin_folder_id, None)
    if obj is None:
      return 0, skin_folder_id
    return -obj.getProperty('business_template_skin_layer_priority',
      obj.meta_type == 'Filesystem Directory View' and -1 or 0), skin_folder_id

  for skin_name in skin_selection_list:

    if (skin_name not in skin_tool.getSkinSelections()) and \
                                          register_skin_selection:
      createSkinSelection(skin_tool, skin_name)
      # add newly created skins to list of skins we care for
      skin_layer_list.append(skin_name)

    selection = skin_tool.getSkinPath(skin_name) or ''
    selection_list = selection.split(',')
    if (skin_folder_id not in selection_list):
      selection_list.insert(0, skin_folder_id)
    if reorder_skin_selection:
      # Sort by skin priority and ID
      selection_list.sort(key=skin_sort_key)
    if (skin_name in skin_layer_list):
      skin_tool.manage_skinLayers(skinpath=selection_list,
                                  skinname=skin_name, add_skin=1)
      skin_tool.getPortalObject().changeSkin(None)

def createSkinSelection(skin_tool, skin_name):
  # This skin selection does not exist, so we create a new one.
  # We'll initialize it with all skin folders, unless:
  #  - they explictly define a list of
  #    "business_template_registered_skin_selections", and we
  #    are not in this list.
  #  - they are not registered in the default skin selection
  skin_path = ''
  for skin_folder in skin_tool.objectValues():
    if skin_name in skin_folder.getProperty(
             'business_template_registered_skin_selections',
             (skin_name, )):
      if skin_folder.getId() in \
          skin_tool.getSkinPath(skin_tool.getDefaultSkin()):
        if skin_path:
          skin_path = '%s,%s' % (skin_path, skin_folder.getId())
        else:
          skin_path= skin_folder.getId()
  # add newly created skins to list of skins we care for
  skin_tool.addSkinSelection(skin_name, skin_path)
  skin_tool.getPortalObject().changeSkin(None)

def deleteSkinSelection(skin_tool, skin_name):
  # Do not delete default skin
  if skin_tool.getDefaultSkin() != skin_name:
    for skin_folder in skin_tool.objectValues():
      try:
        if skin_name in skin_folder.getProperty(
               'business_template_registered_skin_selections', ()):
          break
      except AttributeError:
        pass
    else:
      skin_tool.manage_skinLayers(chosen=[skin_name], del_skin=1)
      skin_tool.getPortalObject().changeSkin(None)

def unregisterSkinFolderId(skin_tool, skin_folder_id, skin_selection_list):
  for skin_selection in skin_selection_list:
    selection = skin_tool.getSkinPath(skin_selection)
    selection = selection.split(',')
    if (skin_folder_id in selection):
      selection.remove(skin_folder_id)
      skin_tool.manage_skinLayers(skinpath=tuple(selection),
                                  skinname=skin_selection, add_skin=1)
      deleteSkinSelection(skin_tool, skin_selection)
      skin_tool.getPortalObject().changeSkin(None)