BusinessTemplate.py 222 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
31
from Shared.DC.ZRDB import Aqueduct
32
from Shared.DC.ZRDB.Connection import Connection as RDBConnection
33
from Products.ERP5Type.DiffUtils import DiffFile
34
from Products.ERP5Type.Globals import Persistent, PersistentMapping
35
from Acquisition import Implicit, aq_base, aq_inner, aq_parent
36
from AccessControl import ClassSecurityInfo, Unauthorized, getSecurityManager
37
from AccessControl.SecurityInfo import ModuleSecurityInfo
Jean-Paul Smets's avatar
Jean-Paul Smets committed
38
from Products.CMFCore.utils import getToolByName
39
from Products.PythonScripts.PythonScript import PythonScript
40
from Products.ERP5Type.Accessor.Constant import PropertyGetter as ConstantGetter
41
from Products.ERP5Type.Base import WorkflowMethod
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
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 Products.ERP5Type import tarfile
84
from urllib import quote, unquote
85
from difflib import unified_diff
86
import posixpath
Julien Muchembled's avatar
Julien Muchembled committed
87
import transaction
88

89 90 91 92 93 94
import threading

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

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

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

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

117 118 119 120 121 122
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:
123 124 125 126
    try:
      return acquisition_context.getPortalObject().portal_catalog.objectIds('SQLCatalog')[0]
    except IndexError:
      return None
127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145
  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

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

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

161 162 163 164 165 166 167
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
  """
168
  pw = context.getPortalObject().portal_workflow
169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189
  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)

190 191 192 193 194 195 196 197 198 199 200 201 202 203
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]
204 205 206
  # recompile the method
  method._arg = Aqueduct.parse(method.arguments_src)
  method.template = method.template_class(method.src)
207

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

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

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

238 239 240 241 242 243 244 245 246
  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 ''
247
    selection_list = selection.split(',')
248
    if (skin_folder_id not in selection_list):
249
      selection_list.insert(0, skin_folder_id)
250
    if reorder_skin_selection:
251 252
      # Sort by skin priority and ID
      selection_list.sort(key=skin_sort_key)
253
    if (skin_name in skin_layer_list):
254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312
      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 registred 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:

    skin_selection_registered = False
    for skin_folder in skin_tool.objectValues():
      try:
        skin_selection_list = skin_folder.getProperty(
               'business_template_registered_skin_selections', ())
        if skin_name in skin_selection_list:
          skin_selection_registered = True
          break
      except AttributeError:
        pass

    if (not skin_selection_registered):
      skin_tool.manage_skinLayers(chosen=[skin_name], 
                                  del_skin=1)
      skin_tool.getPortalObject().changeSkin(None)

def unregisterSkinFolder(skin_tool, skin_folder, skin_selection_list):
  skin_folder_id = skin_folder.getId()

  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
313 314 315
class BusinessTemplateArchive:
  """
    This is the base class for all Business Template archives
316
  """
317
  def _initCreation(self, path, **kw):
318
    self.path = path
Aurel's avatar
Aurel committed
319 320 321 322 323 324 325

  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)

326
  def addObject(self, obj, name, path=None, ext='.xml'):
327 328 329 330 331 332
    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)
333
    try:
334 335 336 337 338 339 340 341 342 343
      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)
344

345
  def finishCreation(self):
Aurel's avatar
Aurel committed
346 347 348 349
    pass

class BusinessTemplateFolder(BusinessTemplateArchive):
  """
Christophe Dumez's avatar
Christophe Dumez committed
350
    Class archiving business template into a folder tree
351
  """
352 353 354 355 356 357 358 359 360 361 362 363
  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
364 365 366 367 368 369 370
    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
371

372
  def importFiles(self, item, **kw):
Aurel's avatar
Aurel committed
373 374 375
    """
      Import file from a local folder
    """
376
    class_name = item.__class__.__name__
377 378
    root_path_len = self.root_path_len
    prefix_len = root_path_len + len(class_name) + len(os.sep)
379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398
    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
399

Aurel's avatar
Aurel committed
400 401 402 403 404
class BusinessTemplateTarball(BusinessTemplateArchive):
  """
    Class archiving businnes template into a tarball file
  """

405 406
  def _initCreation(self, **kw):
    BusinessTemplateArchive._initCreation(self, **kw)
Aurel's avatar
Aurel committed
407 408 409
    # init tarfile obj
    self.fobj = StringIO()
    self.tar = tarfile.open('', 'w:gz', self.fobj)
410 411 412 413 414 415 416 417 418 419 420 421 422
    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
423 424 425
    self.tar.close()
    return self.fobj

426
  def _initImport(self, file, **kw):
427 428 429 430 431 432 433 434 435 436 437 438
    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
439

440
  def importFiles(self, item, **kw):
Aurel's avatar
Aurel committed
441 442
    """
      Import all file from the archive to the site
443
    """
444 445 446
    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
447

448
class TemplateConditionError(Exception): pass
449
class TemplateConflictError(Exception): pass
450
class BusinessTemplateMissingDependency(Exception): pass
451

452 453 454
ModuleSecurityInfo(__name__).declarePublic('BusinessTemplateMissingDependency',
  'TemplateConditionError', 'TemplateConflictError')

455
class BaseTemplateItem(Implicit, Persistent):
456
  """
457
    This class is the base class for all template items.
458
    is_bt_for_diff means This BT is used to compare self temporary BT with installed BT
459
  """
460
  is_bt_for_diff = None
Jean-Paul Smets's avatar
Jean-Paul Smets committed
461

462
  def __init__(self, id_list, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
463
    self.__dict__.update(kw)
464
    self._archive = PersistentMapping()
Aurel's avatar
Aurel committed
465
    self._objects = PersistentMapping()
466
    for id in id_list:
467 468
      if id is not None and id != '':
        self._archive[id] = None
469 470 471 472

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

473
  def preinstall(self, context, installed_item, **kw):
Vincent Pelletier's avatar
Vincent Pelletier committed
474 475
    """
      Build a list of added/removed/changed files between the BusinessTemplate
476
      being installed (self) and the installed one (installed_item).
Vincent Pelletier's avatar
Vincent Pelletier committed
477 478 479 480 481
      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
482 483
    modified_object_list = {}
    if context.getTemplateFormatVersion() == 1:
484
      new_keys = self._objects.keys()
485
      for path in new_keys:
486
        if installed_item._objects.has_key(path):
Vincent Pelletier's avatar
Vincent Pelletier committed
487
          # compare objects to see it there are changes
488
          new_obj_xml = self.generateXml(path=path)
489
          old_obj_xml = installed_item.generateXml(path=path)
490 491
          if new_obj_xml != old_obj_xml:
            modified_object_list.update({path : ['Modified', self.__class__.__name__[:-12]]})
Vincent Pelletier's avatar
Vincent Pelletier committed
492
          # else, compared versions are identical, don't overwrite the old one
493 494
        else: # new object
          modified_object_list.update({path : ['New', self.__class__.__name__[:-12]]})
Vincent Pelletier's avatar
Vincent Pelletier committed
495
      # list removed objects
496
      old_keys = installed_item._objects.keys()
497 498 499 500 501 502
      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):
503
    pass
504 505 506

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

508
  def remove(self, context, **kw):
Vincent Pelletier's avatar
Vincent Pelletier committed
509 510 511 512 513 514
    """
      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.
    """
515 516 517
    remove_dict = kw.get('remove_object_dict', {})
    keys = self._objects.keys()
    keys.sort()
518
    keys.reverse()
519 520 521 522 523 524 525 526 527 528 529
    # 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)
530 531 532 533
        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
534

535

536 537 538 539
  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
540
  def export(self, context, bta, **kw):
541
    pass
Aurel's avatar
Aurel committed
542

543 544
  def getKeys(self):
    return self._objects.keys()
545

Aurel's avatar
Aurel committed
546
  def importFile(self, bta, **kw):
547
    bta.importFiles(item=self)
548

549
  def removeProperties(self, obj, export, keep_workflow_history=False):
550 551
    """
    Remove unneeded properties for export
552
    """
553
    obj._p_activate()
554 555
    klass = obj.__class__
    classname = klass.__name__
556

557
    attr_set = set(('_dav_writelocks', '_filepath', '_owner', 'uid',
558
                    '__ac_local_roles__'))
559
    if export:
560 561
      if not keep_workflow_history:
        attr_set.add('workflow_history')
562 563
      # PythonScript covers both Zope Python scripts
      # and ERP5 Python Scripts
564
      if isinstance(obj, PythonScript):
565
        attr_set.update(('func_code', 'func_defaults', '_code',
566
                         '_lazy_compilation', 'Python_magic'))
567 568 569
        for attr in 'errors', 'warnings', '_proxy_roles':
          if not obj.__dict__.get(attr, 1):
            delattr(obj, attr)
570 571
      elif classname == 'SQL' and klass.__module__== 'Products.ZSQLMethods.SQL':
        attr_set.update(('_arg', 'template'))
572
      elif interfaces.IIdGenerator.providedBy(obj):
573
        attr_set.update(('last_max_id_dict', 'last_id_dict'))
574 575
      elif classname == 'Types Tool' and klass.__module__ == 'erp5.portal_type':
        attr_set.add('type_provider_list')
576

577 578
    for attr in obj.__dict__.keys():
      if attr in attr_set or attr.startswith('_cache_cookie_'):
579 580
        delattr(obj, attr)

581
    if classname == 'PDFForm':
582
      if not obj.getProperty('business_template_include_content', 1):
583
        obj.deletePdfContent()
584 585
    return obj

586 587 588 589 590 591 592 593 594
  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]

595
  def restrictedResolveValue(self, context=None, path='', default=_MARKER):
596 597 598 599
    """
      Get the value with checking the security.
      This method does not acquire the parent.
    """
600 601
    return self.unrestrictedResolveValue(context, path, default=default,
                                         restricted=1)
602

603 604
  def unrestrictedResolveValue(self, context=None, path='', default=_MARKER,
                               restricted=0):
605 606 607 608 609 610 611 612 613 614 615
    """
      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:
616
        portal = aq_inner(self.getPortalObject())
617 618 619 620
        container = portal
      else:
        container = context

621 622 623 624
      if restricted:
        validate = getSecurityManager().validate

      while stack:
625
        key = stack.pop()
626 627 628 629 630 631 632 633
        try:
          value = container[key]
        except KeyError:
          LOG('BusinessTemplate', WARNING, 
              'Could not access object %s' % (path,))
          if default is _MARKER:
            raise
          return default
634

635 636 637 638 639 640 641 642 643 644 645 646
        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
647

648 649 650
      return value
    else:
      return context
651

652 653 654
class ObjectTemplateItem(BaseTemplateItem):
  """
    This class is used for generic objects and as a subclass.
655
  """
656

657 658 659 660
  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()
661
      self._archive.clear()
662 663 664
      for id in id_list :
        if id != '':
          self._archive["%s/%s" % (tool_id, id)] = None
665

666
  def export(self, context, bta, **kw):
Vincent Pelletier's avatar
Vincent Pelletier committed
667 668 669 670
    """
      Export the business template : fill the BusinessTemplateArchive with
      objects exported as XML, hierarchicaly organised.
    """
671 672
    if len(self._objects.keys()) == 0:
      return
673
    path = self.__class__.__name__
Vincent Pelletier's avatar
Vincent Pelletier committed
674
    for key, obj in self._objects.iteritems():
675
      # export object in xml
676
      f = StringIO()
677
      XMLExportImport.exportXML(obj._p_jar, obj._p_oid, f)
678
      bta.addObject(f, key, path=path)
679

Aurel's avatar
Aurel committed
680
  def build_sub_objects(self, context, id_list, url, **kw):
Vincent Pelletier's avatar
Vincent Pelletier committed
681
    # XXX duplicates code from build
Aurel's avatar
Aurel committed
682 683
    p = context.getPortalObject()
    for id in id_list:
684
      relative_url = '/'.join([url,id])
685 686
      obj = p.unrestrictedTraverse(relative_url)
      obj = obj._getCopy(context)
687 688
      keep_workflow_history = self.isKeepWorkflowObject(relative_url)
      obj = self.removeProperties(obj, 1, keep_workflow_history)
Vincent Pelletier's avatar
Vincent Pelletier committed
689 690 691
      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
692
        groups = deepcopy(obj.groups)
693
      if id_list:
Aurel's avatar
Aurel committed
694
        self.build_sub_objects(context, id_list, relative_url)
695
        for id_ in list(id_list):
696
          obj._delObject(id_)
697
      if hasattr(aq_base(obj), 'groups'):
698 699 700
        obj.groups = groups
      self._objects[relative_url] = obj
      obj.wl_clearLocks()
Aurel's avatar
Aurel committed
701

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

730
  def _compileXML(self, file):
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 770 771 772 773 774 775 776 777
    # 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
778
      p.Parse(data)
779 780 781 782 783 784 785 786 787 788 789

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

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

791 792 793 794 795 796 797
  def getConnection(self, obj):
    while True:
      connection = obj._p_jar
      if connection is not None:
        return connection
      obj = obj.aq_parent

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

813
  def preinstall(self, context, installed_item, **kw):
Yoshinori Okuji's avatar
Yoshinori Okuji committed
814 815
    modified_object_list = {}
    if context.getTemplateFormatVersion() == 1:
Julien Muchembled's avatar
Julien Muchembled committed
816 817
      upgrade_list = []
      type_name = self.__class__.__name__.split('TemplateItem')[-2]
818
      for path, obj in self._objects.iteritems():
819
        if installed_item._objects.has_key(path):
820
          upgrade_list.append((path, installed_item._objects[path]))
821
        else: # new object
Julien Muchembled's avatar
Julien Muchembled committed
822
          modified_object_list[path] = 'New', type_name
823

Julien Muchembled's avatar
Julien Muchembled committed
824 825 826 827 828 829 830 831 832
      # 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()
833 834 835
        try:
          OFS.XMLExportImport.exportXML(old_object._p_jar, old_object._p_oid, old_io)
          old_obj_xml = old_io.getvalue()
836 837
        except (ImportError, UnicodeDecodeError), e: # module is already
                                                     # removed etc.
838
          old_obj_xml = '(%s: %s)' % (e.__class__.__name__, e)
Julien Muchembled's avatar
Julien Muchembled committed
839 840 841
        new_io.close()
        old_io.close()
        if new_obj_xml != old_obj_xml:
842 843 844 845
          if context.isKeepObject(path):
            modified_object_list[path] = 'Modified but should be kept', type_name
          else:
            modified_object_list[path] = 'Modified', type_name
846
      # get removed object
847
      for path in set(installed_item._objects) - set(self._objects):
848 849 850 851
        if context.isKeepObject(path):
          modified_object_list[path] = 'Removed but should be kept', type_name
        else:
          modified_object_list[path] = 'Removed', type_name
852 853
    return modified_object_list

854
  def _backupObject(self, action, trashbin, container_path, object_id, **kw):
855 856 857
    """
      Backup the object in portal trash if necessery and return its subobjects
    """
858
    subobjects_dict = {}
859
    if trashbin is None: # must return subobjects
860 861 862 863 864 865
      object_path = container_path + [object_id]
      obj = self.unrestrictedTraverse(object_path)
      for subobject_id in list(obj.objectIds()):
        subobject_path = object_path + [subobject_id]
        subobject = self.unrestrictedTraverse(subobject_path)
        subobject_copy = subobject._p_jar.exportFile(subobject._p_oid)
866
        subobjects_dict[subobject_id] = subobject_copy
867
      return subobjects_dict
868
    # XXX btsave is for backward compatibility
869
    if action in ('backup', 'btsave', 'save_and_remove',):
870 871 872
      subobjects_dict = self.portal_trash.backupObject(trashbin, 
                                                container_path, object_id, 
                                                save=1, **kw)
873
    elif action in ('install', 'remove'):
874 875 876
      subobjects_dict = self.portal_trash.backupObject(trashbin, 
                                                container_path, object_id, 
                                                save=0, **kw)
877 878 879
    else:
      # As the list of available actions is not strictly defined,
      # prevent mistake if an action is not handled
880
      raise NotImplementedError, 'Unknown action "%s"' % action
881
    return subobjects_dict
882

883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898
  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

899
  def onNewObject(self, obj):
900 901 902 903
    """
      Installation hook.
      Called when installation process determined that object to install is
      new on current site (it's not replacing an existing object).
904
      `obj` parameter is the newly created object in its acquisition context.
905 906 907 908
      Can be overridden by subclasses.
    """
    pass

909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925
  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

926 927 928 929 930 931
  def _getObjectKeyList(self):
    # sort to add objects before their subobjects
    keys = self._objects.keys()
    keys.sort()
    return keys

932
  def install(self, context, trashbin, **kw):
933
    self.beforeInstall()
934 935
    update_dict = kw.get('object_to_update')
    force = kw.get('force')
Yoshinori Okuji's avatar
Yoshinori Okuji committed
936
    if context.getTemplateFormatVersion() == 1:
937 938 939 940 941 942
      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):
943
        uid = getattr(aq_base(document), 'uid', None)
944 945 946 947 948 949 950 951 952 953 954 955
        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
956
      groups = {}
957
      old_groups = {}
958
      portal = context.getPortalObject()
959 960
      # set safe activities execution order
      original_reindex_parameters = self.setSafeReindexationMode(context)
961 962
      object_key_list = self._getObjectKeyList()
      for path in object_key_list:
963 964 965 966 967
        # 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

968 969
        if update_dict.has_key(path) or force:
          # get action for the oject
970
          action = 'backup'
971 972 973 974 975
          if not force:
            action = update_dict[path]
            if action == 'nothing':
              continue
          # get subobjects in path
976 977 978
          path_list = path.split('/')
          container_path = path_list[:-1]
          object_id = path_list[-1]
979
          try:
980
            container = self.unrestrictedResolveValue(portal, container_path)
981 982
          except KeyError:
            # parent object can be set to nothing, in this case just go on
983
            container_url = '/'.join(container_path)
984 985 986 987
            if update_dict.get(container_url) == 'nothing':
              continue
            # If container's container is portal_catalog,
            # then automatically create the container.
988
            elif len(container_path) > 1 and container_path[-2] == 'portal_catalog':
989 990
              # The id match, but better double check with the meta type
              # while avoiding the impact of systematic check
991
              container_container = portal.unrestrictedTraverse(container_path[:-1])
992 993 994 995 996
              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)
997 998
            else:
              raise
999
          saved_uid_dict = {}
1000
          subobjects_dict = {}
1001
          portal_type_dict = {}
1002
          old_obj = container._getOb(object_id, None)
1003
          object_existed = old_obj is not None
1004
          if object_existed:
1005 1006 1007 1008
            if context.isKeepObject(path):
              # do nothing if the object is specified in keep list in
              # force mode.
              continue
1009
            # Object already exists
1010
            recurse(saveHook, old_obj)
1011
            if getattr(aq_base(old_obj), 'groups', None) is not None:
1012 1013 1014 1015 1016
              # 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)
1017
            subobjects_dict = self._backupObject(action, trashbin,
1018
                                                 container_path, object_id)
1019
            # in case of portal types, we want to keep some properties
1020
            if interfaces.ITypeProvider.providedBy(container):
1021
              for attr in ('allowed_content_types',
1022 1023 1024 1025 1026 1027
                           '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, '')
1028
            container.manage_delObjects([object_id])
1029

1030
          # install object
1031
          obj = self._objects[path]
1032 1033 1034 1035
          # 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.
1036
          if getattr(aq_base(obj), 'groups', None) is not None:
1037
            # we must keep original order groups
1038
            # because they change when we add subobjects
1039
            groups[path] = deepcopy(obj.groups)
1040
          # copy the object
1041 1042 1043 1044 1045 1046 1047
          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
1048
                'Cleaning corrupted BTreeFolder2 object at %r.' % (path,))
1049
            obj._initBTrees()
1050
          obj = obj._getCopy(container)
1051
          self.removeProperties(obj, 0)
1052 1053
          __traceback_info__ = (container, object_id, obj)
          container._setObject(object_id, obj)
1054
          obj = container._getOb(object_id)
1055 1056 1057 1058 1059

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

1060 1061
          # mark a business template installation so in 'PortalType_afterClone' scripts
          # we can implement logical for reseting or not attributes (i.e reference).
1062
          self.REQUEST.set('is_business_template_installation', 1)
1063 1064 1065 1066
          # 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
1067
          obj.isIndexable = ConstantGetter('isIndexable', value=False)
1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100
          # 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
1101 1102 1103
          del obj.isIndexable
          if getattr(aq_base(obj), 'reindexObject', None) is not None:
            obj.reindexObject()
1104
          obj.wl_clearLocks()
1105
          if portal_type_dict:
1106
            # set workflow chain
1107
            wf_chain = portal_type_dict.pop('workflow_chain')
1108 1109
            chain_dict = getChainByType(context)[1]
            default_chain = ''
1110
            chain_dict['chain_%s' % (object_id)] = wf_chain
1111
            context.portal_workflow.manage_changeWorkflows(default_chain, props=chain_dict)
1112 1113
            # restore some other properties
            obj.__dict__.update(portal_type_dict)
1114
          # import sub objects if there is
1115
          if subobjects_dict:
1116
            # get a jar
1117
            connection = self.getConnection(obj)
1118
            # import subobjects
1119
            for subobject_id, subobject_data in subobjects_dict.iteritems():
1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130
              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))
1131
          if obj.meta_type in ('Z SQL Method',):
1132
            fixZSQLMethod(portal, obj)
1133 1134 1135
          # portal transforms specific initialization
          elif obj.meta_type in ('Transform', 'TransformsChain'):
            assert container.meta_type == 'Portal Transforms'
1136 1137 1138
            # skip transforms that couldn't have been initialized
            if obj.title != 'BROKEN':
              container._mapTransform(obj)
1139
          elif obj.meta_type in ('ERP5 Ram Cache',
1140
                                 'ERP5 Distributed Ram Cache',):
1141 1142
            assert container.meta_type == 'ERP5 Cache Factory'
            container.getParentValue().updateCache()
1143 1144
          elif (container.meta_type == 'CMF Skins Tool') and \
              (old_obj is not None):
1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158
            # 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')
1159 1160 1161 1162 1163 1164 1165 1166
          # 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)
1167

1168
          recurse(restoreHook, obj)
1169
      # now put original order group
1170 1171
      # we remove object not added in forms
      # we put old objects we have kept
1172
      for path, new_groups_dict in groups.iteritems():
1173 1174 1175 1176 1177 1178 1179 1180 1181
        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
1182
          # excetp the one that had to be removed
1183 1184 1185 1186
          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'):
1187 1188
              continue
            widget_in_form = 0
1189 1190
            for group_id, group_value_list in new_groups_dict.iteritems():
              if widget_id in group_value_list:
1191 1192 1193 1194 1195 1196
                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:
1197
              for old_group_id, old_group_values in old_groups_dict.iteritems():
1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208
                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,]
1209
                  obj.group_list = list(obj.group_list) + ['not_assigned']
1210
          # second check all widget_id in order are in form
1211 1212
          for group_id, group_value_list in new_groups_dict.iteritems():
            for widget_id in tuple(group_value_list):
1213 1214 1215
              if widget_id not in widget_id_list:
                # if we don't find the widget id in the form
                # remove it fro the group
1216
                group_value_list.remove(widget_id)
1217
          # now set new group object
1218
          obj.groups = new_groups_dict
1219
      # restore previous activities execution order
1220
      context.setPlacelessDefaultReindexParameters(**original_reindex_parameters)
1221
      # Do not forget to delete all remaining objects if asked by user
1222 1223 1224 1225 1226 1227
      # 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:
1228 1229
            if from_path in recursive_path_list:
              continue
1230 1231 1232 1233 1234
            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()])
1235
      fillRecursivePathList(object_key_list)
1236 1237 1238
      for recursive_path in recursive_path_list:
        if recursive_path in update_dict:
          action = update_dict[recursive_path]
1239
          if action in ('remove', 'save_and_remove'):
1240
            document = self.unrestrictedResolveValue(portal, recursive_path, None)
1241 1242 1243
            if document is None:
              # It happens if the parent of target path is removed before
              continue
1244 1245 1246 1247 1248 1249 1250 1251 1252 1253
            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
1254
    else:
1255 1256
      # for old business template format
      BaseTemplateItem.install(self, context, trashbin, **kw)
Aurel's avatar
Aurel committed
1257
      portal = context.getPortalObject()
1258
      for relative_url in self._archive.keys():
1259
        obj = self._archive[relative_url]
Aurel's avatar
Aurel committed
1260 1261 1262 1263
        container_path = relative_url.split('/')[0:-1]
        object_id = relative_url.split('/')[-1]
        container = portal.unrestrictedTraverse(container_path)
        container_ids = container.objectIds()
1264
        if object_id in container_ids:    # Object already exists
1265
          self._backupObject('backup', trashbin, container_path, object_id)
1266
          container.manage_delObjects([object_id])
Aurel's avatar
Aurel committed
1267
        # Set a hard link
1268 1269 1270 1271 1272 1273
        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',):
1274
          fixZSQLMethod(portal, obj)
1275
    self.afterInstall()
1276 1277 1278

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

1303
        container.manage_delObjects([object_id])
1304
        if container.aq_parent.meta_type == 'ERP5 Catalog' and not len(container):
1305
          # We are removing a ZSQLMethod, remove the SQLCatalog if empty
1306
          container.