BusinessTemplate.py 261 KB
Newer Older
Nicolas Delaby's avatar
Nicolas Delaby committed
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.ZSQLMethods.SQL import SQL
42
from Products.ERP5Type.Accessor.Constant import PropertyGetter as ConstantGetter
43
from Products.ERP5Type.Cache import transactional_cached
44
from Products.ERP5Type.Message import translateString
Nicolas Dumazet's avatar
Nicolas Dumazet committed
45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
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
63
from Products.ERP5Type.Utils import convertToUpperCase
64
from Products.ERP5Type import Permissions, PropertySheet, interfaces
Jean-Paul Smets's avatar
Jean-Paul Smets committed
65
from Products.ERP5Type.XMLObject import XMLObject
Julien Muchembled's avatar
Julien Muchembled committed
66
from Products.ERP5Type.dynamic.lazy_class import ERP5BaseBroken
67
from Products.ERP5Type.dynamic.portal_type_class import synchronizeDynamicModules
68
from Products.ERP5Type.Core.PropertySheet import PropertySheet as PropertySheetDocument
69
from Products.ERP5Type.TransactionalVariable import getTransactionalVariable
70
from Products.ERP5.Document.File import File
Aurel's avatar
Aurel committed
71
from OFS.Traversable import NotFound
72
from OFS import SimpleItem, XMLExportImport
73
from OFS.Image import Pdata
74
from cStringIO import StringIO
Aurel's avatar
Aurel committed
75
from copy import deepcopy
76
from zExceptions import BadRequest
Aurel's avatar
Aurel committed
77
import OFS.XMLExportImport
78
from Products.ERP5Type.patches.ppml import importXML
Aurel's avatar
Aurel committed
79
customImporters={
80
    XMLExportImport.magic: importXML,
Aurel's avatar
Aurel committed
81
    }
82
from Products.ERP5Type.patches.WorkflowTool import WorkflowHistoryList
83
from zLOG import LOG, WARNING, INFO
84
from warnings import warn
85
from lxml.etree import parse
86
from xml.sax.saxutils import escape
87
from Products.CMFCore.Expression import Expression
88
from urllib import quote, unquote
89
from difflib import unified_diff
90
import posixpath
Julien Muchembled's avatar
Julien Muchembled committed
91
import transaction
92

93
import threading
Julien Muchembled's avatar
Julien Muchembled committed
94
from ZODB.broken import Broken, BrokenModified
95 96
from Products.ERP5.genbt5list import BusinessTemplateRevision, \
  item_name_list, item_set
97 98 99 100 101

CACHE_DATABASE_PATH = None
try:
  if int(os.getenv('ERP5_BT5_CACHE', 0)):
    from App.config import getConfiguration
102
    import gdbm
103 104 105 106 107
    instancehome = getConfiguration().instancehome
    CACHE_DATABASE_PATH = os.path.join(instancehome, 'bt5cache.db')
except TypeError:
  pass
cache_database = threading.local()
108 109
from Products.MimetypesRegistry.common import MimeTypeException
import imghdr
110

111 112
# those attributes from CatalogMethodTemplateItem are kept for
# backward compatibility
Aurel's avatar
Aurel committed
113 114
catalog_method_list = ('_is_catalog_list_method_archive',
                       '_is_uncatalog_method_archive',
115 116
                       '_is_clear_method_archive',
                       '_is_filtered_archive',)
117

118
catalog_method_filter_list = ('_filter_expression_archive',
119
                              '_filter_expression_cache_key_archive',
120
                              '_filter_type_archive',)
121

122
INSTALLED_BT_FOR_DIFF = 'installed_bt_for_diff'
123
_MARKER = []
124

125
SEPARATELY_EXPORTED_PROPERTY_DICT = {
126 127 128 129 130
  # For objects whose class name is 'class_name', the 'property_name'
  # attribute is removed from the XML export, and the value is exported in a
  # separate file, with extension specified by 'extension'.
  # 'extension' must be None for auto-detection.
  #
Julien Muchembled's avatar
Julien Muchembled committed
131
  # class_name: (extension, unicode_data, property_name),
132
  "Document Component":  ("py",   0, "text_content"),
133
  "DTMLDocument":        (None,   0, "raw"),
134 135 136 137
  "DTMLMethod":          (None,   0, "raw"),
  "Extension Component": ("py",   0, "text_content"),
  "File":                (None,   0, "data"),
  "Image":               (None,   0, "data"),
138
  "Interface Component": ("py",   0, "text_content"),
139
  "OOoTemplate":         ("oot",  1, "_text"),
140
  "Mixin Component":     ("py",   0, "text_content"),
141
  "PDF":                 ("pdf",  0, "data"),
142
  "PDFForm":             ("pdf",  0, "data"),
143
  "PyData Script":       ("py",   0, "_body"),
144 145 146 147
  "Python Script":       ("py",   0, "_body"),
  "PythonScript":        ("py",   0, "_body"),
  "Spreadsheet":         (None,   0, "data"),
  "SQL":                 ("sql",  0, "src"),
148
  "SQL Method":          ("sql",  0, "src"),
149 150 151 152 153 154
  "Test Component":      ("py",   0, "text_content"),
  "Test Page":           (None,   0, "text_content"),
  "Web Page":            (None,   0, "text_content"),
  "Web Script":          (None,   0, "text_content"),
  "Web Style":           (None,   0, "text_content"),
  "ZopePageTemplate":    ("zpt",  1, "_text"),
155 156
}

157 158
def _getCatalog(acquisition_context):
  """
159
    Return the id of the Catalog which correspond to the current BT.
160 161 162
  """
  catalog_method_id_list = acquisition_context.getTemplateCatalogMethodIdList()
  if len(catalog_method_id_list) == 0:
163
    try:
164
      return acquisition_context.getPortalObject().portal_catalog.objectIds()[0]
165 166
    except IndexError:
      return None
167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185
  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

186 187 188 189 190
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.
  """
191
  if getattr(aq_base(obj), 'uid', _MARKER) is not _MARKER:
192
    obj.uid = None
193 194 195
  for subobj in obj.objectValues():
    _recursiveRemoveUid(subobj)

196 197 198
def _delObjectWithoutHook(obj, id):
  """OFS.ObjectManager._delObject without calling manage_beforeDelete."""
  ob = obj._getOb(id)
199 200
  if obj._objects:
    obj._objects = tuple([i for i in obj._objects if i['id'] != id])
201 202 203 204 205 206
  obj._delOb(id)
  try:
    ob._v__object_deleted__ = 1
  except:
    pass

207
def removeAll(entry):
208 209 210
  warn('removeAll is deprecated; use shutil.rmtree instead.',
       DeprecationWarning)
  shutil.rmtree(entry, True)
Aurel's avatar
Aurel committed
211

212 213 214 215 216 217 218
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
  """
219
  pw = context.getPortalObject().portal_workflow
220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240
  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)

241 242 243 244 245 246 247 248 249 250 251 252 253 254
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]
255 256 257
  # recompile the method
  method._arg = Aqueduct.parse(method.arguments_src)
  method.template = method.template_class(method.src)
258

259 260
def registerSkinFolder(skin_tool, skin_folder):
  request = skin_tool.REQUEST
261 262 263
  # 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)
264 265
  register_skin_selection = request.get('your_register_skin_selection', 1)
  reorder_skin_selection = request.get('your_reorder_skin_selection', 1)
266 267
  skin_layer_list = request.get('your_skin_layer_list',
                                skin_tool.getSkinSelections())
268 269 270 271 272

  skin_folder_id = skin_folder.getId()

  try:
    skin_selection_list = skin_folder.getProperty(
273
                 'business_template_registered_skin_selections',
274 275 276 277 278
                 skin_tool.getSkinSelections()
                 )
  except AttributeError:
    skin_selection_list = skin_tool.getSkinSelections()

279 280 281
  if isinstance(skin_selection_list, basestring):
    skin_selection_list = skin_selection_list.split()

282 283 284 285 286 287 288
  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

289 290 291 292 293
  for skin_name in skin_selection_list:

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

    selection = skin_tool.getSkinPath(skin_name) or ''
298
    selection_list = selection.split(',')
299
    if (skin_folder_id not in selection_list):
300
      selection_list.insert(0, skin_folder_id)
301
    if reorder_skin_selection:
302 303
      # Sort by skin priority and ID
      selection_list.sort(key=skin_sort_key)
304
    if (skin_name in skin_layer_list):
305 306 307 308 309 310 311 312 313 314
      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
315
  #  - they are not registered in the default skin selection
316 317 318 319 320 321 322 323 324 325 326
  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()
327
  # add newly created skins to list of skins we care for
328 329 330 331 332 333 334 335
  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:
336 337
        if skin_name in skin_folder.getProperty(
               'business_template_registered_skin_selections', ()):
338 339 340
          break
      except AttributeError:
        pass
341 342
    else:
      skin_tool.manage_skinLayers(chosen=[skin_name], del_skin=1)
343 344
      skin_tool.getPortalObject().changeSkin(None)

345
def unregisterSkinFolderId(skin_tool, skin_folder_id, skin_selection_list):
346 347 348 349 350 351 352 353 354 355
  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)

356
class BusinessTemplateArchive(object):
Aurel's avatar
Aurel committed
357 358
  """
    This is the base class for all Business Template archives
359
  """
360
  def __init__(self, path, **kw):
361
    self.path = path
362
    self.revision = BusinessTemplateRevision()
Aurel's avatar
Aurel committed
363

364
  def addObject(self, obj, name, path=None, ext='.xml'):
365 366 367 368 369 370
    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)
371
    try:
372 373 374 375 376
      write = self._writeFile
    except AttributeError:
      if not isinstance(obj, str):
        obj.seek(0)
        obj = obj.read()
377
      self.revision.hash(path, obj)
378 379 380
      self._writeString(obj, path)
    else:
      if isinstance(obj, str):
381
        self.revision.hash(path, obj)
382
        obj = StringIO(obj)
383 384 385
      else:
        obj.seek(0)
        self.revision.hash(path, obj.read())
386
      write(obj, path)
387

388
  def finishCreation(self):
Aurel's avatar
Aurel committed
389 390
    pass

391 392 393
  def getRevision(self):
    return self.revision.digest()

Aurel's avatar
Aurel committed
394 395
class BusinessTemplateFolder(BusinessTemplateArchive):
  """
Christophe Dumez's avatar
Christophe Dumez committed
396
    Class archiving business template into a folder tree
397
  """
398 399 400 401 402 403 404 405 406 407
  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()

408
  def importFiles(self, item):
Aurel's avatar
Aurel committed
409 410 411
    """
      Import file from a local folder
    """
412
    join = os.path.join
413 414
    item_name = item.__class__.__name__
    root = join(os.path.normpath(self.path), item_name, '')
415
    root_path_len = len(root)
416 417 418 419 420 421
    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:
422 423 424 425
      for root, dirs, files in os.walk(root):
        for file_name in files:
          file_name = join(root, file_name)
          with open(file_name, 'rb') as f:
426
            file_name = posixpath.normpath(file_name[root_path_len:])
427 428
            if '%' in file_name:
              file_name = unquote(file_name)
429 430 431 432
            elif item_name == 'bt' and file_name == 'revision':
              continue
            self.revision.hash(item_name + '/' + file_name, f.read())
            f.seek(0)
433
            item._importFile(file_name, f)
434 435 436 437
    finally:
      if hasattr(cache_database, 'db'):
        cache_database.db.close()
        del cache_database.db
438

Aurel's avatar
Aurel committed
439 440 441 442 443
class BusinessTemplateTarball(BusinessTemplateArchive):
  """
    Class archiving businnes template into a tarball file
  """

444 445 446 447 448 449 450 451 452 453 454 455 456 457 458
  def __init__(self, path, creation=0, importing=0, **kw):
    super(BusinessTemplateTarball, self).__init__(path, **kw)
    if creation:
      self.fobj = StringIO()
      self.tar = tarfile.open('', 'w:gz', self.fobj)
      self.time = time.time()
    elif importing:
      self.tar = tarfile.open(path, 'r:gz')
      self.item_dict = item_dict = defaultdict(list)
      for info in self.tar.getmembers():
        if info.isreg():
          path = info.name.split('/')
          if path[0] == '.':
            del path[0]
          item_dict[path[1]].append(('/'.join(path[2:]), info))
459 460 461 462 463 464 465 466 467 468 469 470

  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
471 472 473
    self.tar.close()
    return self.fobj

474
  def importFiles(self, item):
Aurel's avatar
Aurel committed
475 476
    """
      Import all file from the archive to the site
477
    """
478
    extractfile = self.tar.extractfile
479 480
    item_name = item.__class__.__name__
    for file_name, info in self.item_dict.get(item_name, ()):
481 482
      if '%' in file_name:
        file_name = unquote(file_name)
483 484 485 486 487 488
      elif item_name == 'bt' and file_name == 'revision':
        continue
      f = extractfile(info)
      self.revision.hash(item_name + '/' + file_name, f.read())
      f.seek(0)
      item._importFile(file_name, f)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
489

490
class TemplateConditionError(Exception): pass
491
class TemplateConflictError(Exception): pass
492
class BusinessTemplateMissingDependency(Exception): pass
493

494 495 496
ModuleSecurityInfo(__name__).declarePublic('BusinessTemplateMissingDependency',
  'TemplateConditionError', 'TemplateConflictError')

497
class BaseTemplateItem(Implicit, Persistent):
498
  """
499
    This class is the base class for all template items.
500
    is_bt_for_diff means This BT is used to compare self temporary BT with installed BT
501
  """
502
  is_bt_for_diff = None
Jean-Paul Smets's avatar
Jean-Paul Smets committed
503

504
  def __init__(self, id_list, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
505
    self.__dict__.update(kw)
506
    self._archive = PersistentMapping()
Aurel's avatar
Aurel committed
507
    self._objects = PersistentMapping()
508
    for id in id_list:
509 510
      if id is not None and id != '':
        self._archive[id] = None
511 512 513 514

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

515
  def preinstall(self, context, installed_item, **kw):
Vincent Pelletier's avatar
Vincent Pelletier committed
516 517
    """
      Build a list of added/removed/changed files between the BusinessTemplate
518
      being installed (self) and the installed one (installed_item).
Vincent Pelletier's avatar
Vincent Pelletier committed
519 520 521 522 523
      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
524
    modified_object_list = {}
525 526 527 528 529 530
    for path in self._objects:
      if installed_item._objects.has_key(path):
        # compare objects to see it there are changes
        new_obj_xml = self.generateXml(path=path)
        old_obj_xml = installed_item.generateXml(path=path)
        if new_obj_xml != old_obj_xml:
531
          modified_object_list[path] = 'Modified', self.__class__.__name__[:-12]
532 533
        # else, compared versions are identical, don't overwrite the old one
      else: # new object
534
        modified_object_list[path] = 'New', self.__class__.__name__[:-12]
535 536 537 538
    # list removed objects
    old_keys = installed_item._objects.keys()
    for path in old_keys:
      if path not in self._objects:
539
        modified_object_list[path] = 'Removed', self.__class__.__name__[:-12]
540 541 542
    return modified_object_list

  def install(self, context, trashbin, **kw):
543
    pass
544 545 546

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

548
  def remove(self, context, **kw):
Vincent Pelletier's avatar
Vincent Pelletier committed
549 550 551 552 553 554
    """
      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.
    """
555 556 557
    remove_dict = kw.get('remove_object_dict', {})
    keys = self._objects.keys()
    keys.sort()
558
    keys.reverse()
559 560 561 562 563 564 565 566 567 568 569
    # 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)
570 571 572 573
        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
574

575

576 577 578 579
  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
580
  def export(self, context, bta, **kw):
581
    pass
Aurel's avatar
Aurel committed
582

583 584
  def getKeys(self):
    return self._objects.keys()
585

Aurel's avatar
Aurel committed
586
  def importFile(self, bta, **kw):
587
    bta.importFiles(self)
588

589
  def _removeAllButLastWorkflowHistory(self, obj):
590 591 592 593 594 595
    workflow_history = getattr(obj, 'workflow_history', None)
    if workflow_history is None:
      return
    for workflow_id in workflow_history.keys():
      workflow_history[workflow_id] = WorkflowHistoryList(
        [workflow_history[workflow_id][-1]])
596 597 598 599 600 601

  def removeProperties(self,
                       obj,
                       export,
                       keep_workflow_history=False,
                       keep_workflow_history_last_history_only=False):
602 603
    """
    Remove unneeded properties for export
604
    """
605
    obj._p_activate()
606 607
    klass = obj.__class__
    classname = klass.__name__
608

609 610 611
    attr_set = {'_dav_writelocks', '_filepath', '_owner', '_related_index',
                'last_id', 'uid',
                '__ac_local_roles__', '__ac_local_roles_group_id_dict__'}
612
    if export:
613 614 615
      if keep_workflow_history_last_history_only:
        self._removeAllButLastWorkflowHistory(obj)
      elif not keep_workflow_history:
616
        attr_set.add('workflow_history')
617 618
      # PythonScript covers both Zope Python scripts
      # and ERP5 Python Scripts
619
      if isinstance(obj, PythonScript):
620 621 622
        # `expression_instance` is included so as to add compatibility for
        # exporting older catalog methods which might have them as their
        # properties or in their attribute dict.
623
        attr_set.update(('func_code', 'func_defaults', '_code',
624 625
                         '_lazy_compilation', 'Python_magic',
                         'expression_instance'))
626 627 628
        for attr in 'errors', 'warnings', '_proxy_roles':
          if not obj.__dict__.get(attr, 1):
            delattr(obj, attr)
629 630
      elif classname in ('File', 'Image'):
        attr_set.update(('_EtagSupport__etag', 'size'))
631 632
      # SQL covers both ZSQL Methods and ERP5 SQL Methods
      elif isinstance(obj, SQL):
633 634 635 636
        # `expression_instance` is included so as to add compatibility for
        # exporting older catalog methods which might have them as their
        # properties or in their attribute dict.
        attr_set.update(('_arg', 'template', 'expression_instance'))
637
      elif interfaces.IIdGenerator.providedBy(obj):
Nicolas Dumazet's avatar
Nicolas Dumazet committed
638
        attr_set.update(('last_max_id_dict', 'last_id_dict'))
639 640
      elif classname == 'Types Tool' and klass.__module__ == 'erp5.portal_type':
        attr_set.add('type_provider_list')
641

642 643
    for attr in obj.__dict__.keys():
      if attr in attr_set or attr.startswith('_cache_cookie_'):
644 645
        delattr(obj, attr)

646
    if classname == 'PDFForm':
647
      if not obj.getProperty('business_template_include_content', 1):
648
        obj.deletePdfContent()
649 650
    return obj

651 652
  def getTemplateTypeName(self):
    """
653
     Get a meaningfull class Name without 'TemplateItem'. Used to
654 655 656 657 658 659
     present to the user.

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

660
  def restrictedResolveValue(self, context=None, path='', default=_MARKER):
661 662 663 664
    """
      Get the value with checking the security.
      This method does not acquire the parent.
    """
665 666
    return self.unrestrictedResolveValue(context, path, default=default,
                                         restricted=1)
667

668 669
  def unrestrictedResolveValue(self, context=None, path='', default=_MARKER,
                               restricted=0):
670 671 672 673 674 675 676 677 678 679 680
    """
      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:
681
        portal = aq_inner(self.getPortalObject())
682 683 684 685
        container = portal
      else:
        container = context

686 687 688 689
      if restricted:
        validate = getSecurityManager().validate

      while stack:
690
        key = stack.pop()
691 692 693
        try:
          value = container[key]
        except KeyError:
694
          LOG('BusinessTemplate', WARNING,
695 696 697 698
              'Could not access object %s' % (path,))
          if default is _MARKER:
            raise
          return default
699

700 701 702 703 704 705 706 707 708 709 710 711
        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
712

713 714 715
      return value
    else:
      return context
716

717 718 719 720 721 722 723 724 725 726 727 728 729 730 731
  def _resetDynamicModules(self):
    # before any import, flush all ZODB caches to force a DB reload
    # otherwise we could have objects trying to get commited while
    # holding reference to a class that is no longer the same one as
    # the class in its import location and pickle doesn't tolerate it.
    # First we do a savepoint to dump dirty objects to temporary
    # storage, so that all references to them can be freed.
    transaction.savepoint(optimistic=True)
    # Then we need to flush from all caches, not only the one from this
    # connection
    portal = self.getPortalObject()
    portal._p_jar.db().cacheMinimize()
    synchronizeDynamicModules(portal, force=True)
    gc.collect()

732 733 734
class ObjectTemplateItem(BaseTemplateItem):
  """
    This class is used for generic objects and as a subclass.
735
  """
736

737 738 739 740
  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()
741
      self._archive.clear()
742 743 744
      for id in id_list :
        if id != '':
          self._archive["%s/%s" % (tool_id, id)] = None
745

746
  def _guessFilename(self, document, key, data):
747 748
    # Try to guess the extension based on the id of the document
    yield key
749
    document_base = aq_base(document)
750
    # Try to guess the extension based on the reference of the document
751
    if hasattr(document_base, 'getReference'):
752
      yield document.getReference()
753 754
    elif isinstance(document_base, ERP5BaseBroken):
      yield getattr(document_base, "reference", None)
755
    # Try to guess the extension based on the title of the document
756
    yield getattr(document_base, "title", None)
757
    # Try to guess from content
758 759 760 761 762 763 764
    if data:
      for test in imghdr.tests:
        extension = test(data, None)
        if extension:
          yield 'x.' + extension

  def guessExtensionOfDocument(self, document, key, data=None):
765 766 767
    """Guesses and returns the extension of an ERP5 document.

    The process followed is:
768 769 770
    1. Try to guess extension by the id of the document
    2. Try to guess extension by the title of the document
    3. Try to guess extension by the reference of the document
771
    4. Try to guess from content (only image data is tested)
772 773

    If there's a content type, we only return an extension that matches.
774 775 776 777 778

    In case everything fails then:
    - '.bin' is returned for binary files
    - '.txt' is returned for text
    """
779
    document_base = aq_base(document)
780 781
    # XXX Zope items like DTMLMethod would not implement getContentType method
    mime = None
782
    if hasattr(document_base, 'getContentType'):
783
      content_type = document.getContentType()
784 785
    elif isinstance(document_base, ERP5BaseBroken):
      content_type = getattr(document_base, "content_type", None)
786 787 788 789 790 791 792
    else:
      content_type = None
    # For stable export, people must have a MimeTypes Registry, so do not
    # fallback on mimetypes. We prefer the mimetypes_registry because there
    # are more extensions and we can have preferred extensions.
    # See also https://bugs.python.org/issue1043134
    mimetypes_registry = self.getPortalObject()['mimetypes_registry']
793
    if content_type:
794
      try:
795 796 797 798
        mime = mimetypes_registry.lookup(content_type)[0]
      except (IndexError, MimeTypeException):
        pass

799
    for key in self._guessFilename(document, key, data):
800
      if key:
801 802 803
        ext = os.path.splitext(key)[1][1:].lower()
        if ext and (mimetypes_registry.lookupExtension(ext) is mime if mime
               else mimetypes_registry.lookupExtension(ext)):
804 805 806 807 808
          return ext

    if mime:
      # return first registered extension (if any)
      if mime.extensions:
809
        return mime.extensions[0]
810 811
      for ext in mime.globs:
        if ext[0] == "*" and ext.count(".") == 1:
812
          return ext[2:].encode("utf-8")
813

814 815 816 817
    # in case we could not read binary flag from mimetypes_registry then return
    # '.bin' for all the Portal Types where exported_property_type is data
    # (File, Image, Spreadsheet). Otherwise, return .bin if binary was returned
    # as 1.
818
    binary = getattr(mime, 'binary', None)
819 820
    if binary or binary is None is not data:
      return 'bin'
821
    # in all other cases return .txt
822
    return 'txt'
823

824
  def export(self, context, bta, catalog_method_template_item = 0, **kw):
Vincent Pelletier's avatar
Vincent Pelletier committed
825 826 827 828
    """
      Export the business template : fill the BusinessTemplateArchive with
      objects exported as XML, hierarchicaly organised.
    """
829 830
    if len(self._objects.keys()) == 0:
      return
831
    path = self.__class__.__name__ + '/'
Vincent Pelletier's avatar
Vincent Pelletier committed
832
    for key, obj in self._objects.iteritems():
833 834 835 836 837 838
      # Back compatibility with filesystem Documents
      if isinstance(obj, str):
        if not key.startswith(path):
          key = path + key
        bta.addObject(obj, name=key, ext='.py')
      else:
839
        try:
840
          extension, unicode_data, record_id = \
841 842 843 844 845
            SEPARATELY_EXPORTED_PROPERTY_DICT[obj.__class__.__name__]
        except KeyError:
          pass
        else:
          while 1: # not a loop
846
            obj = obj._getCopy(context)
847 848 849 850 851 852 853 854 855 856 857 858
            data = getattr(aq_base(obj), record_id, None)
            if unicode_data:
              if type(data) is not unicode:
                break
              try:
                data = data.encode(aq_base(obj).output_encoding)
              except (AttributeError, UnicodeEncodeError):
                break
            elif type(data) is not bytes:
              if not isinstance(data, Pdata):
                break
              data = bytes(data)
859 860 861 862 863 864 865 866 867 868
            try:
              # Delete this attribute from the object.
              # in case the related Portal Type does not exist, the object may be broken.
              # So we cannot delattr, but we can delete the key of its its broken state
              if isinstance(obj, ERP5BaseBroken):
                del obj.__Broken_state__[record_id]
                obj._p_changed = 1
              else:
                delattr(obj, record_id)
            except (AttributeError, KeyError):
869
              # property was acquired on a class,
870
              # do nothing, only .xml metadata will be exported
871 872 873 874 875 876 877 878 879 880 881 882
              break
            # export a separate file with the data
            if not extension:
              extension = self.guessExtensionOfDocument(obj, key,
                data if record_id == 'data' else None)
            bta.addObject(StringIO(data), key, path=path,
              ext='._xml' if extension == 'xml' else '.' + extension)
            break
          # since we get the obj from context we should
          # again remove useless properties
          obj = self.removeProperties(obj, 1, keep_workflow_history = True)
          transaction.savepoint(optimistic=True)
883 884 885 886 887 888 889 890 891

        f = StringIO()
        XMLExportImport.exportXML(obj._p_jar, obj._p_oid, f)
        bta.addObject(f, key, path=path)
        
      if catalog_method_template_item:
        # add all datas specific to catalog inside one file
        xml_data = self.generateXml(key)
        bta.addObject(xml_data, key + '.catalog_keys', path=path)
Julien Muchembled's avatar
Julien Muchembled committed
892 893 894 895 896 897 898 899 900 901 902

  def _restoreSeparatelyExportedProperty(self, obj, data):
    unicode_data, property_name = SEPARATELY_EXPORTED_PROPERTY_DICT[
      obj.__class__.__name__][1:]
    if unicode_data:
      data = data.decode(obj.output_encoding)
    try:
      setattr(obj, property_name, data)
    except BrokenModified:
      obj.__Broken_state__[property_name] = data
      obj._p_changed = 1
903 904 905 906 907 908
    else:
      # Revert any work done by __setstate__.
      # XXX: This is enough for all objects we currently split in 2 files,
      #      but __setstate__ could behave badly with the missing attribute
      #      and newly added types may require more than this.
      self.removeProperties(obj, 1, keep_workflow_history=True)
Julien Muchembled's avatar
Julien Muchembled committed
909

910 911 912 913 914 915 916 917 918 919 920 921 922
  def _importFile(self, file_name, file_obj, catalog_method_template_item = 0):
    obj_key, file_ext = os.path.splitext(file_name)
    # id() for installing several bt5 in the same transaction
    transactional_variable_obj_key = "%s-%s" % (id(self), obj_key)
    if file_ext != '.xml':
      # if the document has not been migrated yet (its class is file and
      # it is not in portal_components) use legacy importer
      if issubclass(self.__class__, FilesystemDocumentTemplateItem) and file_obj.name.rsplit(os.path.sep, 2)[-2] != 'portal_components':
        FilesystemDocumentTemplateItem._importFile(self, file_name, file_obj)
      else:
        # For ZODB Components: if .xml have been processed before, set the
        # source code property, otherwise store it in a transactional variable
        # so that it can be set once the .xml has been processed
923
        data = file_obj.read()
924 925 926
        try:
          obj = self._objects[obj_key]
        except KeyError:
927
          getTransactionalVariable()[transactional_variable_obj_key] = data
928
        else:
Julien Muchembled's avatar
Julien Muchembled committed
929
          self._restoreSeparatelyExportedProperty(obj, data)
930 931 932 933 934 935 936 937 938 939 940
    else:
      connection = self.getConnection(self.aq_parent)
      __traceback_info__ = 'Importing %s' % file_name
      if hasattr(cache_database, 'db') and isinstance(file_obj, file):
        obj = connection.importFile(self._compileXML(file_obj))
      else:
        # FIXME: Why not use the importXML function directly? Are there any BT5s
        # with actual .zexp files on the wild?
        obj = connection.importFile(file_obj, customImporters=customImporters)
      self._objects[obj_key] = obj

941 942
      data = getTransactionalVariable().get(transactional_variable_obj_key)
      if data is not None:
Julien Muchembled's avatar
Julien Muchembled committed
943
        self._restoreSeparatelyExportedProperty(obj, data)
944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959

      # When importing a Business Template, there is no way to determine if it
      # has been already migrated or not in __init__() when it does not
      # already exist, therefore BaseTemplateItem.__init__() is called which
      # does not set _archive with portal_components/ like
      # ObjectTemplateItem.__init__()
      # XXX - the above comment is a bit unclear, 
      # still not sure if this is handled correctly
      if file_obj.name.rsplit(os.path.sep, 2)[-2] == 'portal_components':
        self._archive[obj_key] = None
        try:
          del self._archive[obj_key[len('portal_components/'):]]
        except KeyError:
          pass
      if catalog_method_template_item:
        self.removeProperties(obj, 0)
960

Aurel's avatar
Aurel committed
961
  def build_sub_objects(self, context, id_list, url, **kw):
Vincent Pelletier's avatar
Vincent Pelletier committed
962
    # XXX duplicates code from build
Aurel's avatar
Aurel committed
963
    for id in id_list:
964
      relative_url = '/'.join([url,id])
965
      obj = context._getOb(id)
966 967 968
      obj = self.removeProperties(obj, 1,
                                  self.isKeepWorkflowObject(relative_url),
                                  self.isKeepWorkflowObjectLastHistoryOnly(relative_url))
Vincent Pelletier's avatar
Vincent Pelletier committed
969 970 971
      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
972
        groups = deepcopy(obj.groups)
973
      if id_list:
974
        self.build_sub_objects(obj, id_list, relative_url, copied=True)
975
        for id_ in list(id_list):
976
          _delObjectWithoutHook(obj, id_)
977
      if hasattr(aq_base(obj), 'groups'):
978 979 980
        obj.groups = groups
      self._objects[relative_url] = obj
      obj.wl_clearLocks()
Aurel's avatar
Aurel committed
981

982 983 984 985
  def build(self, context, **kw):
    BaseTemplateItem.build(self, context, **kw)
    p = context.getPortalObject()
    for relative_url in self._archive.keys():
986 987 988 989
      try:
        obj = p.unrestrictedTraverse(relative_url)
      except ValueError:
        raise ValueError, "Can not access to %s" % relative_url
990 991 992 993
      try:
        obj = obj._getCopy(context)
      except AttributeError:
        raise AttributeError, "Could not find object '%s' during business template processing." % relative_url
994
      _recursiveRemoveUid(obj)
995 996 997
      obj = self.removeProperties(obj, 1,
                                  self.isKeepWorkflowObject(relative_url),
                                  self.isKeepWorkflowObjectLastHistoryOnly(relative_url))
998
      id_list = obj.objectIds()
Vincent Pelletier's avatar
Vincent Pelletier committed
999 1000
      if hasattr(aq_base(obj), 'groups'): # XXX should check metatype instead
        # we must keep groups because they are deleted along with subobjects
1001
        groups = deepcopy(obj.groups)
Aurel's avatar
Aurel committed
1002
      if len(id_list) > 0:
1003
        self.build_sub_objects(obj, id_list, relative_url)
1004
        for id_ in list(id_list):
1005
          _delObjectWithoutHook(obj, id_)
1006
      if hasattr(aq_base(obj), 'groups'):
1007 1008 1009
        obj.groups = groups
      self._objects[relative_url] = obj
      obj.wl_clearLocks()
1010

1011
  def _compileXML(self, file):
1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058
    # 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
1059
      p.Parse(data)
1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070

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

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

1072 1073 1074 1075 1076 1077 1078
  def getConnection(self, obj):
    while True:
      connection = obj._p_jar
      if connection is not None:
        return connection
      obj = obj.aq_parent

1079
  def preinstall(self, context, installed_item, **kw):
Yoshinori Okuji's avatar
Yoshinori Okuji committed
1080
    modified_object_list = {}
1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106
    upgrade_list = []
    type_name = self.__class__.__name__.split('TemplateItem')[-2]
    for path, obj in self._objects.iteritems():
      if installed_item._objects.has_key(path):
        upgrade_list.append((path, installed_item._objects[path]))
      else: # new object
        modified_object_list[path] = 'New', type_name

    # 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()
      try:
        OFS.XMLExportImport.exportXML(old_object._p_jar, old_object._p_oid, old_io)
        old_obj_xml = old_io.getvalue()
      except (ImportError, UnicodeDecodeError), e: # module is already
                                                   # removed etc.
        old_obj_xml = '(%s: %s)' % (e.__class__.__name__, e)
      new_io.close()
      old_io.close()
      if new_obj_xml != old_obj_xml:
1107
        if context.isKeepObject(path):
1108
          modified_object_list[path] = 'Modified but should be kept', type_name
1109
        else:
1110 1111 1112 1113 1114 1115 1116
          modified_object_list[path] = 'Modified', type_name
    # get removed object
    for path in set(installed_item._objects) - set(self._objects):
      if context.isKeepObject(path):
        modified_object_list[path] = 'Removed but should be kept', type_name
      else:
        modified_object_list[path] = 'Removed', type_name
1117 1118
    return modified_object_list

1119
  def _backupObject(self, action, trashbin, container_path, object_id, **kw):
1120 1121 1122
    """
      Backup the object in portal trash if necessery and return its subobjects
    """
1123 1124 1125 1126
    if "portal_integrations" in container_path and "module" in object_id:
      # XXX It is impossible to backup integration module as
      # it will call the request and try to get remote data
      return
1127
    p = self.getPortalObject()
1128
    if trashbin is None: # must return subobjects
1129 1130 1131 1132 1133 1134 1135
      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
1136
    # XXX btsave is for backward compatibility
1137
    if action in ('backup', 'btsave', 'save_and_remove',):
1138
      save = 1
1139
    elif action in ('install', 'remove'):
1140
      save = 0
1141 1142 1143
    else:
      # As the list of available actions is not strictly defined,
      # prevent mistake if an action is not handled
1144
      raise NotImplementedError, 'Unknown action "%s"' % action
1145 1146 1147
    return p.portal_trash.backupObject(trashbin, container_path, object_id,
                                       save=save, **kw)

1148

1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164
  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

1165 1166 1167 1168 1169 1170 1171 1172
  def afterUninstall(self):
    """
      Uninstallation hook.
      Called right before returning in "uninstall" method.
      Can be overridden by subclasses.
    """
    pass

1173
  def onNewObject(self, obj):
1174 1175 1176 1177
    """
      Installation hook.
      Called when installation process determined that object to install is
      new on current site (it's not replacing an existing object).
1178
      `obj` parameter is the newly created object in its acquisition context.
1179 1180 1181 1182
      Can be overridden by subclasses.
    """
    pass

1183 1184 1185 1186 1187 1188 1189 1190
  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.
    """
1191 1192 1193 1194 1195 1196 1197
    kw = context.getPlacelessDefaultReindexParameters()
    if kw is None:
      kw = {}
    context.setPlacelessDefaultReindexParameters(**dict(kw,
      activate_kw=dict(kw.get('activate_kw', ()),
                       after_method_id='unindexObject')))
    return kw
1198

1199 1200 1201 1202 1203 1204
  def _getObjectKeyList(self):
    # sort to add objects before their subobjects
    keys = self._objects.keys()
    keys.sort()
    return keys

1205
  def unindexBrokenObject(self, item_path):
1206 1207 1208 1209 1210 1211 1212 1213
    """
      Unindex broken objects.

      Corresponding catalog record is not unindexed even after a broken object
      is removed, since the broken object does not implement 'CopySupport'.
      This situation triggers a FATAL problem on SQLCatalog.catalogObjectList
      when upgrading a broken path by ObjectTemplateItem with BusinessTemplate.
      We often get this problem when we are upgrading a quite old ERP5 site
1214
      to new one, as several old classes may be already removed/replaced
1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229
      in the file system, thus several objects tend to be broken.

      Keyword arguments:
      item_path -- the path specified by the ObjectTemplateItem
    """
    def flushActivity(obj, invoke=0, **kw):
      try:
        activity_tool = self.getPortalObject().portal_activities
      except AttributeError:
        return # Do nothing if no portal_activities
      # flush all activities related to this object
      activity_tool.flush(obj, invoke=invoke, **kw)

    class fakeobject:
       def __init__(self, path):
1230
         self._physical_path = tuple(path.split('/'))
1231 1232
       def getPhysicalPath(self):
         return self._physical_path
1233

1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257
    def recursiveUnindex(catalog, item_path, root_document_path):
      # search the object + sub-objects
      result = catalog(relative_url=(item_path,
                                     item_path.replace('_', r'\_') + '/%'))
      for x in result:
        uid = x.uid
        path = x.path
        unindex(root_document_path, path, uid)

    def unindex(root_document_path, path, uid):
      LOG('Products.ERP5.Document.BusinessTemplate', WARNING,
          'Unindex Broken object at %r.' % (path,))
      # Make sure there is not activity for this object
      flushActivity(fakeobject(path))
      # Set the path as deleted without lock
      catalog.beforeUnindexObject(None,path=path,uid=uid)
      # Then start activity in order to remove lines in catalog,
      # sql wich generate locks
      catalog.activate(activity='SQLQueue',
                       tag='%s' % uid,
                       group_method_id='portal_catalog/uncatalogObjectList',
                       serialization_tag=root_document_path
                       ).unindexObject(uid=uid)

1258 1259 1260 1261 1262 1263 1264 1265 1266 1267
    portal = self.getPortalObject()
    try:
      catalog = portal.portal_catalog
    except AttributeError:
      pass
    else:
      # given item_path is a relative_url in reality
      root_path = "/".join(item_path.split('/')[:2])
      root_document_path = '/%s/%s' % (portal.getId(), root_path)
      recursiveUnindex(catalog, item_path, root_document_path)
1268

1269 1270 1271 1272
  def fixBrokenObject(self, obj):
    if isinstance(obj, ERP5BaseBroken):
      self._resetDynamicModules()

1273
  def install(self, context, trashbin, **kw):
1274
    self.beforeInstall()
1275 1276
    update_dict = kw.get('object_to_update')
    force = kw.get('force')
1277

1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333
    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):
      uid = getattr(aq_base(document), 'uid', None)
      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
    groups = {}
    old_groups = {}
    portal = context.getPortalObject()
    # set safe activities execution order
    original_reindex_parameters = self.setSafeReindexationMode(context)
    object_key_list = self._getObjectKeyList()
    for path in object_key_list:
      __traceback_info__ = path
      # 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

      if update_dict.has_key(path) or force:
        # get action for the oject
        action = 'backup'
        if not force:
          action = update_dict[path]
          if action == 'nothing':
            continue
        # get subobjects in path
        path_list = path.split('/')
        container_path = path_list[:-1]
        object_id = path_list[-1]
        try:
          container = self.unrestrictedResolveValue(portal, container_path)
        except KeyError:
          # parent object can be set to nothing, in this case just go on
          container_url = '/'.join(container_path)
          if update_dict.get(container_url) == 'nothing':
            continue
          # If container's container is portal_catalog,
          # then automatically create the container.
          elif len(container_path) > 1 and container_path[-2] == 'portal_catalog':
            # The id match, but better double check with the meta type
            # while avoiding the impact of systematic check
            container_container = portal.unrestrictedTraverse(container_path[:-1])
1334 1335 1336 1337
            # Check for meta_type of container before creating Catalog
            if container_container.meta_type == 'Catalog Tool':
              container_container.newContent(portal_type='Catalog', id=container_path[-1], title='')
            elif container_container.meta_type == 'ERP5 Catalog':
1338
              container_container.manage_addProduct['ZSQLCatalog'].manage_addSQLCatalog(id=container_path[-1], title='')
1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354
            else:
              # Raise in case meta_type don't match
              raise ValueError(
                'No meta_type exists for %r during Catalog installation' % (
                container_container.title,
                ),
              )

            # Update default catalog ID
            if len(container_container.objectIds()) == 1:
              # Set the default catalog. Here, thanks to consistency between
              # ERP5CatalogTool and ZSQLCatalog, we can use the explicit accessor
              # `_setDefaultSqlCatalogId` to update both `default_sql_catalog_id`
              # and `default_erp5_catalog_id`
              container_container._setDefaultSqlCatalogId(container_path[-1])
            container = portal.unrestrictedTraverse(container_path)
1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369
          else:
            raise
        saved_uid_dict = {}
        subobjects_dict = {}
        portal_type_dict = {}
        old_obj = container._getOb(object_id, None)
        object_existed = old_obj is not None
        if object_existed:
          if context.isKeepObject(path) and force:
            # do nothing if the object is specified in keep list in
            # force mode.
            continue
          # Object already exists
          recurse(saveHook, old_obj)
          if getattr(aq_base(old_obj), 'groups', None) is not None:
1370
            # we must keep original order groups
1371 1372 1373 1374
            # 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)
1375 1376
          # we force backup since it was an existing object
          subobjects_dict = self._backupObject('backup', trashbin,
1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396
                                               container_path, object_id)
          # in case of portal types, we want to keep some properties
          if interfaces.ITypeProvider.providedBy(container):
            for attr in ('allowed_content_types',
                         '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, '')
          container.manage_delObjects([object_id])
          # unindex here when it is a broken object
          if isinstance(old_obj, Broken):
            new_obj = self._objects[path]
            # check isIndexable with new one, because the old one is broken
            if new_obj.isIndexable():
              self.unindexBrokenObject(path)

        # install object
        obj = self._objects[path]
1397
        self.fixBrokenObject(obj)
1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411
        # 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.
        if getattr(aq_base(obj), 'groups', None) is not None:
          # we must keep original order groups
          # because they change when we add subobjects
          groups[path] = deepcopy(obj.groups)
        # copy the object
        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
1412
          # .objectItems(). In these cases, force the
1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496
          LOG('Products.ERP5.Document.BusinessTemplate', WARNING,
              'Cleaning corrupted BTreeFolder2 object at %r.' % (path,))
          obj._initBTrees()
        obj = obj._getCopy(container)
        self.removeProperties(obj, 0)
        __traceback_info__ = (container, object_id, obj)
        container._setObject(object_id, obj)
        obj = container._getOb(object_id)

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

        # mark a business template installation so in 'PortalType_afterClone' scripts
        # we can implement logical for reseting or not attributes (i.e reference).
        self.REQUEST.set('is_business_template_installation', 1)
        # 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
        obj.isIndexable = ConstantGetter('isIndexable', value=False)
        # 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
        del obj.isIndexable
        if getattr(aq_base(obj), 'reindexObject', None) is not None:
          obj.reindexObject()
        obj.wl_clearLocks()
        if portal_type_dict:
          # set workflow chain
          wf_chain = portal_type_dict.pop('workflow_chain')
          chain_dict = getChainByType(context)[1]
          default_chain = ''
          chain_dict['chain_%s' % (object_id)] = wf_chain
          context.portal_workflow.manage_changeWorkflows(default_chain, props=chain_dict)
          # restore some other properties
          obj.__dict__.update(portal_type_dict)
        # import sub objects if there is
        if subobjects_dict:
          # get a jar
          connection = self.getConnection(obj)
          # import subobjects
          for subobject_id, subobject_data in subobjects_dict.iteritems():
            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))
1497
        if obj.meta_type in ('Z SQL Method', 'ERP5 SQL Method'):
1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509
          fixZSQLMethod(portal, obj)
        # portal transforms specific initialization
        elif obj.meta_type in ('Transform', 'TransformsChain'):
          assert container.meta_type == 'Portal Transforms'
          # skip transforms that couldn't have been initialized
          if obj.title != 'BROKEN':
            container._mapTransform(obj)
        elif obj.meta_type in ('ERP5 Ram Cache',
                               'ERP5 Distributed Ram Cache',):
          assert container.meta_type in ('ERP5 Cache Factory',
                                         'ERP5 Cache Bag')
          container.getParentValue().updateCache()
1510 1511 1512
        elif obj.__class__.__name__ in ('File', 'Image'):
          if "data" in obj.__dict__:
            File._setData.__func__(obj, obj.data)
1513 1514 1515 1516 1517
        elif (container.meta_type == 'CMF Skins Tool') and \
            (old_obj is not None):
          # Keep compatibility with previous export format of
          # business_template_registered_skin_selections
          # and do not modify exported value
1518
          if obj.getProperty('business_template_registered_skin_selections',
1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558
                             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')
        # 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)

        recurse(restoreHook, obj)
    # now put original order group
    # we remove object not added in forms
    # we put old objects we have kept
    for path, new_groups_dict in groups.iteritems():
      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
        # excetp the one that had to be removed
        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'):
            continue
          widget_in_form = 0
1559
          for group_id, group_value_list in new_groups_dict.iteritems():
1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573
            if widget_id in group_value_list:
              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:
            for old_group_id, old_group_values in old_groups_dict.iteritems():
              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
1574
            else:
1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590
              if new_groups_dict.has_key('not_assigned'):
                new_groups_dict['not_assigned'].append(widget_id)
              else:
                new_groups_dict['not_assigned'] = [widget_id,]
                obj.group_list = list(obj.group_list) + ['not_assigned']
        # second check all widget_id in order are in form
        for group_id, group_value_list in new_groups_dict.iteritems():
          for widget_id in tuple(group_value_list):
            if widget_id not in widget_id_list:
              # if we don't find the widget id in the form
              # remove it fro the group
              group_value_list.remove(widget_id)
        # now set new group object
        obj.groups = new_groups_dict
    # restore previous activities execution order
    context.setPlacelessDefaultReindexParameters(**original_reindex_parameters)
1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602
    to_delete_dict = {}
    # XXX: it is not clear why update_dict would contain subojects of any
    # element of object_key_list, and not just these objects themselves.
    # XXX: why does update_dict contain the path of documents not managed
    # by current instance ?
    for path, action in update_dict.iteritems():
      if action not in ('remove', 'save_and_remove'):
        continue
      path_match = path + '/'
      for object_key in object_key_list:
        if path_match.startswith(object_key + '/'):
          to_delete_dict[path] = action
1603 1604
    # Sort by path so that, for example, form is created before its fields.
    for path, action in sorted(to_delete_dict.iteritems()):
1605 1606 1607 1608 1609 1610 1611 1612 1613 1614
      document = self.unrestrictedResolveValue(portal, path, None)
      if document is None:
        continue
      if getattr(aq_base(document), 'getParentValue', None) is None:
        parent = document.aq_parent
      else:
        parent = document.getParentValue()
      document_id = document.getId()
      self._backupObject(action, trashbin, path.split('/')[:-1],
                         document_id)
1615 1616 1617 1618
      try:
        parent.manage_delObjects([document_id])
      except BadRequest:
        pass # removed manually
1619

1620
    self.afterInstall()
1621 1622 1623

  def uninstall(self, context, **kw):
    portal = context.getPortalObject()
1624
    trash = kw.get('trash', 0)
1625 1626 1627 1628 1629
    trashbin = kw.get('trashbin', None)
    object_path = kw.get('object_path', None)
    if object_path is not None:
      object_keys = [object_path]
    else:
Aurel's avatar
Aurel committed
1630
      object_keys = self._archive.keys()
1631
    for relative_url in object_keys:
1632 1633
      container_path = relative_url.split('/')[0:-1]
      object_id = relative_url.split('/')[-1]
1634
      try:
1635
        container = self.unrestrictedResolveValue(portal, container_path)
1636
        container._getOb(object_id) # We force access to the object to be sure
1637 1638
                                        # that appropriate exception is thrown
                                        # in case object is already backup and/or removed
1639
        if trash and trashbin is not None:
1640
          self.portal_trash.backupObject(trashbin, container_path, object_id, save=1, keep_subobjects=1)
1641
        if container.meta_type == 'CMF Skins Tool':
1642
          # we are removing a skin folder, check and
1643
          # remove if registered skin selection
1644
          unregisterSkinFolderId(container, object_id,
1645 1646
              container.getSkinSelections())

1647
        container.manage_delObjects([object_id])
1648
        if container.aq_parent.meta_type == 'ERP5 Catalog' and not len(container):
1649
          # We are removing a ZSQLMethod, remove the SQLCatalog if empty
1650
          container.getParentValue().manage_delObjects([container.id])
1651
      except (NotFound, KeyError, BadRequest, AttributeError):
1652
        # object is already backup and/or removed
1653
        pass
1654
    BaseTemplateItem.uninstall(self, context, **kw)
1655
    self.afterUninstall()
1656

1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668
class PathTemplateItem(ObjectTemplateItem):
  """
    This class is used to store objects with wildcards supported.
  """
  def __init__(self, id_list, tool_id=None, **kw):
    BaseTemplateItem.__init__(self, id_list, tool_id=tool_id, **kw)
    id_list = self._archive.keys()
    self._archive.clear()
    self._path_archive = PersistentMapping()
    for id in id_list:
      self._path_archive[id] = None

1669 1670 1671 1672 1673 1674 1675 1676 1677
  def uninstall(self, context, **kw):
    portal = context.getPortalObject()
    trash = kw.get('trash', 0)
    trashbin = kw.get('trashbin', None)
    object_path = kw.get('object_path', None)
    if object_path is not None:
      object_keys = [object_path]
    else:
      object_keys = self._path_archive.keys()
1678 1679
    object_keys.sort()
    object_keys.reverse()
1680 1681
    p = context.getPortalObject()
    for path in object_keys:
1682 1683 1684 1685 1686
      try:
        path_list = self._resolvePath(p, [], path.split('/'))
      except AttributeError:
        # path seems to not exist anymore
        continue
1687 1688 1689
      path_list.sort()
      path_list.reverse()
      for relative_url in path_list:
1690
        try:
Aurel's avatar
Aurel committed
1691 1692
          container_path = relative_url.split('/')[0:-1]
          object_id = relative_url.split('/')[-1]
1693
          container = self.unrestrictedResolveValue(portal, container_path)
1694
          if trash and trashbin is not None:
1695 1696
            self.portal_trash.backupObject(trashbin, container_path,
                                           object_id, save=1,
1697
                                           keep_subobjects=1)
1698 1699 1700 1701 1702 1703
          container.manage_delObjects([object_id])
        except (NotFound, KeyError):
          # object is already backup and/or removed
          pass
    BaseTemplateItem.uninstall(self, context, **kw)

1704 1705 1706
  def _resolvePath(self, folder, relative_url_list, id_list):
    """
      This method calls itself recursively.
1707

1708 1709 1710 1711 1712 1713 1714 1715 1716
      The folder is the current object which contains sub-objects.
      The list of ids are path components. If the list is empty,
      the current folder is valid.
    """
    if len(id_list) == 0:
      return ['/'.join(relative_url_list)]
    id = id_list[0]
    if re.search('[\*\?\[\]]', id) is None:
      # If the id has no meta character, do not have to check all objects.
1717 1718 1719
      obj = folder._getOb(id, None)
      if obj is None:
        raise AttributeError, "Could not resolve '%s' during business template processing." % id
1720
      return self._resolvePath(obj, relative_url_list + [id], id_list[1:])
1721 1722
    path_list = []
    for object_id in fnmatch.filter(folder.objectIds(), id):
1723
      if object_id != "":
1724
        path_list.extend(self._resolvePath(
1725
            folder._getOb(object_id),
1726
            relative_url_list + [object_id], id_list[1:]))
1727
    return path_list
Aurel's avatar
Aurel committed
1728

1729 1730 1731
  def build(self, context, **kw):
    BaseTemplateItem.build(self, context, **kw)
    p = context.getPortalObject()
Aurel's avatar
Aurel committed
1732
    keys = self._path_archive.keys()
1733
    keys.sort()
Aurel's avatar
Aurel committed
1734
    for path in keys:
1735
      include_subobjects = 0
Aurel's avatar
Aurel committed
1736
      if path.endswith("**"):
1737
        include_subobjects = 1
1738
      for relative_url in self._resolvePath(p, [], path.split('/')):
1739 1740
        obj = p.unrestrictedTraverse(relative_url)
        obj = obj._getCopy(context)
1741
        obj = obj.__of__(context)
1742
        _recursiveRemoveUid(obj)
1743
        id_list = obj.objectIds()
1744 1745 1746
        obj = self.removeProperties(obj, 1,
                                    self.isKeepWorkflowObject(relative_url),
                                    self.isKeepWorkflowObjectLastHistoryOnly(relative_url))
1747
        if hasattr(aq_base(obj), 'groups'):
1748
          # we must keep groups because it's ereased when we delete subobjects
1749
          groups = deepcopy(obj.groups)
1750
        if len(id_list) > 0:
1751
          if include_subobjects:
1752
            self.build_sub_objects(obj, id_list, relative_url)
1753
          for id_ in list(id_list):
1754
            _delObjectWithoutHook(obj, id_)
1755
        if hasattr(aq_base(obj), 'groups'):
1756 1757 1758
          obj.groups = groups
        self._objects[relative_url] = obj
        obj.wl_clearLocks()
1759

1760 1761 1762
  def install(self, context, *args, **kw):
    super(PathTemplateItem, self).install(context, *args, **kw)

1763
    # Regenerate local roles for all paths in this business template
1764 1765
    p = context.getPortalObject()
    portal_type_role_list_len_dict = {}
1766
    update_dict = defaultdict(list)
1767
    for path in self._objects:
1768
      obj = p.unrestrictedTraverse(path, None)
1769
      # Ignore any object without PortalType (non-ERP5 objects)
1770 1771 1772
      try:
        portal_type = aq_base(obj).getPortalType()
      except Exception, e:
1773 1774 1775 1776 1777 1778
        pass
      else:
        if portal_type not in p.portal_types:
          LOG("BusinessTemplate", WARNING,
              "Could not update Local Roles as Portal Type '%s' could not "
              "be found" % portal_type)
1779

1780
          continue
1781

1782 1783 1784
        if portal_type not in portal_type_role_list_len_dict:
          portal_type_role_list_len_dict[portal_type] = \
              len(p.portal_types[portal_type].getRoleInformationList())
1785

1786 1787
        if portal_type_role_list_len_dict[portal_type]:
          update_dict[portal_type].append(obj)
1788

1789 1790 1791 1792 1793 1794 1795 1796 1797 1798
    if update_dict:
      def updateLocalRolesOnDocument():
        for portal_type, obj_list in update_dict.iteritems():
          update = p.portal_types[portal_type].updateLocalRolesOnDocument
          for obj in obj_list:
            update(obj)
            LOG("BusinessTemplate", INFO,
                "Updated Local Roles for '%s' (%s)"
                % (portal_type, obj.getRelativeUrl()))
      transaction.get().addBeforeCommitHook(updateLocalRolesOnDocument)
1799

Yoshinori Okuji's avatar
Yoshinori Okuji committed
1800 1801 1802 1803 1804 1805 1806 1807
class ToolTemplateItem(PathTemplateItem):
  """This class is used only for making a distinction between other objects
  and tools, because tools may not be backed up."""
  def _backupObject(self, action, trashbin, container_path, object_id, **kw):
    """Fake as if a trashbin is not available."""
    return PathTemplateItem._backupObject(self, action, None, container_path,
                                          object_id, **kw)

1808
  def install(self, context, trashbin, **kw):
1809
    """ When we install a tool that is a type provider not
1810
    registered on types tool, register it into the type provider.
1811
    """
1812 1813 1814 1815
    PathTemplateItem.install(self, context, trashbin, **kw)
    portal = context.getPortalObject()
    types_tool = portal.portal_types
    for type_container_id, obj in self._objects.iteritems():
1816 1817 1818
      if (interfaces.ITypeProvider.providedBy(obj) and
          type_container_id != types_tool.id and
          type_container_id not in types_tool.type_provider_list):
1819 1820 1821 1822 1823 1824 1825 1826 1827 1828 1829 1830 1831 1832 1833 1834 1835 1836 1837 1838 1839
        types_tool.type_provider_list = tuple(types_tool.type_provider_list) + \
                                        (type_container_id,)

  def uninstall(self, context, **kw):
    """ When we uninstall a tool, unregister it from the type provider. """
    portal = context.getPortalObject()
    types_tool = portal.portal_types
    object_path = kw.get('object_path', None)
    if object_path is not None:
      object_keys = [object_path]
    else:
      object_keys = self._path_archive.keys()
    for tool_id in object_keys:
      types_tool.type_provider_list = tuple([ \
        x for x in types_tool.type_provider_list \
        if x != tool_id])
    PathTemplateItem.uninstall(self, context, **kw)

  def remove(self, context, **kw):
    """ When we remove a tool, unregister it from the type provider. """
    portal = context.getPortalObject()
1840
    types_tool = portal.portal_types
1841 1842 1843 1844 1845 1846 1847 1848 1849 1850
    remove_dict = kw.get('remove_object_dict', {})
    keys = self._objects.keys()
    for tool_id in keys:
      if remove_dict.has_key(tool_id):
        action = remove_dict[tool_id]
        if 'remove' in action:
          types_tool.type_provider_list = tuple([ \
            x for x in types_tool.type_provider_list \
            if x != tool_id])
    PathTemplateItem.remove(self, context, **kw)
1851

1852 1853 1854 1855 1856 1857 1858 1859 1860 1861 1862 1863 1864 1865 1866
class PreferenceTemplateItem(PathTemplateItem):
  """
  This class is used to store preference objects
  """
  def _resolvePath(self, folder, relative_url_list, id_list):
    """
    This method calls itself recursively.

    The folder is the current object which contains sub-objects.
    The list of ids are path components. If the list is empty,
    the current folder is valid.
    """
    if relative_url_list != []:
      LOG("PreferenceTemplateItem, _resolvePath", WARNING,
          "Should be empty")
1867
    if len(id_list) != 1:
1868 1869 1870 1871 1872 1873 1874 1875 1876 1877 1878 1879
      LOG("PreferenceTemplateItem, _resolvePath", WARNING,
          "Should contain only one element")
    # XXX hardcoded
    return ['portal_preferences/%s' % id_list[0]]

  def install(self, context, trashbin, **kw):
    """
    Enable Preference
    """
    PathTemplateItem.install(self, context, trashbin, **kw)
    portal = context.getPortalObject()
    for object_path in self._objects.keys():
1880
      pref = portal.unrestrictedTraverse(object_path)
1881
      # XXX getPreferenceState is a bad name
1882
      if pref.getPreferenceState() == 'disabled':
1883 1884
        # set safe activities execution order
        original_reindex_parameters = self.setSafeReindexationMode(context)
1885 1886
        portal.portal_workflow.doActionFor(
                      pref,
1887 1888 1889
                      'enable_action',
                      comment="Initialized during Business Template " \
                              "installation.")
1890
        # restore previous activities execution order
1891
        context.setPlacelessDefaultReindexParameters(**original_reindex_parameters)
1892

1893 1894
class CategoryTemplateItem(ObjectTemplateItem):

1895 1896
  def __init__(self, id_list, tool_id='portal_categories', **kw):
    ObjectTemplateItem.__init__(self, id_list, tool_id=tool_id, **kw)
1897

1898 1899 1900
  def build_sub_objects(self, context, id_list, url, **kw):
    for id in id_list:
      relative_url = '/'.join([url,id])
1901
      obj = context._getOb(id)
1902 1903 1904
      obj = self.removeProperties(obj, 1,
                                  self.isKeepWorkflowObject(relative_url),
                                  self.isKeepWorkflowObjectLastHistoryOnly(relative_url))
1905
      id_list = obj.objectIds()
1906
      if id_list:
1907
        self.build_sub_objects(obj, id_list, relative_url)
1908
        for id_ in list(id_list):
1909
          _delObjectWithoutHook(obj, id_)
1910 1911
      self._objects[relative_url] = obj
      obj.wl_clearLocks()
1912 1913 1914 1915 1916

  def build(self, context, **kw):
    BaseTemplateItem.build(self, context, **kw)
    p = context.getPortalObject()
    for relative_url in self._archive.keys():
1917 1918 1919 1920 1921 1922 1923 1924
      try:
        obj = p.unrestrictedTraverse(relative_url)
        obj = obj._getCopy(context)
      except (KeyError, AttributeError):
        if self.is_bt_for_diff:
          continue
        else:
          raise ValueError, "%s not found" % relative_url
1925
      _recursiveRemoveUid(obj)
1926 1927 1928
      obj = self.removeProperties(obj, 1,
                                  self.isKeepWorkflowObject(relative_url),
                                  self.isKeepWorkflowObjectLastHistoryOnly(relative_url))
Yoshinori Okuji's avatar
Yoshinori Okuji committed
1929
      include_sub_categories = obj.__of__(context).getProperty('business_template_include_sub_categories', 0)
1930
      id_list = obj.objectIds()
1931
      if len(id_list) > 0 and include_sub_categories:
1932 1933 1934
        self.build_sub_objects(obj, id_list, relative_url)
      for id_ in list(id_list):
        _delObjectWithoutHook(obj, id_)
1935 1936
      self._objects[relative_url] = obj
      obj.wl_clearLocks()
1937

1938 1939 1940
  def beforeInstall(self):
    self._installed_new_category = False

1941
  def onNewObject(self, obj):
1942 1943 1944 1945
    self._installed_new_category = True

  def afterInstall(self):
    if self._installed_new_category:
1946
      # reset accessors if we installed a new category
1947
      self.portal_types.resetDynamicDocumentsOnceAtTransactionBoundary()
1948

1949 1950
class SkinTemplateItem(ObjectTemplateItem):

1951 1952
  def __init__(self, id_list, tool_id='portal_skins', **kw):
    ObjectTemplateItem.__init__(self, id_list, tool_id=tool_id, **kw)
1953

1954 1955 1956 1957 1958 1959 1960 1961 1962 1963
  def build(self, context, **kw):
    ObjectTemplateItem.build(self, context, **kw)
    for relative_url in self._objects.keys():
      obj = self._objects[relative_url]
      if (getattr(obj, 'meta_type', None) == 'Folder') and \
        (obj.getProperty('business_template_registered_skin_selections', None) \
            is not None):
          obj._delProperty(
              'business_template_registered_skin_selections')

1964 1965
  def preinstall(self, context, installed_item, **kw):
    modified_object_list = ObjectTemplateItem.preinstall(self, context, installed_item, **kw)
1966 1967 1968 1969 1970 1971 1972 1973 1974 1975
    # We must install/update an ERP5 Form if one of its widget is modified.
    # This allow to keep the widget order and the form layout after an update
    #   from a BT to another one.
    for (bt_obj_path, bt_obj) in self._objects.items():
      if getattr(bt_obj, 'meta_type', None) == 'ERP5 Form':
        # search sub-objects of ERP5 Forms that are marked as "modified"
        for upd_obj_path in modified_object_list.keys():
          if upd_obj_path.startswith(bt_obj_path):
            # a child of the ERP5 Form must be updated, so the form too
            if not modified_object_list.has_key(bt_obj_path):
1976
              modified_object_list[bt_obj_path] = 'Modified', self.__class__.__name__[:-12]
1977 1978
    return modified_object_list

1979
  def install(self, context, trashbin, **kw):
1980
    ObjectTemplateItem.install(self, context, trashbin, **kw)
1981 1982
    update_dict = kw.get('object_to_update')
    force = kw.get('force')
1983
    p = context.getPortalObject()
1984
    skin_tool = p.portal_skins
1985
    for relative_url in self._objects.keys():
1986
      # Do not register skin which were explicitely ask not to be installed
1987 1988
      if not force and update_dict.get(relative_url)  == 'nothing':
        continue
1989
      folder = self.unrestrictedResolveValue(p, relative_url)
1990
      for obj in folder.objectValues(spec=('Z SQL Method', 'ERP5 SQL Method')):
1991
        fixZSQLMethod(p, obj)
1992 1993 1994 1995
      if folder.aq_parent.meta_type == 'CMF Skins Tool':
        registerSkinFolder(skin_tool, folder)

class RegisteredSkinSelectionTemplateItem(BaseTemplateItem):
1996 1997 1998 1999 2000 2001 2002 2003
  # BUG: Let's suppose old BT defines
  #         some_skin | Skin1
  #         some_skin | Skin2
  #      and new BT has:
  #         some_skin | Skin1
  #      Because 'some_skin' is still defined, it will be updated (actually
  #      'install') and not removed ('uninstall'). But we don't compare with
  #      old BT so we don't know we must unregister Skin2.
2004 2005 2006 2007 2008 2009 2010 2011 2012 2013 2014 2015

  def build(self, context, **kw):
    portal = context.getPortalObject()
    skin_tool = getToolByName(portal, 'portal_skins')

    for key in self._archive.keys():
      skin_folder_id, skin_selection_id = key.split(' | ')

      skin_folder = skin_tool[skin_folder_id]
      selection_list = skin_folder.getProperty(
          'business_template_registered_skin_selections',
          [])
2016 2017 2018
      # Backward compatibility, some values can be string
      if isinstance(selection_list, str):
        selection_list = selection_list.replace(',', ' ').split(' ')
2019
      if skin_selection_id in selection_list:
2020
        self._objects.setdefault(skin_folder_id, []).append(skin_selection_id)
2021
      else:
2022 2023 2024 2025 2026 2027 2028 2029 2030 2031 2032 2033 2034
        raise NotFound, 'No skin selection %s found for skin folder %s.' \
                          % (skin_selection_id, skin_folder_id)

  # Function to generate XML Code Manually
  def generateXml(self, path=None):
    xml_data = '<registered_skin_selection>'
    keys = self._objects.keys()
    keys.sort()
    for key in keys:
      skin_selection_list = self._objects[key]
      xml_data += '\n <skin_folder_selection>'
      xml_data += '\n  <skin_folder>%s</skin_folder>' % key
      xml_data += '\n  <skin_selection>%s</skin_selection>' \
2035
                      % ','.join(sorted(skin_selection_list))
2036 2037 2038 2039 2040
      xml_data += '\n </skin_folder_selection>'
    xml_data += '\n</registered_skin_selection>'
    return xml_data

  def export(self, context, bta, **kw):
2041
    if not self._objects:
2042 2043
      return
    # export workflow chain
2044 2045 2046
    bta.addObject(self.generateXml(),
                  name='registered_skin_selection',
                  path=self.__class__.__name__)
2047 2048 2049 2050 2051 2052 2053 2054 2055 2056 2057 2058 2059 2060 2061

  def install(self, context, trashbin, **kw):
    update_dict = kw.get('object_to_update')
    force = kw.get('force')
    portal = context.getPortalObject()
    skin_tool = getToolByName(portal, 'portal_skins')

    for skin_folder_id in self._objects.keys():

      if update_dict.has_key(skin_folder_id) or force:
        if not force:
          action = update_dict[skin_folder_id]
          if action == 'nothing':
            continue
        skin_folder = skin_tool[skin_folder_id]
2062 2063 2064
        default_value = []
        selection_list = skin_folder.getProperty(
          'business_template_registered_skin_selections', default_value)
2065

2066
        if selection_list is default_value:
2067
          create_property = True
2068
          selection_list = self._objects[skin_folder_id]
2069
        else:
2070
          create_property = False
2071 2072 2073 2074 2075
          if isinstance(selection_list, basestring):
            selection_list = selection_list.replace(',', ' ').split(' ')
          elif isinstance(selection_list, tuple):
            selection_list = list(selection_list)
          selection_list.extend(self._objects[skin_folder_id])
2076 2077

        # Remove duplicate
2078
        selection_list = list(set(selection_list))
2079 2080 2081
        if create_property:
          skin_folder._setProperty(
              'business_template_registered_skin_selections',
2082
              selection_list, type='tokens')
2083 2084 2085
        else:
          skin_folder._updateProperty(
              'business_template_registered_skin_selections',
2086
              selection_list)
2087

2088 2089
        unregisterSkinFolderId(skin_tool, skin_folder_id,
                               skin_tool.getSkinSelections())
2090
        registerSkinFolder(skin_tool, skin_folder)
2091 2092

  def uninstall(self, context, **kw):
2093 2094
    portal = context.getPortalObject()
    skin_tool = getToolByName(portal, 'portal_skins')
2095 2096
    object_path = kw.get('object_path')
    for skin_folder_id in (object_path,) if object_path else self._objects:
2097
      skin_selection_list = self._objects[skin_folder_id]
2098 2099
      if isinstance(skin_selection_list, str):
        skin_selection_list = skin_selection_list.replace(',', ' ').split(' ')
2100 2101 2102 2103 2104 2105 2106
      skin_folder = skin_tool.get(skin_folder_id)
      if skin_folder is not None:
        current_selection_set = set(skin_folder.getProperty(
          'business_template_registered_skin_selections', ()))
        current_selection_set.difference_update(skin_selection_list)
        if current_selection_set:
          skin_folder._updateProperty(
2107
            'business_template_registered_skin_selections',
2108 2109 2110 2111 2112 2113 2114 2115 2116 2117 2118
            list(current_selection_set))
          # Unregister skin folder from skin selection
          unregisterSkinFolderId(skin_tool, skin_folder_id, skin_selection_list)
          continue
      # Delete all skin selection
      for skin_selection in skin_selection_list:
        deleteSkinSelection(skin_tool, skin_selection)
      if skin_folder is not None:
        del skin_folder.business_template_registered_skin_selections
        # Register to all other skin selection
        registerSkinFolder(skin_tool, skin_folder)
2119

2120
  def preinstall(self, context, installed_item, **kw):
2121
    modified_object_list = {}
2122 2123 2124 2125 2126 2127
    for path in self._objects:
      if installed_item._objects.has_key(path):
        # compare object to see it there is changes
        new_object = self._objects[path]
        old_object = installed_item._objects[path]
        if new_object != old_object:
2128
          modified_object_list[path] = 'Modified', self.__class__.__name__[:-12]
2129
      else: # new object
2130
        modified_object_list[path] = 'New', self.__class__.__name__[:-12]
2131 2132 2133 2134
    # get removed object
    old_keys = installed_item._objects.keys()
    for path in old_keys:
      if path not in self._objects:
2135
        modified_object_list[path] = 'Removed', self.__class__.__name__[:-12]
2136 2137 2138 2139
    return modified_object_list

  def _importFile(self, file_name, file):
    if not file_name.endswith('.xml'):
2140
      LOG('Business Template', 0, 'Skipping file "%s"' % (file_name, ))
2141 2142 2143 2144
      return
    # import workflow chain for portal_type
    skin_selection_dict = {}
    xml = parse(file)
2145 2146 2147
    for skin_folder_selection in xml.getroot():
      skin_folder_id = skin_folder_selection.find('skin_folder').text
      selection_string = skin_folder_selection.find('skin_selection').text
2148 2149
      if not selection_string:
        selection_list = []
2150
      else:
2151 2152
        selection_list = selection_string.split(',')
      skin_selection_dict[skin_folder_id] = selection_list
2153
    self._objects = skin_selection_dict
2154

2155 2156 2157 2158 2159 2160 2161 2162 2163 2164 2165 2166 2167 2168 2169
class RegisteredVersionPrioritySelectionTemplateItem(BaseTemplateItem):
  def _fillObjectDictFromArchive(self):
    for version_priority in self._archive:
      try:
        version, priority = version_priority.split('|')
        priority = float(priority)
      except ValueError:
        version = version_priority
        priority = 0.

      self._objects[version.strip()] = priority

  def build(self, context, **kw):
    self._fillObjectDictFromArchive()

2170 2171 2172
  def beforeInstall(self):
    self.__is_new_version_priority_installed = False

2173 2174 2175 2176
  def install(self, context, trashbin, **kw):
    if not self._objects:
      return

2177 2178
    self.beforeInstall()

2179 2180 2181 2182 2183 2184 2185 2186 2187 2188 2189 2190 2191 2192 2193 2194 2195 2196 2197 2198 2199 2200 2201 2202 2203 2204 2205 2206 2207 2208 2209 2210 2211 2212 2213 2214 2215 2216 2217 2218 2219 2220 2221 2222 2223
    portal = context.getPortalObject()
    registered_tuple_list = []
    for value in portal.getVersionPriorityList():
      try:
        version, priority = value.split('|')
        priority = float(priority)
      except ValueError:
        version = value
        priority = 0.

      registered_tuple_list.append((version.strip(), priority))

    update_dict = kw.get('object_to_update')
    force = kw.get('force')
    registered_name_list = set(portal.getVersionPriorityNameList())
    for new_version, new_priority in self._objects.iteritems():
      action = update_dict.get(new_version)
      if (not action or action == 'nothing') and not force:
        continue

      # Merge version and priority defined on this bt and already registered
      # version and priority
      inserted = False
      index = 0
      for (version, priority) in registered_tuple_list:
        if new_version == version:
          if new_priority == priority:
            inserted = True
            break
          else:
            del registered_tuple_list[index]
            continue
        elif not inserted:
          if new_priority > priority:
            registered_tuple_list.insert(index, (new_version, new_priority))
            inserted = True
          elif new_priority == priority and new_version >= version:
            registered_tuple_list.insert(index, (new_version, new_priority))
            inserted = True

        index += 1

      if not inserted:
        registered_tuple_list.append((new_version, new_priority))

2224 2225
      self.__is_new_version_priority_installed = True

2226 2227 2228
    portal.setVersionPriorityList(('%s | %s' % (version, priority)
                                   for version, priority in registered_tuple_list))

2229 2230 2231 2232 2233 2234 2235
    self.afterInstall()

  def afterInstall(self):
    if self.__is_new_version_priority_installed:
      self.portal_components.reset(force=True,
                                   reset_portal_type_at_transaction_boundary=True)

2236 2237 2238 2239 2240 2241 2242 2243
  def preinstall(self, context, installed_item, **kw):
    modified_object_list = {}
    class_name_prefix = self.__class__.__name__[:-12]
    for path, new_object in self._objects.iteritems():
      old_object = installed_item._objects.get(path)
      if old_object is not None:
        # Compare object to see it there is any change
        if new_object != old_object:
2244
          modified_object_list[path] = 'Modified', class_name_prefix
2245
      else:
2246
        modified_object_list[path] = 'New', class_name_prefix
2247 2248 2249 2250

    # Get removed objects
    for path in installed_item._objects:
      if path not in self._objects:
2251
        modified_object_list[path] = 'Removed', class_name_prefix
2252 2253 2254 2255 2256 2257 2258 2259 2260 2261 2262 2263 2264 2265 2266 2267 2268 2269 2270 2271 2272 2273 2274 2275

    return modified_object_list

  def importFile(self, bta, **kw):
    super(RegisteredVersionPrioritySelectionTemplateItem,
          self).importFile(bta, **kw)

    self._objects.clear()
    self._fillObjectDictFromArchive()

  def uninstall(self, context, **kw):
    object_path = kw.get('object_path')
    object_list = object_path and (object_path,) or self._objects

    portal = context.getPortalObject()
    registered_list = list(portal.getVersionPriorityList())
    index = 0
    for version in portal.getVersionPriorityNameList():
      if version in object_list:
        del registered_list[index]
      else:
        index += 1

    portal.setVersionPriorityList(registered_list)
2276

2277
class WorkflowTemplateItem(ObjectTemplateItem):
2278

2279
  def __init__(self, id_list, tool_id='portal_workflow', **kw):
2280
    ObjectTemplateItem.__init__(self, id_list, tool_id=tool_id, **kw)
2281

2282 2283 2284 2285 2286
  # When the root object of a workflow is modified, the entire workflow is
  # recreated: all subobjects are discarded and must be reinstalled.
  # So we hide modified subobjects to the user and we always reinstall
  # (or remove) everything.

2287
  def preinstall(self, context, installed_item, installed_bt, **kw):
2288
    modified_object_dict = ObjectTemplateItem.preinstall(self, context,
2289
                                                         installed_item, **kw)
2290 2291 2292 2293
    modified_workflow_dict = {}
    for modified_object, state in modified_object_dict.iteritems():
      path = modified_object.split('/')
      if len(path) > 2:
2294
        modified_workflow_dict.setdefault('/'.join(path[:2]), ('Modified', state[1]))
2295 2296
      else:
        modified_workflow_dict[modified_object] = state
2297 2298 2299 2300 2301 2302 2303 2304 2305 2306
    removed_workflow_id_list = [x[0].split('/', 1)[1] \
                                for x in modified_workflow_dict.iteritems() \
                                if x[1][0] == 'Removed']
    if len(removed_workflow_id_list) > 0:
      installed_chain_list = [[y.strip() for y in x.split('|')] for x in \
                                installed_bt.getTemplatePortalTypeWorkflowChainList()]
      new_chain_list = [[y.strip() for y in x.split('|')] for x in \
                          context.getTemplatePortalTypeWorkflowChainList()]
      chain_dict = getChainByType(context)[1]
      for workflow_id in removed_workflow_id_list:
2307 2308 2309 2310 2311 2312
        affected_portal_type_set = {x[6:] for x, y in chain_dict.iteritems()
          if any(workflow_id == y.strip() for y in y.split(','))}
        safe_portal_type_set = {x for x, y in installed_chain_list
                                  if y == workflow_id}
        safe_portal_type_set.difference_update(x for x, y in new_chain_list
                                                 if y == workflow_id)
2313 2314 2315 2316
        if affected_portal_type_set - safe_portal_type_set:
          value = modified_workflow_dict['portal_workflow/%s' % workflow_id]
          modified_workflow_dict['portal_workflow/%s' % workflow_id] = \
              ('Removed but used', value[1])
2317 2318
    return modified_workflow_dict

2319
  def install(self, context, trashbin, **kw):
2320 2321 2322 2323 2324 2325 2326 2327 2328 2329 2330 2331 2332 2333 2334 2335 2336 2337 2338 2339
    portal = context.getPortalObject()
    update_dict = kw.get('object_to_update')
    force = kw.get('force')
    # sort to add objects before their subobjects
    for path in sorted(self._objects):
        if force:
          action = 'backup'
        else:
          action = update_dict.get('/'.join(path.split('/')[:2]))
          if action in (None, 'nothing'):
            continue
        container_path = path.split('/')[:-1]
        object_id = path.split('/')[-1]
        try:
          container = self.unrestrictedResolveValue(portal, container_path)
        except KeyError:
          # parent object can be set to nothing, in this case just go on
          container_url = '/'.join(container_path)
          if update_dict.has_key(container_url):
            if update_dict[container_url] == 'nothing':
Julien Muchembled's avatar
Julien Muchembled committed
2340
              continue
2341 2342 2343 2344 2345 2346 2347 2348 2349 2350 2351 2352
          raise
        container_ids = container.objectIds()
        if object_id in container_ids:    # Object already exists
          self._backupObject(action, trashbin, container_path, object_id, keep_subobjects=1)
          container.manage_delObjects([object_id])
        obj = self._objects[path]
        obj = obj._getCopy(container)
        self.removeProperties(obj, 0)
        container._setObject(object_id, obj)
        obj = container._getOb(object_id)
        obj.manage_afterClone(obj)
        obj.wl_clearLocks()
2353

2354 2355 2356 2357 2358 2359
  def uninstall(self, context, **kw):
    object_path = kw.get('object_path', None)
    if object_path is not None:
      object_keys = [object_path]
    else:
      object_keys = self._archive.keys()
2360
    removed_workflow_id_list = {x.split('/', 1)[1] for x in object_keys}
2361 2362
    (default_chain, chain_dict) = getChainByType(context)
    for portal_type, workflow_ids in chain_dict.iteritems():
2363
      workflow_ids = {x.strip() for x in workflow_ids.split(',')} - \
2364 2365 2366 2367 2368
                     removed_workflow_id_list
      chain_dict[portal_type] = ', '.join(workflow_ids)
    context.portal_workflow.manage_changeWorkflows(default_chain,
                                                   props=chain_dict)
    ObjectTemplateItem.uninstall(self, context, **kw)
2369

2370 2371
class PortalTypeTemplateItem(ObjectTemplateItem):

2372 2373
  def __init__(self, id_list, tool_id='portal_types', **kw):
    ObjectTemplateItem.__init__(self, id_list, tool_id=tool_id, **kw)
2374 2375
    # XXX : this statement can be removed once all bt5 have separated
    # workflow-chain information
2376 2377 2378
    self._workflow_chain_archive = PersistentMapping()

  def build(self, context, **kw):
2379 2380
    p = context.getPortalObject()
    for relative_url in self._archive.keys():
2381
      obj = p.unrestrictedTraverse(relative_url)
2382 2383 2384 2385
      # normalize relative_url, not all type informations are stored in
      # "portal_types"
      relative_url = '%s/%s' % (obj.getPhysicalPath()[-2:])

2386
      obj = obj._getCopy(context)
2387
      obj._p_activate()
2388
      for attr in obj.__dict__.keys():
2389 2390 2391 2392 2393 2394
        if attr == '_property_domain_dict':
          continue
        if attr[0] == '_' or attr in ('allowed_content_types',
                                      'hidden_content_type_list',
                                      'property_sheet_list',
                                      'base_category_list',
2395 2396 2397
                                      'last_id', 'uid') or \
            (attr == 'workflow_history' and
             not self.isKeepWorkflowObject(relative_url)):
2398
          delattr(obj, attr)
2399 2400
      self._objects[relative_url] = obj
      obj.wl_clearLocks()
2401

2402 2403 2404 2405 2406 2407 2408 2409 2410 2411
  def _getObjectKeyList(self):
    # Sort portal types to install according to their dependencies
    object_key_list = self._objects.keys()
    path_dict = dict(x.split('/')[1:] + [x] for x in object_key_list)
    cache = {}
    def solveDependency(path):
      score = cache.get(path)
      if score is None:
        obj = self._objects[path]
        klass = obj.__class__
2412
        if klass.__module__ != 'erp5.portal_type':
2413 2414 2415 2416
          portal_type = obj.portal_type
        else:
          portal_type = klass.__name__
        depend = path_dict.get(portal_type)
2417 2418 2419 2420 2421 2422
        # Prevent infinite recursion for 'portal_types/Base Type',
        # only relevant with Portal Types classes because 'Base Type'
        # is an 'erp5.portal_type.Base Type' class
        if depend == path:
          assert depend == 'portal_types/Base Type'
          return 0, path
2423 2424
        cache[path] = score = depend and 1 + solveDependency(depend)[0] or 0
      return score, path
2425
    object_key_list.sort(key=solveDependency)
2426 2427
    return object_key_list

2428 2429
  # XXX : this method is kept temporarily, but can be removed once all bt5 are
  # re-exported with separated workflow-chain information
2430 2431 2432 2433
  def install(self, context, trashbin, **kw):
    ObjectTemplateItem.install(self, context, trashbin, **kw)
    update_dict = kw.get('object_to_update')
    force = kw.get('force')
2434 2435
    # We now need to setup the list of workflows corresponding to
    # each portal type
2436
    (default_chain, chain_dict) = getChainByType(context)
2437
    # Set the default chain to the empty string is probably the
2438
    # best solution, by default it is 'default_workflow', which is
2439
    # not very usefull
2440
    default_chain = ''
2441
    for path, obj in self._objects.iteritems():
2442 2443 2444 2445
      if update_dict.has_key(path) or force:
        if not force:
          action = update_dict[path]
          if action == 'nothing':
2446
            continue
2447
        portal_type = obj.id
2448 2449 2450
        if self._workflow_chain_archive.has_key(portal_type):
          chain_dict['chain_%s' % portal_type] = \
              self._workflow_chain_archive[portal_type]
2451 2452
    context.portal_workflow.manage_changeWorkflows(default_chain,
                                                   props=chain_dict)
2453 2454
  # XXX : this method is kept temporarily, but can be removed once all bt5 are
  # re-exported with separated workflow-chain information
2455 2456 2457
  def _importFile(self, file_name, file):
    if 'workflow_chain_type.xml' in file_name:
      # import workflow chain for portal_type
2458
      result_dict = {}
2459
      xml = parse(file)
2460
      chain_list = xml.findall('//chain')
2461
      for chain in chain_list:
2462 2463 2464 2465
        portal_type = chain.find('type').text
        workflow = chain.find('workflow').text or ''
        result_dict[portal_type] = workflow
      self._workflow_chain_archive = result_dict
2466 2467 2468
    else:
      ObjectTemplateItem._importFile(self, file_name, file)

2469
class PortalTypeWorkflowChainTemplateItem(BaseTemplateItem):
2470

2471 2472 2473
  _chain_string_prefix = 'chain_'
  _chain_string_separator = ', '

2474
  def build(self, context, **kw):
2475 2476 2477 2478 2479
    # we can either specify nothing, +, - or = before the chain
    # this is used to know how to manage the chain
    # if nothing or +, chain is added to the existing one
    # if - chain is removed from the exisiting one
    # if = chain replaced the existing one
2480
    (default_chain, chain_dict) = getChainByType(context)
Aurel's avatar
Aurel committed
2481
    for key in self._archive.keys():
2482 2483 2484
      wflist = key.split(' | ')
      if len(wflist) == 2:
        portal_type = wflist[0]
Aurel's avatar
Aurel committed
2485
        workflow = wflist[1]
2486
      else:
2487
        # portal type with no workflow defined
Aurel's avatar
Aurel committed
2488
        portal_type = wflist[0][:-2]
2489
        workflow = ''
2490 2491 2492 2493 2494
      portal_type_key = '%s%s' % (self._chain_string_prefix, portal_type)
      if portal_type_key in chain_dict:
        workflow_name = workflow.lstrip('+-=')
        if workflow[0] != '-' and workflow_name not in \
           chain_dict[portal_type_key].split(self._chain_string_separator):
2495
          if not self.is_bt_for_diff:
2496 2497 2498
            # here, we use 'LOG' instead of 'raise', because it can
            # happen when a workflow is removed from the chain by
            # another business template.
Jérome Perrin's avatar
Jérome Perrin committed
2499
            LOG('BusinessTemplate', WARNING, 'workflow %s not found '\
2500
                       'in chain for portal_type %s' % (workflow_name, portal_type))
2501
        self._objects.setdefault(portal_type, []).append(workflow)
2502
      elif not self.is_bt_for_diff:
2503
        raise NotFound, 'No workflow chain found for portal type %s. This '\
Vincent Pelletier's avatar
Vincent Pelletier committed
2504
                        'is probably a sign of a missing dependency.'\
2505
                                                    % portal_type
2506

Christophe Dumez's avatar
Christophe Dumez committed
2507
  # Function to generate XML Code Manually
2508 2509
  def generateXml(self, path=None):
    xml_data = '<workflow_chain>'
2510 2511 2512
    key_list = self._objects.keys()
    key_list.sort()
    for key in key_list:
2513
      workflow_list = self._objects[key]
2514 2515
      xml_data += '\n <chain>'
      xml_data += '\n  <type>%s</type>' %(key,)
2516 2517
      xml_data += '\n  <workflow>%s</workflow>' %(
        self._chain_string_separator.join(sorted(workflow_list)))
2518 2519
      xml_data += '\n </chain>'
    xml_data += '\n</workflow_chain>'
2520 2521 2522
    return xml_data

  def export(self, context, bta, **kw):
2523
    if not self._objects:
2524
      return
2525 2526 2527 2528 2529 2530 2531 2532 2533 2534
    # 'portal_type_workflow_chain/' is added in _importFile
    # and if the template is not built,
    # it should be removed here from the key
    new_objects = PersistentMapping()
    for key, value in self._objects.iteritems():
      new_key = deepcopy(key)
      if 'portal_type_workflow_chain/' in key:
        new_key = new_key.replace('portal_type_workflow_chain/', '')
      new_objects[new_key] = value
    self._objects = new_objects
2535 2536
    # export workflow chain
    xml_data = self.generateXml()
2537 2538
    bta.addObject(xml_data, name='workflow_chain_type',
                  path=self.__class__.__name__)
2539 2540 2541 2542

  def install(self, context, trashbin, **kw):
    update_dict = kw.get('object_to_update')
    force = kw.get('force')
2543 2544 2545 2546 2547 2548
    installed_bt = kw.get('installed_bt')
    if installed_bt is not None:
      previous_portal_type_workflow_chain_list = list(installed_bt\
          .getTemplatePortalTypeWorkflowChainList())
    else:
      previous_portal_type_workflow_chain_list = []
2549 2550
    # We now need to setup the list of workflows corresponding to
    # each portal type
2551
    (default_chain, chain_dict) = getChainByType(context)
2552
    # First convert all workflow_ids into list.
2553 2554
    for key, value in chain_dict.iteritems():
      chain_dict[key] = value.split(self._chain_string_separator)
2555
    orig_chain_dict = chain_dict.copy()
2556
    # Set the default chain to the empty string is probably the
Christophe Dumez's avatar
Christophe Dumez committed
2557
    # best solution, by default it is 'default_workflow', which is
2558 2559
    # not very usefull
    default_chain = ''
2560 2561
    for path in self._objects:
      if path in update_dict or force:
2562 2563 2564
        if not force:
          action = update_dict[path]
          if action == 'nothing':
2565
            continue
Jérome Perrin's avatar
Jérome Perrin committed
2566 2567
        path_splitted = path.split('/', 1)
        # XXX: to avoid crashing when no portal_type
2568
        if not path_splitted:
Jérome Perrin's avatar
Jérome Perrin committed
2569
          continue
2570
        portal_type = path_splitted[-1]
2571 2572
        chain_key = '%s%s' % (self._chain_string_prefix, portal_type)
        if chain_key in chain_dict:
Aurel's avatar
Aurel committed
2573
          # XXX we don't use the chain (Default) in erp5 so don't keep it
2574 2575 2576 2577 2578 2579
          old_chain_list = [workflow_id for workflow_id in\
                            chain_dict[chain_key] if workflow_id not in\
                            ('(Default)', '',)]
          old_chain_workflow_id_set = set(old_chain_list)
          # get new workflow id list
          workflow_id_list = self._objects[path]
2580 2581 2582 2583 2584 2585 2586 2587 2588 2589 2590 2591 2592 2593 2594 2595 2596 2597 2598 2599
          # fetch list of new workflows which shall be added to chains
          addative_workflow_id_list = [q.lstrip('+') for q in workflow_id_list\
              if not q.startswith('-') and not q.startswith('=')]
          for previous_line in [q for q in \
              previous_portal_type_workflow_chain_list \
              if q.startswith(portal_type)]:
            previous_portal_type, previous_workflow_id = previous_line.split(
                '|')
            previous_portal_type = previous_portal_type.strip()
            previous_workflow_id = previous_workflow_id.strip()
            if not previous_workflow_id.startswith('-') \
                and not previous_workflow_id.startswith('='):
              # else: nothing can be done if previously workflow was removed
              # or replaced as this requires introspection on global system
              previous_workflow_id = previous_workflow_id.lstrip('+')
              if previous_workflow_id not in addative_workflow_id_list:
                # In previous Business Template workflow was chained with
                # portal type, but current Business Template cancels this
                # so it shall be removed
                workflow_id_list.append('-%s' % previous_workflow_id)
2600 2601 2602 2603 2604 2605 2606 2607 2608 2609 2610
          for wf_id in workflow_id_list:
            if wf_id[0] == '-':
              # remove wf id if already present
              if wf_id[1:] in old_chain_workflow_id_set:
                old_chain_workflow_id_set.remove(wf_id[1:])
            elif wf_id[0] == '=':
              # replace existing chain by this one
              old_chain_workflow_id_set = set()
              old_chain_workflow_id_set.add(wf_id[1:])
            # then either '+' or nothing, add wf id to the list
            else:
Łukasz Nowak's avatar
Łukasz Nowak committed
2611
              wf_id = wf_id.lstrip('+')
2612
              old_chain_workflow_id_set.add(wf_id)
2613
            # create the new chain
2614 2615
            chain_dict[chain_key] = list(old_chain_workflow_id_set)
          if not workflow_id_list:
Romain Courteaud's avatar
Romain Courteaud committed
2616 2617
            # Check if it has normally to remove a workflow chain, in order to
            # improve the error message
2618
            for wf_id in self._objects[path]:
Romain Courteaud's avatar
Romain Courteaud committed
2619 2620 2621
              if wf_id.startswith('-'):
                raise ValueError, '"%s" is not a workflow ID for %s' % \
                                  (wf_id, portal_type)
2622
            chain_dict[chain_key] = self._objects[path]
Aurel's avatar
Aurel committed
2623
        else:
2624
          if context.portal_types.getTypeInfo(portal_type) is None:
2625
            raise ValueError('Cannot chain workflow %r to non existing '
2626 2627 2628 2629
                           'portal type %r' % (self._chain_string_separator\
                                                     .join(self._objects[path])
                                               , portal_type))
          chain_dict[chain_key] = self._objects[path]
2630 2631 2632
    if orig_chain_dict == chain_dict:
      return
    self._resetDynamicModules()
2633
    # convert workflow list into string only at the end.
2634 2635
    for key, value in chain_dict.iteritems():
      chain_dict[key] =  self._chain_string_separator.join(value)
2636 2637
    context.portal_workflow.manage_changeWorkflows(default_chain,
                                                   props=chain_dict)
2638

2639 2640
  def uninstall(self, context, **kw):
    (default_chain, chain_dict) = getChainByType(context)
2641 2642
    object_path = kw.get('object_path', None)
    if object_path is not None:
2643
      object_key_list = [object_path]
2644
    else:
2645 2646 2647
      object_key_list = self._objects.keys()
    for object_key in object_key_list:
      path_splitted = object_key.split('/', 1)
2648 2649 2650
      if len(path_splitted) < 2:
        continue
      portal_type = path_splitted[1]
2651 2652 2653 2654 2655 2656 2657 2658 2659 2660
      path = '%s%s' % (self._chain_string_prefix, portal_type)
      if path in chain_dict:
        workflow_id_list = chain_dict[path].\
                                            split(self._chain_string_separator)
        removed_workflow_id_list = self._objects[object_key]
        for workflow_id in removed_workflow_id_list:
          for i in range(workflow_id_list.count(workflow_id)):
            workflow_id_list.remove(workflow_id)
        if not workflow_id_list:
          del chain_dict[path]
2661
        else:
2662 2663 2664 2665
          chain_dict[path] = self._chain_string_separator.\
                                                  join(workflow_id_list)
    context.getPortalObject().portal_workflow.\
                                   manage_changeWorkflows('', props=chain_dict)
2666

2667
  def preinstall(self, context, installed_item, **kw):
2668
    modified_object_list = {}
2669 2670 2671 2672 2673 2674 2675 2676
    new_dict = PersistentMapping()
    # Fix key from installed bt if necessary
    for key, value in installed_item._objects.iteritems():
      if not 'portal_type_workflow_chain/' in key:
        key = 'portal_type_workflow_chain/%s' % (key)
      new_dict[key] = value
    if new_dict:
      installed_item._objects = new_dict
2677 2678 2679 2680 2681 2682 2683 2684 2685 2686 2687 2688
    for path in self._objects:
      if path in installed_item._objects:
        # compare object to see it there is changes
        new_object = self._objects[path]
        old_object = installed_item._objects[path]
        if isinstance(new_object, str):
          new_object = new_object.split(self._chain_string_separator)
        if isinstance(old_object, str):
          old_object = old_object.split(self._chain_string_separator)
        new_object.sort()
        old_object.sort()
        if new_object != old_object:
2689
          modified_object_list[path] = 'Modified', self.getTemplateTypeName()
2690
      else: # new object
2691
        modified_object_list[path] = 'New', self.getTemplateTypeName()
2692 2693 2694
    # get removed object
    for path in installed_item._objects:
      if path not in self._objects:
2695
        modified_object_list[path] = 'Removed', self.getTemplateTypeName()
2696 2697
    return modified_object_list

2698
  def _importFile(self, file_name, file):
2699
    if not file_name.endswith('.xml'):
2700
      LOG('Business Template', 0, 'Skipping file "%s"' % (file_name, ))
2701
      return
2702
    # import workflow chain for portal_type
2703
    result_dict = {}
2704
    xml = parse(file)
2705
    chain_list = xml.findall('chain')
2706
    for chain in chain_list:
2707
      portal_type = chain.find('type').text
2708
      workflow_chain = chain.find('workflow').text or ''
2709 2710 2711 2712
      if 'portal_type_workflow_chain/' not in portal_type:
        key = 'portal_type_workflow_chain/%s' % (portal_type,)
      else:
        key = portal_type
2713
      result_dict[key] = workflow_chain.split(self._chain_string_separator)
2714
    self._objects = result_dict
2715

2716 2717
# just for backward compatibility
PortalTypeTemplateWorkflowChainItem = PortalTypeWorkflowChainTemplateItem
2718

2719
class PortalTypeAllowedContentTypeTemplateItem(BaseTemplateItem):
2720 2721 2722
  # XXX This class is subclassed for hidden types, propertysheets, base
  # categories ...
  name = 'Allowed Content Type'
2723 2724
  xml_tag = 'allowed_content_type_list'
  class_property = 'allowed_content_types'
2725
  business_template_class_property = '_portal_type_allowed_content_type_item'
2726 2727

  def build(self, context, **kw):
2728
    types_tool = getToolByName(self.getPortalObject(), 'portal_types')
2729
    for key in self._archive.keys():
2730 2731 2732 2733
      try:
        portal_type, allowed_type = key.split(' | ')
      except ValueError:
        raise ValueError('Invalid item %r in %s' % (key, self.name))
2734
      ob = types_tool.getTypeInfo(portal_type)
2735
      # check properties corresponds to what is defined in site
2736
      if ob is None:
2737
        raise ValueError, "Portal Type %r not found in site" %(portal_type,)
2738
      prop_value = getattr(ob, self.class_property, ())
2739 2740 2741 2742 2743 2744 2745
      if allowed_type in prop_value:
        if self.class_property not in portal_type:
          key = '%s/%s' % (self.class_property, portal_type)
        else:
          key = portal_type
        self._objects.setdefault(key, []).append(allowed_type)
      elif not self.is_bt_for_diff:
Jérome Perrin's avatar
Jérome Perrin committed
2746
        raise ValueError, "%s %s not found in portal type %s" % (
2747 2748
                             getattr(self, 'name', self.__class__.__name__),
                             allowed_type, portal_type)
2749

Christophe Dumez's avatar
Christophe Dumez committed
2750
  # Function to generate XML Code Manually
2751 2752
  def generateXml(self, path=None):
    xml_data = '<%s>' %(self.xml_tag,)
2753 2754 2755
    key_list = self._objects.keys()
    key_list.sort()
    for key in key_list:
2756
      id_value = key.replace('%s/' % self.class_property, '')
2757
      allowed_item_list = sorted(self._objects[key])
2758
      xml_data += '\n <portal_type id="%s">' % (id_value)
2759
      for allowed_item in allowed_item_list:
2760 2761 2762
        xml_data += '\n  <item>%s</item>' %(allowed_item,)
      xml_data += '\n </portal_type>'
    xml_data += '\n</%s>' %(self.xml_tag,)
2763 2764 2765
    return xml_data

  def export(self, context, bta, **kw):
2766
    if not self._objects:
2767 2768
      return
    xml_data = self.generateXml(path=None)
2769 2770
    bta.addObject(xml_data, name=self.class_property,
                  path=self.__class__.__name__)
2771

2772
  def preinstall(self, context, installed_item, **kw):
2773
    modified_object_list = {}
2774 2775 2776 2777 2778 2779 2780 2781 2782 2783 2784 2785 2786 2787 2788 2789
    new_dict = PersistentMapping()
    # fix key if necessary in installed bt for diff
    for key, value in installed_item._objects.iteritems():
      if self.class_property not in key:
        key = '%s/%s' % (self.class_property, key)
      new_dict[key] = value
    if new_dict:
      installed_item._objects = new_dict
    for path in self._objects:
      if path in installed_item._objects:
        # compare object to see it there is changes
        new_object = self._objects[path]
        old_object = installed_item._objects[path]
        new_object.sort()
        old_object.sort()
        if new_object != old_object:
2790
          modified_object_list[path] = 'Modified', self.getTemplateTypeName()
2791
      else: # new object
2792
        modified_object_list[path] = 'New', self.getTemplateTypeName()
2793 2794 2795
    # get removed object
    for path in installed_item._objects:
      if path not in self._objects:
2796
        modified_object_list[path] = 'Removed', self.getTemplateTypeName()
2797 2798
    return modified_object_list

2799
  def _importFile(self, file_name, file):
2800
    if not file_name.endswith('.xml'):
2801
      LOG('Business Template', 0, 'Skipping file "%s"' % (file_name, ))
2802
      return
2803
    path, name = posixpath.split(file_name)
2804
    xml = parse(file)
2805
    portal_type_list = xml.findall('portal_type')
2806
    for portal_type in portal_type_list:
2807 2808
      id = portal_type.get('id')
      item_type_list = [item.text for item in portal_type.findall('item')]
Nicolas Delaby's avatar
typo  
Nicolas Delaby committed
2809
      if self.class_property not in id:
2810 2811 2812 2813
        key = '%s/%s' % (self.class_property, id,)
      else:
        key = id
      self._objects[key] = item_type_list
2814 2815

  def install(self, context, trashbin, **kw):
Nicolas Delaby's avatar
Nicolas Delaby committed
2816 2817
    portal = context.getPortalObject()
    types_tool = getToolByName(portal, 'portal_types')
2818 2819
    update_dict = kw.get('object_to_update')
    force = kw.get('force')
2820 2821
    installed_bt = kw.get('installed_bt')
    if installed_bt is not None:
2822 2823 2824 2825 2826
      item = getattr(installed_bt, self.business_template_class_property, None)
      if item is not None:
        old_objects = item._objects
      else:
        old_objects = {}
2827 2828
    else:
      old_objects = {}
2829
    for key in set(self._objects.keys()).union(old_objects.keys()):
Nicolas Delaby's avatar
Nicolas Delaby committed
2830
      if key in update_dict or force:
2831 2832 2833 2834
        if not force:
          action = update_dict[key]
          if action == 'nothing':
            continue
2835
        portal_id = key.split('/')[-1]
2836
        property_list = self._objects.get(key, [])
2837 2838
        type_information = types_tool.getTypeInfo(portal_id)
        if type_information is None:
2839 2840
          if not property_list:
            continue
2841 2842
          raise AttributeError, "Portal type '%s' not found while " \
              "installing %s" % (portal_id, self.getTitle())
2843
        old_property_list = old_objects.get(key, ())
2844
        object_property_list = getattr(type_information, self.class_property, ())
Nicolas Delaby's avatar
Nicolas Delaby committed
2845 2846 2847 2848 2849 2850 2851 2852 2853
        # merge differences between portal types properties
        # for example:
        # * current value : [A,B,C]
        # * in new BT : [A,D]
        # * in old BT : [A,B]
        # -> [A,D,C] i.e. C is merged but B is not merged
        for id in object_property_list:
          if id not in property_list and id not in old_property_list:
            property_list.append(id)
2854
        setattr(type_information, self.class_property, tuple(property_list))
2855

Aurel's avatar
Aurel committed
2856
  def uninstall(self, context, **kw):
2857
    object_path = kw.get('object_path', None)
Nicolas Delaby's avatar
Nicolas Delaby committed
2858 2859
    portal = context.getPortalObject()
    types_tool = getToolByName(portal, 'portal_types')
2860
    if object_path is not None:
Nicolas Delaby's avatar
Nicolas Delaby committed
2861
      object_key_list = [object_path]
2862
    else:
Nicolas Delaby's avatar
Nicolas Delaby committed
2863 2864
      object_key_list = self._objects.keys()
    for key in object_key_list:
2865 2866 2867 2868 2869
      portal_id = key.split('/')[-1]
      type_information = types_tool.getTypeInfo(portal_id)
      if type_information is None:
        LOG("BusinessTemplate", WARNING,
            "Portal type %r not found while uninstalling" % (portal_id,))
2870 2871
        continue
      property_list = self._objects[key]
2872
      original_property_list = list(getattr(type_information,
2873
                                    self.class_property, ()))
Aurel's avatar
Aurel committed
2874 2875
      for id in property_list:
        if id in original_property_list:
2876
          original_property_list.remove(id)
2877
      setattr(type_information, self.class_property, tuple(original_property_list))
2878

2879

2880 2881
class PortalTypeHiddenContentTypeTemplateItem(PortalTypeAllowedContentTypeTemplateItem):

2882
  name = 'Hidden Content Type'
2883 2884
  xml_tag = 'hidden_content_type_list'
  class_property = 'hidden_content_type_list'
2885
  business_template_class_property = '_portal_type_hidden_content_type_item'
2886

2887

2888 2889
class PortalTypePropertySheetTemplateItem(PortalTypeAllowedContentTypeTemplateItem):

2890
  name = 'Property Sheet'
2891 2892
  xml_tag = 'property_sheet_list'
  class_property = 'property_sheet_list'
2893
  business_template_class_property = '_portal_type_property_sheet_item'
2894

2895

2896 2897
class PortalTypeBaseCategoryTemplateItem(PortalTypeAllowedContentTypeTemplateItem):

2898
  name = 'Base Category'
2899 2900
  xml_tag = 'base_category_list'
  class_property = 'base_category_list'
2901
  business_template_class_property = '_portal_type_base_category_item'
2902

2903

2904
class CatalogMethodTemplateItem(ObjectTemplateItem):
2905
  """Template Item for catalog methods.
2906

2907 2908 2909 2910 2911
    This template item stores catalog method and install them in the
    default catalog.
    The use Catalog makes for methods is saved as well and recreated on
    installation.
  """
2912

2913 2914
  def __init__(self, id_list, tool_id='portal_catalog', **kw):
    ObjectTemplateItem.__init__(self, id_list, tool_id=tool_id, **kw)
2915
    # a mapping to store properties of methods.
2916 2917 2918 2919 2920
    # the mapping contains an entry for each method, and this entry is
    # another mapping having the id of the catalog property as key and a
    # boolean value to say wether the method is part of this catalog
    # configuration property.
    self._method_properties = PersistentMapping()
2921

2922
    self._is_filtered_archive = PersistentMapping()
2923 2924
    for method in catalog_method_filter_list:
      setattr(self, method, PersistentMapping())
2925

2926 2927 2928 2929
  def _extractMethodProperties(self, catalog, method_id):
    """Extracts properties for a given method in the catalog.
    Returns a mapping of property name -> boolean """
    method_properties = PersistentMapping()
2930 2931 2932 2933 2934 2935 2936

    if catalog.meta_type == 'ERP5 Catalog':
      property_list = list(catalog.propertyMap())
    else:
      property_list = list(catalog._properties)

    for prop in property_list:
2937
      if prop.get('select_variable') == 'getCatalogMethodIds':
2938

2939 2940 2941 2942 2943 2944
        # In case the properties are defined via property sheet 'Catalog', the
        # object would have two IDs if it is of type 'selection' or
        # 'multiple_selection': 'id' and 'base_id', usage of base_id is preferred
        # while building objects as it maintains consistency between the old
        # catalog and new erp5 catalog
        prop_id = prop.get('base_id', prop['id'])
2945 2946 2947 2948 2949 2950 2951 2952

        # IMPORTANT: After migration of Catalog, the properties which were of
        # 'selection' type in ZSQL Catalog made more sense to be of 'string'
        # type as they only contained one value. Also, putting them in
        # 'selection' type, we would've ended up having to deal with accessors
        # which end with '_list' which would've made no sense. So, we decided
        # to move them to 'string' type
        if prop['type'] in ('string', 'selection') and \
2953
            getattr(catalog, prop_id, None) == method_id:
2954
          method_properties[prop_id] = 1
2955

2956
        elif prop['type'] == 'multiple selection' and \
2957
            method_id in getattr(catalog, prop_id, ()):
2958
          method_properties[prop_id] = 1
2959

2960 2961
    return method_properties

2962 2963
  def build(self, context, **kw):
    ObjectTemplateItem.build(self, context, **kw)
2964

2965
    catalog = _getCatalogValue(self)
2966
    if catalog is None:
2967
      LOG('BusinessTemplate build', 0, 'catalog not found')
2968
      return
2969

2970 2971 2972
    # upgrade old
    if not hasattr(self, '_method_properties'):
      self._method_properties = PersistentMapping()
2973

2974 2975
    for obj in self._objects.values():
      method_id = obj.id
2976
      # Check if the method is sub-object of Catalog
2977 2978 2979
      if method_id in catalog.objectIds():
        self._method_properties[method_id] = self._extractMethodProperties(
                                                            catalog, method_id)
2980

2981 2982 2983 2984 2985
  def generateXml(self, path):
    obj = self._objects[path]
    method_id = obj.id
    xml_data = '<catalog_method>'
    if self._method_properties.has_key(method_id):
2986
      for method_property, value in self._method_properties[method_id].items():
2987 2988 2989
        xml_data += '\n <item key="%s" type="int">' %(method_property,)
        xml_data += '\n  <value>%s</value>' %(value,)
        xml_data += '\n </item>'
2990

2991
    xml_data += '\n</catalog_method>\n'
Christophe Dumez's avatar
Christophe Dumez committed
2992
    return xml_data
2993

2994
  def preinstall(self, context, installed_item, **kw):
2995 2996 2997 2998 2999 3000 3001 3002 3003
    """Compute diffs from catalog methods metadata and objects.

    To support `template_keep_path_list`, we give priority to
    ObjectTemplateItem.preinstall which may return 'Removed but should be kept'
    """
    # from catalog methods properies (from generateXML)
    modified_object_dict = BaseTemplateItem.preinstall(self, context, installed_item, **kw)
    # diffs from actual objects
    modified_object_dict.update(ObjectTemplateItem.preinstall(self, context, installed_item, **kw))
3004 3005 3006
    return modified_object_dict

  def export(self, context, bta, **kw):
3007
    ObjectTemplateItem.export(self, context, bta, catalog_method_template_item=1)
3008

3009 3010
  def install(self, context, trashbin, **kw):
    ObjectTemplateItem.install(self, context, trashbin, **kw)
3011
    catalog = _getCatalogValue(self)
3012 3013 3014 3015 3016 3017 3018
    if catalog is None:
      LOG('BusinessTemplate', 0, 'no SQL catalog was available')
      return
    # Make copies of attributes of the default catalog of portal_catalog.
    sql_catalog_object_list = list(catalog.sql_catalog_object_list)
    sql_uncatalog_object = list(catalog.sql_uncatalog_object)
    sql_clear_catalog = list(catalog.sql_clear_catalog)
3019

3020 3021 3022 3023
    update_dict = kw.get('object_to_update')
    force = kw.get('force')
    values = []

3024 3025 3026 3027 3028 3029 3030 3031 3032 3033 3034 3035 3036 3037 3038 3039 3040 3041 3042 3043 3044 3045 3046 3047 3048
    # When the default catalog is of 'ERP5 Catalog' meta_type, its better to ..
    # convert all the CatalogMethodTemplateItems in the current BT to the
    # allowed types for ERP5 Catalog, i.e, to ERP5 SQLMethod and ERP5 Python Script
    # and update the self._objects dict accordingly
    if catalog.meta_type == 'ERP5 Catalog':
      import erp5
      from Products.ERP5.Extensions.CheckPortalTypes import changeObjectClass

      # We need the dynamic portal_type classes for changing object classes
      sql_class = getattr(erp5.portal_type, 'SQL Method')
      script_class = getattr(erp5.portal_type, 'Python Script')

      portal = self.getPortalObject()
      # Will be modifying dict, so better to use .items()
      # XXX: In python3 it should be .copy.items().
      for path, obj in self._objects.items():
        method = self.unrestrictedResolveValue(portal, path)
        method_id = path.split('/')[-1]
        if method.meta_type == 'Z SQL Method':
          method = changeObjectClass(catalog, method_id, sql_class)
        if method.meta_type == 'Script (Python)':
          method = changeObjectClass(catalog, method_id, script_class)
        new_obj  = method.aq_base
        self._objects[path] = new_obj

3049
    if force: # get all objects
3050
      values = self._objects.values()
3051
    else: # get only selected object
3052
      for key, value in self._objects.iteritems():
3053 3054 3055 3056
        if update_dict.has_key(key) or force:
          if not force:
            action = update_dict[key]
            if action == 'nothing':
3057
              continue
3058
          values.append(value)
3059

3060 3061
    for obj in values:
      method_id = obj.id
3062

3063 3064 3065 3066 3067 3068
      # Restore catalog properties for methods
      if hasattr(self, '_method_properties'):
        for key in self._method_properties.get(method_id, {}).keys():
          old_value = getattr(catalog, key, None)
          if isinstance(old_value, str):
            setattr(catalog, key, method_id)
Kazuhiko Shiozaki's avatar
Kazuhiko Shiozaki committed
3069
          elif isinstance(old_value, (list, tuple)):
3070 3071 3072 3073
            if method_id not in old_value:
              new_value = list(old_value) + [method_id]
              new_value.sort()
              setattr(catalog, key, tuple(new_value))
3074

3075
      method = catalog._getOb(method_id)
3076 3077 3078 3079 3080 3081 3082 3083 3084 3085

      # Restore filter:
      #
      # Here we have to handle two cases:
      # 1. CatalogMethodTemplateItem with _is_filtered_archive (possible for
      #    methods who still have filter attributes in `_catalog_keys.xml` file).
      # 2. CatalogMethodTemplateItem where methods have filter properties
      #    directly on xml file of method rather than in `_catalog_keys.xml`.
      #    This would be case for BT which have been exported after catalog
      #    migration.
3086
      if self._is_filtered_archive.get(method_id, 0):
3087
        expression = self._filter_expression_archive[method_id]
3088 3089 3090 3091 3092
        method.setFiltered(1)
        method.setExpression(expression)
        method.setExpressionCacheKey(
          self._filter_expression_cache_key_archive.get(method_id, ()))
        method.setTypeList(self._filter_type_archive.get(method_id, ()))
3093 3094 3095 3096
      # If there is no filter archive and the the meta_type of the catalog
      # method isn't one of the ERP5-ified Catalog Method Document, then
      # set the filter to 0
      elif method.meta_type not in ('ERP5 SQL Method', 'ERP5 Python Script'):
3097
        method.setFiltered(0)
3098

3099 3100 3101 3102 3103 3104 3105 3106 3107 3108 3109 3110 3111 3112 3113 3114 3115 3116 3117 3118 3119 3120 3121 3122 3123 3124
      # backward compatibility
      if hasattr(self, '_is_catalog_list_method_archive'):
        LOG("BusinessTemplate.CatalogMethodTemplateItem", 0,
            "installing old style catalog method configuration")
        is_catalog_list_method = int(
                  self._is_catalog_list_method_archive[method_id])
        is_uncatalog_method = int(
                  self._is_uncatalog_method_archive[method_id])
        is_clear_method = int(
                  self._is_clear_method_archive[method_id])

        if is_catalog_list_method and method_id not in sql_catalog_object_list:
          sql_catalog_object_list.append(method_id)
        elif not is_catalog_list_method and\
                        method_id in sql_catalog_object_list:
          sql_catalog_object_list.remove(method_id)

        if is_uncatalog_method and method_id not in sql_uncatalog_object:
          sql_uncatalog_object.append(method_id)
        elif not is_uncatalog_method and method_id in sql_uncatalog_object:
          sql_uncatalog_object.remove(method_id)

        if is_clear_method and method_id not in sql_clear_catalog:
          sql_clear_catalog.append(method_id)
        elif not is_clear_method and method_id in sql_clear_catalog:
          sql_clear_catalog.remove(method_id)
3125

3126 3127 3128 3129 3130 3131
        sql_catalog_object_list.sort()
        catalog.sql_catalog_object_list = tuple(sql_catalog_object_list)
        sql_uncatalog_object.sort()
        catalog.sql_uncatalog_object = tuple(sql_uncatalog_object)
        sql_clear_catalog.sort()
        catalog.sql_clear_catalog = tuple(sql_clear_catalog)
3132

3133
  def uninstall(self, context, **kw):
3134
    catalog = _getCatalogValue(self)
3135 3136 3137
    if catalog is None:
      LOG('BusinessTemplate', 0, 'no SQL catalog was available')
      return
3138

3139 3140 3141 3142
    values = []
    object_path = kw.get('object_path', None)
    # get required values
    if object_path is None:
3143
      values = self._objects.values()
3144
    else:
3145
      try:
3146
        value = self._objects[object_path]
3147 3148
      except KeyError:
        value = None
3149 3150
      if value is not None:
        values.append(value)
3151
    for obj in values:
3152
      method_id = obj.id
3153 3154 3155 3156 3157
      if catalog.meta_type == 'ERP5 Catalog':
        property_list = list(catalog.propertyMap())
      else:
        property_list = list(catalog._properties)

3158
      # remove method references in portal_catalog
3159
      for catalog_prop in property_list:
3160 3161
        if catalog_prop.get('select_variable') == 'getCatalogMethodIds'\
            and catalog_prop['type'] == 'multiple selection':
3162 3163 3164 3165 3166 3167 3168
          # In case the properties are defined via property sheet 'Catalog', the
          # object would have two IDs if it is of type 'selection' or
          # 'multiple_selection': 'id' and 'base_id', usage of base_id is preferred
          # while building objects as it maintains consistency between the old
          # catalog and new erp5 catalog
          catalog_prop_id = catalog_prop.get('base_id', catalog_prop['id'])
          old_value = getattr(catalog, catalog_prop_id, ())
3169 3170 3171
          if method_id in old_value:
            new_value = list(old_value)
            new_value.remove(method_id)
3172 3173 3174
            # Better to set the attribute value as tuple as it would be consistent
            # with both SQL Catalog and ERP5 Catalog.
            setattr(catalog, catalog_prop_id, tuple(new_value))
3175

3176 3177 3178 3179 3180
      filter_dict = catalog._getFilterDict()
      try:
        del filter_dict[method_id]
      except KeyError:
        pass
3181

3182
    # uninstall objects
3183
    ObjectTemplateItem.uninstall(self, context, **kw)
3184

3185
  def _importFile(self, file_name, file):
3186
    if file_name.endswith('.catalog_keys.xml'):
3187
      # recreate data mapping specific to catalog method
3188 3189
      name = os.path.basename(file_name)
      id = name.split('.', 1)[0]
3190
      xml = parse(file)
3191
      method_list = xml.findall('item')
3192
      for method in method_list:
3193 3194 3195
        key = method.get('key')
        key_type = method.get('type')
        value_node = method.find('value')
3196
        if key_type == "str":
3197
          value = value_node.text or ''
3198
        elif key_type == "int":
3199
          value = int(value_node.text)
3200
        elif key_type == "tuple":
3201
          value = tuple([value_node.text for value_node in method.findall('value')])
3202
        else:
3203
          LOG('BusinessTemplate import CatalogMethod, type unknown', 0, key_type)
3204
          continue
3205
        if key in catalog_method_list or key in catalog_method_filter_list:
3206
          getattr(self, key)[id] = value
3207 3208 3209
        else:
          # new style key
          self._method_properties.setdefault(id, PersistentMapping())[key] = 1
3210
    else:
3211 3212
      ObjectTemplateItem._importFile(self, file_name, file, catalog_method_template_item=1)

3213

3214
class ActionTemplateItem(ObjectTemplateItem):
3215 3216

  def __init__(self, id_list, **kw):
3217
    # XXX It's look like ObjectTemplateItem __init__
3218 3219 3220 3221 3222 3223
    BaseTemplateItem.__init__(self, id_list, **kw)
    id_list = self._archive.keys()
    self._archive.clear()
    for id in id_list:
      self._archive["%s/%s" % ('portal_types', id)] = None

3224 3225
  def _splitPath(self, path):
    """
Leonardo Rochael Almeida's avatar
typo  
Leonardo Rochael Almeida committed
3226
      Split path tries to split a complex path such as:
3227 3228 3229 3230 3231 3232 3233 3234 3235 3236 3237 3238 3239 3240 3241 3242 3243 3244

      "foo/bar[id=zoo]"

      into

      "foo/bar", "id", "zoo"

      This is used mostly for generic objects
    """
    # Add error checking here
    if path.find('[') >= 0 and path.find(']') > path.find('=') and path.find('=') > path.find('['):
      relative_url = path[0:path.find('[')]
      id_block = path[path.find('[')+1:path.find(']')]
      key = id_block.split('=')[0]
      value = id_block.split('=')[1]
      return relative_url, key, value
    return path, None, None

3245 3246 3247 3248 3249 3250 3251
  def _getPortalTypeActionCopy(self, obj, value):
    id_id = 'reference'
    for action in obj.getActionInformationList():
      if getattr(action, id_id, None) == value:
        return obj._exportOldAction(action)

  def _getPortalToolActionCopy(self, obj, context, value):
3252 3253 3254 3255 3256 3257
    from Products.CMFCore.interfaces import IActionProvider
    if not IActionProvider.providedBy(obj):
      # look for the action in portal_actions, instead of the original object
      LOG('Products.ERP5.Document.BusinessTemplate', WARNING,
          'Redirected action export',
          'Attempted to retrieve action %r from %r which is no longer an '
Jérome Perrin's avatar
typo  
Jérome Perrin committed
3258
          'IActionProvider. Retrieving action from portal_actions instead' %
3259 3260
          (value, obj.getId()))
      obj = context.getPortalObject().portal_actions
3261 3262 3263 3264 3265 3266 3267 3268 3269 3270
    id_id = 'id'
    for action in obj.listActions():
      if getattr(action, id_id, None) == value:
        return action._getCopy(context)

  def _getActionCopy(self, obj, context, value):
    """
    Gets action copy from action provider given the action id or reference
    """
    # Several tools still use CMF actions
3271
    if interfaces.ITypeProvider.providedBy(obj.getParentValue()):
3272 3273 3274 3275
      return self._getPortalTypeActionCopy(obj, value)
    else:
      return self._getPortalToolActionCopy(obj, context, value)

3276 3277 3278 3279
  def build(self, context, **kw):
    BaseTemplateItem.build(self, context, **kw)
    p = context.getPortalObject()
    for id in self._archive.keys():
3280 3281 3282
      url, value = id.split(' | ')
      url = posixpath.split(url)
      obj = p.unrestrictedTraverse(url)
3283
      # normalize url
3284 3285 3286 3287 3288
      url = p.portal_url.getRelativeContentPath(obj)
      if len(url) == 1:
        # global actions are stored under 'portal_types', mostly for
        # compatibility
        url = 'portal_types', url[0]
3289 3290
      action = self._getActionCopy(obj, context, value)
      if action is None:
3291 3292 3293
        if self.is_bt_for_diff:
          continue
        raise NotFound('Action %r not found' % id)
3294
      key = posixpath.join(url[-2], url[-1], value)
3295 3296 3297 3298 3299
      self._objects[key] = self.removeProperties(
        action, 1,
        self.isKeepWorkflowObject(key),
        self.isKeepWorkflowObjectLastHistoryOnly(key))

3300
      self._objects[key].wl_clearLocks()
Aurel's avatar
Aurel committed
3301

3302 3303 3304
  def install(self, context, trashbin, **kw):
    update_dict = kw.get('object_to_update')
    force = kw.get('force')
3305 3306 3307 3308 3309 3310 3311
    portal_type_dict = {}
    p = context.getPortalObject()
    for id in self._objects.keys():
      if update_dict.has_key(id) or force:
        if not force:
          action = update_dict[id]
          if action == 'nothing':
3312
            continue
3313 3314
        obj = self._objects[id]
        path, id = id.rsplit('/', 1)
3315
        container = p.unrestrictedTraverse(path)
3316 3317 3318 3319 3320 3321 3322 3323 3324 3325 3326 3327 3328 3329 3330 3331

        if interfaces.ITypeProvider.providedBy(aq_parent(aq_inner(container))):
          # XXX future BT should use 'reference' instead of 'id'
          reference = getattr(obj, 'reference', None) or obj.id
          portal_type_dict.setdefault(path, {})[reference] = obj
          continue

        # Following code is for actions outside Types Tool.
        # It will be removed when they are also converted to ERP5 actions.
        from Products.CMFCore.interfaces import IActionProvider
        if not IActionProvider.providedBy(container):
          # some tools stopped being ActionProviders in CMF 2.x. Drop the
          # action into portal_actions.
          LOG('Products.ERP5.Document.BusinessTemplate', WARNING,
              'Redirected action import',
              'Attempted to store action %r in %r which is no longer an '
Jérome Perrin's avatar
typo  
Jérome Perrin committed
3332
              'IActionProvider. Storing action on portal_actions instead' %
3333 3334 3335 3336 3337 3338 3339 3340 3341 3342 3343 3344 3345 3346 3347 3348 3349 3350 3351 3352 3353 3354 3355 3356 3357 3358 3359 3360 3361 3362 3363 3364 3365 3366 3367 3368 3369 3370 3371 3372 3373
              (id, path))
          container = p.portal_actions
        obj, action = container, obj
        action_list = obj.listActions()
        for index in range(len(action_list)):
          if action_list[index].id == id:
            # remove previous action
            obj.deleteActions(selections=(index,))
        action_text = action.action
        if isinstance(action_text, Expression):
          action_text = action_text.text
        obj.addAction(
                      id = action.id
                    , name = action.title
                    , action = action_text
                    , condition = action.getCondition()
                    , permission = action.permissions
                    , category = action.category
                    , visible = action.visible
                    , icon = getattr(action, 'icon', None)\
                              and action.icon.text or ''
                    , priority = action.priority
                    , description = action.description
                  )
        # sort action based on the priority define on it
        # XXX suppose that priority are properly on actions
        new_priority = action.priority
        action_list = obj.listActions()
        move_down_list = []
        for index in range(len(action_list)):
          action = action_list[index]
          if action.priority > new_priority:
            move_down_list.append(str(index))
        obj.moveDownActions(selections=tuple(move_down_list))
    for path, action_dict in portal_type_dict.iteritems():
      container = p.unrestrictedTraverse(path)
      container.manage_delObjects([obj.id
        for obj in container.getActionInformationList()
        if obj.getReference() in action_dict])
      for name, obj in action_dict.iteritems():
        container._importOldAction(obj).aq_base
3374

3375 3376
  def uninstall(self, context, **kw):
    p = context.getPortalObject()
3377 3378
    object_path = kw.get("object_path", None)
    if object_path is not None:
3379 3380 3381 3382 3383 3384 3385 3386
      if '/' in object_path:
        # here object_path is the path of the actions, something like
        # portal_type/Person/view
        ti, action_id = object_path.rsplit('/', 1)
        keys = ['%s | %s' % (ti, action_id)]
      else:
        # compatibility ?
        keys = [object_path]
3387
    else:
3388
      keys = self._archive.keys()
3389
    for id in keys:
3390 3391
      if  '|' in id:
        relative_url, value = id.split(' | ')
3392 3393 3394
        obj = p.unrestrictedTraverse(relative_url, None)
        # Several tools still use CMF actions
        if obj is not None:
3395
          is_new_action = interfaces.ITypeProvider.providedBy(obj.getParentValue())
3396
          key = is_new_action and 'reference' or 'id'
3397
      else:
3398
        relative_url, key, value = self._splitPath(id)
3399
        obj = p.unrestrictedTraverse(relative_url, None)
3400 3401 3402
      if obj is not None:
        action_list = obj.listActions()
        for index in range(len(action_list)):
3403
          if getattr(action_list[index], key, None) == value:
3404 3405
            obj.deleteActions(selections=(index,))
            break
3406 3407
      LOG('BusinessTemplate', WARNING,
          'Unable to uninstall action at %s, ignoring' % relative_url )
3408 3409
    BaseTemplateItem.uninstall(self, context, **kw)

3410 3411 3412
class PortalTypeRolesTemplateItem(BaseTemplateItem):

  def __init__(self, id_list, **kw):
3413
    id_list = ['portal_type_roles/%s' % id for id in id_list if id != '']
3414 3415 3416 3417 3418 3419 3420
    BaseTemplateItem.__init__(self, id_list, **kw)

  def build(self, context, **kw):
    p = context.getPortalObject()
    for relative_url in self._archive.keys():
      obj = p.unrestrictedTraverse("portal_types/%s" %
          relative_url.split('/', 1)[1])
3421 3422
      # normalize url
      relative_url = '%s/%s' % (obj.getPhysicalPath()[-2:])
3423
      self._objects[relative_url] = type_role_list = []
3424
      for role in obj.getRoleInformationList():
3425
        type_role_dict = {}
3426 3427 3428 3429 3430 3431 3432 3433 3434
        for k, v in aq_base(role).__getstate__().iteritems():
          if k == 'condition':
            if not v:
              continue
            v = v.text
          elif k in ('role_base_category', 'role_category'):
            k = k[5:]
          elif k == 'role_name':
            k, v = 'id', '; '.join(v)
3435
          elif k not in ('title', 'description', 'categories'):
3436 3437 3438
            k = {'id': 'object_id', # for stable sort
                 'role_base_category': 'base_category',
                 'role_base_category_script_id': 'base_category_script',
3439 3440
                 'role_category': 'category',
                 'local_roles_group_id': 'local_roles_group_id'}.get(k)
3441 3442 3443
            if not k:
              continue
          type_role_dict[k] = v
3444 3445
        if 'id' in type_role_dict:
          type_role_list.append(type_role_dict)
3446
      type_role_list.sort(key=lambda x: (x.get('title'), x['object_id'],))
3447

Christophe Dumez's avatar
Christophe Dumez committed
3448
  # Function to generate XML Code Manually
3449 3450 3451 3452
  def generateXml(self, path=None):
    type_role_list = self._objects[path]
    xml_data = '<type_roles>'
    for role in type_role_list:
3453
      xml_data += "\n  <role id='%s'>" % role['id']
3454
      # uniq
3455
      for property in ('title', 'description', 'condition',
3456 3457 3458
          'base_category_script'):
        prop_value = role.get(property)
        if prop_value:
Nicolas Delaby's avatar
Nicolas Delaby committed
3459
          if isinstance(prop_value, str):
3460
            prop_value = escape(prop_value.decode('utf-8'))
3461
          xml_data += "\n   <property id='%s'>%s</property>" % \
3462 3463
              (property, prop_value)
      # multi
3464
      for property in ('categories', 'category', 'base_category'):
3465
        for prop_value in role.get(property, []):
Nicolas Delaby's avatar
Nicolas Delaby committed
3466
          if isinstance(prop_value, str):
3467
            prop_value = escape(prop_value.decode('utf-8'))
3468
          xml_data += "\n   <multi_property "\
3469
          "id='%s'>%s</multi_property>" % (property, prop_value)
3470 3471
      xml_data += "\n  </role>"
    xml_data += '\n</type_roles>'
3472
    return xml_data
3473

3474 3475 3476
  def export(self, context, bta, **kw):
    if len(self._objects.keys()) == 0:
      return
3477
    path = self.__class__.__name__
3478 3479
    for key in self._objects.keys():
      xml_data = self.generateXml(key)
3480 3481
      if isinstance(xml_data, unicode):
        xml_data = xml_data.encode('utf-8')
3482
      name = key.split('/', 1)[1]
3483
      bta.addObject(xml_data, name=name, path=path)
3484 3485

  def _importFile(self, file_name, file):
3486 3487 3488
    if not file_name.endswith('.xml'):
      LOG('Business Template', 0, 'Skipping file "%s"' % (file_name, ))
      return
3489 3490
    type_roles_list = []
    xml = parse(file)
3491
    xml_type_roles_list = xml.findall('role')
3492
    for role in xml_type_roles_list:
3493 3494 3495 3496
      id = role.get('id')
      if isinstance(id, unicode):
        id = id.encode('utf_8', 'backslashreplace')
      type_role_property_dict = {'id': id}
3497
      # uniq
3498 3499 3500 3501 3502 3503 3504 3505
      property_list = role.findall('property')
      for property_node in property_list:
        property_id = property_node.get('id')
        if property_node.text:
          value = property_node.text
          if isinstance(value, unicode):
            value = value.encode('utf_8', 'backslashreplace')
          type_role_property_dict[property_id] = value
3506
      # multi
3507 3508 3509 3510 3511 3512 3513 3514
      multi_property_list = role.findall('multi_property')
      for property_node in multi_property_list:
        property_id = property_node.get('id')
        if property_node.text:
          value = property_node.text
          if isinstance(value, unicode):
            value = value.encode('utf_8', 'backslashreplace')
          type_role_property_dict.setdefault(property_id, []).append(value)
3515
      type_roles_list.append(type_role_property_dict)
3516
    self._objects['portal_type_roles/%s' % (file_name[:-4],)] = type_roles_list
3517 3518 3519 3520 3521 3522 3523 3524 3525 3526 3527

  def install(self, context, trashbin, **kw):
    update_dict = kw.get('object_to_update')
    force = kw.get('force')
    p = context.getPortalObject()
    for roles_path in self._objects.keys():
      if update_dict.has_key(roles_path) or force:
        if not force:
          action = update_dict[roles_path]
          if action == 'nothing':
            continue
3528 3529 3530
        path = 'portal_types/%s' % roles_path.split('/', 1)[1]
        obj = p.unrestrictedTraverse(path, None)
        if obj is not None:
3531
          # reset roles before applying
3532
          obj.manage_delObjects([x.id for x in obj.getRoleInformationList()])
3533
          type_roles_list = self._objects[roles_path] or []
3534 3535
          for role_property_dict in type_roles_list:
            obj._importRole(role_property_dict)
3536
        else:
3537
          raise AttributeError("Path %r not found while "
3538
                               "installing roles" % (path, ))
3539 3540 3541

  def uninstall(self, context, **kw):
    p = context.getPortalObject()
Aurel's avatar
Aurel committed
3542 3543 3544 3545 3546 3547
    object_path = kw.get('object_path', None)
    if object_path is not None:
      keys = [object_path]
    else:
      keys = self._objects.keys()
    for roles_path in keys:
3548
      path = 'portal_types/%s' % roles_path.split('/', 1)[1]
3549 3550
      try:
        obj = p.unrestrictedTraverse(path)
3551
        obj.manage_delObjects([x.id for x in obj.getRoleInformationList()])
3552 3553
      except (NotFound, KeyError):
        pass
3554

3555 3556 3557 3558 3559 3560
class SitePropertyTemplateItem(BaseTemplateItem):

  def build(self, context, **kw):
    BaseTemplateItem.build(self, context, **kw)
    p = context.getPortalObject()
    for id in self._archive.keys():
3561 3562
      for property in p.propertyMap():
        if property['id'] == id:
3563
          obj = p.getProperty(id)
3564
          prop_type = property['type']
3565 3566
          break
      else:
3567
        obj = None
3568
      if obj is None and not self.is_bt_for_diff:
3569
        raise NotFound, 'the property %s is not found' % id
3570
      self._objects[id] = (prop_type, obj)
Aurel's avatar
Aurel committed
3571

3572 3573
  def _importFile(self, file_name, file):
    # recreate list of site property from xml file
3574
    if not file_name.endswith('.xml'):
3575
      LOG('Business Template', 0, 'Skipping file "%s"' % (file_name, ))
3576
      return
3577
    xml = parse(file)
3578 3579 3580 3581 3582 3583 3584 3585 3586
    for property_node in xml.getroot().findall('property'):
      property_id = property_node.find('id').text
      prop_type = property_node.find('type').text
      value_node = property_node.find('value')
      if prop_type in ('lines', 'tokens'):
        value = [item.text for item in value_node.findall('item')]
      else:
        value = value_node.text
      self._objects[property_id] = (prop_type, value)
3587

3588 3589 3590
  def install(self, context, trashbin, **kw):
    update_dict = kw.get('object_to_update')
    force = kw.get('force')
3591 3592 3593 3594 3595 3596 3597 3598 3599 3600 3601 3602
    p = context.getPortalObject()
    for path in self._objects.keys():
      if update_dict.has_key(path) or force:
        if not force:
          action = update_dict[path]
          if action == 'nothing':
            continue
        dir, id = posixpath.split(path)
        prop_type, property = self._objects[path]
        if p.hasProperty(id):
          if p.getPropertyType(id) != prop_type:
            p._delProperty(id)
3603
            p._setProperty(id, property, type=prop_type)
3604 3605 3606 3607
          else:
            p._updateProperty(id, property)
        else:
          p._setProperty(id, property, type=prop_type)
3608 3609 3610

  def uninstall(self, context, **kw):
    p = context.getPortalObject()
3611 3612 3613 3614 3615 3616
    object_path = kw.get('object_path', None)
    if object_path is not None:
      keys = [object_path]
    else:
      keys = self._archive.keys()
    for id in keys:
3617 3618 3619 3620
      if p.hasProperty(id):
        p._delProperty(id)
    BaseTemplateItem.uninstall(self, context, **kw)

Christophe Dumez's avatar
Christophe Dumez committed
3621
  # Function to generate XML Code Manually
3622
  def generateXml(self, path=None):
3623
    xml_data = ''
3624
    prop_type, obj = self._objects[path]
3625
    xml_data += '\n <property>'
3626 3627
    xml_data += '\n  <id>%s</id>' % escape(str(path))
    xml_data += '\n  <type>%s</type>' % escape(str(prop_type))
3628
    if prop_type in ('lines', 'tokens'):
3629
      xml_data += '\n  <value>'
3630
      for item in obj:
3631
        if item != '':
3632
          xml_data += '\n   <item>%s</item>' % escape(str(item))
3633
      xml_data += '\n  </value>'
3634
    else:
3635
      xml_data += '\n  <value>%s</value>' % escape(str(obj))
3636
    xml_data += '\n </property>'
3637 3638
    return xml_data

Aurel's avatar
Aurel committed
3639 3640 3641
  def export(self, context, bta, **kw):
    if len(self._objects.keys()) == 0:
      return
3642 3643 3644 3645
    xml_data = '<site_property>'
    keys = self._objects.keys()
    keys.sort()
    for path in keys:
3646
      xml_data += self.generateXml(path)
3647
    xml_data += '\n</site_property>'
3648
    bta.addObject(xml_data, name='properties', path=self.__class__.__name__)
3649

3650 3651 3652 3653 3654
class ModuleTemplateItem(BaseTemplateItem):

  def build(self, context, **kw):
    BaseTemplateItem.build(self, context, **kw)
    p = context.getPortalObject()
3655 3656 3657 3658 3659 3660 3661
    for module_id in self._archive.keys():
      module = p.unrestrictedTraverse(module_id)
      mapping = {}
      mapping['id'] = module.getId()
      mapping['title'] = module.getTitle()
      mapping['portal_type'] = module.getPortalType()
      mapping['permission_list'] = module.showPermissions()
3662
      mapping['category_list'] = module.getCategoryList()
3663
      self._objects[module_id] = mapping
Jean-Paul Smets's avatar
Jean-Paul Smets committed
3664

Christophe Dumez's avatar
Christophe Dumez committed
3665
  # Function to generate XML Code Manually
3666
  def generateXml(self, path=None):
3667
    mapping = self._objects[path]
3668
    xml_data = ['<module>']
3669
    keys = mapping.keys()
3670 3671
    for key in sorted(keys):
      if key == 'permission_list':
3672
        # separe permission dict into xml
3673
        xml_data.append(' <%s>' % (key, ))
3674
        permission_list = mapping[key]
3675
        for perm in permission_list:
3676
          # the type of the permission defined if we use acquired or not
3677
          if isinstance(perm[1], list):
3678 3679 3680
            ptype = "list"
          else:
            ptype = "tuple"
Aurel's avatar
Aurel committed
3681
          role_list = list(perm[1])
3682 3683 3684
          # Skip if permission is not configured (i.e. no role at all
          # with acquire permission, or Manager only without acquire
          # permission).
3685
          if (len(role_list) == 0 and ptype == 'list') or \
3686 3687
                 (role_list == ['Manager'] and ptype == 'tuple'):
            continue
3688
          role_list.sort()
3689 3690
          xml_data.append("  <permission type='%s'>" % (ptype, ))
          xml_data.append('   <name>%s</name>' % (perm[0], ))
3691
          for role in role_list:
3692 3693 3694
            xml_data.append('   <role>%s</role>' % (role, ))
          xml_data.append('  </permission>')
        xml_data.append(' </%s>' % (key, ))
3695 3696 3697 3698 3699 3700 3701 3702
      elif key == 'category_list':
        category_list = mapping[key]
        if not category_list:
          continue
        xml_data.append(' <%s>' % (key, ))
        for category in category_list:
          xml_data.append('  <category>%s</category>' % (category, ))
        xml_data.append(' </%s>' % (key, ))
3703
      else:
3704
        xml_data.append(' <%s>%s</%s>' % (key, mapping[key], key))
3705 3706
    xml_data.append('</module>')
    return '\n'.join(xml_data)
3707

3708
  def export(self, context, bta, **kw):
3709
    if len(self._objects) == 0:
3710
      return
3711
    path = self.__class__.__name__
3712 3713
    keys = self._objects.keys()
    keys.sort()
3714
    for key in keys:
Jérome Perrin's avatar
typo  
Jérome Perrin committed
3715
      # export modules one by one
3716
      xml_data = self.generateXml(path=key)
3717
      bta.addObject(xml_data, name=key, path=path)
3718

3719
  def install(self, context, trashbin, **kw):
3720
    portal = context.getPortalObject()
3721 3722
    update_dict = kw.get('object_to_update')
    force = kw.get('force')
3723 3724
    valid_permissions = dict.fromkeys([x[0] for x in
                                       context.ac_inherited_permissions(all=1)])
3725
    for path, mapping in self._objects.iteritems():
3726
      if update_dict.has_key(path) or force:
3727
        if not force:
3728
          action = update_dict[path]
3729 3730
          if action == 'nothing':
            continue
3731 3732
        path, module_id = posixpath.split(path)
        portal_type = str(mapping['portal_type'])
3733
        module = portal._getOb(module_id, None)
3734 3735
        if module is not None:
          module.portal_type = portal_type
3736
        else:
3737
          module = portal.newContent(id=module_id, portal_type=portal_type)
3738
        module.setTitle(str(mapping['title']))
3739 3740
        permission_dict = dict(mapping['permission_list'])
        for name in valid_permissions.iterkeys():
3741
          # By default, Manager only without acquire permission
3742 3743
          role_list = permission_dict.get(name, ('Manager',))
          acquire = isinstance(role_list, list)
3744
          module.manage_permission(name, roles=role_list, acquire=acquire)
3745 3746
        if 'category_list' in mapping:
          module.setCategoryList(mapping['category_list'])
3747 3748

  def _importFile(self, file_name, file):
3749 3750 3751
    if not file_name.endswith('.xml'):
      LOG('Business Template', 0, 'Skipping file "%s"' % (file_name, ))
      return
3752
    mapping = {}
3753
    xml = parse(file)
3754
    for key in ('portal_type', 'id', 'title', 'permission_list'):
3755
      key_node = xml.find(key)
3756
      if key == 'permission_list':
3757 3758 3759 3760 3761 3762 3763
        permission_list = []
        for permission in key_node:
          permission_type = permission.get('type', None)
          name = permission.find('name').text
          role_list = [role.text for role in permission.findall('role')]
          if permission_type in ('list', None):
            perm_tuple = (name, list(role_list))
3764
          else:
3765 3766 3767
            perm_tuple = (name, tuple(role_list))
          permission_list.append(perm_tuple)
        mapping[key] = permission_list
3768
      else:
3769
        mapping[key] = key_node.text or ''
3770 3771

    category_list = []
3772 3773 3774 3775
    category_list_node = xml.find('category_list')
    if category_list_node is not None:
      category_list.extend(node.text for node\
                            in category_list_node.findall('category'))
3776 3777
    mapping['category_list'] = category_list

3778
    self._objects[file_name[:-4]] = mapping
3779

3780
  def uninstall(self, context, **kw):
Aurel's avatar
Aurel committed
3781 3782 3783
    trash = kw.get('trash', 0)
    if trash:
      return
3784 3785 3786 3787 3788 3789
    object_path = kw.get('object_path', None)
    trashbin = kw.get('trashbin', None)
    if object_path is None:
      keys = self._archive.keys()
    else:
      keys = [object_path]
3790 3791
    p = context.getPortalObject()
    id_list = p.objectIds()
3792 3793
    for key in keys:
      if key in id_list:
3794
        try:
3795
          if trash and trashbin is not None:
3796
            container_path = key.split('/')
3797
            self.portal_trash.backupObject(trashbin, container_path,
3798
                                           key, save=1, keep_subobjects=1)
3799
          p.manage_delObjects([key])
3800
        except NotFound:
3801
          pass
3802 3803
    BaseTemplateItem.uninstall(self, context, **kw)

3804 3805 3806
  def trash(self, context, new_item, **kw):
    # Do not remove any module for safety.
    pass
3807

3808
# XXX-arnau: when everything has been migrated to Components, this class
3809
# should be removed and only _ZodbComponentTemplateItem should remain
3810
class FilesystemDocumentTemplateItem(BaseTemplateItem):
3811 3812 3813 3814
  local_file_reader_name = staticmethod(readLocalDocument)
  local_file_writer_name = staticmethod(writeLocalDocument)
  local_file_importer_name = staticmethod(importLocalDocument)
  local_file_remover_name = staticmethod(removeLocalDocument)
3815

3816 3817 3818 3819 3820 3821 3822 3823 3824 3825
  def _getKey(self, path):
    """Magical method to generate dynamic unique path"""
    return '/'.join((self.getTemplateTypeName(), path))

  def _getPath(self, key):
    """Magical method to extract real path"""
    if '/' in key:
      return key.split('/')[1]
    return key

3826 3827
  def build(self, context, **kw):
    BaseTemplateItem.build(self, context, **kw)
3828 3829
    for key in self._archive.iterkeys():
      self._objects[key] = self.local_file_reader_name(key)
Aurel's avatar
Aurel committed
3830

3831
  def preinstall(self, context, installed_item, **kw):
Yoshinori Okuji's avatar
Yoshinori Okuji committed
3832
    modified_object_list = {}
3833 3834 3835 3836 3837 3838 3839 3840 3841 3842 3843 3844 3845
    # fix key if necessary in installed bt for diff
    extra_prefix = self.__class__.__name__ + '/'
    for key in installed_item._objects.keys():
      if key.startswith(extra_prefix):
        new_key = key[len(extra_prefix):]
        installed_item._objects[new_key] = installed_item._objects[key]
        del installed_item._objects[key]
    for path in self._objects:
      if installed_item._objects.has_key(path):
        # compare object to see if there is changes
        new_obj_code = self._objects[path]
        old_obj_code = installed_item._objects[path]
        if new_obj_code != old_obj_code:
3846
          # Note: Magical way to have unique paths
3847
          modified_object_list[self._getKey(path)] = 'Modified', self.__class__.__name__[:-12]
3848 3849
      else: # new object
        # Note: Magical way to have unique paths
3850
        modified_object_list[self._getKey(path)] = 'New', self.__class__.__name__[:-12]
3851 3852 3853 3854 3855
        # get removed object
    old_keys = installed_item._objects.keys()
    for path in old_keys:
      if path not in self._objects:
        # Note: Magical way to have unique paths
3856
        modified_object_list[self._getKey(path)] = 'Removed', self.__class__.__name__[:-12]
3857 3858 3859 3860 3861
    return modified_object_list

  def install(self, context, trashbin, **kw):
    update_dict = kw.get('object_to_update')
    force = kw.get('force')
3862 3863 3864 3865 3866 3867 3868 3869 3870 3871 3872
    need_reset = isinstance(self, FilesystemDocumentTemplateItem)
    for key in self._objects.keys():
      # to achieve non data migration fresh installation parameters
      # differ from upgrade parameteres, so here the check have to be
      # care of both cases
      upgraded_key = self._getKey(key)
      if update_dict.has_key(key) or update_dict.has_key(upgraded_key) \
          or force:
        if not force:
          action = update_dict.get(key, update_dict.get(upgraded_key))
          if action == 'nothing':
3873
            continue
3874 3875 3876 3877 3878 3879 3880 3881 3882 3883 3884 3885 3886 3887 3888 3889
        text = self._objects[key]
        path, name = posixpath.split(key)
        try:
          self.local_file_writer_name(name, text, create=0)
        except IOError, error:
          LOG(self.__class__.__name__, WARNING,
              "Cannot install class %r on file system" % name)
          if error.errno:
            raise
          continue
        if self.local_file_importer_name is None:
          continue
        if need_reset:
          self._resetDynamicModules()
          need_reset = False
        self.local_file_importer_name(name)
3890

3891 3892 3893
  def remove(self, context, **kw):
    """Conversion of magically uniqued paths to real ones"""
    remove_object_dict = kw.get('remove_object_dict', {})
3894 3895 3896
    kw['remove_object_dict'] = {self._getPath(k): v
      for k, v in remove_object_dict.iteritems()
      if k.startswith(self.getTemplateTypeName()+'/')}
3897 3898
    BaseTemplateItem.remove(self, context, **kw)

3899
  def uninstall(self, context, **kw):
3900
    object_path = kw.get('object_path', None)
3901 3902 3903 3904
    if object_path is not None:
      object_keys = [object_path]
    else:
      object_keys = self._archive.keys()
3905
    if object_keys:
3906
      if isinstance(self, FilesystemDocumentTemplateItem):
3907 3908 3909
        self._resetDynamicModules()
      for key in object_keys:
        self.local_file_remover_name(key)
3910 3911
    BaseTemplateItem.uninstall(self, context, **kw)

Aurel's avatar
Aurel committed
3912 3913 3914
  def export(self, context, bta, **kw):
    if len(self._objects.keys()) == 0:
      return
3915 3916 3917 3918 3919 3920
    extra_prefix = self.__class__.__name__ + '/'
    for key in self._objects.keys():
      obj = self._objects[key]
      # BBB the prefix was put into each key in the previous implementation.
      if not key.startswith(extra_prefix):
        key = extra_prefix + key
3921
      bta.addObject(obj, name=key, ext='.py')
Aurel's avatar
Aurel committed
3922

3923
  def _importFile(self, file_name, file):
3924 3925 3926
    if not file_name.endswith('.py'):
      LOG('Business Template', 0, 'Skipping file "%s"' % (file_name, ))
      return
3927
    text = file.read()
3928
    self._objects[file_name[:-3]] = text
3929

3930
class FilesystemToZodbTemplateItem(FilesystemDocumentTemplateItem,
3931
                                   ObjectTemplateItem):
3932
  """
3933
  Abstract class to allow migration from FilesystemDocumentTemplateItem to
3934 3935
  ObjectTemplateItem, this is useful for migration from filesystem to ZODB for
  PropertySheets and Components
3936
  """
3937 3938
  # If set to False, then the migration from filesystem to ZODB will be
  # performed, meaningful only until the code is stable
3939
  _perform_migration = True
3940

3941 3942 3943 3944 3945 3946 3947 3948 3949
  _tool_id = None

  @staticmethod
  def _getZodbObjectId(id):
    return id

  def __init__(self, id_list, tool_id=None, context=None, **kw):
    if tool_id is None:
      tool_id = self._tool_id
3950

3951 3952 3953 3954 3955
    tool = None
    if context is not None and len(id_list):
      # XXX looking up a tool early in the install process might
      # cause issues. If it does, we'll have to consider moving this
      # to build()
3956
      tool = getattr(context.getPortalObject(), self._tool_id, None)
3957
    if tool is not None:
3958
      existing_set = set(tool.objectIds())
3959
      for i, id in enumerate(id_list):
3960 3961 3962
        if id in existing_set:
          # if the object is on ZODB, use it.
          id_list[i] = "%s/%s" % (self._tool_id, self._getZodbObjectId(id))
3963

3964 3965
    BaseTemplateItem.__init__(self, id_list, **kw)

3966
  def _is_already_migrated(self, object_key_list):
3967
    """
3968 3969 3970
    Objects have already been migrated if any keys within the given
    object_key_list (either '_objects.keys()' or '_archive.keys()') contains a
    key starting by 'tool_id/'
3971 3972
    """
    return len(object_key_list) != 0 and \
3973
        object_key_list[0].startswith(self._tool_id + '/')
3974 3975 3976

  def _filesystemCompatibilityWrapper(method_name, object_dict_name):
    """
3977
    Call ObjectTemplateItem method when the objects have already been
3978
    migrated, otherwise fallback on FilesystemDocumentTemplateItem method for
3979
    backward-compatibility
3980 3981 3982
    """
    def inner(self, *args, **kw):
      if self._is_already_migrated(getattr(self, object_dict_name).keys()):
3983
        result = getattr(ObjectTemplateItem, method_name)(self, *args, **kw)
3984
      else:
3985 3986
        result = getattr(FilesystemDocumentTemplateItem,
                         method_name)(self, *args, **kw)
3987

3988 3989 3990
      if method_name == 'preinstall':
        old_result = result.copy()
        for k, v in old_result.iteritems():
3991 3992 3993 3994 3995
          # Magical way to have unique path (without duplicating the prefix
          # neither) in case of not yet migrated property sheets available on
          # preinstall list
          if not (k.startswith(self._tool_id + '/') or
                  k.startswith(self.getTemplateTypeName())):
3996 3997 3998
            result.pop(k)
            k = self._getKey(k)
          result[k] = v
3999
      return result
4000 4001 4002 4003 4004 4005 4006
    return inner

  export = _filesystemCompatibilityWrapper('export', '_objects')
  build = _filesystemCompatibilityWrapper('build', '_archive')
  preinstall = _filesystemCompatibilityWrapper('preinstall', '_objects')

  def _importFile(self, file_name, *args, **kw):
4007 4008 4009 4010
    """
    Import file by calling the appropriate base class according to the file
    name extensions
    """
4011 4012 4013
    if file_name.endswith('.xml'):
      return ObjectTemplateItem._importFile(self, file_name, *args, **kw)
    else:
4014 4015
      return FilesystemDocumentTemplateItem._importFile(self, file_name,
                                                        *args, **kw)
4016 4017 4018 4019 4020 4021 4022 4023 4024 4025

  def uninstall(self, *args, **kw):
    # Only for uninstall, the path of objects can be given as a
    # parameter, otherwise it fallbacks on '_archive'
    object_path = kw.get('object_path', None)
    if object_path is not None:
      object_keys = [object_path]
    else:
      object_keys = self._archive.keys()

4026
    if self._is_already_migrated(object_keys):
4027
      ObjectTemplateItem.uninstall(self, *args, **kw)
4028
    else:
4029 4030
      FilesystemDocumentTemplateItem.uninstall(self, *args, **kw)

4031
  def remove(self, context, **kw):
4032
    """
4033
    Conversion of magically uniqued paths to real ones
4034
    """
4035
    remove_object_dict = kw.get('remove_object_dict', {})
4036 4037 4038
    kw['remove_object_dict'] = {self._getPath(k): v
      for k, v in remove_object_dict.iteritems()
      if k.startswith(self.getTemplateTypeName()+'/')}
4039
    ObjectTemplateItem.remove(self, context, **kw)
4040 4041

  @staticmethod
4042 4043
  def _getFilesystemPath(class_id):
    raise NotImplementedError
4044

4045 4046 4047 4048 4049 4050 4051 4052 4053
  @staticmethod
  def _migrateFromFilesystem(tool, filesystem_path, filesystem_file, class_id):
    raise NotImplementedError

  def _migrateAllFromFilesystem(self,
                                context,
                                migrate_object_dict,
                                object_dict,
                                update_parameter_dict):
4054 4055 4056 4057 4058 4059 4060
    """
    Migrate all Property Sheets from 'migrate_object_dict' and, if
    necessary, remove old references in 'object_dict' too (with format
    version 1 of Business Template, the former would be '_objects' and
    the latter '_archive'), and finally removing the useless Property
    Sheet on the filesystem
    """
4061 4062 4063
    # Migrate all the filesystem classes of the Business Template if any
    tool = getattr(context.getPortalObject(), self._tool_id)
    id_set = set(tool.objectIds())
4064

4065 4066 4067
    # careful, that dictionary will change
    class_id_list = migrate_object_dict.keys()
    for class_id in class_id_list:
4068 4069 4070
      # If the Property Sheet already exists in ZODB, then skip it,
      # otherwise it should not be needed anymore once the deletion
      # code of the filesystem Property Sheets is enabled
4071
      if class_id in id_set:
Nicolas Delaby's avatar
Typo  
Nicolas Delaby committed
4072
        # XXX a Conduit must be able to merge modifications
4073
        # from FS PropertySheets into ZODB PropertySheets
4074 4075 4076
        warn('Conflict when migrating classes %s: already exists in %s and '\
               'cannot be updated automatically for now.' % (class_id,
                                                             self._tool_id),
4077
             UserWarning)
4078 4079 4080
        del migrate_object_dict[class_id]
        if class_id in object_dict:
          del object_dict[class_id]
4081
        continue
4082

4083
      filesystem_path = self._getFilesystemPath(class_id)
4084 4085 4086 4087

      # A filesystem Property Sheet may already exist in the instance
      # home if the Business Template has been previously installed,
      # otherwise it is created
4088 4089
      if os.path.exists(filesystem_path):
        filesystem_file = open(filesystem_path)
4090
      else:
Arnaud Fontaine's avatar
Arnaud Fontaine committed
4091
        filesystem_file = open(filesystem_path, 'w+')
4092 4093
        filesystem_file.write(migrate_object_dict[class_id])
        filesystem_file.seek(0)
4094 4095

      try:
4096 4097 4098 4099
        migrated_object = self._migrateFromFilesystem(tool,
                                                      filesystem_path,
                                                      filesystem_file,
                                                      class_id).aq_base
4100
      finally:
4101
        filesystem_file.close()
4102 4103 4104 4105

      # Delete the file only if there was no error encountered during
      # migration
      os.remove(filesystem_path)
4106 4107

      # Update 'migrate_object_dict' with the new path
4108
      key = '%s/%s' % (self._tool_id, migrated_object.getId())
4109

4110
      migrate_object_dict[key] = migrated_object
4111 4112 4113 4114 4115 4116 4117 4118 4119 4120 4121 4122 4123
      del migrate_object_dict[class_id]

      # Remove old reference in 'object_dict' as it does not make
      # sense to keep it anymore
      if class_id in object_dict:
        object_dict[key] = None
        del object_dict[class_id]

      # Skip meaningless backup of the object as it has just been
      # migrated
      update_parameter_dict[key] = 'migrate'

  def install(self, context, **kw):
4124 4125 4126 4127 4128 4129
    """
    Install Business Template items and perform migration
    automatically only if the tool is available
    """
    if (not self._perform_migration or
        getattr(context.getPortalObject(), self._tool_id, None) is None):
4130
      return FilesystemDocumentTemplateItem.install(self, context, **kw)
4131

4132
    if not self._is_already_migrated(self._objects.keys()):
4133 4134 4135 4136
      self._migrateAllFromFilesystem(context,
                                     self._objects,
                                     self._archive,
                                     kw.get('object_to_update'))
4137 4138 4139

    return ObjectTemplateItem.install(self, context, **kw)

4140 4141 4142 4143 4144 4145 4146 4147 4148 4149 4150 4151 4152 4153
class PropertySheetTemplateItem(FilesystemToZodbTemplateItem):
  """
  Property Sheets are now stored in ZODB rather than the filesystem.
  However, some Business Templates may still have filesystem Property
  Sheets, which need to be migrated to the ZODB.

  This migration is performed in two steps:

  1/ Specify explicitly in the web user interface that the Property
     Sheets should be migrated.

  2/ The Property Sheets will all be migrated when installing the
     Business Template.

4154 4155 4156
  Therefore, this is an all or nothing migration, meaning that only methods of
  FilesystemDocumentTemplateItem will be called before the migration has been
  performed, then ObjectTemplateItem methods afterwards.
4157 4158 4159 4160 4161 4162 4163 4164 4165 4166 4167 4168 4169 4170 4171 4172 4173 4174 4175
  """
  # Only meaningful for filesystem Property Sheets
  local_file_reader_name = staticmethod(readLocalPropertySheet)
  local_file_writer_name = staticmethod(writeLocalPropertySheet)
  local_file_importer_name = staticmethod(importLocalPropertySheet)
  local_file_remover_name = staticmethod(removeLocalPropertySheet)

  _tool_id = 'portal_property_sheets'

  @staticmethod
  def _getFilesystemPath(class_id):
    """
    From the given class identifier, return the complete path of the
    filesystem Property Sheet class. Only meaningful when the Business
    Template has already been installed previously, otherwise the
    """
    from App.config import getConfiguration
    return os.path.join(getConfiguration().instancehome,
                        "PropertySheet",
4176
                        class_id + ".py")
4177 4178 4179 4180 4181 4182 4183 4184 4185 4186 4187 4188 4189 4190 4191 4192 4193 4194 4195 4196 4197 4198 4199 4200 4201 4202 4203 4204 4205

  @staticmethod
  def _migrateFromFilesystem(tool,
                             filesystem_path,
                             filesystem_file,
                             class_id):
    """
    Migration of a filesystem Property Sheet involves loading the
    class from 'instancehome/PropertySheet/<class_id>', then create
    the ZODB Property Sheets in portal_property_sheets from its
    filesystem definition
    """
    # The first parameter of 'load_source' is the module name where
    # the class will be stored, thus don't only use the class name as
    # it may clash with already loaded module, such as
    # BusinessTemplate.
    module = imp.load_source('Migrate%sFilesystemPropertySheet' % class_id,
                             filesystem_path,
                             filesystem_file)

    try:
      klass = getattr(module, class_id)
    except AttributeError:
      raise AttributeError("filesystem Property Sheet '%s' should " \
                           "contain a class with the same name" % \
                           class_id)

    return PropertySheetDocument.importFromFilesystemDefinition(tool, klass)

4206
class ConstraintTemplateItem(FilesystemDocumentTemplateItem):
4207 4208 4209 4210
  local_file_reader_name = staticmethod(readLocalConstraint)
  local_file_writer_name = staticmethod(writeLocalConstraint)
  local_file_importer_name = staticmethod(importLocalConstraint)
  local_file_remover_name = staticmethod(removeLocalConstraint)
4211

4212 4213 4214 4215 4216 4217 4218 4219 4220 4221 4222 4223 4224 4225 4226 4227 4228 4229 4230 4231 4232 4233 4234 4235 4236 4237 4238 4239 4240 4241 4242 4243
class _ZodbComponentTemplateItem(ObjectTemplateItem):
  @staticmethod
  def _getZodbObjectId(id):
    raise NotImplementedError

  def __init__(self, id_list, tool_id='portal_components', **kw):
    ObjectTemplateItem.__init__(self, id_list, tool_id=tool_id, **kw)

  def isKeepWorkflowObjectLastHistoryOnly(self, path):
    """
    Component Validation Workflow last History of ZODB Components must always be
    kept, without explicitly adding them to the field which requires an extra
    action for developers
    """
    return True

  def _removeAllButLastWorkflowHistory(self, obj):
    """
    Only export the last state of component_validation_workflow, because only
    the source code and its state to load it is necessary for ZODB Components
    and too much history would be exported (edit_workflow)
    """
    for wf_id in obj.workflow_history.keys():
      if wf_id != 'component_validation_workflow':
        del obj.workflow_history[wf_id]
        continue

      wf_history = obj.workflow_history[wf_id][-1]
      # Remove useless modifcation 'time' and 'actor' (conflicts with VCSs)
      wf_history.pop('time', None)
      wf_history.pop('actor', None)
      wf_history.pop('comment', None)
4244

4245 4246 4247 4248 4249 4250 4251 4252 4253 4254 4255 4256 4257 4258 4259 4260 4261 4262 4263 4264 4265 4266 4267 4268 4269 4270 4271 4272 4273 4274 4275 4276 4277 4278 4279
      obj.workflow_history[wf_id] = WorkflowHistoryList([wf_history])

  def afterInstall(self):
    """
    Reset component on the fly, because it is possible that those components
    are required in the middle of the transaction. For example:
      - A method in a component is called while installing.
      - A document component is used in a different business template, and
        those business templates are installed in a single transaction by
        upgrader.

    This reset is called at most 3 times in one business template
    installation. (for Document, Test, Extension)
    """
    self.portal_components.reset(force=True)

  def afterUninstall(self):
    self.portal_components.reset(force=True,
                                 reset_portal_type_at_transaction_boundary=True)

from Products.ERP5Type.Core.InterfaceComponent import InterfaceComponent
class InterfaceTemplateItem(_ZodbComponentTemplateItem):
  @staticmethod
  def _getZodbObjectId(id):
    return InterfaceComponent.getIdPrefix() + '.' + id

from Products.ERP5Type.Core.MixinComponent import MixinComponent
class MixinTemplateItem(_ZodbComponentTemplateItem):
  @staticmethod
  def _getZodbObjectId(id):
    return MixinComponent.getIdPrefix() + '.' + id

from Products.ERP5Type.Core.DocumentComponent import DocumentComponent
class DocumentTemplateItem(FilesystemToZodbTemplateItem,
                           _ZodbComponentTemplateItem):
4280
  """
4281 4282 4283
  Documents are now stored in ZODB rather than on the filesystem. However,
  some Business Templates may still have filesystem Documents which need to be
  migrated to the ZODB.
4284 4285 4286

  The migration is performed in two steps:

4287 4288 4289 4290 4291 4292
    1/ Copy the Business Template to be migrated;

    2/ Run the migration script which will update properly the Document IDs in
       the Business Template.

  Upon import or export, two files will be created:
4293

4294 4295 4296 4297 4298
    - XML file: contains metadata
    - Python file: contains the source code itself

  This allows to keep Git history and having readable source code instead of
  being crippled into an XML file
4299
  """
4300 4301
  @staticmethod
  def _getZodbObjectId(id):
4302
    return DocumentComponent.getIdPrefix() + '.' + id
4303

4304 4305 4306 4307
  ## All the methods/attributes below are for FS compatibility *only* and
  ## should be removed when all bt5s have been migrated
  _tool_id = 'portal_components'

4308 4309 4310 4311 4312
  @staticmethod
  def _getFilesystemPath(class_id):
    from App.config import getConfiguration
    return os.path.join(getConfiguration().instancehome,
                        "Document",
4313
                        class_id + ".py")
4314

4315 4316 4317 4318 4319 4320 4321
  def isKeepWorkflowObjectLastHistoryOnly(self, path):
    """
    Component Validation Workflow last History of ZODB Components must always be
    kept, without explicitly adding them to the field which requires an extra
    action for developers
    """
    return path.startswith(self._tool_id + '/')
4322 4323
  
  # XXX temporary should be eliminated from here
4324
  def _importFile(self, file_name, file_obj):
4325 4326 4327
    ObjectTemplateItem._importFile(self, file_name, file_obj)
  
  # XXX temporary should be eliminated from here
4328
  def export(self, context, bta, **kw):
4329
    ObjectTemplateItem.export(self, context, bta, **kw)  
4330

4331
  def getTemplateIdList(self):
4332 4333 4334 4335
    """
    Getter for Document property on the Business Template, must be overriden
    in children classes (e.g. ExtensionDocumentTemplateItem for example)
    """
4336 4337 4338 4339 4340 4341
    return self.getTemplateDocumentIdList()

  def build(self, context, **kw):
    if not self._archive:
      return

4342
    # After running the migration script, update bt5 property accordingly
4343 4344
    if not self._is_already_migrated(self._archive.keys()):
      document_id_list = self.getTemplateIdList()
4345 4346
      if document_id_list[0] not in getattr(context.getPortalObject(),
                                            'portal_components', ()):
4347
        return FilesystemDocumentTemplateItem.build(self, context, **kw)
4348 4349 4350
      self._archive.clear()
      for name in document_id_list:
        self._archive['portal_components/' + name] = None
4351 4352 4353 4354 4355 4356 4357 4358 4359

    return ObjectTemplateItem.build(self, context, **kw)

  def install(self, context, **kw):
    """
    In contrary to ZODB Property Sheets, Components are not migrated
    automatically as the version must be set manually. This should not be an
    issue as there are not so many Documents in bt5...
    """
4360
    if self._is_already_migrated(self._objects.keys()):
4361
      _ZodbComponentTemplateItem.install(self, context, **kw)
4362
    else:
4363 4364
      FilesystemDocumentTemplateItem.install(self, context, **kw)

4365 4366 4367 4368 4369 4370
  _removeAllButLastWorkflowHistory = _ZodbComponentTemplateItem._removeAllButLastWorkflowHistory

  # Only for ObjectTemplateItem (ZODB Components) and thus no need to check
  # whether they have already been migrated or not
  afterInstall = _ZodbComponentTemplateItem.afterInstall
  afterUninstall = _ZodbComponentTemplateItem.afterUninstall
4371

4372 4373
from Products.ERP5Type.Core.ExtensionComponent import ExtensionComponent

4374 4375 4376 4377 4378 4379 4380 4381 4382 4383 4384 4385 4386
class ExtensionTemplateItem(DocumentTemplateItem):
  """
  Extensions are now stored in ZODB rather than on the filesystem. However,
  some Business Templates may still have filesystem Extensions which need to
  be migrated to the ZODB.
  """
  # Only meaningful for filesystem Extensions
  local_file_reader_name = staticmethod(readLocalExtension)
  local_file_writer_name = staticmethod(writeLocalExtension)
  # Extension needs no import
  local_file_importer_name = None
  local_file_remover_name = staticmethod(removeLocalExtension)

4387 4388
  @staticmethod
  def _getZodbObjectId(id):
4389
    return ExtensionComponent.getIdPrefix() + '.' + id
4390

4391 4392
  def getTemplateIdList(self):
    return self.getTemplateExtensionIdList()
4393

4394 4395
from Products.ERP5Type.Core.TestComponent import TestComponent

4396
class TestTemplateItem(DocumentTemplateItem):
4397 4398 4399 4400 4401
  """
  Live Tests are now stored in ZODB rather than on the filesystem. However,
  some Business Templates may still have filesystem Live Tests which need to
  be migrated to the ZODB.
  """
4402 4403
  local_file_reader_name = staticmethod(readLocalTest)
  local_file_writer_name = staticmethod(writeLocalTest)
4404
  # Test needs no import
4405
  local_file_importer_name = None
4406
  local_file_remover_name = staticmethod(removeLocalTest)
4407

4408 4409
  @staticmethod
  def _getZodbObjectId(id):
4410
    return TestComponent.getIdPrefix() + '.' + id
4411 4412 4413

  def getTemplateIdList(self):
    return self.getTemplateTestIdList()
Aurel's avatar
Aurel committed
4414

4415 4416 4417
class ProductTemplateItem(BaseTemplateItem):
  # XXX Not implemented yet
  pass
4418 4419 4420

class RoleTemplateItem(BaseTemplateItem):

Aurel's avatar
Aurel committed
4421
  def build(self, context, **kw):
4422 4423
    for key in self._archive.iterkeys():
      self._objects[key] = 1
Aurel's avatar
Aurel committed
4424

4425
  def preinstall(self, context, installed_item, **kw):
Yoshinori Okuji's avatar
Yoshinori Okuji committed
4426
    modified_object_list = {}
4427 4428 4429 4430 4431 4432 4433 4434 4435 4436
    # BBB it might be necessary to change the data structure.
    obsolete_key = self.__class__.__name__ + '/role_list'
    if obsolete_key in installed_item._objects:
      for role in installed_item._objects[obsolete_key]:
        installed_item._objects[role] = 1
      del installed_item._objects[obsolete_key]
    for role in self._objects:
      if installed_item._objects.has_key(role):
        continue
      else: # only show new roles
4437
        modified_object_list[role] = 'New', 'Role'
4438 4439 4440 4441
    # get removed roles
    old_roles = installed_item._objects.keys()
    for role in old_roles:
      if role not in self._objects:
4442
        modified_object_list[role] = 'Removed', self.__class__.__name__[:-12]
4443 4444 4445
    return modified_object_list

  def install(self, context, trashbin, **kw):
4446
    p = context.getPortalObject()
4447
    # get roles
4448
    role_set = set(self._objects)
4449 4450 4451 4452
    # set roles in PAS
    if p.acl_users.meta_type == 'Pluggable Auth Service':
      role_manager_list = p.acl_users.objectValues('ZODB Role Manager')
      for role_manager in role_manager_list:
4453 4454
        for role in role_set.difference(role_manager.listRoleIds()):
          role_manager.addRole(role)
4455
    # set roles on portal
4456
    p.__ac_roles__ = tuple(role_set.union(p.__ac_roles__))
4457 4458

  def _importFile(self, file_name, file):
4459
    if not file_name.endswith('.xml'):
4460
      LOG('Business Template', 0, 'Skipping file "%s"' % (file_name, ))
4461
      return
4462
    xml = parse(file)
4463 4464 4465
    for role in xml.getroot():
      value = role.text
      self._objects[value] = 1
4466 4467 4468 4469 4470 4471 4472 4473 4474 4475 4476 4477

  def uninstall(self, context, **kw):
    p = context.getPortalObject()
    roles = {}
    for role in p.__ac_roles__:
      roles[role] = 1
    for role in self._archive.keys():
      if role in roles:
        del roles[role]
    p.__ac_roles__ = tuple(roles.keys())
    BaseTemplateItem.uninstall(self, context, **kw)

4478 4479 4480 4481 4482 4483 4484 4485 4486
  def trash(self, context, new_item, **kw):
    p = context.getPortalObject()
    new_roles = {}
    for role in new_item._archive.keys():
      new_roles[role] = 1
    roles = {}
    for role in p.__ac_roles__:
      roles[role] = 1
    for role in self._archive.keys():
Yoshinori Okuji's avatar
Yoshinori Okuji committed
4487
      if role in roles and role not in new_roles:
4488 4489 4490
        del roles[role]
    p.__ac_roles__ = tuple(roles.keys())

Christophe Dumez's avatar
Christophe Dumez committed
4491
  # Function to generate XML Code Manually
4492 4493
  def generateXml(self):
    role_list = self._objects.keys()
4494
    xml_data = '<role_list>'
4495 4496
    for role in sorted(role_list):
      xml_data += '\n <role>%s</role>' % (role,)
4497
    xml_data += '\n</role_list>'
4498 4499
    return xml_data

Aurel's avatar
Aurel committed
4500
  def export(self, context, bta, **kw):
4501
    if len(self._objects) == 0:
Aurel's avatar
Aurel committed
4502
      return
4503 4504 4505 4506 4507 4508 4509 4510
    # BBB it might be necessary to change the data structure.
    obsolete_key = self.__class__.__name__ + '/role_list'
    if obsolete_key in self._objects:
      for role in self._objects[obsolete_key]:
        self._objects[role] = 1
      del self._objects[obsolete_key]
    xml_data = self.generateXml()
    path = obsolete_key
4511
    bta.addObject(xml_data, name=path)
4512

4513
class CatalogKeyTemplateItemBase(BaseTemplateItem):
4514

Aurel's avatar
Aurel committed
4515
  def build(self, context, **kw):
4516
    catalog = _getCatalogValue(self)
4517 4518 4519
    if catalog is None:
      LOG('BusinessTemplate', 0, 'no SQL catalog was available')
      return
4520
    catalog_key_list = list(getattr(catalog, self.key_list_attr, []))
4521
    key_list = []
4522
    for key in self._archive.keys():
4523
      if key in catalog_key_list:
4524
        key_list.append(key)
4525
      elif not self.is_bt_for_diff:
4526
        raise NotFound, '%s %r not found in catalog' %(self.key_title, key)
4527
    if len(key_list) > 0:
4528
      self._objects[self.key_list_title] = key_list
4529 4530

  def _importFile(self, file_name, file):
4531 4532 4533
    if not file_name.endswith('.xml'):
      LOG('Business Template', 0, 'Skipping file "%s"' % (file_name, ))
      return
4534
    xml = parse(file)
4535 4536
    key_list = [key.text for key in xml.getroot()]
    self._objects[file_name[:-4]] = key_list
4537

4538 4539 4540 4541
  def _getUpdateDictAction(self, update_dict):
    action = update_dict.get(self.key_list_title, 'nothing')
    return action

4542
  def install(self, context, trashbin, **kw):
4543
    catalog = _getCatalogValue(self)
4544 4545 4546
    if catalog is None:
      LOG('BusinessTemplate', 0, 'no SQL catalog was available')
      return
4547

4548
    catalog_key_list = list(getattr(catalog, self.key_list_attr, []))
4549 4550 4551 4552 4553
    if len(self._objects.keys()) == 0: # needed because of pop()
      return
    keys = []
    for k in self._objects.values().pop(): # because of list of list
      keys.append(k)
4554 4555
    update_dict = kw.get('object_to_update')
    force = kw.get('force')
4556 4557
    if force or self._getUpdateDictAction(update_dict) != 'nothing':
      catalog_key_list = self._getUpdatedCatalogKeyList(catalog_key_list, keys)
4558
      setattr(catalog, self.key_list_attr, catalog_key_list)
4559

4560
  def _getUpdatedCatalogKeyList(self, catalog_key_list, new_key_list):
4561 4562 4563
    catalog_key_set = set(catalog_key_list) # copy
    catalog_key_set.update(new_key_list)
    return sorted(catalog_key_set)
4564

4565
  def uninstall(self, context, **kw):
4566
    catalog = _getCatalogValue(self)
4567 4568 4569
    if catalog is None:
      LOG('BusinessTemplate', 0, 'no SQL catalog was available')
      return
4570
    catalog_key_list = list(getattr(catalog, self.key_list_attr, []))
4571
    object_path = kw.get('object_path', None)
4572 4573 4574 4575 4576
    if object_path is not None:
      object_keys = [object_path]
    else:
      object_keys = self._archive.keys()
    for key in object_keys:
4577 4578 4579
      if key in catalog_key_list:
        catalog_key_list.remove(key)
    setattr(catalog, self.key_list_attr, catalog_key_list)
4580 4581
    BaseTemplateItem.uninstall(self, context, **kw)

Christophe Dumez's avatar
Christophe Dumez committed
4582
  # Function to generate XML Code Manually
4583
  def generateXml(self, path=None):
4584
    obj = self._objects[path]
4585
    xml_data = '<key_list>'
4586 4587
    obj.sort()
    for key in obj:
4588 4589
      xml_data += '\n <key>%s</key>' %(key)
    xml_data += '\n</key_list>'
4590 4591
    return xml_data

Aurel's avatar
Aurel committed
4592 4593 4594
  def export(self, context, bta, **kw):
    if len(self._objects.keys()) == 0:
      return
4595 4596 4597 4598
    for name in self._objects.keys():
      path = self.__class__.__name__
      xml_data = self.generateXml(path=name)
      bta.addObject(xml_data, name=name, path=path)
4599

4600 4601 4602 4603 4604 4605 4606 4607 4608 4609 4610 4611 4612 4613 4614 4615 4616 4617 4618 4619
class CatalogUniqueKeyTemplateItemBase(CatalogKeyTemplateItemBase):
  # like CatalogKeyTemplateItemBase, but for keys which use
  # "key | value" syntax to configure dictionaries.
  # The keys (part before the pipe) must be unique.

  def _getMapFromKeyList(self, key_list):
    # in case of duplicates, only the last installed entry will survive
    return dict(tuple(part.strip() for part in key.split('|', 1))
                for key in key_list)

  def _getListFromKeyMap(self, key_map):
    return [" | ".join(item) for item in sorted(key_map.items())]

  def _getUpdatedCatalogKeyList(self, catalog_key_list, new_key_list):
    # treat key lists as dictionaries, parse and update:
    catalog_key_map = self._getMapFromKeyList(catalog_key_list)
    catalog_key_map.update(self._getMapFromKeyList(new_key_list))
    return self._getListFromKeyMap(catalog_key_map)

class CatalogSearchKeyTemplateItem(CatalogUniqueKeyTemplateItemBase):
4620 4621 4622 4623 4624
  key_list_attr = 'sql_catalog_search_keys'
  key_list_title = 'search_key_list'
  key_title = 'Search key'

class CatalogResultKeyTemplateItem(CatalogKeyTemplateItemBase):
4625 4626 4627
  key_list_attr = 'sql_search_result_keys'
  key_list_title = 'result_key_list'
  key_title = 'Result key'
4628

4629
class CatalogRelatedKeyTemplateItem(CatalogUniqueKeyTemplateItemBase):
4630 4631 4632
  key_list_attr = 'sql_catalog_related_keys'
  key_list_title = 'related_key_list'
  key_title = 'Related key'
4633

4634
  # override this method to support 'key_list' for backward compatibility.
4635 4636 4637 4638 4639
  def _getUpdateDictAction(self, update_dict):
    action = update_dict.get(self.key_list_title, _MARKER)
    if action is _MARKER:
      action = update_dict.get('key_list', 'nothing')
    return action
4640

4641
class CatalogResultTableTemplateItem(CatalogKeyTemplateItemBase):
4642 4643 4644
  key_list_attr = 'sql_search_tables'
  key_list_title = 'result_table_list'
  key_title = 'Result table'
4645

4646
# keyword
4647
class CatalogKeywordKeyTemplateItem(CatalogKeyTemplateItemBase):
4648 4649 4650
  key_list_attr = 'sql_catalog_keyword_search_keys'
  key_list_title = 'keyword_key_list'
  key_title = 'Keyword key'
4651

4652
# datetime
4653
class CatalogDateTimeKeyTemplateItem(CatalogKeyTemplateItemBase):
4654 4655 4656
  key_list_attr = 'sql_catalog_datetime_search_keys'
  key_list_title = 'datetime_key_list'
  key_title = 'DateTime key'
4657

4658
# full text
4659
class CatalogFullTextKeyTemplateItem(CatalogKeyTemplateItemBase):
4660 4661 4662
  key_list_attr = 'sql_catalog_full_text_search_keys'
  key_list_title = 'full_text_key_list'
  key_title = 'Fulltext key'
4663

4664
# request
4665
class CatalogRequestKeyTemplateItem(CatalogKeyTemplateItemBase):
4666 4667 4668
  key_list_attr = 'sql_catalog_request_keys'
  key_list_title = 'request_key_list'
  key_title = 'Request key'
4669

4670
# multivalue
4671
class CatalogMultivalueKeyTemplateItem(CatalogKeyTemplateItemBase):
4672 4673 4674
  key_list_attr = 'sql_catalog_multivalue_keys'
  key_list_title = 'multivalue_key_list'
  key_title = 'Multivalue key'
4675 4676

# topic
4677
class CatalogTopicKeyTemplateItem(CatalogKeyTemplateItemBase):
4678 4679 4680 4681
  key_list_attr = 'sql_catalog_topic_search_keys'
  key_list_title = 'topic_key_list'
  key_title = 'Topic key'

4682
class CatalogScriptableKeyTemplateItem(CatalogUniqueKeyTemplateItemBase):
4683 4684 4685 4686
  key_list_attr = 'sql_catalog_scriptable_keys'
  key_list_title = 'scriptable_key_list'
  key_title = 'Scriptable key'

4687
class CatalogRoleKeyTemplateItem(CatalogUniqueKeyTemplateItemBase):
4688 4689 4690 4691
  key_list_attr = 'sql_catalog_role_keys'
  key_list_title = 'role_key_list'
  key_title = 'Role key'

4692
class CatalogLocalRoleKeyTemplateItem(CatalogUniqueKeyTemplateItemBase):
4693 4694 4695
  key_list_attr = 'sql_catalog_local_role_keys'
  key_list_title = 'local_role_key_list'
  key_title = 'LocalRole key'
4696

4697 4698 4699 4700 4701 4702
class CatalogSecurityUidColumnTemplateItem(CatalogSearchKeyTemplateItem):
  key_list_attr = 'sql_catalog_security_uid_columns'
  key_list_title = 'security_uid_column_list'
  key_title = 'Security Uid Columns'


4703 4704 4705 4706
class MessageTranslationTemplateItem(BaseTemplateItem):

  def build(self, context, **kw):
    localizer = context.getPortalObject().Localizer
4707 4708 4709
    for lang_key in self._archive.keys():
      if '|' in lang_key:
        lang, catalog = lang_key.split(' | ')
4710
      else: # XXX backward compatibility
4711 4712
        lang = lang_key
        catalog = 'erp5_ui'
4713
      path = posixpath.join(lang, catalog)
4714 4715
      mc = localizer._getOb(catalog)
      self._objects[path] = mc.manage_export(lang)
4716 4717 4718
      if lang not in self._objects:
        name = localizer.get_language_name(lang)
        self._objects[lang] = name
4719

4720
  def preinstall(self, context, installed_item, **kw):
4721
    modified_object_list = {}
4722 4723 4724 4725 4726 4727
    for path in self._objects:
      if installed_item._objects.has_key(path):
        # compare object to see if there is changes
        new_obj_code = self._objects[path]
        old_obj_code = installed_item._objects[path]
        if new_obj_code != old_obj_code:
4728
          modified_object_list[path] = 'Modified', self.__class__.__name__[:-12]
4729
      else: # new object
4730
        modified_object_list[path] = 'New', self.__class__.__name__[:-12]
4731 4732 4733 4734
    # get removed object
    old_keys = installed_item._objects.keys()
    for path in old_keys:
      if path not in self._objects:
4735
        modified_object_list[path] = 'Removed', self.__class__.__name__[:-12]
4736 4737
    return modified_object_list

4738 4739 4740 4741 4742 4743 4744 4745 4746 4747 4748 4749 4750 4751 4752 4753
  def _splitKey(self,key):
    path = key.split('/')
    if len(path) == 1:
      lang = path[0]
      catalog = None
    elif len(path) == 2:
      lang = path[0]
      catalog = path[1]
    else:
      lang = path[-3]
      catalog = path[-2]
    return lang, catalog

  def _importCatalogLanguage(self, localizer, catalog, lang, po):
    if catalog not in localizer.objectIds():
      dispatcher = localizer.manage_addProduct['Localizer']
4754
      dispatcher.manage_addMessageCatalog(id=catalog,
4755 4756 4757 4758 4759 4760 4761 4762 4763 4764 4765
                                          title='Message Catalog',
                                          languages=['en'])
    mc = localizer._getOb(catalog)
    if lang not in mc.get_languages():
      mc.manage_addLanguage(lang)
    mc.manage_import(lang, po)

  def install(self, context, trashbin, localizer=None, **kw):
    if localizer is None:
      localizer = context.getPortalObject().Localizer
    update_dict = kw.get('object_to_update', {})
4766
    force = kw.get('force')
4767 4768 4769 4770 4771 4772 4773 4774 4775 4776 4777 4778 4779 4780 4781 4782 4783 4784 4785
    for key in sorted(self._objects.keys()):
      if update_dict.has_key(key) or force:
        if not force:
          action = update_dict[key]
          if action == 'nothing':
            continue
        lang, catalog = self._splitKey(key)

        if catalog is None:
          name = self._objects[key]
          for lang_dict in localizer.get_all_languages():
            if lang_dict['code'] == lang:
              # When the Localizer has the language as a user-defined
              # language, make sure that the name is updated.
              old_name = localizer.get_user_defined_language_name(lang)
              if old_name is not None and old_name != name:
                localizer._del_user_defined_language(lang)
                localizer._add_user_defined_language(name, lang)
              break
4786
          else:
4787 4788 4789 4790 4791 4792 4793 4794 4795 4796
            # if the Localizer does not know the language code, it must be
            # defined as a user-defined language.
            localizer._add_user_defined_language(name, lang)
          if lang not in localizer.get_languages():
            localizer.manage_addLanguage(lang)
        else:
          po = self._objects[key]
          if lang not in localizer.get_languages():
            localizer.manage_addLanguage(lang)
          self._importCatalogLanguage(localizer, catalog, lang, po)
4797 4798 4799 4800 4801 4802 4803 4804 4805 4806 4807 4808 4809 4810 4811 4812 4813 4814 4815 4816 4817 4818 4819 4820 4821 4822 4823 4824 4825

  def uninstall(self, context, remove_translations=False, **kw):
    if not remove_translations:
      return
    portal = context.getPortalObject()
    localizer = portal.Localizer
    from Products.Localizer.Localizer import Localizer
    fake_localizer = Localizer('Fake Localizer',
                               languages=['en']).__of__(portal)
    # roundabout way of keeping BW compatibility, allow install() to do the
    # heavy lifting so we can extract the original catalogs and messages:
    self.install(context, None, localizer=fake_localizer, force=True, **kw)
    # now scan the actual message_catalog to remove messages present in the
    # fake one.
    for fake_message_catalog in fake_localizer.objectValues():
      message_catalog = localizer._getOb(fake_message_catalog.getId())
      # get list of messages present in both the fake and the real catalog
      # UGH! direct attribute access... but there is no real API to access
      # all messages here.
      messages = set(fake_message_catalog._messages.keys())
      messages.intersection_update(message_catalog._messages.keys())
      for message in messages:
        # delete translations from the real catalog that are present in the
        # fake one
        fake_translations = fake_message_catalog.get_translations(message)
        translations = message_catalog.get_translations(message)
        for lang in fake_translations.keys():
          # XXX: should we check they're still the same before removing?
          translations.pop(lang, None)
4826 4827

  def export(self, context, bta, **kw):
4828
    if len(self._objects) == 0:
4829
      return
4830
    root_path = self.__class__.__name__
4831
    for key, obj in self._objects.iteritems():
4832
      path = os.path.join(root_path, key)
4833
      if '/' in key:
4834
        bta.addObject(obj, 'translation', ext='.po', path=path)
4835 4836 4837 4838 4839
      else:
        xml_data = ['<language>']
        xml_data.append(' <code>%s</code>' % (escape(key), ))
        xml_data.append(' <name>%s</name>' % (escape(obj), ))
        xml_data.append('</language>')
4840
        bta.addObject('\n'.join(xml_data), 'language', path=path)
4841 4842

  def _importFile(self, file_name, file):
4843 4844
    name = posixpath.split(file_name)[1]
    if name == 'translation.po':
4845
      text = file.read()
4846
      self._objects[file_name[:-len(name)]] = text
4847 4848
    elif name == 'language.xml':
      xml = parse(file)
4849 4850
      name = xml.find('name').text
      code = xml.find('code').text
4851
      self._objects[code] = name
4852

Aurel's avatar
Aurel committed
4853 4854
class LocalRolesTemplateItem(BaseTemplateItem):

4855
  def __init__(self, id_list, **kw):
4856
    id_list = ['local_roles/%s' % id for id in id_list if id != '']
4857 4858
    BaseTemplateItem.__init__(self, id_list, **kw)

Aurel's avatar
Aurel committed
4859 4860 4861
  def build(self, context, **kw):
    p = context.getPortalObject()
    for path in self._archive.keys():
4862
      obj = p.unrestrictedTraverse(path.split('/', 1)[1])
4863 4864
      local_roles_dict = getattr(obj, '__ac_local_roles__',
                                        {}) or {}
4865 4866 4867
      local_roles_group_id_dict = getattr(
        obj, '__ac_local_roles_group_id_dict__', {}) or {}
      self._objects[path] = (local_roles_dict, local_roles_group_id_dict)
Aurel's avatar
Aurel committed
4868

Christophe Dumez's avatar
Christophe Dumez committed
4869
  # Function to generate XML Code Manually
Aurel's avatar
Aurel committed
4870
  def generateXml(self, path=None):
4871 4872 4873 4874 4875 4876 4877 4878 4879 4880
    # With local roles groups id, self._object contains for each path a tuple
    # containing the dict of local roles and the dict of local roles group ids.
    # Before it was only containing the dict of local roles. This method is
    # also used on installed business templates to show a diff during
    # installation, so it might be called on old format objects.
    if len(self._objects[path]) == 2:
      # new format
      local_roles_dict, local_roles_group_id_dict = self._objects[path]
    else:
      # old format, before local roles group id
4881
      local_roles_group_id_dict = None
4882 4883
      local_roles_dict, = self._objects[path]

Aurel's avatar
Aurel committed
4884
    xml_data = '<local_roles_item>'
4885
    # local roles
4886
    xml_data += '\n <local_roles>'
4887 4888 4889 4890 4891 4892 4893 4894 4895
    for user_id, role_list in sorted(local_roles_dict.items()):
      if 'Owner' in role_list:
        # We don't export Owner role as it set automatically when installing business template.
        role_list.remove('Owner')
      if role_list:
        xml_data += "\n  <role id='%s'>" %(user_id,)
        for role in role_list:
          xml_data += "\n   <item>%s</item>" %(role,)
        xml_data += '\n  </role>'
4896
    xml_data += '\n </local_roles>'
4897 4898 4899 4900

    if local_roles_group_id_dict:
      # local roles group id dict (not included by default to be stable with
      # old bts)
4901 4902 4903 4904 4905 4906 4907 4908
      xml_data += '\n <local_role_group_ids>'
      for local_role_group_id, local_roles_group_id_list in sorted(local_roles_group_id_dict.items()):
        xml_data += "\n  <local_role_group_id id='%s'>" % escape(local_role_group_id)
        for principal, role in sorted(local_roles_group_id_list):
          xml_data += "\n    <principal id='%s'>%s</principal>" % \
                (escape(principal), escape(role))
        xml_data += "\n  </local_role_group_id>"
      xml_data += '\n </local_role_group_ids>'
4909

4910
    xml_data += '\n</local_roles_item>'
Vincent Pelletier's avatar
Vincent Pelletier committed
4911
    if isinstance(xml_data, unicode):
4912
      xml_data = xml_data.encode('utf8')
Aurel's avatar
Aurel committed
4913
    return xml_data
4914

Aurel's avatar
Aurel committed
4915
  def export(self, context, bta, **kw):
4916 4917
    path = self.__class__.__name__
    for key in self._objects:
Aurel's avatar
Aurel committed
4918
      xml_data = self.generateXml(key)
4919 4920
      assert key[:12] == 'local_roles/'
      bta.addObject(xml_data, key[12:], path=path)
Aurel's avatar
Aurel committed
4921 4922

  def _importFile(self, file_name, file):
4923 4924 4925
    if not file_name.endswith('.xml'):
      LOG('Business Template', 0, 'Skipping file "%s"' % (file_name, ))
      return
Aurel's avatar
Aurel committed
4926 4927
    xml = parse(file)
    # local roles
4928
    local_roles_list = xml.findall('//role')
Aurel's avatar
Aurel committed
4929 4930
    local_roles_dict = {}
    for role in local_roles_list:
4931 4932
      id = role.get('id')
      item_type_list = [item.text for item in role]
Aurel's avatar
Aurel committed
4933
      local_roles_dict[id] = item_type_list
4934 4935 4936

    # local roles group id
    local_roles_group_id_dict = {}
4937 4938 4939 4940 4941
    for local_role_group_id in xml.findall('//local_role_group_id'):
      role_set = set()
      for principal in local_role_group_id.findall('./principal'):
        role_set.add((principal.get('id'), principal.text))
      local_roles_group_id_dict[local_role_group_id.get('id')] = role_set
4942 4943
    self._objects['local_roles/%s' % (file_name[:-4],)] = (
      local_roles_dict, local_roles_group_id_dict)
Aurel's avatar
Aurel committed
4944 4945 4946 4947 4948 4949 4950 4951 4952 4953 4954

  def install(self, context, trashbin, **kw):
    update_dict = kw.get('object_to_update')
    force = kw.get('force')
    p = context.getPortalObject()
    for roles_path in self._objects.keys():
      if update_dict.has_key(roles_path) or force:
        if not force:
          action = update_dict[roles_path]
          if action == 'nothing':
            continue
4955 4956
        path = roles_path.split('/')[1:]
        obj = p.unrestrictedTraverse(path)
4957 4958 4959 4960 4961
        # again we might be installing an business template in format before
        # existance of local roles group id.
        if len(self._objects[roles_path]) == 2:
          local_roles_dict, local_roles_group_id_dict = self._objects[roles_path]
        else:
4962
          local_roles_group_id_dict = None
4963
          local_roles_dict, = self._objects[roles_path]
4964 4965 4966 4967 4968 4969 4970 4971 4972 4973 4974 4975

        # We ignore the owner defined in local_roles_dict and set it to the user installing that business template.
        local_roles_dict = deepcopy(local_roles_dict)
        for user_id, group_list in list(local_roles_dict.items()):
          if group_list == ["Owner"]:
            del local_roles_dict[user_id]
        current_user = getSecurityManager().getUser()
        if current_user is not None:
          current_user_id = current_user.getId()
          if current_user_id is not None:
            local_roles_dict.setdefault(current_user_id, []).append('Owner')

4976
        obj.__ac_local_roles__ = local_roles_dict
4977
        if local_roles_group_id_dict:
4978
          obj.__ac_local_roles_group_id_dict__ = local_roles_group_id_dict
4979 4980
          # we try to have __ac_local_roles_group_id_dict__ set only if
          # it is actually defining something else than default
4981 4982 4983 4984 4985
        else:
          try:
            del obj.__ac_local_roles_group_id_dict__
          except AttributeError:
            pass
4986
        obj.reindexObject()
Aurel's avatar
Aurel committed
4987

4988
  def uninstall(self, context, object_path=None, **kw):
Aurel's avatar
Aurel committed
4989
    p = context.getPortalObject()
4990 4991 4992 4993 4994
    if object_path is not None:
      keys = [object_path]
    else:
      keys = self._objects.keys()
    for roles_path in keys:
Aurel's avatar
Aurel committed
4995
      path = roles_path.split('/')[1:]
4996 4997 4998 4999 5000
      # if document does not exists anymore longer,
      # there is no needs to fail
      obj = p.unrestrictedTraverse(path, None)
      if obj is not None:
        setattr(obj, '__ac_local_roles__', {})
5001 5002 5003
        if getattr(aq_base(obj), '__ac_local_roles_group_id_dict__',
                    None) is not None:
          delattr(obj, '__ac_local_roles_group_id_dict__')
5004
        obj.reindexObject()
5005

5006 5007 5008 5009 5010 5011 5012
class bt(dict):
  """Fake 'bt' item to read bt/* files through BusinessTemplateArchive"""

  def _importFile(self, file_name, file):
    self[file_name] = file.read()


5013 5014 5015
class BusinessTemplate(XMLObject):
    """
    A business template allows to construct ERP5 modules
Christophe Dumez's avatar
Christophe Dumez committed
5016
    in part or completely. Each object is separated from its
5017 5018 5019 5020 5021 5022 5023 5024 5025 5026
    subobjects and exported in xml format.
    It may include:

    - catalog definition
      - SQL method objects
      - SQL methods including:
        - purpose (catalog, uncatalog, etc.)
        - filter definition

    - portal_types definition
Christophe Dumez's avatar
Christophe Dumez committed
5027 5028
      - object without optimal actions
      - list of relation between portal type and workflow
5029 5030 5031 5032 5033 5034 5035 5036 5037 5038

    - module definition
      - id
      - title
      - portal type
      - roles/security

    - site property definition
      - id
      - type
5039
      - value
Jean-Paul Smets's avatar
Jean-Paul Smets committed
5040

5041 5042
    - document/propertysheet/extension/test definition
      - copy of the local file
5043

5044
    - message transalation definition
5045
      - .po file
Jean-Paul Smets's avatar
Jean-Paul Smets committed
5046

5047 5048
    The Business Template properties are exported to the bt folder with
    one property per file
5049

Jean-Paul Smets's avatar
Jean-Paul Smets committed
5050 5051
    Technology:

5052 5053
    - download a gzip file or folder tree (from the web, from a CVS repository,
      from local file system) (import/donwload)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
5054

5055
    - install files to the right location (install)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
5056 5057 5058 5059

    Use case:

    - install core ERP5 (the minimum)
5060

5061
    - go to "BT" menu. Import BT. Select imported BT. Click install.
Jean-Paul Smets's avatar
Jean-Paul Smets committed
5062

5063 5064
    - go to "BT" menu. Create new BT.
      Define BT elements (workflow, methods, attributes, etc.).
5065
      Build BT and export or save it
Jean-Paul Smets's avatar
Jean-Paul Smets committed
5066 5067 5068 5069 5070
      Done.
    """

    meta_type = 'ERP5 Business Template'
    portal_type = 'Business Template'
5071
    add_permission = Permissions.AddPortalContent
Jean-Paul Smets's avatar
Jean-Paul Smets committed
5072 5073 5074

    # Declarative security
    security = ClassSecurityInfo()
5075
    security.declareObjectProtected(Permissions.AccessContentsInformation)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
5076 5077 5078 5079

    # Declarative properties
    property_sheets = ( PropertySheet.Base
                      , PropertySheet.XMLObject
5080
                      , PropertySheet.SimpleItem
Jean-Paul Smets's avatar
Jean-Paul Smets committed
5081
                      , PropertySheet.CategoryCore
Jean-Paul Smets's avatar
Jean-Paul Smets committed
5082
                      , PropertySheet.Version
Jean-Paul Smets's avatar
Jean-Paul Smets committed
5083
                      , PropertySheet.BusinessTemplate
5084
                      , PropertySheet.Comment
Jean-Paul Smets's avatar
Jean-Paul Smets committed
5085 5086
                      )

5087 5088 5089 5090 5091 5092
    # Factory Type Information
    factory_type_information = \
      {    'id'             : portal_type
         , 'meta_type'      : meta_type
         , 'description'    : """\
Business Template is a set of definitions, such as skins, portal types and categories. This is used to set up a new ERP5 site very efficiently."""
5093
         , 'icon'           : 'file_icon.gif'
5094 5095
         , 'product'        : 'ERP5Type'
         , 'factory'        : 'addBusinessTemplate'
5096
         , 'type_class'     : 'BusinessTemplate'
5097 5098 5099 5100 5101 5102 5103
         , 'immediate_view' : 'BusinessTemplate_view'
         , 'allow_discussion'     : 1
         , 'allowed_content_types': (
                                      )
         , 'filter_content_types' : 1
      }

5104 5105
    def __init__(self, *args, **kw):
      XMLObject.__init__(self, *args, **kw)
Yoshinori Okuji's avatar
Yoshinori Okuji committed
5106 5107
      self._clean()

5108
    security.declarePrivate('manage_afterAdd')
5109 5110 5111 5112 5113 5114 5115
    def manage_afterAdd(self, item, container):
      """
        This is called when a new business template is added or imported.
      """
      portal_workflow = getToolByName(self, 'portal_workflow')
      if portal_workflow is not None:
        # Make sure that the installation state is "not installed".
5116 5117
        if portal_workflow.getStatusOf(
                'business_template_installation_workflow', self) is not None:
5118
          # XXX Not good to access the attribute directly,
5119 5120 5121
          # but there is no API for clearing the history.
          self.workflow_history[
                            'business_template_installation_workflow'] = None
5122

5123 5124
    security.declareProtected(Permissions.AccessContentsInformation,
                              'getShortRevision')
5125 5126 5127 5128
    def getShortRevision(self):
      """Returned a shortened revision"""
      r = self.getRevision()
      return r and r[:5]
5129

5130 5131
    security.declareProtected(Permissions.ManagePortal, 'storeTemplateItemData')
    def storeTemplateItemData(self):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
5132
      """
5133
        Instanciate and Store Template items into properties.
Jean-Paul Smets's avatar
Jean-Paul Smets committed
5134
      """
5135
      # Store all Data
5136 5137
      self._portal_type_item = \
          PortalTypeTemplateItem(self.getTemplatePortalTypeIdList())
5138
      self._portal_type_workflow_chain_item = \
5139
          PortalTypeWorkflowChainTemplateItem(self.getTemplatePortalTypeWorkflowChainList())
5140 5141 5142 5143
      self._workflow_item = \
          WorkflowTemplateItem(self.getTemplateWorkflowIdList())
      self._skin_item = \
          SkinTemplateItem(self.getTemplateSkinIdList())
5144 5145 5146
      self._registered_skin_selection_item = \
          RegisteredSkinSelectionTemplateItem(
              self.getTemplateRegisteredSkinSelectionList())
5147 5148 5149
      self._registered_version_priority_selection_item = \
          RegisteredVersionPrioritySelectionTemplateItem(
              self.getTemplateRegisteredVersionPrioritySelectionList())
5150 5151 5152 5153 5154 5155
      self._category_item = \
          CategoryTemplateItem(self.getTemplateBaseCategoryList())
      self._catalog_method_item = \
          CatalogMethodTemplateItem(self.getTemplateCatalogMethodIdList())
      self._action_item = \
          ActionTemplateItem(self.getTemplateActionPathList())
5156
      self._portal_type_roles_item = \
5157
          PortalTypeRolesTemplateItem(self.getTemplatePortalTypeRoleList())
5158 5159 5160 5161 5162 5163 5164
      self._site_property_item = \
          SitePropertyTemplateItem(self.getTemplateSitePropertyIdList())
      self._module_item = \
          ModuleTemplateItem(self.getTemplateModuleIdList())
      self._document_item = \
          DocumentTemplateItem(self.getTemplateDocumentIdList())
      self._property_sheet_item = \
5165 5166
          PropertySheetTemplateItem(self.getTemplatePropertySheetIdList(),
                                    context=self)
5167 5168
      self._constraint_item = \
          ConstraintTemplateItem(self.getTemplateConstraintIdList())
5169 5170 5171 5172 5173 5174 5175 5176 5177 5178 5179 5180 5181 5182 5183 5184 5185 5186 5187 5188
      self._extension_item = \
          ExtensionTemplateItem(self.getTemplateExtensionIdList())
      self._test_item = \
          TestTemplateItem(self.getTemplateTestIdList())
      self._product_item = \
          ProductTemplateItem(self.getTemplateProductIdList())
      self._role_item = \
          RoleTemplateItem(self.getTemplateRoleList())
      self._catalog_result_key_item = \
          CatalogResultKeyTemplateItem(
               self.getTemplateCatalogResultKeyList())
      self._catalog_related_key_item = \
          CatalogRelatedKeyTemplateItem(
               self.getTemplateCatalogRelatedKeyList())
      self._catalog_result_table_item = \
          CatalogResultTableTemplateItem(
               self.getTemplateCatalogResultTableList())
      self._message_translation_item = \
          MessageTranslationTemplateItem(
               self.getTemplateMessageTranslationList())
5189 5190 5191 5192 5193 5194 5195 5196 5197 5198 5199 5200
      self._portal_type_allowed_content_type_item = \
           PortalTypeAllowedContentTypeTemplateItem(
               self.getTemplatePortalTypeAllowedContentTypeList())
      self._portal_type_hidden_content_type_item = \
           PortalTypeHiddenContentTypeTemplateItem(
               self.getTemplatePortalTypeHiddenContentTypeList())
      self._portal_type_property_sheet_item = \
           PortalTypePropertySheetTemplateItem(
               self.getTemplatePortalTypePropertySheetList())
      self._portal_type_base_category_item = \
           PortalTypeBaseCategoryTemplateItem(
               self.getTemplatePortalTypeBaseCategoryList())
5201 5202
      self._path_item = \
               PathTemplateItem(self.getTemplatePathList())
5203 5204
      self._preference_item = \
               PreferenceTemplateItem(self.getTemplatePreferenceList())
5205 5206 5207
      self._catalog_search_key_item = \
          CatalogSearchKeyTemplateItem(
               self.getTemplateCatalogSearchKeyList())
5208 5209
      self._catalog_keyword_key_item = \
          CatalogKeywordKeyTemplateItem(
5210
               self.getTemplateCatalogKeywordKeyList())
5211 5212 5213
      self._catalog_datetime_key_item = \
          CatalogDateTimeKeyTemplateItem(
               self.getTemplateCatalogDatetimeKeyList())
5214 5215
      self._catalog_full_text_key_item = \
          CatalogFullTextKeyTemplateItem(
5216
               self.getTemplateCatalogFullTextKeyList())
5217 5218
      self._catalog_request_key_item = \
          CatalogRequestKeyTemplateItem(
5219
               self.getTemplateCatalogRequestKeyList())
5220 5221
      self._catalog_multivalue_key_item = \
          CatalogMultivalueKeyTemplateItem(
5222
               self.getTemplateCatalogMultivalueKeyList())
5223 5224 5225
      self._catalog_topic_key_item = \
          CatalogTopicKeyTemplateItem(
               self.getTemplateCatalogTopicKeyList())
Aurel's avatar
Aurel committed
5226 5227
      self._local_roles_item = \
          LocalRolesTemplateItem(
5228
               self.getTemplateLocalRoleList())
Yoshinori Okuji's avatar
Yoshinori Okuji committed
5229 5230 5231
      self._tool_item = \
          ToolTemplateItem(
               self.getTemplateToolIdList())
5232 5233 5234
      self._catalog_scriptable_key_item = \
          CatalogScriptableKeyTemplateItem(
               self.getTemplateCatalogScriptableKeyList())
5235 5236 5237 5238 5239 5240
      self._catalog_role_key_item = \
          CatalogRoleKeyTemplateItem(
               self.getTemplateCatalogRoleKeyList())
      self._catalog_local_role_key_item = \
          CatalogLocalRoleKeyTemplateItem(
               self.getTemplateCatalogLocalRoleKeyList())
5241 5242
      try:
        self._catalog_security_uid_column_item = \
5243 5244
          CatalogSecurityUidColumnTemplateItem(
               self.getTemplateCatalogSecurityUidColumnList())
5245 5246 5247 5248
        self._interface_item = \
          InterfaceTemplateItem(self.getTemplateInterfaceIdList())
        self._mixin_item = \
          MixinTemplateItem(self.getTemplateMixinIdList())
5249 5250 5251 5252
      except AttributeError:
        # be backwards compatible with old zope instances which
        # do not contain recent version of erp5_property_sheets
        pass
5253

5254
    security.declareProtected(Permissions.ManagePortal, 'build')
5255
    def build(self, no_action=0, update_revision=True):
5256 5257 5258 5259 5260 5261 5262 5263 5264 5265 5266
      """
        Copy existing portal objects to self
      """
      if no_action: return
        # this is use at import of Business Template to get the status built
      # Make sure that everything is sane.
      self.clean()

      self._setTemplateFormatVersion(1)
      self.storeTemplateItemData()

5267
      # Build each part
5268
      for item_name in item_name_list:
5269
        item = getattr(self, item_name)
5270 5271
        if item is None:
          continue
5272 5273 5274
        if self.getBtForDiff():
          item.is_bt_for_diff = 1
        item.build(self)
5275 5276
      # update _p_jar property of objects cleaned by removeProperties
      transaction.savepoint(optimistic=True)
5277 5278
      if update_revision:
        self._export()
5279 5280

    def publish(self, url, username=None, password=None):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
5281
      """
5282
        Publish in a format or another
Jean-Paul Smets's avatar
Jean-Paul Smets committed
5283
      """
5284
      return self.portal_templates.publish(self, url, username=username,
5285
                                           password=password)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
5286

5287
    security.declareProtected(Permissions.ManagePortal, 'update')
5288
    def update(self):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
5289
      """
5290
        Update template: download new template definition
Jean-Paul Smets's avatar
Jean-Paul Smets committed
5291
      """
5292
      return self.portal_templates.update(self)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
5293

5294
    security.declareProtected(Permissions.ManagePortal, 'isCatalogUpdatable')
5295 5296 5297 5298 5299
    def isCatalogUpdatable(self):
      """
      Return if catalog will be updated or not by business template installation
      """
      catalog_method = getattr(self, '_catalog_method_item', None)
5300 5301 5302
      default_catalog = self.getPortalObject().portal_catalog.getSQLCatalog()
      my_catalog = _getCatalogValue(self)
      if default_catalog is not None and my_catalog is not None \
5303
             and catalog_method is not None:
5304 5305 5306 5307 5308 5309
        if default_catalog.getId() == my_catalog.getId():
          # It is needed to update the catalog only if the default SQLCatalog is modified.
          for method_id in catalog_method._objects.keys():
            if 'related' not in method_id:
              # must update catalog
              return True
5310 5311
      return False

5312
    security.declareProtected(Permissions.ManagePortal, 'preinstall')
5313
    def preinstall(self, check_dependencies=1, **kw):
5314 5315 5316
      """
        Return the list of modified/new/removed object between a Business Template
        and the one installed if exists
Aurel's avatar
Aurel committed
5317
      """
5318 5319 5320 5321 5322
      if check_dependencies:
        # required because in multi installation, dependencies has already
        # been checked before and it will failed here as dependencies can be
        # installed at the same time
        self.checkDependencies()
5323

5324 5325
      modified_object_list = {}
      bt_title = self.getTitle()
5326 5327 5328 5329 5330 5331 5332

      #  can be call to diff two Business Template in template tool
      bt2 = kw.get('compare_to', None)
      if  bt2 is not None:
        installed_bt = bt2
      else:
        installed_bt = self.portal_templates.getInstalledBusinessTemplate(title=bt_title)
5333

5334
      # if reinstall business template, must compare to object in ZODB
5335
      # and not to those in the installed Business Template because it is itself.
Christophe Dumez's avatar
Christophe Dumez committed
5336
      # same if we make a diff and select only one business template
5337
      reinstall = 0
5338 5339
      if installed_bt == self:
        reinstall = 1
5340
        if self.portal_templates._getOb(INSTALLED_BT_FOR_DIFF, None) is None:
5341
          bt2 = self.portal_templates.manage_clone(ob=installed_bt,
5342
                                                   id=INSTALLED_BT_FOR_DIFF)
5343 5344 5345 5346 5347 5348 5349
          # update portal types properties to get last modifications
          bt2.getPortalTypesProperties()
          bt2.edit(description='tmp bt generated for diff', bt_for_diff=1)
          bt2.build()
          installed_bt = bt2
        else:
          installed_bt = self.portal_templates._getOb(INSTALLED_BT_FOR_DIFF)
5350

5351
      for item_name in item_name_list:
5352
        new_item = getattr(self, item_name, None)
5353
        installed_item = getattr(installed_bt, item_name, None)
5354
        if new_item is not None:
5355
          if installed_item is not None and hasattr(installed_item, '_objects'):
5356
            modified_object = new_item.preinstall(context=self,
5357 5358
                                                  installed_item=installed_item,
                                                  installed_bt=installed_bt)
5359 5360 5361
            if len(modified_object) > 0:
              modified_object_list.update(modified_object)
          else:
5362 5363
            modified_object_list.update(dict.fromkeys(new_item._objects,
              ('New', new_item.__class__.__name__[:-12])))
5364 5365

      if reinstall:
5366
        self.portal_templates.manage_delObjects(ids=[INSTALLED_BT_FOR_DIFF])
5367

5368 5369
      return modified_object_list

5370
    def _install(self, force=1, object_to_update=None, update_translation=0,
5371
                 update_catalog=False, check_dependencies=True, **kw):
5372
      """
Christophe Dumez's avatar
Christophe Dumez committed
5373
        Install a new Business Template, if force, all will be upgraded or installed
5374 5375
        otherwise depends of dict object_to_update
      """
5376 5377 5378 5379
      if object_to_update is not None:
        force=0
      else:
        object_to_update = {}
5380

5381 5382
      site = self.getPortalObject()
      installed_bt = site.portal_templates.getInstalledBusinessTemplate(
5383
                                                           self.getTitle())
5384 5385
      # When reinstalling, installation state should not change to replaced
      if installed_bt not in [None, self]:
5386 5387 5388
        if site.portal_workflow.isTransitionPossible(
            installed_bt, 'replace'):
          installed_bt.replace(self)
5389

5390
      trash_tool = getToolByName(site, 'portal_trash', None)
5391
      if trash_tool is None:
5392 5393
        raise AttributeError, 'Trash Tool is not installed'

5394
      if not force and check_dependencies:
5395 5396
        self.checkDependencies()

5397 5398
      # always created a trash bin because we may to save object already present
      # but not in a previous business templates apart at creation of a new site
5399
      if trash_tool is not None and (len(object_to_update) > 0 or len(self.portal_templates) > 2):
5400 5401 5402 5403 5404
        trashbin = trash_tool.newTrashBin(self.getTitle(), self)
      else:
        trashbin = None

      # Install everything
5405
      if len(object_to_update) or force:
5406
        for item_name in item_name_list:
5407 5408
          item = getattr(self, item_name, None)
          if item is not None:
5409
            item.install(self, force=force, object_to_update=object_to_update,
5410
                               trashbin=trashbin, installed_bt=installed_bt)
5411

5412
      if update_catalog:
5413
        catalog = _getCatalogValue(self)
5414
        if (catalog is None) or (not site.isIndexable):
5415 5416 5417 5418 5419
          LOG('Business Template', 0, 'no SQL Catalog available')
          update_catalog = 0
        else:
          LOG('Business Template', 0, 'Updating SQL Catalog')
          catalog.manage_catalogClear()
5420

5421
      # get objects to remove
5422
      # do remove after because we may need backup object from installation
5423
      remove_object_dict = {}
5424 5425
      for path, action in object_to_update.iteritems():
        if action in ('remove', 'save_and_remove'):
5426
          remove_object_dict[path] = action
5427

5428
      # remove object from old business template
5429
      if len(remove_object_dict):
5430
        # XXX: this code assumes that there is an installed_bt
5431
        for item_name in reversed(item_name_list):
5432
          item = getattr(installed_bt, item_name, None)
5433
          if item is not None:
5434
            item.remove(self, remove_object_dict=remove_object_dict, trashbin=trashbin)
5435

5436 5437

      # update tools if necessary
5438 5439 5440 5441 5442
      if self.getTitle() == 'erp5_core' and self.getTemplateUpdateTool():
        from Products.ERP5.ERP5Site import ERP5Generator
        gen = getattr(site, '_generator_class', ERP5Generator)()
        LOG('Business Template', 0, 'Updating Tools')
        gen.setup(site, 0, update=1)
5443

5444 5445
      # remove trashbin if empty
      if trashbin is not None:
5446
        if len(trashbin) == 0:
5447 5448
          trash_tool.manage_delObjects([trashbin.getId(),])

5449 5450
      if update_catalog:
        site.ERP5Site_reindexAll()
5451

5452 5453
      # Update translation table, in case we added new portal types or
      # workflow states.
5454 5455
      if update_translation:
        site.ERP5Site_updateTranslationTable()
5456

5457 5458
      # Clear cache to avoid reusing cached values with replaced objects.
      site.portal_caches.clearAllCache()
5459

5460
    security.declareProtected(Permissions.ManagePortal, 'install')
5461
    install = _install
5462

5463
    security.declareProtected(Permissions.ManagePortal, 'reinstall')
5464
    reinstall = _install
Jean-Paul Smets's avatar
Jean-Paul Smets committed
5465

5466
    security.declareProtected(Permissions.ManagePortal, 'trash')
5467 5468
    def trash(self, new_bt, **kw):
      """
5469
        Trash unnecessary items before upgrading to a new business
5470
        template.
5471
        This is similar to uninstall, but different in that this does
5472
        not remove all items.
5473
      """
5474
      # Trash everything
5475
      for item_name in reversed(item_name_list):
5476
        item = getattr(self, item_name, None)
5477 5478
        if item is not None:
          item.trash(
5479
                self,
5480
                getattr(new_bt, item_name))
5481

5482
    def _uninstall(self, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
5483
      """
5484
        For uninstall based on paramaters provided in **kw
Jean-Paul Smets's avatar
Jean-Paul Smets committed
5485
      """
5486 5487
      # Uninstall everything
      # Trash everything
5488
      for item_name in reversed(item_name_list):
5489
        item = getattr(self, item_name, None)
5490
        if item is not None:
5491
          item.uninstall(self, **kw)
5492
      # It is better to clear cache because the uninstallation of a
5493
      # template deletes many things from the portal.
Aurel's avatar
Aurel committed
5494
      self.getPortalObject().portal_caches.clearAllCache()
5495

5496 5497
    security.declareProtected(Permissions.ManagePortal, 'uninstall')
    uninstall = _uninstall
5498

Yoshinori Okuji's avatar
Yoshinori Okuji committed
5499
    def _clean(self):
5500
      """
5501
        Clean built information.
5502
      """
5503
      # First, remove obsolete attributes if present.
5504 5505 5506 5507
      for attr in ( '_action_archive',
                    '_document_archive',
                    '_extension_archive',
                    '_test_archive',
5508
                    '_module_archive',
5509 5510 5511
                    '_object_archive',
                    '_portal_type_archive',
                    '_property_archive',
5512
                    '_property_sheet_archive'):
5513 5514 5515
        if hasattr(self, attr):
          delattr(self, attr)
      # Secondly, make attributes empty.
5516
      for item_name in item_name_list:
5517
        setattr(self, item_name, None)
5518

5519 5520
    security.declareProtected(Permissions.ManagePortal, 'clean')
    clean = _clean
Jean-Paul Smets's avatar
Jean-Paul Smets committed
5521

5522
    security.declareProtected(Permissions.AccessContentsInformation,
5523
                              'getBuildingState')
5524
    def getBuildingState(self, default=None, id_only=1):
5525
      """
5526
        Returns the current state in building
5527
      """
5528
      portal_workflow = getToolByName(self, 'portal_workflow')
5529 5530
      wf = portal_workflow.getWorkflowById(
                          'business_template_building_workflow')
5531
      return wf._getWorkflowStateOf(self, id_only=id_only )
5532

5533
    security.declareProtected(Permissions.AccessContentsInformation,
5534
                              'getInstallationState')
5535
    def getInstallationState(self, default=None, id_only=1):
5536
      """
5537
        Returns the current state in installation
5538
      """
5539
      portal_workflow = getToolByName(self.getPortalObject(), 'portal_workflow')
5540 5541
      wf = portal_workflow.getWorkflowById(
                           'business_template_installation_workflow')
5542
      return wf._getWorkflowStateOf(self, id_only=id_only )
Jean-Paul Smets's avatar
Jean-Paul Smets committed
5543

Yoshinori Okuji's avatar
Yoshinori Okuji committed
5544 5545 5546 5547 5548 5549
    security.declareProtected(Permissions.AccessContentsInformation, 'toxml')
    def toxml(self):
      """
        Return this Business Template in XML
      """
      portal_templates = getToolByName(self, 'portal_templates')
5550
      export_string = portal_templates.manage_exportObject(
5551 5552
                                               id=self.getId(),
                                               toxml=1,
5553
                                               download=1)
Yoshinori Okuji's avatar
Yoshinori Okuji committed
5554
      return export_string
5555

5556
    def _getOrderedList(self, id):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
5557
      """
5558 5559
        We have to set this method because we want an
        ordered list
Jean-Paul Smets's avatar
Jean-Paul Smets committed
5560
      """
5561 5562
      method_id = '_baseGet%sList' % convertToUpperCase(id)
      result = getattr(self, method_id)(())
5563 5564 5565 5566
      if result is None: result = ()
      if result != ():
        result = list(result)
        result.sort()
5567
        # XXX Why do we need to return a tuple ?
5568 5569
        result = tuple(result)
      return result
Jean-Paul Smets's avatar
Jean-Paul Smets committed
5570

5571
    security.declareProtected(Permissions.AccessContentsInformation, 'getTemplateCatalogMethodIdList')
5572
    def getTemplateCatalogMethodIdList(self):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
5573
      """
5574 5575
      We have to set this method because we want an
      ordered list
Jean-Paul Smets's avatar
Jean-Paul Smets committed
5576
      """
5577
      return self._getOrderedList('template_catalog_method_id')
Jean-Paul Smets's avatar
Jean-Paul Smets committed
5578

5579
    security.declareProtected(Permissions.AccessContentsInformation, 'getTemplateBaseCategoryList')
5580
    def getTemplateBaseCategoryList(self):
5581
      """
5582 5583
      We have to set this method because we want an
      ordered list
5584
      """
5585
      return self._getOrderedList('template_base_category')
5586

5587
    security.declareProtected(Permissions.AccessContentsInformation, 'getTemplateWorkflowIdList')
5588
    def getTemplateWorkflowIdList(self):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
5589
      """
5590 5591
      We have to set this method because we want an
      ordered list
Jean-Paul Smets's avatar
Jean-Paul Smets committed
5592
      """
5593
      return self._getOrderedList('template_workflow_id')
5594

5595
    security.declareProtected(Permissions.AccessContentsInformation, 'getTemplatePortalTypeIdList')
5596
    def getTemplatePortalTypeIdList(self):
5597
      """
5598 5599
      We have to set this method because we want an
      ordered list
5600
      """
5601
      return self._getOrderedList('template_portal_type_id')
5602

5603
    security.declareProtected(Permissions.AccessContentsInformation, 'getTemplatePortalTypeWorkflowChainList')
5604 5605 5606 5607 5608 5609 5610
    def getTemplatePortalTypeWorkflowChainList(self):
      """
      We have to set this method because we want an
      ordered list
      """
      return self._getOrderedList('template_portal_type_workflow_chain')

5611
    security.declareProtected(Permissions.AccessContentsInformation, 'getTemplatePathList')
Alexandre Boeglin's avatar
Alexandre Boeglin committed
5612 5613 5614 5615 5616 5617 5618
    def getTemplatePathList(self):
      """
      We have to set this method because we want an
      ordered list
      """
      return self._getOrderedList('template_path')

5619
    security.declareProtected(Permissions.AccessContentsInformation, 'getTemplatePreferenceList')
5620 5621 5622 5623 5624 5625 5626
    def getTemplatePreferenceList(self):
      """
      We have to set this method because we want an
      ordered list
      """
      return self._getOrderedList('template_preference')

5627
    security.declareProtected(Permissions.AccessContentsInformation, 'getTemplatePortalTypeAllowedContentTypeList')
5628 5629 5630 5631 5632 5633
    def getTemplatePortalTypeAllowedContentTypeList(self):
      """
      We have to set this method because we want an
      ordered list
      """
      return self._getOrderedList('template_portal_type_allowed_content_type')
5634

5635
    security.declareProtected(Permissions.AccessContentsInformation, 'getTemplatePortalTypeHiddenContentTypeList')
5636 5637 5638 5639 5640 5641 5642
    def getTemplatePortalTypeHiddenContentTypeList(self):
      """
      We have to set this method because we want an
      ordered list
      """
      return self._getOrderedList('template_portal_type_hidden_content_type')

5643
    security.declareProtected(Permissions.AccessContentsInformation, 'getTemplatePortalTypePropertySheetList')
5644 5645 5646 5647 5648 5649 5650
    def getTemplatePortalTypePropertySheetList(self):
      """
      We have to set this method because we want an
      ordered list
      """
      return self._getOrderedList('template_portal_type_property_sheet')

5651
    security.declareProtected(Permissions.AccessContentsInformation, 'getTemplatePortalTypeBaseCategoryList')
5652 5653 5654 5655 5656 5657 5658
    def getTemplatePortalTypeBaseCategoryList(self):
      """
      We have to set this method because we want an
      ordered list
      """
      return self._getOrderedList('template_portal_type_base_category')

5659
    security.declareProtected(Permissions.AccessContentsInformation, 'getTemplateActionPathList')
5660
    def getTemplateActionPathList(self):
5661
      """
5662 5663
      We have to set this method because we want an
      ordered list
5664
      """
5665
      return self._getOrderedList('template_action_path')
5666

5667
    security.declareProtected(Permissions.AccessContentsInformation, 'getTemplatePortalTypeRoleList')
5668
    def getTemplatePortalTypeRoleList(self):
5669 5670 5671 5672
      """
      We have to set this method because we want an
      ordered list
      """
5673
      return self._getOrderedList('template_portal_type_role')
5674

5675
    security.declareProtected(Permissions.AccessContentsInformation, 'getTemplateLocalRoleList')
5676
    def getTemplateLocalRoleList(self):
5677 5678 5679 5680
      """
      We have to set this method because we want an
      ordered list
      """
5681
      return self._getOrderedList('template_local_role')
5682

5683
    security.declareProtected(Permissions.AccessContentsInformation, 'getTemplateSkinIdList')
5684
    def getTemplateSkinIdList(self):
5685
      """
5686 5687
      We have to set this method because we want an
      ordered list
5688
      """
5689
      return self._getOrderedList('template_skin_id')
5690

5691
    security.declareProtected(Permissions.AccessContentsInformation, 'getTemplateRegisteredSkinSelectionList')
5692 5693 5694 5695 5696 5697 5698
    def getTemplateRegisteredSkinSelectionList(self):
      """
      We have to set this method because we want an
      ordered list
      """
      return self._getOrderedList('template_registered_skin_selection')

5699
    security.declareProtected(Permissions.AccessContentsInformation, 'getTemplateRegisteredVersionPrioritySelectionList')
5700 5701 5702 5703 5704
    def getTemplateRegisteredVersionPrioritySelectionList(self):
      """
      We have to set this method because we want an
      ordered list
      """
5705 5706 5707 5708 5709 5710
      try:
        return self._getOrderedList('template_registered_version_priority_selection')
      # This property may not be defined if erp5_property_sheets has not been
      # upgraded yet
      except AttributeError:
        return ()
5711

5712
    security.declareProtected(Permissions.AccessContentsInformation, 'getTemplateModuleIdList')
5713
    def getTemplateModuleIdList(self):
5714
      """
5715 5716
      We have to set this method because we want an
      ordered list
5717
      """
5718
      return self._getOrderedList('template_module_id')
5719

5720
    security.declareProtected(Permissions.AccessContentsInformation, 'getTemplateMessageTranslationList')
5721 5722 5723 5724 5725 5726
    def getTemplateMessageTranslationList(self):
      """
      We have to set this method because we want an
      ordered list
      """
      return self._getOrderedList('template_message_translation')
5727

5728
    security.declareProtected(Permissions.AccessContentsInformation, 'getTemplateToolIdList')
Yoshinori Okuji's avatar
Yoshinori Okuji committed
5729 5730 5731 5732 5733 5734 5735
    def getTemplateToolIdList(self):
      """
      We have to set this method because we want an
      ordered list
      """
      return self._getOrderedList('template_tool_id')

5736
    def _isInKeepList(self, keep_list, path):
5737 5738 5739
      for keep_path in keep_list:
        if keep_path.endswith('**') and path.startswith(keep_path[:-2]):
          return True
5740 5741 5742
        elif keep_path.endswith('*') and path.startswith(keep_path[:-1])\
            and len(keep_path.split('/')) == len(path.split('/')):
          return True
5743 5744 5745 5746
        elif path == keep_path:
          return True
      return False

5747
    security.declarePrivate('isKeepObject')
5748 5749 5750 5751 5752 5753
    def isKeepObject(self, path):
      """
      Return True if path is included in keep object list.
      """
      return self._isInKeepList(self.getTemplateKeepPathList(), path)

5754
    security.declarePrivate('isKeepWorkflowObject')
5755 5756 5757 5758
    def isKeepWorkflowObject(self, path):
      """
      Return True if path is included in keep workflow object list.
      """
5759 5760
      return self._isInKeepList(self.getTemplateKeepWorkflowPathList(), path)

5761
    security.declarePrivate('isKeepWorkflowObjectLastHistoryOnly')
5762 5763 5764 5765 5766 5767
    def isKeepWorkflowObjectLastHistoryOnly(self, path):
      """
      Return True if path is included in keep workflow last state only list
      """
      return self._isInKeepList(self.getTemplateKeepLastWorkflowHistoryOnlyPathList(),
                                path)
5768

5769
    security.declarePrivate('getExportPath')
5770 5771 5772 5773 5774
    def getExportPath(self):
      preferences = self.getPortalObject().portal_preferences
      bt_name = self.getTitle()
      from App.config import getConfiguration
      instance_home = getConfiguration().instancehome
5775
      for path in (preferences.getPreferredWorkingCopyList() or ['bt5']):
5776
        path = os.path.expanduser(path)
5777 5778 5779 5780 5781 5782 5783 5784 5785
        if not os.path.isabs(path):
          path = os.path.join(instance_home, path)
        bt_path = os.path.join(path, bt_name)
        if os.path.isdir(bt_path):
          return bt_path
        for bt_path in glob.glob(os.path.join(path, '*', bt_name)):
          if os.path.isdir(bt_path):
            return bt_path

5786 5787 5788
    @transactional_cached(lambda self, vcs=None, path=None, restricted=False:
                          (self, vcs, path, restricted))
    def _getVcsTool(self, vcs=None, path=None, restricted=False):
5789
      from Products.ERP5VCS.WorkingCopy import getVcsTool
5790 5791
      if not (path or vcs):
        path = self.getExportPath()
5792 5793 5794 5795
      return getVcsTool(vcs, path, restricted).__of__(self)

    def getVcsTool(self, vcs=None, path=None):
      return self._getVcsTool(vcs, path, True)
5796 5797 5798

    def isVcsType(self, *vcs):
      # could be moved to Products.ERP5.Base.Base
5799
      from Products.ERP5VCS.WorkingCopy import NotAWorkingCopyError
5800 5801 5802 5803 5804
      try:
        return self.getVcsTool().reference in vcs
      except NotAWorkingCopyError:
        return None in vcs

5805
    security.declareProtected(Permissions.ManagePortal, 'export')
5806
    def export(self, path=None, local=0, bta=None, **kw):
Aurel's avatar
Aurel committed
5807 5808 5809
      """
        Export this Business Template
      """
5810
      if self.getBuildingState() != 'built':
5811
        raise TemplateConditionError, \
Christophe Dumez's avatar
Christophe Dumez committed
5812
              'Business Template must be built before export'
5813
      return self._export(path, local, bta)
5814

5815
    def _export(self, path=None, local=0, bta=None):
5816 5817 5818
      if bta is None:
        if local:
          # we export into a folder tree
5819
          bta = BusinessTemplateFolder(path, creation=1)
5820 5821 5822 5823
        else:
          # We export BT into a tarball file
          if path is None:
            path = self.getTitle()
5824
          bta = BusinessTemplateTarball(path, creation=1)
Aurel's avatar
Aurel committed
5825

5826
      # export bt
Aurel's avatar
Aurel committed
5827
      for prop in self.propertyMap():
5828
        prop_type = prop['type']
Aurel's avatar
Aurel committed
5829
        id = prop['id']
5830
        if id in ('id', 'uid', 'rid', 'sid', 'id_group', 'last_id', 'revision',
5831
                  'install_object_list_list', 'id_generator', 'bt_for_diff'):
5832
          continue
Aurel's avatar
Aurel committed
5833
        value = self.getProperty(id)
5834
        if not value:
5835
          continue
5836
        if prop_type in ('text', 'string', 'int', 'boolean'):
5837
          bta.addObject(str(value), name=id, path='bt', ext='')
5838
        elif prop_type in ('lines', 'tokens'):
5839
          bta.addObject('\n'.join(value), name=id, path='bt', ext='')
5840

Aurel's avatar
Aurel committed
5841
      # Export each part
5842
      for item_name in item_name_list:
5843 5844 5845
        item = getattr(self, item_name, None)
        if item is not None:
          item.export(context=self, bta=bta)
5846

5847
      self._setRevision(bta.getRevision())
5848
      return bta.finishCreation()
Aurel's avatar
Aurel committed
5849

5850
    security.declareProtected(Permissions.ManagePortal, 'importFile')
5851
    def importFile(self, path):
Aurel's avatar
Aurel committed
5852
      """
5853
        Import all xml files in Business Template
Aurel's avatar
Aurel committed
5854
      """
5855 5856
      bta = (BusinessTemplateFolder if os.path.isdir(path) else
             BusinessTemplateTarball)(path, importing=1)
5857 5858 5859 5860 5861 5862 5863 5864 5865 5866 5867 5868 5869 5870 5871 5872
      bt_item = bt()
      bta.importFiles(bt_item)
      prop_dict = {}
      for prop in self.propertyMap():
        pid = prop['id']
        if pid != 'id':
          prop_type = prop['type']
          value = bt_item.get(pid)
          if prop_type in ('text', 'string'):
            prop_dict[pid] = value or ''
          elif prop_type in ('int', 'boolean'):
            prop_dict[pid] = value or 0
          elif prop_type in ('lines', 'tokens'):
            prop_dict[pid[:-5]] = (value or '').splitlines()
      self._edit(**prop_dict)

5873 5874 5875 5876 5877
      from Products.ERP5VCS.WorkingCopy import NotAWorkingCopyError
      try:
        vcs_tool = self._getVcsTool(path=path)
      except NotAWorkingCopyError:
        pass
Aurel's avatar
Aurel committed
5878
      else:
5879 5880 5881 5882 5883 5884 5885
        comment = translateString(
          'Downloaded from ${type} repository at revision ${revision}',
          mapping={'type': vcs_tool.title,
                   'revision': vcs_tool.getRevision(True)})
        workflow_tool = self.getPortalObject().portal_workflow
        workflow_tool.business_template_building_workflow.notifyWorkflowMethod(
          self, 'edit', kw={'comment': comment})
5886 5887

      self.storeTemplateItemData()
5888

5889 5890
      # Create temporary modules/classes for classes defined by this BT.
      # This is required if the BT contains instances of one of these classes.
5891 5892
      # XXX This is not required with portal types as classes.
      #     It is still there for compatibility with non-migrated objects.
5893
      module_id_list = []
5894
      for template_id in self.getTemplateDocumentIdList():
5895 5896 5897
        module_id = 'Products.ERP5Type.Document.' + template_id
        if module_id not in sys.modules:
          module_id_list.append(module_id)
5898
          sys.modules[module_id] = module = imp.new_module(module_id)
5899 5900
          setattr(module, template_id, type(template_id,
            (SimpleItem.SimpleItem,), {'__module__': module_id}))
5901

5902
      for item_name in item_name_list:
Ivan Tyagov's avatar
Ivan Tyagov committed
5903
        item_object = getattr(self, item_name, None)
5904 5905
        # this check is due to backwards compatability when there can be a
        # difference between install erp5_property_sheets (esp. BusinessTemplate
5906
        # property sheet)
5907 5908
        if item_object is not None:
          item_object.importFile(bta)
5909

5910 5911
      # Remove temporary modules created above to allow import of real modules
      # (during the installation).
5912 5913
      for module_id in module_id_list:
        del sys.modules[module_id]
5914

5915 5916
      self._setRevision(bta.getRevision())

5917
    security.declareProtected(Permissions.AccessContentsInformation, 'getItemsList')
5918 5919 5920 5921
    def getItemsList(self):
      """Return list of items in business template
      """
      items_list = []
5922
      for item_name in item_name_list:
5923 5924 5925
        item = getattr(self, item_name, None)
        if item is not None:
          items_list.extend(item.getKeys())
5926
      return items_list
5927

5928
    security.declareProtected(Permissions.ManagePortal, 'checkDependencies')
5929 5930 5931 5932 5933 5934
    def checkDependencies(self):
      """
       Check if all the dependencies of the business template
       are installed. Raise an exception with the list of
       missing dependencies if some are missing
      """
5935 5936
      missing_dep_list = self.getMissingDependencyList()
      if len(missing_dep_list) != 0:
5937 5938 5939
        raise BusinessTemplateMissingDependency, \
          'Impossible to install %s, please install the following dependencies before: %s' \
          %(self.getTitle(), repr(missing_dep_list))
5940

5941
    security.declareProtected(Permissions.ManagePortal, 'getMissingDependencyList')
5942 5943 5944 5945
    def getMissingDependencyList(self):
      """
      Retuns a list of missing dependencies.
      """
5946 5947
      missing_dep_list = []
      dependency_list = self.getDependencyList()
5948
      if len(dependency_list) > 0:
5949
        for dependency_couple in dependency_list:
5950
          dependency_couple_list = dependency_couple.strip().split(' ', 1)
5951
          dependency = dependency_couple_list[0]
5952 5953
          if dependency in (None, ''):
            continue
5954 5955
          version_restriction = None
          if len(dependency_couple_list) > 1:
5956
            version_restriction = dependency_couple_list[1]
5957 5958 5959
            if version_restriction.startswith('('):
              # Something like "(>= 1.0rc6)".
              version_restriction = version_restriction[1:-1]
5960 5961 5962
          installed_bt = self.portal_templates.getInstalledBusinessTemplate(dependency)
          if (not self.portal_templates.IsOneProviderInstalled(dependency)) \
             and ((installed_bt is None) \
5963
                  or (version_restriction not in (None, '') and
5964 5965
                     (not self.portal_templates.compareVersionStrings(installed_bt.getVersion(), version_restriction)))):
            missing_dep_list.append((dependency, version_restriction or ''))
5966
      return [' '.join([y for y in x if y]) for x in missing_dep_list]
5967

5968
    security.declareProtected(Permissions.ManagePortal, 'diffObjectAsHTML')
5969 5970 5971
    def diffObjectAsHTML(self, REQUEST, **kw):
      """
        Convert diff into a HTML format before reply
5972
        This is compatible with ERP5VCS look and feel but
5973 5974 5975 5976
        it is preferred in future we use more difflib python library.
      """
      return DiffFile(self.diffObject(REQUEST, **kw)).toHTML()

5977
    security.declareProtected(Permissions.ManagePortal, 'diffObject')
Aurel's avatar
Aurel committed
5978
    def diffObject(self, REQUEST, **kw):
5979 5980
      """
        Make a diff between an object in the Business Template
5981
        and the same in the Business Template installed in the site
5982 5983 5984 5985
      """

      class_name_dict = {
        'Product' : '_product_item',
5986
        'PropertySheet' : '_property_sheet_item',
5987
        'Constraint' : '_constraint_item',
5988
        'Document' : '_document_item',
5989 5990
        'Interface': '_interface_item',
        'Mixin': '_mixin_item',
5991 5992 5993 5994 5995 5996 5997 5998
        'Extension' : '_extension_item',
        'Test' : '_test_item',
        'Role' : '_role_item',
        'MessageTranslation' : '_message_translation_item',
        'Workflow' : '_workflow_item',
        'CatalogMethod' : '_catalog_method_item',
        'SiteProperty' : '_site_property_item',
        'PortalType' : '_portal_type_item',
5999
        'PortalTypeWorkflowChain' : '_portal_type_workflow_chain_item',
6000 6001 6002 6003
        'PortalTypeAllowedContentType' : '_portal_type_allowed_content_type_item',
        'PortalHiddenAllowedContentType' : '_portal_type_hidden_content_type_item',
        'PortalTypePropertySheet' : '_portal_type_property_sheet_item',
        'PortalTypeBaseCategory' : '_portal_type_base_category_item',
6004 6005 6006
        'Category' : '_category_item',
        'Module' : '_module_item',
        'Skin' : '_skin_item',
6007
        'RegisteredSkinSelection' : '_registered_skin_selection_item',
6008
        'Path' : '_path_item',
6009
        'Preference' : '_preference_item',
6010
        'Action' : '_action_item',
6011
        'PortalTypeRoles' : '_portal_type_roles_item',
Aurel's avatar
Aurel committed
6012
        'LocalRoles' : '_local_roles_item',
6013 6014 6015
        'CatalogResultKey' : '_catalog_result_key_item',
        'CatalogRelatedKey' : '_catalog_related_key_item',
        'CatalogResultTable' : '_catalog_result_table_item',
6016
        'CatalogSearchKey' : '_catalog_search_key_item',
6017
        'CatalogKeywordKey' : '_catalog_keyword_key_item',
6018
        'CatalogDateTimeKey' : '_catalog_datetime_key_item',
6019 6020 6021 6022
        'CatalogFullTextKey' : '_catalog_full_text_key_item',
        'CatalogRequestKey' : '_catalog_request_key_item',
        'CatalogMultivalueKey' : '_catalog_multivalue_key_item',
        'CatalogTopicKey' : '_catalog_topic_key_item',
Yoshinori Okuji's avatar
Yoshinori Okuji committed
6023
        'Tool': '_tool_item',
6024
        'CatalogScriptableKey' : '_catalog_scriptable_key_item',
6025 6026
        'CatalogRoleKey' : '_catalog_role_key_item',
        'CatalogLocalRoleKey' : '_catalog_local_role_key_item',
6027
        'CatalogSecurityUidColumn' : '_catalog_security_uid_column_item',
6028
        }
6029

6030 6031
      object_id = REQUEST.object_id
      object_class = REQUEST.object_class
6032

Christophe Dumez's avatar
Christophe Dumez committed
6033
      # Get objects
6034
      item_name = class_name_dict[object_class]
Aurel's avatar
Aurel committed
6035

6036
      new_bt = self
Christophe Dumez's avatar
Christophe Dumez committed
6037
      # Compare with a given business template
6038
      compare_to_zodb = 0
Aurel's avatar
Aurel committed
6039 6040
      bt2_id = kw.get('compare_with', None)
      if bt2_id is not None:
6041 6042
        if bt2_id == self.getId():
          compare_to_zodb = 1
6043
          installed_bt = self.getInstalledBusinessTemplate(title=self.getTitle())
6044 6045
        else:
          installed_bt = self.portal_templates._getOb(bt2_id)
Aurel's avatar
Aurel committed
6046 6047 6048
      else:
        installed_bt = self.getInstalledBusinessTemplate(title=self.getTitle())
        if installed_bt == new_bt:
Aurel's avatar
Aurel committed
6049 6050
          compare_to_zodb = 1
      if compare_to_zodb:
6051
        bt2 = self.portal_templates.manage_clone(ob=installed_bt,
6052
                                                 id=INSTALLED_BT_FOR_DIFF)
Christophe Dumez's avatar
Christophe Dumez committed
6053
        # Update portal types properties to get last modifications
Aurel's avatar
Aurel committed
6054 6055 6056
        bt2.getPortalTypesProperties()
        bt2.edit(description='tmp bt generated for diff')
        installed_bt = bt2
6057

6058 6059
      new_item = getattr(new_bt, item_name)
      installed_item = getattr(installed_bt, item_name)
6060
      if compare_to_zodb:
Aurel's avatar
Aurel committed
6061
        # XXX maybe only build for the given object to gain time
6062
        installed_item.build(self)
6063 6064 6065
      new_object = new_item._objects[object_id]
      installed_object = installed_item._objects[object_id]
      diff_msg = ''
6066

Christophe Dumez's avatar
Christophe Dumez committed
6067
      # Real Zope Objects (can be exported into XML directly by Zope)
6068 6069
      # XXX Bad naming
      item_list_1 = ['_product_item', '_workflow_item', '_portal_type_item',
6070
                     '_category_item', '_path_item', '_preference_tem',
Yoshinori Okuji's avatar
Yoshinori Okuji committed
6071
                     '_skin_item', '_action_item', '_tool_item', ]
6072

Christophe Dumez's avatar
Christophe Dumez committed
6073
      # Not considered as objects by Zope (will be exported into XML manually)
6074
      # XXX Bad naming
6075 6076
      item_list_2 = ['_site_property_item',
                     '_module_item',
6077
                     '_catalog_result_key_item',
6078
                     '_catalog_related_key_item',
6079
                     '_catalog_result_table_item',
6080
                     '_catalog_search_key_item',
6081
                     '_catalog_keyword_key_item',
6082
                     '_catalog_datetime_key_item',
6083 6084 6085 6086
                     '_catalog_full_text_key_item',
                     '_catalog_request_key_item',
                     '_catalog_multivalue_key_item',
                     '_catalog_topic_key_item',
6087
                     '_catalog_scriptable_key_item',
6088 6089
                     '_catalog_role_key_item',
                     '_catalog_local_role_key_item',
6090
                     '_catalog_security_uid_column_item',
6091 6092 6093 6094 6095 6096
                     '_portal_type_allowed_content_type_item',
                     '_portal_type_hidden_content_type_item',
                     '_portal_type_property_sheet_item',
                     '_portal_type_roles_item',
                     '_portal_type_base_category_item',
                     '_local_roles_item',
6097
                     '_portal_type_workflow_chain_item',]
6098

Christophe Dumez's avatar
Christophe Dumez committed
6099
      # Text objects (no need to export them into XML)
6100
      # XXX Bad naming
6101 6102
      item_list_3 = ['_document_item', '_interface_item', '_mixin_item',
                     '_property_sheet_item',
6103
                     '_constraint_item', '_extension_item',
6104
                     '_test_item', '_message_translation_item',]
6105

6106
      if item_name in item_list_1:
Christophe Dumez's avatar
Christophe Dumez committed
6107 6108 6109
        f1 = StringIO() # for XML export of New Object
        f2 = StringIO() # For XML export of Installed Object
        # Remove unneeded properties
6110 6111
        new_object = new_item.removeProperties(new_object, 1)
        installed_object = installed_item.removeProperties(installed_object, 1)
6112
        # XML Export in memory
6113
        OFS.XMLExportImport.exportXML(new_object._p_jar, new_object._p_oid, f1)
6114
        OFS.XMLExportImport.exportXML(installed_object._p_jar,
6115
                                      installed_object._p_oid, f2)
6116
        new_obj_xml = f1.getvalue()
6117
        f1.close()
6118 6119 6120 6121
        installed_obj_xml = f2.getvalue()
        f2.close()
        new_ob_xml_lines = new_obj_xml.splitlines()
        installed_ob_xml_lines = installed_obj_xml.splitlines()
6122
        # End of XML export
6123

6124
        # Diff between XML objects
6125 6126
        diff_list = list(unified_diff(installed_ob_xml_lines, new_ob_xml_lines, tofile=new_bt.getId(), fromfile=installed_bt.getId(), lineterm=''))
        if len(diff_list) != 0:
Christophe Dumez's avatar
Christophe Dumez committed
6127
          diff_msg += '\n\nObject %s diff :\n' % (object_id,)
6128 6129 6130
          diff_msg += '\n'.join(diff_list)
        else:
          diff_msg = 'No diff'
6131

6132
      elif item_name in item_list_2:
6133
        # Generate XML code manually
6134 6135
        new_obj_xml = new_item.generateXml(path= object_id)
        installed_obj_xml = installed_item.generateXml(path= object_id)
6136 6137
        new_obj_xml_lines = new_obj_xml.splitlines()
        installed_obj_xml_lines = installed_obj_xml.splitlines()
6138
        # End of XML Code Generation
6139

6140
        # Diff between XML objects
6141 6142
        diff_list = list(unified_diff(installed_obj_xml_lines, new_obj_xml_lines, tofile=new_bt.getId(), fromfile=installed_bt.getId(), lineterm=''))
        if len(diff_list) != 0:
Christophe Dumez's avatar
Christophe Dumez committed
6143
          diff_msg += '\n\nObject %s diff :\n' % (object_id,)
6144 6145 6146
          diff_msg += '\n'.join(diff_list)
        else:
          diff_msg = 'No diff'
6147

6148
      elif item_name in item_list_3:
6149
        # Diff between text objects
6150 6151
        if hasattr(new_object, 'getTextContent'): # ZODB component
          new_object = new_object.getTextContent()
6152
        new_obj_lines = new_object.splitlines()
6153 6154
        if hasattr(installed_object, 'getTextContent'):
          installed_object = installed_object.getTextContent()
6155 6156 6157
        installed_obj_lines = installed_object.splitlines()
        diff_list = list(unified_diff(installed_obj_lines, new_obj_lines, tofile=new_bt.getId(), fromfile=installed_bt.getId(), lineterm=''))
        if len(diff_list) != 0:
Christophe Dumez's avatar
Christophe Dumez committed
6158
          diff_msg += '\n\nObject %s diff :\n' % (object_id,)
6159 6160
          diff_msg += '\n'.join(diff_list)
        else:
6161
          diff_msg = 'No diff'
6162

6163
      else:
6164
        diff_msg += 'Unsupported file !'
Christophe Dumez's avatar
Christophe Dumez committed
6165

6166
      if compare_to_zodb:
6167
        self.portal_templates.manage_delObjects(ids=[INSTALLED_BT_FOR_DIFF])
6168

6169
      return diff_msg
6170

6171

6172
    security.declareProtected(Permissions.AccessContentsInformation, 'getPortalTypesProperties')
6173 6174 6175 6176
    def getPortalTypesProperties(self, **kw):
      """
      Fill field about properties for each portal type
      """
6177 6178
      wtool = self.getPortalObject().portal_workflow
      ttool = self.getPortalObject().portal_types
6179 6180 6181 6182 6183 6184 6185 6186 6187
      bt_allowed_content_type_list = list(
        self.getTemplatePortalTypeAllowedContentTypeList())
      bt_hidden_content_type_list = list(
        self.getTemplatePortalTypeHiddenContentTypeList())
      bt_property_sheet_list = list(
        self.getTemplatePortalTypePropertySheetList())
      bt_base_category_list = list(
        self.getTemplatePortalTypeBaseCategoryList())
      bt_action_list = list(self.getTemplateActionPathList())
6188
      bt_portal_types_id_list = list(self.getTemplatePortalTypeIdList())
6189 6190
      bt_portal_type_roles_list = list(self.getTemplatePortalTypeRoleList())
      bt_wf_chain_list = list(self.getTemplatePortalTypeWorkflowChainList())
6191

6192
      for id in bt_portal_types_id_list:
6193 6194
        portal_type = ttool.getTypeInfo(id)
        if portal_type is None:
6195
          continue
Julien Muchembled's avatar
Julien Muchembled committed
6196
        if portal_type.getRoleInformationList():
6197 6198
          if id not in bt_portal_type_roles_list:
            bt_portal_type_roles_list.append(id)
6199

6200 6201 6202 6203
        allowed_content_type_list = []
        hidden_content_type_list = []
        property_sheet_list = []
        base_category_list = []
6204
        action_list = []
6205 6206 6207 6208 6209 6210 6211
        if hasattr(portal_type, 'allowed_content_types'):
          allowed_content_type_list = portal_type.allowed_content_types
        if hasattr(portal_type, 'hidden_content_type_list'):
          hidden_content_type_list = portal_type.hidden_content_type_list
        if hasattr(portal_type, 'property_sheet_list'):
          property_sheet_list = portal_type.property_sheet_list
        if hasattr(portal_type, 'base_category_list'):
6212
          base_category_list = portal_type.base_category_list
Julien Muchembled's avatar
Julien Muchembled committed
6213 6214
        for action in portal_type.getActionInformationList():
          action_list.append(action.getReference())
6215

6216 6217 6218 6219
        for a_id in allowed_content_type_list:
          allowed_id = id+' | '+a_id
          if allowed_id not in bt_allowed_content_type_list:
            bt_allowed_content_type_list.append(allowed_id)
6220

6221
        for h_id in hidden_content_type_list:
6222 6223 6224
          hidden_id = id+' | '+h_id
          if hidden_id not in bt_hidden_content_type_list:
            bt_hidden_content_type_list.append(hidden_id)
6225

6226 6227 6228 6229
        for ps_id in property_sheet_list:
          p_sheet_id = id+' | '+ps_id
          if p_sheet_id not in bt_property_sheet_list:
            bt_property_sheet_list.append(p_sheet_id)
6230

6231 6232 6233 6234
        for bc_id in base_category_list:
          base_cat_id = id+' | '+bc_id
          if base_cat_id not in bt_base_category_list:
            bt_base_category_list.append(base_cat_id)
6235

6236 6237 6238 6239
        for act_id in action_list:
          action_id = id+' | '+act_id
          if action_id not in bt_action_list:
            bt_action_list.append(action_id)
6240

6241
        for workflow_id in [chain for chain in wtool.getChainFor(id)
6242
                                    if chain != '(Default)']:
6243 6244 6245
          wf_id = id+' | '+workflow_id
          if wf_id not in bt_wf_chain_list:
            bt_wf_chain_list.append(wf_id)
6246

6247 6248 6249 6250
      bt_allowed_content_type_list.sort()
      bt_hidden_content_type_list.sort()
      bt_property_sheet_list.sort()
      bt_base_category_list.sort()
6251
      bt_action_list.sort()
6252
      bt_wf_chain_list.sort()
6253

6254 6255 6256 6257 6258 6259 6260
      self.setTemplatePortalTypeWorkflowChainList(bt_wf_chain_list)
      self.setTemplatePortalTypeRoleList(bt_portal_type_roles_list)
      self.setTemplatePortalTypeAllowedContentTypeList(bt_allowed_content_type_list)
      self.setTemplatePortalTypeHiddenContentTypeList(bt_hidden_content_type_list)
      self.setTemplatePortalTypePropertySheetList(bt_property_sheet_list)
      self.setTemplatePortalTypeBaseCategoryList(bt_base_category_list)
      self.setTemplateActionPathList(bt_action_list)
6261 6262


6263 6264
    security.declareProtected(Permissions.AccessContentsInformation,
                              'guessPortalTypes')
6265 6266 6267 6268 6269 6270
    def guessPortalTypes(self, **kw):
      """
      This method guesses portal types based on modules define in the Business Template
      """
      bt_module_id_list = list(self.getTemplateModuleIdList())
      if len(bt_module_id_list) == 0:
6271 6272
        raise TemplateConditionError, 'No module defined in business template'

6273 6274 6275 6276 6277 6278 6279 6280
      bt_portal_types_id_list = list(self.getTemplatePortalTypeIdList())

      def getChildPortalType(type_id):
        type_list = {}
        p = self.getPortalObject()
        try:
          portal_type = p.unrestrictedTraverse('portal_types/'+type_id)
        except KeyError:
6281
          return type_list
6282 6283 6284 6285 6286 6287 6288 6289 6290 6291 6292 6293 6294 6295

        allowed_content_type_list = []
        hidden_content_type_list = []
        if hasattr(portal_type, 'allowed_content_types'):
          allowed_content_type_list = portal_type.allowed_content_types
        if hasattr(portal_type, 'hidden_content_type_list'):
          hidden_content_type_list = portal_type.hidden_content_type_list
        type_list[type_id] = ()
        # get same info for allowed portal types and hidden portal types
        for allowed_ptype_id in allowed_content_type_list:
          if allowed_ptype_id not in type_list.keys():
            type_list.update(getChildPortalType(allowed_ptype_id))
        for hidden_ptype_id in hidden_content_type_list:
          if hidden_ptype_id not in type_list.keys():
6296
            type_list.update(getChildPortalType(hidden_ptype_id))
6297
        return type_list
6298

6299 6300 6301 6302 6303 6304 6305 6306 6307 6308 6309 6310 6311 6312 6313 6314 6315 6316 6317 6318 6319 6320 6321 6322 6323 6324 6325 6326 6327 6328 6329 6330 6331 6332 6333 6334
      p = self.getPortalObject()
      portal_dict = {}
      for module_id in bt_module_id_list:
        module = p.unrestrictedTraverse(module_id)
        portal_type_id = module.getPortalType()
        try:
          portal_type = p.unrestrictedTraverse('portal_types/'+portal_type_id)
        except KeyError:
          continue
        allowed_content_type_list = []
        hidden_content_type_list = []
        if hasattr(portal_type, 'allowed_content_types'):
          allowed_content_type_list = portal_type.allowed_content_types
        if hasattr(portal_type, 'hidden_content_type_list'):
          hidden_content_type_list = portal_type.hidden_content_type_list

        portal_dict[portal_type_id] = ()

        for allowed_type_id in allowed_content_type_list:
          if allowed_type_id not in portal_dict.keys():
            portal_dict.update(getChildPortalType(allowed_type_id))

        for hidden_type_id in hidden_content_type_list:
          if hidden_type_id not in portal_dict.keys():
            portal_dict.update(getChildPortalType(hidden_type_id))

      # construct portal type list, keep already present portal types
      for id in portal_dict.keys():
        if id not in bt_portal_types_id_list:
          bt_portal_types_id_list.append(id)

      bt_portal_types_id_list.sort()

      setattr(self, 'template_portal_type_id', bt_portal_types_id_list)
      return

6335
    security.declarePrivate('clearPortalTypes')
6336 6337 6338 6339 6340 6341 6342 6343 6344 6345 6346
    def clearPortalTypes(self, **kw):
      """
      clear id list register for portal types
      """
      setattr(self, 'template_portal_type_id', ())
      setattr(self, 'template_portal_type_allowed_content_type', ())
      setattr(self, 'template_portal_type_hidden_content_type', ())
      setattr(self, 'template_portal_type_property_sheet', ())
      setattr(self, 'template_portal_type_base_category', ())
      return

6347 6348 6349 6350 6351 6352 6353 6354 6355 6356 6357 6358 6359 6360 6361 6362 6363 6364 6365 6366 6367 6368 6369 6370 6371 6372 6373 6374 6375 6376 6377 6378 6379 6380 6381 6382 6383 6384 6385
    @staticmethod
    def _getAllFilesystemModuleFromPortalTypeIdList(portal_type_id_list):
      import erp5.portal_type
      import inspect
      import Products.ERP5Type

      product_base_path = inspect.getfile(Products.ERP5Type).rsplit('/', 2)[0]
      seen_cls_set = set()
      for portal_type in portal_type_id_list:
        # According to ObjectTemplateItem.__init__, this could happen (see
        # stepAddPortalTypeToBusinessTemplate)
        if portal_type == '':
          continue

        portal_type_cls = getattr(erp5.portal_type, portal_type)
        # Calling mro() would not load the class...
        try:
          portal_type_cls.loadClass()
        except Exception:
          LOG("BusinessTemplate", WARNING,
              "Could not load Portal Type Class %s, ignored for migration..." %
              portal_type,
              error=True)
          continue

        for cls in portal_type_cls.mro():
          if (not cls.__module__.startswith('erp5.') and
              cls not in seen_cls_set):
            seen_cls_set.add(cls)
            try:
              cls_path = inspect.getfile(cls)
            except TypeError:
              pass
            else:
              if cls_path.startswith(product_base_path):
                cls_name = cls.__name__
                cls_module = cls.__module__
                yield cls_name, cls_module, cls_path

6386 6387
    security.declareProtected(Permissions.ManagePortal,
                              'getMigratableSourceCodeFromFilesystemList')
6388 6389 6390 6391
    def getMigratableSourceCodeFromFilesystemList(self,
                                                  current_bt_only=False,
                                                  *args,
                                                  **kwargs):
6392 6393 6394 6395
      """
      Return the list of Business Template {Extension, Document, Test} Documents
      and Products Documents which can be migrated to ZODB Components.
      """
6396 6397 6398
      import inspect

      bt_migratable_uid_list = []
6399
      migratable_component_list = []
6400 6401 6402 6403 6404 6405 6406 6407 6408 6409 6410 6411 6412 6413 6414 6415 6416 6417 6418 6419
      portal = self.getPortalObject()
      component_tool = portal.portal_components

      from base64 import b64encode
      import cPickle
      def __newTempComponent(portal_type, reference, source_reference, migrate=False):
        uid = b64encode("%s|%s|%s" % (portal_type, reference, source_reference))
        if migrate:
          bt_migratable_uid_list.append(uid)

        obj = component_tool.newContent(temp_object=1,
                                        id="temp_" + uid,
                                        uid=uid,
                                        portal_type=portal_type,
                                        reference=reference,
                                        source_reference=source_reference)

        migratable_component_list.append(obj)

        return obj
6420 6421 6422 6423 6424 6425 6426 6427

      for portal_type, id_list in (
          ('Document Component', self.getTemplateDocumentIdList()),
          ('Extension Component', self.getTemplateExtensionIdList()),
          ('Test Component', self.getTemplateTestIdList())):
        for id_ in id_list:
          existing_component = getattr(component_tool, id_, None)
          if existing_component is None:
6428 6429 6430 6431 6432 6433 6434 6435 6436 6437 6438 6439 6440 6441 6442 6443 6444 6445 6446 6447 6448
            obj = __newTempComponent(portal_type=portal_type,
                                     reference=id_,
                                     source_reference="%s:%s" % (self.getTitle(), id_),
                                     migrate=True)

      # Inspect Portal Types classes mro() of this Business Template to find
      # Products Documents to migrate by default
      portal_type_module_set = set(
        self._getAllFilesystemModuleFromPortalTypeIdList(
          self.getTemplatePortalTypeIdList()))

      # XXX: Only migrate Documents in ERP5 for the moment...
      import Products.ERP5.Document
      for name, obj in Products.ERP5.Document.__dict__.iteritems():
        if not name.startswith('_') and inspect.ismodule(obj):
          source_reference = obj.__name__

          migrate = ((name, source_reference, inspect.getfile(obj))
                     in portal_type_module_set)
          if current_bt_only and not migrate:
            continue
6449

6450 6451 6452 6453 6454 6455 6456 6457 6458 6459 6460 6461 6462 6463 6464 6465 6466 6467 6468 6469 6470 6471 6472 6473 6474 6475 6476 6477
          obj = __newTempComponent(portal_type='Document Component',
                                   reference=name,
                                   source_reference=source_reference,
                                   migrate=migrate)

      if not current_bt_only:
        import Products.ERP5.tests
        from glob import iglob
        for test_path in iglob("%s/test*.py" %
                               inspect.getfile(Products.ERP5.tests).rsplit('/', 1)[0]):
          reference = test_path.rsplit('/', 1)[1][:-3]
          obj = __newTempComponent(portal_type='Test Component',
                                   reference=reference,
                                   source_reference="Products.ERP5.tests." + reference)

      # Automatically select ZODB Components to be migrated in Migration Dialog
      selection_name = kwargs.get('selection_name')
      if (selection_name is not None and
          # XXX: Do not set uids on {check,uncheck}All, better way?
          self.REQUEST.get('listbox_uncheckAll') is None and
          self.REQUEST.get('listbox_checkAll') is None):
        portal.portal_selections.setSelectionCheckedUidsFor(selection_name,
                                                            bt_migratable_uid_list)

      return sorted(migratable_component_list,
                    key=lambda o: (not o.getProperty('migrate', False),
                                   o.getPortalType(),
                                   o.getReference()))
6478

6479 6480 6481
    security.declareProtected(Permissions.ManagePortal,
                              'migrateSourceCodeFromFilesystem')
    def migrateSourceCodeFromFilesystem(self,
6482
                                        version,
6483
                                        **kw):
6484 6485 6486 6487 6488
      """
      Migrate the given components from filesystem to ZODB by calling the
      appropriate importFromFilesystem according to the destination Portal
      Type and then update the Business Template property with migrated IDs
      """
6489 6490
      portal = self.getPortalObject()
      component_tool = portal.portal_components
6491
      failed_import_dict = {}
6492
      list_selection_name = kw.get('list_selection_name')
6493
      migrated_product_module_set = set()
6494

6495 6496 6497
      template_document_id_set = set(self.getTemplateDocumentIdList())
      template_extension_id_set = set(self.getTemplateExtensionIdList())
      template_test_id_set = set(self.getTemplateTestIdList())
6498

6499 6500 6501 6502 6503 6504 6505 6506 6507 6508 6509 6510 6511 6512 6513 6514 6515 6516 6517 6518 6519 6520 6521 6522 6523 6524 6525 6526 6527
      if list_selection_name is None:
        temp_obj_list = self.getMigratableSourceCodeFromFilesystemList(
          current_bt_only=True)
      else:
        from base64 import b64decode
        import cPickle
        temp_obj_list = []
        for uid in portal.portal_selections.getSelectionCheckedUidsFor(
            list_selection_name):
          portal_type, reference, source_reference = b64decode(uid).split('|')
          obj = component_tool.newContent(temp_object=1,
                                          id="temp_" + uid,
                                          uid=uid,
                                          portal_type=portal_type,
                                          reference=reference,
                                          source_reference=source_reference)

          temp_obj_list.append(obj)

      if not temp_obj_list:
        if list_selection_name is not None:
          return self.Base_redirect(
            'view',
            keep_items={'portal_status_message': 'Nothing Selected.'})

        return

      for temp_obj in temp_obj_list:
        source_reference = temp_obj.getSourceReference()
6528 6529 6530
        try:
          obj = temp_obj.importFromFilesystem(component_tool,
                                              temp_obj.getReference(),
6531 6532
                                              version,
                                              source_reference)
6533
        except Exception, e:
6534
          LOG("BusinessTemplate", WARNING,
6535
              "Could not import component '%s' ('%s') from the filesystem" %
6536
              (temp_obj.getReference(),
6537 6538
               temp_obj.getSourceReference()),
              error=True)
6539

6540 6541 6542 6543 6544 6545 6546 6547 6548 6549 6550
          failed_import_dict[temp_obj.getReference()] = str(e)
        else:
          portal_type = obj.getPortalType()
          if portal_type == 'Extension Component':
            id_set = template_extension_id_set
          elif portal_type == 'Test Component':
            id_set = template_test_id_set
          # 'Document Component'
          else:
            id_set = template_document_id_set

6551 6552 6553 6554 6555
          if source_reference.startswith('Products'):
            migrated_product_module_set.add(source_reference)
          else:
            id_set.discard(temp_obj.getReference())

6556 6557 6558 6559 6560 6561 6562 6563 6564 6565 6566 6567 6568 6569 6570 6571 6572 6573 6574 6575
          id_set.add(obj.getId())

      if failed_import_dict:
        message = (
          "The following component could not be imported: " +
          ', '.join([ "%s (%s)" % (name, error)
                      for name, error in failed_import_dict.iteritems() ]))

        if list_selection_name is not None:
          return self.Base_redirect('view',
                                    keep_items={'portal_status_message': message},
                                    abort_transaction=True)

        transaction.abort()
        raise RuntimeError(message)

      self.setTemplateDocumentIdList(sorted(template_document_id_set))
      self.setTemplateExtensionIdList(sorted(template_extension_id_set))
      self.setTemplateTestIdList(sorted(template_test_id_set))

6576 6577 6578 6579 6580 6581 6582 6583 6584 6585 6586 6587 6588 6589 6590 6591 6592 6593 6594 6595 6596
      # This will trigger a reset so that Portal Types mro() can be checked
      # after migration for filesystem Products modules still being used
      transaction.commit()

      still_used_list_dict = {}
      for _, cls_module, _ in self._getAllFilesystemModuleFromPortalTypeIdList(
          portal.portal_types.objectIds()):
        if cls_module in migrated_product_module_set:
          package, module = cls_module.rsplit('.', 1)
          still_used_list_dict.setdefault(package, []).append(module)

      if still_used_list_dict:
        module_still_used_message = ', '.join(
            [ "%s.{%s}" % (package, ','.join(sorted(module_list)))
              for package, module_list in still_used_list_dict.iteritems() ])

        LOG('BusinessTemplate',
            WARNING,
            "The following Documents are still being imported so code need to "
            "be updated: " + module_still_used_message)

6597 6598 6599
      if list_selection_name is not None:
        message = (
          "All components were successfully imported from filesystem to ZODB. "
6600 6601 6602
          "Please note that imported {Document,Interfaces,Mixin,Tool Components} "
          "have not been validated automatically as imports must probably be "
          "adjusted before deleting them from the filesystem.")
6603 6604 6605 6606 6607 6608 6609 6610 6611

        if still_used_list_dict:
          message = (
            message +
            " WARNING: Some migrated Documents have their filesystem Document "
            "still being imported so code need to be updated (see log file).")

        return self.Base_redirect('view',
                                  keep_items={'portal_status_message': message})
6612

6613 6614
# Block acquisition on all _item_name_list properties by setting
# a default class value to None
6615
for key in item_name_list:
6616
  setattr(BusinessTemplate, key, None)
6617 6618
# Check naming convention of items.
assert item_set.issubset(globals()), item_set.difference(globals())