BusinessTemplate.py 239 KB
Newer Older
1
# -*- coding: utf-8 -*-
Jean-Paul Smets's avatar
Jean-Paul Smets committed
2 3 4
##############################################################################
#
# Copyright (c) 2002 Nexedi SARL and Contributors. All Rights Reserved.
5
#                    Jean-Paul Smets-Solanes <jp@nexedi.com>
Jean-Paul Smets's avatar
Jean-Paul Smets committed
6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
#
# 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
import fnmatch, gc, glob, imp, os, re, shutil, sys, time, tarfile
31
from collections import defaultdict
32
from Shared.DC.ZRDB import Aqueduct
33
from Shared.DC.ZRDB.Connection import Connection as RDBConnection
34
from Products.ERP5Type.DiffUtils import DiffFile
35
from Products.ERP5Type.Globals import Persistent, PersistentMapping
36
from Acquisition import Implicit, aq_base, aq_inner, aq_parent
37
from AccessControl import ClassSecurityInfo, Unauthorized, getSecurityManager
38
from AccessControl.SecurityInfo import ModuleSecurityInfo
Jean-Paul Smets's avatar
Jean-Paul Smets committed
39
from Products.CMFCore.utils import getToolByName
40
from Products.PythonScripts.PythonScript import PythonScript
41
from Products.ERP5Type.Accessor.Constant import PropertyGetter as ConstantGetter
42
from Products.ERP5Type.Cache import transactional_cached
Nicolas Dumazet's avatar
Nicolas Dumazet committed
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
from Products.ERP5Type.Utils import readLocalDocument, \
                                    writeLocalDocument, \
                                    importLocalDocument, \
                                    removeLocalDocument
from Products.ERP5Type.Utils import readLocalPropertySheet, \
                                    writeLocalPropertySheet, \
                                    importLocalPropertySheet, \
                                    removeLocalPropertySheet
from Products.ERP5Type.Utils import readLocalConstraint, \
                                    writeLocalConstraint, \
                                    importLocalConstraint, \
                                    removeLocalConstraint
from Products.ERP5Type.Utils import readLocalExtension, \
                                    writeLocalExtension, \
                                    removeLocalExtension
from Products.ERP5Type.Utils import readLocalTest, \
                                    writeLocalTest, \
                                    removeLocalTest
61
from Products.ERP5Type.Utils import convertToUpperCase
62
from Products.ERP5Type import Permissions, PropertySheet, interfaces
Jean-Paul Smets's avatar
Jean-Paul Smets committed
63
from Products.ERP5Type.XMLObject import XMLObject
64
from Products.ERP5Type.dynamic.portal_type_class import synchronizeDynamicModules
65
from Products.ERP5Type.Core.PropertySheet import PropertySheet as PropertySheetDocument
Aurel's avatar
Aurel committed
66
from OFS.Traversable import NotFound
67
from OFS import SimpleItem, XMLExportImport
68
from cStringIO import StringIO
Aurel's avatar
Aurel committed
69
from copy import deepcopy
70
from zExceptions import BadRequest
Aurel's avatar
Aurel committed
71
import OFS.XMLExportImport
72
from Products.ERP5Type.patches.ppml import importXML
Aurel's avatar
Aurel committed
73
customImporters={
74
    XMLExportImport.magic: importXML,
Aurel's avatar
Aurel committed
75
    }
Jean-Paul Smets's avatar
Jean-Paul Smets committed
76

77
from zLOG import LOG, WARNING, INFO
78
from warnings import warn
Aurel's avatar
Aurel committed
79
from gzip import GzipFile
80
from lxml.etree import parse
81
from xml.sax.saxutils import escape
82
from Products.CMFCore.Expression import Expression
83
from urllib import quote, unquote
84
from difflib import unified_diff
85
import posixpath
Julien Muchembled's avatar
Julien Muchembled committed
86
import transaction
87

88 89 90 91 92 93
import threading

CACHE_DATABASE_PATH = None
try:
  if int(os.getenv('ERP5_BT5_CACHE', 0)):
    from App.config import getConfiguration
94
    import gdbm
95 96 97 98 99 100
    instancehome = getConfiguration().instancehome
    CACHE_DATABASE_PATH = os.path.join(instancehome, 'bt5cache.db')
except TypeError:
  pass
cache_database = threading.local()

101 102
# those attributes from CatalogMethodTemplateItem are kept for
# backward compatibility
103 104
catalog_method_list = ('_is_catalog_list_method_archive',
                       '_is_uncatalog_method_archive',
105 106
                       '_is_clear_method_archive',
                       '_is_filtered_archive',)
107

108 109
catalog_method_filter_list = ('_filter_expression_archive',
                              '_filter_expression_instance_archive',
110
                              '_filter_expression_cache_key_archive',
111
                              '_filter_type_archive',)
112

113
INSTALLED_BT_FOR_DIFF = 'installed_bt_for_diff'
114
_MARKER = []
115

116 117 118 119 120 121
def _getCatalog(acquisition_context):
  """
    Return the id of the SQLCatalog which correspond to the current BT.
  """
  catalog_method_id_list = acquisition_context.getTemplateCatalogMethodIdList()
  if len(catalog_method_id_list) == 0:
122 123 124 125
    try:
      return acquisition_context.getPortalObject().portal_catalog.objectIds('SQLCatalog')[0]
    except IndexError:
      return None
126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144
  catalog_method_id = catalog_method_id_list[0]
  return catalog_method_id.split('/')[0]

def _getCatalogValue(acquisition_context):
  """
    Returns the catalog object which correspond to the ZSQLMethods
    stored/to store in the business template.

    NB: acquisition_context must make possible to reach portal object
        and getTemplateCatalogMethodIdList.
  """
  catalog_id = _getCatalog(acquisition_context)
  if catalog_id is None:
    return None
  try:
    return acquisition_context.getPortalObject().portal_catalog[catalog_id]
  except KeyError:
    return None

145 146 147 148 149
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.
  """
150
  if getattr(aq_base(obj), 'uid', _MARKER) is not _MARKER:
151
    obj.uid = None
152 153 154
  for subobj in obj.objectValues():
    _recursiveRemoveUid(subobj)

155
def removeAll(entry):
156 157 158
  warn('removeAll is deprecated; use shutil.rmtree instead.',
       DeprecationWarning)
  shutil.rmtree(entry, True)
159

160 161 162 163 164 165 166
def getChainByType(context):
  """
  This is used in order to construct the full list
  of mapping between type and list of workflow associated
  This is only useful in order to use
  portal_workflow.manage_changeWorkflows
  """
167
  pw = context.getPortalObject().portal_workflow
168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188
  cbt = pw._chains_by_type
  ti = pw._listTypeInfo()
  types_info = []
  for t in ti:
    id = t.getId()
    title = t.Title()
    if title == id:
      title = None
    if cbt is not None and cbt.has_key(id):
      chain = ', '.join(cbt[id])
    else:
      chain = '(Default)'
    types_info.append({'id': id,
                      'title': title,
                      'chain': chain})
  new_dict = {}
  for item in types_info:
    new_dict['chain_%s' % item['id']] = item['chain']
  default_chain=', '.join(pw._default_chain)
  return (default_chain, new_dict)

189 190 191 192 193 194 195 196 197 198 199 200 201 202
def fixZSQLMethod(portal, method):
  """Make sure the ZSQLMethod uses a valid connection.
  """
  if not isinstance(getattr(portal, method.connection_id, None),
                      RDBConnection):
    # if not valid, we assign to the first valid connection found
    sql_connection_list = portal.objectIds(
                          spec=('Z MySQL Database Connection',))
    if (method.connection_id not in sql_connection_list) and \
       (len(sql_connection_list) != 0):
      LOG('BusinessTemplate', WARNING,
          'connection_id for Z SQL Method %s is invalid, using %s' % (
                    method.getId(), sql_connection_list[0]))
      method.connection_id = sql_connection_list[0]
203 204 205
  # recompile the method
  method._arg = Aqueduct.parse(method.arguments_src)
  method.template = method.template_class(method.src)
206

207 208
def registerSkinFolder(skin_tool, skin_folder):
  request = skin_tool.REQUEST
209 210 211
  # 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)
212 213 214 215 216 217 218 219 220 221 222 223 224 225 226
  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()

227 228 229
  if isinstance(skin_selection_list, basestring):
    skin_selection_list = skin_selection_list.split()

230 231 232 233 234 235 236
  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

237 238 239 240 241 242 243 244 245
  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 ''
246
    selection_list = selection.split(',')
247
    if (skin_folder_id not in selection_list):
248
      selection_list.insert(0, skin_folder_id)
249
    if reorder_skin_selection:
250 251
      # Sort by skin priority and ID
      selection_list.sort(key=skin_sort_key)
252
    if (skin_name in skin_layer_list):
253 254 255 256 257 258 259 260 261 262
      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.
Arnaud Fontaine's avatar
Arnaud Fontaine committed
263
  #  - they are not registered in the default skin selection
264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283
  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:
284 285
        if skin_name in skin_folder.getProperty(
               'business_template_registered_skin_selections', ()):
286 287 288
          break
      except AttributeError:
        pass
289 290
    else:
      skin_tool.manage_skinLayers(chosen=[skin_name], del_skin=1)
291 292
      skin_tool.getPortalObject().changeSkin(None)

293
def unregisterSkinFolderId(skin_tool, skin_folder_id, skin_selection_list):
294 295 296 297 298 299 300 301 302 303
  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)

Aurel's avatar
Aurel committed
304 305 306
class BusinessTemplateArchive:
  """
    This is the base class for all Business Template archives
307
  """
308
  def _initCreation(self, path, **kw):
309
    self.path = path
Aurel's avatar
Aurel committed
310 311 312 313 314 315 316

  def __init__(self, creation=0, importing=0, file=None, path=None, **kw):
    if creation:
      self._initCreation(path=path, **kw)
    elif importing:
      self._initImport(file=file, path=path, **kw)

317
  def addObject(self, obj, name, path=None, ext='.xml'):
318 319 320 321 322 323
    if path:
      name = posixpath.join(path, name)
    # XXX required due to overuse of os.path
    name = name.replace('\\', '/').replace(':', '/')
    name = quote(name + ext)
    path = name.replace('/', os.sep)
324
    try:
325 326 327 328 329 330 331 332 333 334
      write = self._writeFile
    except AttributeError:
      if not isinstance(obj, str):
        obj.seek(0)
        obj = obj.read()
      self._writeString(obj, path)
    else:
      if isinstance(obj, str):
        obj = StringIO(obj)
      write(obj, path)
335

336
  def finishCreation(self):
Aurel's avatar
Aurel committed
337 338 339 340
    pass

class BusinessTemplateFolder(BusinessTemplateArchive):
  """
Christophe Dumez's avatar
Christophe Dumez committed
341
    Class archiving business template into a folder tree
342
  """
343 344 345 346 347 348 349 350 351 352 353 354
  def _writeString(self, obj, path):
    object_path = os.path.join(self.path, 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()

  def _initImport(self, file, path, **kw):
    root_path_len = len(os.path.normpath(os.path.join(path, '_'))) - 1
355 356 357 358 359 360 361
    self.root_path_len = root_path_len
    d = {}
    for f in file:
      f = os.path.normpath(f)
      klass = f[root_path_len:].split(os.sep, 1)[0]
      d.setdefault(klass, []).append(f)
    self.file_list_dict = d
Aurel's avatar
Aurel committed
362

363
  def importFiles(self, item, **kw):
Aurel's avatar
Aurel committed
364 365 366
    """
      Import file from a local folder
    """
367
    class_name = item.__class__.__name__
368 369
    root_path_len = self.root_path_len
    prefix_len = root_path_len + len(class_name) + len(os.sep)
370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389
    if CACHE_DATABASE_PATH:
      try:
        cache_database.db = gdbm.open(CACHE_DATABASE_PATH, 'cf')
      except gdbm.error:
        cache_database.db = gdbm.open(CACHE_DATABASE_PATH, 'nf')
    try:
      for file_path in self.file_list_dict.get(class_name, ()):
        if os.path.isfile(file_path):
          file = open(file_path, 'rb')
          try:
            file_name = file_path[prefix_len:]
            if '%' in file_name:
              file_name = unquote(file_name)
            item._importFile(file_name, file)
          finally:
            file.close()
    finally:
      if hasattr(cache_database, 'db'):
        cache_database.db.close()
        del cache_database.db
390

Aurel's avatar
Aurel committed
391 392 393 394 395
class BusinessTemplateTarball(BusinessTemplateArchive):
  """
    Class archiving businnes template into a tarball file
  """

396 397
  def _initCreation(self, **kw):
    BusinessTemplateArchive._initCreation(self, **kw)
Aurel's avatar
Aurel committed
398 399 400
    # init tarfile obj
    self.fobj = StringIO()
    self.tar = tarfile.open('', 'w:gz', self.fobj)
401 402 403 404 405 406 407 408 409 410 411 412 413
    self.time = time.time()

  def _writeFile(self, obj, path):
    if self.path:
      path = posixpath.join(self.path, path)
    info = tarfile.TarInfo(path)
    info.mtime = self.time
    obj.seek(0, 2)
    info.size = obj.tell()
    obj.seek(0)
    self.tar.addfile(info, obj)

  def finishCreation(self):
Aurel's avatar
Aurel committed
414 415 416
    self.tar.close()
    return self.fobj

417
  def _initImport(self, file, **kw):
418 419 420 421 422 423 424 425 426 427 428 429
    self.tar = tarfile.TarFile(fileobj=StringIO(GzipFile(fileobj=file).read()))
    self.item_dict = {}
    setdefault = self.item_dict.setdefault
    for info in self.tar.getmembers():
      if info.isreg():
        path = info.name.split('/')
        if path[0] == '.':
          del path[0]
        file_name = '/'.join(path[2:])
        if '%' in file_name:
          file_name = unquote(file_name)
        setdefault(path[1], []).append((file_name, info))
Aurel's avatar
Aurel committed
430

431
  def importFiles(self, item, **kw):
Aurel's avatar
Aurel committed
432 433
    """
      Import all file from the archive to the site
434
    """
435 436 437
    extractfile = self.tar.extractfile
    for file_name, info in self.item_dict.get(item.__class__.__name__, ()):
      item._importFile(file_name, extractfile(info))
Jean-Paul Smets's avatar
Jean-Paul Smets committed
438

439
class TemplateConditionError(Exception): pass
440
class TemplateConflictError(Exception): pass
441
class BusinessTemplateMissingDependency(Exception): pass
442

443 444 445
ModuleSecurityInfo(__name__).declarePublic('BusinessTemplateMissingDependency',
  'TemplateConditionError', 'TemplateConflictError')

446
class BaseTemplateItem(Implicit, Persistent):
447
  """
448
    This class is the base class for all template items.
449
    is_bt_for_diff means This BT is used to compare self temporary BT with installed BT
450
  """
451
  is_bt_for_diff = None
Jean-Paul Smets's avatar
Jean-Paul Smets committed
452

453
  def __init__(self, id_list, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
454
    self.__dict__.update(kw)
455
    self._archive = PersistentMapping()
Aurel's avatar
Aurel committed
456
    self._objects = PersistentMapping()
457
    for id in id_list:
458 459
      if id is not None and id != '':
        self._archive[id] = None
460 461 462 463

  def build(self, context, **kw):
    pass

464
  def preinstall(self, context, installed_item, **kw):
Vincent Pelletier's avatar
Vincent Pelletier committed
465 466
    """
      Build a list of added/removed/changed files between the BusinessTemplate
467
      being installed (self) and the installed one (installed_item).
Vincent Pelletier's avatar
Vincent Pelletier committed
468 469 470 471 472
      Note : we compare files between BTs, *not* between the installed BT and
      the objects in the DataFS.

      XXX: -12 used here is -len('TemplateItem')
    """
Yoshinori Okuji's avatar
Yoshinori Okuji committed
473 474
    modified_object_list = {}
    if context.getTemplateFormatVersion() == 1:
475
      new_keys = self._objects.keys()
476
      for path in new_keys:
477
        if installed_item._objects.has_key(path):
Vincent Pelletier's avatar
Vincent Pelletier committed
478
          # compare objects to see it there are changes
479
          new_obj_xml = self.generateXml(path=path)
480
          old_obj_xml = installed_item.generateXml(path=path)
481 482
          if new_obj_xml != old_obj_xml:
            modified_object_list.update({path : ['Modified', self.__class__.__name__[:-12]]})
Vincent Pelletier's avatar
Vincent Pelletier committed
483
          # else, compared versions are identical, don't overwrite the old one
484 485
        else: # new object
          modified_object_list.update({path : ['New', self.__class__.__name__[:-12]]})
Vincent Pelletier's avatar
Vincent Pelletier committed
486
      # list removed objects
487
      old_keys = installed_item._objects.keys()
488 489 490 491 492 493
      for path in old_keys:
        if path not in new_keys:
          modified_object_list.update({path : ['Removed', self.__class__.__name__[:-12]]})
    return modified_object_list

  def install(self, context, trashbin, **kw):
494
    pass
495 496 497

  def uninstall(self, context, **kw):
    pass
498

499
  def remove(self, context, **kw):
Vincent Pelletier's avatar
Vincent Pelletier committed
500 501 502 503 504 505
    """
      If 'remove' is chosen on an object containing subobjects, all the
      subobjects will be removed too, even if 'backup' or 'keep' was chosen for
      the subobjects.
      Likewise, for 'save_and_remove' : subobjects will get saved too.
    """
506 507 508
    remove_dict = kw.get('remove_object_dict', {})
    keys = self._objects.keys()
    keys.sort()
509
    keys.reverse()
510 511 512 513 514 515 516 517 518 519 520
    # if you choose remove, the object and all its subobjects will be removed
    # even if you choose backup or keep for subobjects
    # it is same behaviour for backup_and_remove, all we be save
    for path in keys:
      if remove_dict.has_key(path):
        action = remove_dict[path]
        if action == 'save_and_remove':
          # like trash
          self.uninstall(context, trash=1, object_path=path, **kw)
        elif action == 'remove':
          self.uninstall(context, trash=0, object_path=path, **kw)
521 522 523 524
        else:
          # As the list of available actions is not strictly defined,
          # prevent mistake if an action is not handled
          raise ValueError, 'Unknown action "%s"' % action
525

526

527 528 529 530
  def trash(self, context, new_item, **kw):
    # trash is quite similar to uninstall.
    return self.uninstall(context, new_item=new_item, trash=1, **kw)

Aurel's avatar
Aurel committed
531
  def export(self, context, bta, **kw):
532
    pass
Aurel's avatar
Aurel committed
533

534 535
  def getKeys(self):
    return self._objects.keys()
536

Aurel's avatar
Aurel committed
537
  def importFile(self, bta, **kw):
538
    bta.importFiles(item=self)
539

540
  def removeProperties(self, obj, export, keep_workflow_history=False):
541 542
    """
    Remove unneeded properties for export
543
    """
544
    obj._p_activate()
545 546
    klass = obj.__class__
    classname = klass.__name__
547

548 549
    attr_set = set(('_dav_writelocks', '_filepath', '_owner', '_related_index',
                    'last_id', 'uid',
550
                    '__ac_local_roles__', '__ac_local_roles_group_id_dict__'))
551
    if export:
552 553
      if not keep_workflow_history:
        attr_set.add('workflow_history')
554 555
      # PythonScript covers both Zope Python scripts
      # and ERP5 Python Scripts
556
      if isinstance(obj, PythonScript):
557
        attr_set.update(('func_code', 'func_defaults', '_code',
558
                         '_lazy_compilation', 'Python_magic'))
559 560 561
        for attr in 'errors', 'warnings', '_proxy_roles':
          if not obj.__dict__.get(attr, 1):
            delattr(obj, attr)
562 563
      elif classname == 'SQL' and klass.__module__== 'Products.ZSQLMethods.SQL':
        attr_set.update(('_arg', 'template'))
564
      elif interfaces.IIdGenerator.providedBy(obj):
565
        attr_set.update(('last_max_id_dict', 'last_id_dict'))
566 567
      elif classname == 'Types Tool' and klass.__module__ == 'erp5.portal_type':
        attr_set.add('type_provider_list')
568

569 570
    for attr in obj.__dict__.keys():
      if attr in attr_set or attr.startswith('_cache_cookie_'):
571 572
        delattr(obj, attr)

573
    if classname == 'PDFForm':
574
      if not obj.getProperty('business_template_include_content', 1):
575
        obj.deletePdfContent()
576 577
    return obj

578 579 580 581 582 583 584 585 586
  def getTemplateTypeName(self):
    """
     Get a meaningfull class Name without 'TemplateItem'. Used to 
     present to the user.

     XXX: -12 used here is -len('TemplateItem')
    """
    return self.__class__.__name__[:-12]

587
  def restrictedResolveValue(self, context=None, path='', default=_MARKER):
588 589 590 591
    """
      Get the value with checking the security.
      This method does not acquire the parent.
    """
592 593
    return self.unrestrictedResolveValue(context, path, default=default,
                                         restricted=1)
594

595 596
  def unrestrictedResolveValue(self, context=None, path='', default=_MARKER,
                               restricted=0):
597 598 599 600 601 602 603 604 605 606 607
    """
      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:
608
        portal = aq_inner(self.getPortalObject())
609 610 611 612
        container = portal
      else:
        container = context

613 614 615 616
      if restricted:
        validate = getSecurityManager().validate

      while stack:
617
        key = stack.pop()
618 619 620 621 622 623 624 625
        try:
          value = container[key]
        except KeyError:
          LOG('BusinessTemplate', WARNING, 
              'Could not access object %s' % (path,))
          if default is _MARKER:
            raise
          return default
626

627 628 629 630 631 632 633 634 635 636 637 638
        if restricted:
          try:
            if not validate(container, container, key, value):
              raise Unauthorized('unauthorized access to element %s' % key)
          except Unauthorized:
            LOG('BusinessTemplate', WARNING,
                'access to %s is forbidden' % (path,))
          if default is _MARKER:
            raise
          return default

        container = value
639

640 641 642
      return value
    else:
      return context
643

644 645 646
class ObjectTemplateItem(BaseTemplateItem):
  """
    This class is used for generic objects and as a subclass.
647
  """
648

649 650 651 652
  def __init__(self, id_list, tool_id=None, **kw):
    BaseTemplateItem.__init__(self, id_list, tool_id=tool_id, **kw)
    if tool_id is not None:
      id_list = self._archive.keys()
653
      self._archive.clear()
654 655 656
      for id in id_list :
        if id != '':
          self._archive["%s/%s" % (tool_id, id)] = None
657

658
  def export(self, context, bta, **kw):
Vincent Pelletier's avatar
Vincent Pelletier committed
659 660 661 662
    """
      Export the business template : fill the BusinessTemplateArchive with
      objects exported as XML, hierarchicaly organised.
    """
663 664
    if len(self._objects.keys()) == 0:
      return
665
    path = self.__class__.__name__
Vincent Pelletier's avatar
Vincent Pelletier committed
666
    for key, obj in self._objects.iteritems():
667
      # export object in xml
668
      f = StringIO()
669
      XMLExportImport.exportXML(obj._p_jar, obj._p_oid, f)
670
      bta.addObject(f, key, path=path)
671

Aurel's avatar
Aurel committed
672
  def build_sub_objects(self, context, id_list, url, **kw):
Vincent Pelletier's avatar
Vincent Pelletier committed
673
    # XXX duplicates code from build
Aurel's avatar
Aurel committed
674 675
    p = context.getPortalObject()
    for id in id_list:
676
      relative_url = '/'.join([url,id])
677 678
      obj = p.unrestrictedTraverse(relative_url)
      obj = obj._getCopy(context)
679 680
      keep_workflow_history = self.isKeepWorkflowObject(relative_url)
      obj = self.removeProperties(obj, 1, keep_workflow_history)
Vincent Pelletier's avatar
Vincent Pelletier committed
681 682 683
      id_list = obj.objectIds() # FIXME duplicated variable name
      if hasattr(aq_base(obj), 'groups'): # XXX should check metatype instead
        # we must keep groups because they are deleted along with subobjects
684
        groups = deepcopy(obj.groups)
685
      if id_list:
Aurel's avatar
Aurel committed
686
        self.build_sub_objects(context, id_list, relative_url)
687
        for id_ in list(id_list):
688
          obj._delObject(id_)
689
      if hasattr(aq_base(obj), 'groups'):
690 691 692
        obj.groups = groups
      self._objects[relative_url] = obj
      obj.wl_clearLocks()
Aurel's avatar
Aurel committed
693

694 695 696 697
  def build(self, context, **kw):
    BaseTemplateItem.build(self, context, **kw)
    p = context.getPortalObject()
    for relative_url in self._archive.keys():
698 699 700 701
      try:
        obj = p.unrestrictedTraverse(relative_url)
      except ValueError:
        raise ValueError, "Can not access to %s" % relative_url
702 703 704 705
      try:
        obj = obj._getCopy(context)
      except AttributeError:
        raise AttributeError, "Could not find object '%s' during business template processing." % relative_url
706
      _recursiveRemoveUid(obj)
707 708
      keep_workflow_history = self.isKeepWorkflowObject(relative_url)
      obj = self.removeProperties(obj, 1, keep_workflow_history)
709
      id_list = obj.objectIds()
Vincent Pelletier's avatar
Vincent Pelletier committed
710 711
      if hasattr(aq_base(obj), 'groups'): # XXX should check metatype instead
        # we must keep groups because they are deleted along with subobjects
712
        groups = deepcopy(obj.groups)
Aurel's avatar
Aurel committed
713 714
      if len(id_list) > 0:
        self.build_sub_objects(context, id_list, relative_url)
715
        for id_ in list(id_list):
716
          obj._delObject(id_)
717
      if hasattr(aq_base(obj), 'groups'):
718 719 720
        obj.groups = groups
      self._objects[relative_url] = obj
      obj.wl_clearLocks()
721

722
  def _compileXML(self, file):
723 724 725 726 727 728 729 730 731 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
    # This method converts XML to ZEXP. Because the conversion
    # is quite heavy, a persistent cache database is used to
    # store ZEXP, so the second run wouldn't have to re-generate
    # identical data again.
    #
    # For now, a pair of the path to an XML file and its modification time
    # are used as a unique key. In theory, a checksum of the content could
    # be used instead, and it could be more reliable, as modification time
    # might not be updated in some insane filesystems correctly. However,
    # in practice, checksums consume a lot of CPU time, so when the cache
    # does not hit, the increased overhead is significant. In addition, it
    # does rarely happen that two XML files in Business Templates contain
    # the same data, so it may not be expected to have more cache hits
    # with this approach.
    #
    # The disadvantage is that this wouldn't work with the archive format,
    # because each entry in an archive does not have a mtime in itself.
    # However, the plan is to have an archive to retain ZEXP directly
    # instead of XML, so the idea of caching would be completely useless
    # with the archive format.
    name = file.name
    mtime = os.path.getmtime(file.name)
    key = '%s:%s' % (name, mtime)

    try:
      return StringIO(cache_database.db[key])
    except:
      pass

    #LOG('Business Template', 0, 'Compiling %s...' % (name,))
    from Shared.DC.xml import ppml
    from OFS.XMLExportImport import start_zopedata, save_record, save_zopedata
    import xml.parsers.expat
    outfile=StringIO()
    try:
      data=file.read()
      F=ppml.xmlPickler()
      F.end_handlers['record'] = save_record
      F.end_handlers['ZopeData'] = save_zopedata
      F.start_handlers['ZopeData'] = start_zopedata
      F.binary=1
      F.file=outfile
      p=xml.parsers.expat.ParserCreate('utf-8')
      p.returns_unicode = False
      p.CharacterDataHandler=F.handle_data
      p.StartElementHandler=F.unknown_starttag
      p.EndElementHandler=F.unknown_endtag
770
      p.Parse(data)
771 772 773 774 775 776 777 778 779 780 781

      try:
        cache_database.db[key] = outfile.getvalue()
      except:
        pass

      outfile.seek(0)
      return outfile
    except:
      outfile.close()
      raise
782

783 784 785 786 787 788 789
  def getConnection(self, obj):
    while True:
      connection = obj._p_jar
      if connection is not None:
        return connection
      obj = obj.aq_parent

790
  def _importFile(self, file_name, file_obj):
791
    # import xml file
792
    if not file_name.endswith('.xml'):
793
      LOG('Business Template', 0, 'Skipping file "%s"' % (file_name, ))
794
      return
795
    connection = self.getConnection(self.aq_parent)
796
    __traceback_info__ = 'Importing %s' % file_name
797 798
    if hasattr(cache_database, 'db') and isinstance(file_obj, file):
      obj = connection.importFile(self._compileXML(file_obj))
799
    else:
800 801
      # FIXME: Why not use the importXML function directly? Are there any BT5s
      # with actual .zexp files on the wild?
802
      obj = connection.importFile(file_obj, customImporters=customImporters)
803 804
    self._objects[file_name[:-4]] = obj

805
  def preinstall(self, context, installed_item, **kw):
Yoshinori Okuji's avatar
Yoshinori Okuji committed
806 807
    modified_object_list = {}
    if context.getTemplateFormatVersion() == 1:
Julien Muchembled's avatar
Julien Muchembled committed
808 809
      upgrade_list = []
      type_name = self.__class__.__name__.split('TemplateItem')[-2]
810
      for path, obj in self._objects.iteritems():
811
        if installed_item._objects.has_key(path):
812
          upgrade_list.append((path, installed_item._objects[path]))
813
        else: # new object
Julien Muchembled's avatar
Julien Muchembled committed
814
          modified_object_list[path] = 'New', type_name
815

Julien Muchembled's avatar
Julien Muchembled committed
816 817 818 819 820 821 822 823 824
      # update _p_jar property of objects cleaned by removeProperties
      transaction.savepoint(optimistic=True)
      for path, old_object in upgrade_list:
        # compare object to see it there is changes
        new_object = self._objects[path]
        new_io = StringIO()
        old_io = StringIO()
        OFS.XMLExportImport.exportXML(new_object._p_jar, new_object._p_oid, new_io)
        new_obj_xml = new_io.getvalue()
825 826 827
        try:
          OFS.XMLExportImport.exportXML(old_object._p_jar, old_object._p_oid, old_io)
          old_obj_xml = old_io.getvalue()
828 829
        except (ImportError, UnicodeDecodeError), e: # module is already
                                                     # removed etc.
830
          old_obj_xml = '(%s: %s)' % (e.__class__.__name__, e)
Julien Muchembled's avatar
Julien Muchembled committed
831 832 833
        new_io.close()
        old_io.close()
        if new_obj_xml != old_obj_xml:
834 835 836 837
          if context.isKeepObject(path):
            modified_object_list[path] = 'Modified but should be kept', type_name
          else:
            modified_object_list[path] = 'Modified', type_name
838
      # get removed object
839
      for path in set(installed_item._objects) - set(self._objects):
840 841 842 843
        if context.isKeepObject(path):
          modified_object_list[path] = 'Removed but should be kept', type_name
        else:
          modified_object_list[path] = 'Removed', type_name
844 845
    return modified_object_list

846
  def _backupObject(self, action, trashbin, container_path, object_id, **kw):
847 848 849
    """
      Backup the object in portal trash if necessery and return its subobjects
    """
850
    p = self.getPortalObject()
851
    if trashbin is None: # must return subobjects
852 853 854 855 856 857 858
      subobject_dict = {}
      obj = p.unrestrictedTraverse(container_path)[object_id]
      for subobject_id in obj.objectIds():
        subobject = obj[subobject_id]
        subobject_dict[subobject_id] = subobject._p_jar.exportFile(
            subobject._p_oid, StringIO())
      return subobject_dict
859
    # XXX btsave is for backward compatibility
860
    if action in ('backup', 'btsave', 'save_and_remove',):
861
      save = 1
862
    elif action in ('install', 'remove'):
863
      save = 0
864 865 866
    else:
      # As the list of available actions is not strictly defined,
      # prevent mistake if an action is not handled
867
      raise NotImplementedError, 'Unknown action "%s"' % action
868 869 870
    return p.portal_trash.backupObject(trashbin, container_path, object_id,
                                       save=save, **kw)

871

872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887
  def beforeInstall(self):
    """
      Installation hook.
      Called right at the begining of "install" method.
      Can be overridden by subclasses.
    """
    pass

  def afterInstall(self):
    """
      Installation hook.
      Called right before returning in "install" method.
      Can be overridden by subclasses.
    """
    pass

888
  def onNewObject(self, obj):
889 890 891 892
    """
      Installation hook.
      Called when installation process determined that object to install is
      new on current site (it's not replacing an existing object).
893
      `obj` parameter is the newly created object in its acquisition context.
894 895 896 897
      Can be overridden by subclasses.
    """
    pass

898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914
  def setSafeReindexationMode(self, context):
    """
      Postpone indexations after unindexations.
      This avoids alarming error messages about a single uid being used
      by "deleted" path and reindexed object. This can happen here for
      objects on which the uid was restored: previous object was deleted,
      hence the "deleted" path, and new object does have the same uid.
    """
    original_reindex_parameters = context.getPlacelessDefaultReindexParameters()
    if original_reindex_parameters is None:
      original_reindex_parameters = {}
    activate_kw = original_reindex_parameters.get('activate_kw', {}).copy()
    activate_kw['after_method_id'] = 'unindexObject'
    context.setPlacelessDefaultReindexParameters(activate_kw=activate_kw, \
                                                 **original_reindex_parameters)
    return original_reindex_parameters

915 916 917 918 919 920
  def _getObjectKeyList(self):
    # sort to add objects before their subobjects
    keys = self._objects.keys()
    keys.sort()
    return keys

921
  def install(self, context, trashbin, **kw):
922
    self.beforeInstall()
923 924
    update_dict = kw.get('object_to_update')
    force = kw.get('force')
Yoshinori Okuji's avatar
Yoshinori Okuji committed
925
    if context.getTemplateFormatVersion() == 1:
926 927 928 929 930 931
      def recurse(hook, document, prefix=''):
        my_prefix = '%s/%s' % (prefix, document.id)
        if (hook(document, my_prefix)):
          for subdocument in document.objectValues():
            recurse(hook, subdocument, my_prefix)
      def saveHook(document, prefix):
932
        uid = getattr(aq_base(document), 'uid', None)
933 934 935 936 937 938 939 940 941 942 943 944
        if uid is None:
          return 0
        else:
          saved_uid_dict[prefix] = uid
          return 1
      def restoreHook(document, prefix):
        uid = saved_uid_dict.get(prefix)
        if uid is None:
          return 0
        else:
          document.uid = uid
          return 1
945
      groups = {}
946
      old_groups = {}
947
      portal = context.getPortalObject()
948 949
      # set safe activities execution order
      original_reindex_parameters = self.setSafeReindexationMode(context)
950 951
      object_key_list = self._getObjectKeyList()
      for path in object_key_list:
952
        __traceback_info__ = path
953 954 955 956 957
        # We do not need to perform any backup because the object was
        # created during the Business Template installation
        if update_dict.get(path) == 'migrate':
          continue

958 959
        if update_dict.has_key(path) or force:
          # get action for the oject
960
          action = 'backup'
961 962 963 964 965
          if not force:
            action = update_dict[path]
            if action == 'nothing':
              continue
          # get subobjects in path
966 967 968
          path_list = path.split('/')
          container_path = path_list[:-1]
          object_id = path_list[-1]
969
          try:
970
            container = self.unrestrictedResolveValue(portal, container_path)
971 972
          except KeyError:
            # parent object can be set to nothing, in this case just go on
973
            container_url = '/'.join(container_path)
974 975 976 977
            if update_dict.get(container_url) == 'nothing':
              continue
            # If container's container is portal_catalog,
            # then automatically create the container.
978
            elif len(container_path) > 1 and container_path[-2] == 'portal_catalog':
979 980
              # The id match, but better double check with the meta type
              # while avoiding the impact of systematic check
981
              container_container = portal.unrestrictedTraverse(container_path[:-1])
982 983 984 985 986
              if container_container.meta_type == 'ERP5 Catalog':
                container_container.manage_addProduct['ZSQLCatalog'].manage_addSQLCatalog(id=container_path[-1], title='')
                if len(container_container.objectIds()) == 1:
                  container_container.default_sql_catalog_id = container_path[-1]
                container = portal.unrestrictedTraverse(container_path)
987 988
            else:
              raise
989
          saved_uid_dict = {}
990
          subobjects_dict = {}
991
          portal_type_dict = {}
992
          old_obj = container._getOb(object_id, None)
993
          object_existed = old_obj is not None
994
          if object_existed:
995
            if context.isKeepObject(path) and force:
996 997 998
              # do nothing if the object is specified in keep list in
              # force mode.
              continue
999
            # Object already exists
1000
            recurse(saveHook, old_obj)
1001
            if getattr(aq_base(old_obj), 'groups', None) is not None:
1002 1003 1004 1005 1006
              # we must keep original order groups
              # from old form in case we keep some
              # old widget, thus we can readd them in
              # the right order group
              old_groups[path] = deepcopy(old_obj.groups)
1007
            subobjects_dict = self._backupObject(action, trashbin,
1008
                                                 container_path, object_id)
1009
            # in case of portal types, we want to keep some properties
1010
            if interfaces.ITypeProvider.providedBy(container):
1011
              for attr in ('allowed_content_types',
1012 1013 1014 1015 1016 1017
                           'hidden_content_type_list',
                           'property_sheet_list',
                           'base_category_list'):
                portal_type_dict[attr] = getattr(old_obj, attr, ())
              portal_type_dict['workflow_chain'] = \
                getChainByType(context)[1].get('chain_' + object_id, '')
1018
            container.manage_delObjects([object_id])
1019

1020
          # install object
1021
          obj = self._objects[path]
1022 1023 1024 1025
          # XXX Following code make Python Scripts compile twice, because
          #     _getCopy returns a copy without the result of the compilation.
          #     A solution could be to add a specific _getCopy method to
          #     Python Scripts.
1026
          if getattr(aq_base(obj), 'groups', None) is not None:
1027
            # we must keep original order groups
1028
            # because they change when we add subobjects
1029
            groups[path] = deepcopy(obj.groups)
1030
          # copy the object
1031 1032 1033 1034 1035 1036 1037
          if (getattr(aq_base(obj), '_mt_index', None) is not None and
              obj._count() == 0):
            # some btrees were exported in a corrupted state. They're empty but
            # their metadata-index (._mt_index) contains entries which in
            # Zope 2.12 are used for .objectIds(), .objectValues() and
            # .objectItems(). In these cases, force the 
            LOG('Products.ERP5.Document.BusinessTemplate', WARNING,
Łukasz Nowak's avatar
Łukasz Nowak committed
1038
                'Cleaning corrupted BTreeFolder2 object at %r.' % (path,))
1039
            obj._initBTrees()
1040
          obj = obj._getCopy(container)
1041
          self.removeProperties(obj, 0)
1042 1043
          __traceback_info__ = (container, object_id, obj)
          container._setObject(object_id, obj)
1044
          obj = container._getOb(object_id)
1045 1046 1047 1048 1049

          if not object_existed:
            # A new object was added, call the hook
            self.onNewObject(obj)

1050 1051
          # mark a business template installation so in 'PortalType_afterClone' scripts
          # we can implement logical for reseting or not attributes (i.e reference).
1052
          self.REQUEST.set('is_business_template_installation', 1)
1053 1054 1055 1056
          # We set isIndexable to 0 before calling
          # manage_afterClone in order to not call recursiveReindex, this is
          # useless because we will already reindex every created object, so
          # we avoid duplication of reindexation
1057
          obj.isIndexable = ConstantGetter('isIndexable', value=False)
1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090
          # START:part of ERP5Type.CopySupport.manage_afterClone
          # * reset uid
          # * reset owner
          # * do not reset workflow
          # * do not call recursively
          # * do not call type-based afterClone script
          #
          # Change uid attribute so that Catalog thinks object was not yet catalogued
          aq_base(obj).uid = portal.portal_catalog.newUid()
          # Give the Owner local role to the current user, zope only does this if no
          # local role has been defined on the object, which breaks ERP5Security
          if getattr(aq_base(obj), '__ac_local_roles__', None) is not None:
            user=getSecurityManager().getUser()
            if user is not None:
              userid=user.getId()
              if userid is not None:
                #remove previous owners
                local_role_dict = obj.__ac_local_roles__
                removable_role_key_list = []
                for key, value in local_role_dict.items():
                  if 'Owner' in value:
                    value.remove('Owner')
                  if len(value) == 0:
                    removable_role_key_list.append(key)
                # there is no need to keep emptied keys after cloning, it makes
                # unstable local roles -- if object is cloned it can be different when
                # after being just added
                for key in removable_role_key_list:
                  local_role_dict.pop(key)
                #add new owner
                l=local_role_dict.setdefault(userid, [])
                l.append('Owner')
          # END:part of ERP5Type.CopySupport.manage_afterClone
1091 1092 1093
          del obj.isIndexable
          if getattr(aq_base(obj), 'reindexObject', None) is not None:
            obj.reindexObject()
1094
          obj.wl_clearLocks()
1095
          if portal_type_dict:
1096
            # set workflow chain
1097
            wf_chain = portal_type_dict.pop('workflow_chain')
1098 1099
            chain_dict = getChainByType(context)[1]
            default_chain = ''
1100
            chain_dict['chain_%s' % (object_id)] = wf_chain
1101
            context.portal_workflow.manage_changeWorkflows(default_chain, props=chain_dict)
1102 1103
            # restore some other properties
            obj.__dict__.update(portal_type_dict)
1104
          # import sub objects if there is
1105
          if subobjects_dict:
1106
            # get a jar
1107
            connection = self.getConnection(obj)
1108
            # import subobjects
1109
            for subobject_id, subobject_data in subobjects_dict.iteritems():
1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120
              try:
                if obj._getOb(subobject_id, None) is None:
                  subobject_data.seek(0)
                  subobject = connection.importFile(subobject_data)
                  obj._setObject(subobject_id, subobject)
              except AttributeError:
                # XXX this may happen when an object which can contain
                # sub-objects (e.g. ERP5 Form) has been replaced with
                # an object which cannot (e.g. External Method).
                LOG('BusinessTemplate', WARNING,
                    'could not restore %r in %r' % (subobject_id, obj))
1121
          if obj.meta_type in ('Z SQL Method',):
1122
            fixZSQLMethod(portal, obj)
1123 1124 1125
          # portal transforms specific initialization
          elif obj.meta_type in ('Transform', 'TransformsChain'):
            assert container.meta_type == 'Portal Transforms'
1126 1127 1128
            # skip transforms that couldn't have been initialized
            if obj.title != 'BROKEN':
              container._mapTransform(obj)
1129
          elif obj.meta_type in ('ERP5 Ram Cache',
1130
                                 'ERP5 Distributed Ram Cache',):
1131 1132
            assert container.meta_type in ('ERP5 Cache Factory',
                                           'ERP5 Cache Bag')
1133
            container.getParentValue().updateCache()
1134 1135
          elif (container.meta_type == 'CMF Skins Tool') and \
              (old_obj is not None):
1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149
            # Keep compatibility with previous export format of
            # business_template_registered_skin_selections
            # and do not modify exported value
            if obj.getProperty('business_template_registered_skin_selections', 
                               None) is None:
              # Keep previous value of register skin selection for skin folder
              skin_selection_list = old_obj.getProperty(
                  'business_template_registered_skin_selections', None)
              if skin_selection_list is not None:
                if isinstance(skin_selection_list, basestring):
                  skin_selection_list = skin_selection_list.split(' ')
                obj._setProperty(
                    'business_template_registered_skin_selections',
                    skin_selection_list, type='tokens')
1150 1151 1152 1153 1154 1155 1156 1157
          # in case the portal ids, we want keep the property dict
          elif interfaces.IIdGenerator.providedBy(obj) and \
            old_obj is not None:
            for dict_name in ('last_max_id_dict', 'last_id_dict'):
              # Keep previous last id dict
              if getattr(old_obj, dict_name, None) is not None:
                old_dict = getattr(old_obj, dict_name, None)
                setattr(obj, dict_name, old_dict)
1158

1159
          recurse(restoreHook, obj)
1160
      # now put original order group
1161 1162
      # we remove object not added in forms
      # we put old objects we have kept
1163
      for path, new_groups_dict in groups.iteritems():
1164 1165 1166 1167 1168 1169 1170 1171 1172
        if not old_groups.has_key(path):
          # installation of a new form
          obj = portal.unrestrictedTraverse(path)
          obj.groups = new_groups_dict
        else:
          # upgrade of a form
          old_groups_dict = old_groups[path]
          obj = portal.unrestrictedTraverse(path)
          # first check that all widgets are in new order
1173
          # excetp the one that had to be removed
1174 1175 1176 1177
          widget_id_list = obj.objectIds()
          for widget_id in widget_id_list:
            widget_path = path+'/'+widget_id
            if update_dict.has_key(widget_path) and update_dict[widget_path] in ('remove', 'save_and_remove'):
1178 1179
              continue
            widget_in_form = 0
1180 1181
            for group_id, group_value_list in new_groups_dict.iteritems():
              if widget_id in group_value_list:
1182 1183 1184 1185 1186 1187
                widget_in_form = 1
                break
            # if not, add it in the same groups
            # defined on the former form
            previous_group_id = None
            if not widget_in_form:
1188
              for old_group_id, old_group_values in old_groups_dict.iteritems():
1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199
                if widget_id in old_group_values:
                  previous_group_id = old_group_id
              # if we find same group in new one, add widget to it
              if previous_group_id is not None and new_groups_dict.has_key(previous_group_id):
                new_groups_dict[previous_group_id].append(widget_id)
              # otherwise use a specific group
              else:
                if new_groups_dict.has_key('not_assigned'):
                  new_groups_dict['not_assigned'].append(widget_id)
                else:
                  new_groups_dict['not_assigned'] = [widget_id,]
1200
                  obj.group_list = list(obj.group_list) + ['not_assigned']
1201
          # second check all widget_id in order are in form
1202 1203
          for group_id, group_value_list in new_groups_dict.iteritems():
            for widget_id in tuple(group_value_list):
1204 1205 1206
              if widget_id not in widget_id_list:
                # if we don't find the widget id in the form
                # remove it fro the group
1207
                group_value_list.remove(widget_id)
1208
          # now set new group object
1209
          obj.groups = new_groups_dict
1210
      # restore previous activities execution order
1211
      context.setPlacelessDefaultReindexParameters(**original_reindex_parameters)
1212
      # Do not forget to delete all remaining objects if asked by user
1213 1214 1215 1216 1217 1218
      # Fetch all sub objects path recursively
      recursive_path_list = []
      def fillRecursivePathList(from_path_list):
        for from_path in from_path_list:
          container = portal.unrestrictedTraverse(from_path, None)
          if container is not None:
1219 1220
            if from_path in recursive_path_list:
              continue
1221 1222 1223 1224 1225
            recursive_path_list.append(from_path)
            # Check that container support iteration of sub_content_id
            if getattr(aq_base(container), 'objectIds', None) is not None:
              fillRecursivePathList(['%s/%s' % (from_path, sub_content_id) for\
                                        sub_content_id in container.objectIds()])
1226
      fillRecursivePathList(object_key_list)
1227 1228 1229
      for recursive_path in recursive_path_list:
        if recursive_path in update_dict:
          action = update_dict[recursive_path]
1230
          if action in ('remove', 'save_and_remove'):
1231
            document = self.unrestrictedResolveValue(portal, recursive_path, None)
1232 1233 1234
            if document is None:
              # It happens if the parent of target path is removed before
              continue
1235 1236 1237 1238 1239 1240 1241 1242 1243 1244
            if getattr(aq_base(document), 'getParentValue', None) is not None:
              # regular ERP5 object
              parent = document.getParentValue()
            else:
              parent = document.aq_parent
            document_id = document.getId()
            container_path_list = recursive_path.split('/')[:-1]
            self._backupObject(action, trashbin, container_path_list,
                               document_id)
            parent.manage_delObjects([document_id])
Aurel's avatar
Aurel committed
1245
    else:
1246 1247
      # for old business template format
      BaseTemplateItem.install(self, context, trashbin, **kw)
Aurel's avatar
Aurel committed
1248
      portal = context.getPortalObject()
1249
      for relative_url in self._archive.keys():
1250
        obj = self._archive[relative_url]
Aurel's avatar
Aurel committed
1251 1252 1253 1254
        container_path = relative_url.split('/')[0:-1]
        object_id = relative_url.split('/')[-1]
        container = portal.unrestrictedTraverse(container_path)
        container_ids = container.objectIds()
1255
        if object_id in container_ids:    # Object already exists
1256
          self._backupObject('backup', trashbin, container_path, object_id)
1257
          container.manage_delObjects([object_id])
Aurel's avatar
Aurel committed
1258
        # Set a hard link
1259 1260 1261 1262 1263 1264
        obj = obj._getCopy(container)
        container._setObject(object_id, obj)
        obj = container._getOb(object_id)
        obj.manage_afterClone(obj)
        obj.wl_clearLocks()
        if obj.meta_type in ('Z SQL Method',):
1265
          fixZSQLMethod(portal, obj)
1266
    self.afterInstall()
1267 1268 1269

  def uninstall(self, context, **kw):
    portal = context.getPortalObject()
1270
    trash = kw.get('trash', 0)
1271 1272 1273 1274 1275
    trashbin = kw.get('trashbin', None)
    object_path = kw.get('object_path', None)
    if object_path is not None:
      object_keys = [object_path]
    else:
1276
      object_keys = self._archive.keys()
1277
    for relative_url in object_keys:
1278 1279
      container_path = relative_url.split('/')[0:-1]
      object_id = relative_url.split('/')[-1]
1280
      try:
1281
        container = self.unrestrictedResolveValue(portal, container_path)
1282
        container._getOb(object_id) # We force access to the object to be sure
1283 1284
                                        # that appropriate exception is thrown
                                        # in case object is already backup and/or removed
1285
        if trash and trashbin is not None:
1286
          self.portal_trash.backupObject(trashbin, container_path, object_id, save=1, keep_subobjects=1)
1287
        if container.meta_type == 'CMF Skins Tool':
1288 1289
          # we are removing a skin folder, check and 
          # remove if registered skin selection
1290
          unregisterSkinFolderId(container, object_id,
1291 1292