TemplateTool.py 110 KB
Newer Older
1
# -*- coding: utf-8 -*-
Jean-Paul Smets's avatar
Jean-Paul Smets committed
2 3 4
##############################################################################
#
# Copyright (c) 2002 Nexedi SARL and Contributors. All Rights Reserved.
Jean-Paul Smets's avatar
Jean-Paul Smets committed
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
from webdav.client import Resource
Jean-Paul Smets's avatar
Jean-Paul Smets committed
31

Yoshinori Okuji's avatar
Yoshinori Okuji committed
32
from App.config import getConfiguration
33
import os
34
import shutil
35
import sys
36
import hashlib
37
import pprint
38
import transaction
Yoshinori Okuji's avatar
Yoshinori Okuji committed
39

40
from Acquisition import Implicit, Explicit
Jean-Paul Smets's avatar
Jean-Paul Smets committed
41
from AccessControl import ClassSecurityInfo
42
from AccessControl.SecurityInfo import ModuleSecurityInfo
43
from Products.CMFActivity.ActiveResult import ActiveResult
44
from Products.PythonScripts.PythonScript import PythonScript
45
from Products.ERP5Type.Globals import InitializeClass, DTMLFile, PersistentMapping
46
from Products.ERP5Type.DiffUtils import DiffFile
Jean-Paul Smets's avatar
Jean-Paul Smets committed
47
from Products.ERP5Type.Tool.BaseTool import BaseTool
48
from Products.ERP5Type.Cache import transactional_cached
49
from Products.ERP5Type import Permissions, interfaces
50
from Products.ERP5.Document.BusinessTemplate import BusinessTemplateMissingDependency
51
from Products.ERP5Type.Accessor.Constant import PropertyGetter as ConstantGetter
52
from Products.ERP5.genbt5list import generateInformation
53
from Acquisition import aq_base
54
from tempfile import mkstemp, mkdtemp
Jean-Paul Smets's avatar
Jean-Paul Smets committed
55
from Products.ERP5 import _dtmldir
Aurel's avatar
Aurel committed
56
from cStringIO import StringIO
57
from urllib import pathname2url, urlopen, splittype, urlretrieve
58
import urllib2
59 60
import re
from xml.dom.minidom import parse
61
from xml.parsers.expat import ExpatError
62 63
import struct
import cPickle
64
from base64 import b64encode, b64decode
65
from Products.ERP5Type.Message import translateString
66
from zLOG import LOG, INFO, WARNING
67
from base64 import decodestring
68
from difflib import unified_diff
69
from operator import attrgetter
70
import subprocess
71
import time
Jean-Paul Smets's avatar
Jean-Paul Smets committed
72

73
WIN = os.name == 'nt'
74

75 76
CATALOG_UPDATABLE = object()
ModuleSecurityInfo(__name__).declarePublic('CATALOG_UPDATABLE')
77

78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93
class BusinessTemplateUnknownError(Exception):
  """ Exception raised when the business template
      is impossible to find in the repositories
  """
  pass

class UnsupportedComparingOperator(Exception):
  """ Exception when the comparing string is unsupported
  """
  pass

class BusinessTemplateIsMeta(Exception):
  """ Exception when the business template is provided by another one
  """
  pass

94 95
ModuleSecurityInfo(__name__).declarePublic('BusinessTemplateUnknownError')

Jean-Paul Smets's avatar
Jean-Paul Smets committed
96
class TemplateTool (BaseTool):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
97
    """
98
      TemplateTool manages Business Templates.
Jean-Paul Smets's avatar
Jean-Paul Smets committed
99

100 101 102 103 104 105
      TemplateTool provides some methods to deal with Business Templates:
        - download
        - publish
        - install
        - update
        - save
Jean-Paul Smets's avatar
Jean-Paul Smets committed
106 107
    """
    id = 'portal_templates'
108
    title = 'Template Tool'
Jean-Paul Smets's avatar
Jean-Paul Smets committed
109
    meta_type = 'ERP5 Template Tool'
Jean-Paul Smets's avatar
Jean-Paul Smets committed
110
    portal_type = 'Template Tool'
111 112 113 114 115
    allowed_types = (
      'ERP5 Business Template',
      'ERP5 Business Package',
      'ERP5 Business Manager',
      )
116

117 118
    # This stores information on repositories.
    repository_dict = {}
Jean-Paul Smets's avatar
Jean-Paul Smets committed
119 120 121 122

    # Declarative Security
    security = ClassSecurityInfo()

Rafael Monnerat's avatar
Rafael Monnerat committed
123 124
    security.declareProtected(Permissions.ManagePortal, 'manage_overview')
    manage_overview = DTMLFile('explainTemplateTool', _dtmldir)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
125

126 127
    security.declareProtected(Permissions.AccessContentsInformation,
                              'getInstalledBusinessTemplate')
128
    def getInstalledBusinessTemplate(self, title, strict=False, **kw):
129
      """Returns an installed version of business template of a given title.
130

131
        Returns None if business template is not installed or has been uninstalled.
132 133
        It not "installed" business template is found, look at replaced ones.
        This is mostly usefull if we are looking for the installed business
134 135
        template in a transaction replacing an existing business template.
        If strict is true, we do not take care of "replaced" business templates.
136 137
      """
      # This can be slow if, say, 10000 business templates are present.
Vincent Pelletier's avatar
Vincent Pelletier committed
138 139 140
      # However, that unlikely happens, and using a Z SQL Method has a
      # potential danger because business templates may exchange catalog
      # methods, so the database could be broken temporarily.
141
      last_bt = last_time = None
142 143 144 145
      for bt in self.objectValues(portal_type=['Business Template',
                                               'Business Package',
                                               'Business Manager']):
        if bt.getPortalType() == 'Business Manager':
146
          if bt.getInstallationState() == 'installed' and bt.title == title:
147
            return bt
148 149
          else:
            continue
150
          return None
151
        if bt.getTitle() == title or title in bt.getProvisionList():
152 153 154
          state = bt.getInstallationState()
          if state == 'installed':
            return bt
155 156 157 158 159 160 161 162 163
          if state == 'not_installed':
            last_transition = bt.workflow_history \
              ['business_template_installation_workflow'][-1]
            if last_transition['action'] == 'uninstall': # There is not uninstalled state !
              t = last_transition['time']
              if last_time < t:
                last_bt = None
                last_time = t
          elif state == 'replaced' and not strict:
164 165 166 167 168 169
            t = bt.workflow_history \
              ['business_template_installation_workflow'][-1]['time']
            if last_time < t:
              last_bt = bt
              last_time = t
      return last_bt
170

171 172
    security.declareProtected(Permissions.AccessContentsInformation,
                              'getInstalledBusinessTemplatesList')
173
    def getInstalledBusinessTemplatesList(self):
174 175 176 177 178
      """Deprecated.
      """
      DeprecationWarning('getInstalledBusinessTemplatesList is deprecated; Use getInstalledBusinessTemplateList instead.', DeprecationWarning)
      return self.getInstalledBusinessTemplateList()

179
    def _getInstalledBusinessTemplateList(self, only_title=0):
180
      """Get the list of installed business templates.
181 182
      """
      installed_bts = []
183 184 185
      for bt in self.contentValues(portal_type=['Business Template',
                                                'Business Package',
                                                'Business Manager']):
186
        if bt.getInstallationState() == 'installed':
187 188 189 190
          bt5 = bt
          if only_title:
            bt5 = bt.getTitle()
          installed_bts.append(bt5)
191
      return installed_bts
192

193 194
    security.declareProtected(Permissions.AccessContentsInformation,
                              'getInstalledBusinessTemplateList')
195 196 197 198 199
    def getInstalledBusinessTemplateList(self):
      """Get the list of installed business templates.
      """
      return self._getInstalledBusinessTemplateList(only_title=0)

200 201
    security.declareProtected(Permissions.AccessContentsInformation,
                              'getInstalledBusinessTemplateTitleList')
202 203 204 205 206
    def getInstalledBusinessTemplateTitleList(self):
      """Get the list of installed business templates.
      """
      return self._getInstalledBusinessTemplateList(only_title=1)

207 208
    security.declareProtected(Permissions.AccessContentsInformation,
                              'getInstalledBusinessTemplateRevision')
209 210 211 212 213 214
    def getInstalledBusinessTemplateRevision(self, title, **kw):
      """
        Return the revision of business template installed with the title
        given
      """
      bt = self.getInstalledBusinessTemplate(title)
215 216 217
      if bt is not None:
        return bt.getRevision()
      return None
218

219 220
    security.declareProtected(Permissions.AccessContentsInformation,
                              'getBuiltBusinessTemplateList')
221 222
    def getBuiltBusinessTemplateList(self):
      """Get the list of built and not installed business templates.
223
      """
224 225 226
      return [bt for bt in self.objectValues(portal_type='Business Template')
                 if bt.getInstallationState() == 'not_installed' and
                    bt.getBuildingState() == 'built']
227

228 229 230 231 232 233
    @property
    def asRepository(self):
      class asRepository(Explicit):
        """Export business template by their title

        Provides a view of template tool allowing a user to download the last
234
        edited business template with a URL like:
235 236 237 238 239 240 241 242 243 244 245
          http://.../erp5/portal_templates/asRepository/erp5_core
        """
        def __before_publishing_traverse__(self, self2, request):
          path = request['TraversalRequestNameStack']
          self.subpath = tuple(reversed(path))
          del path[:]
        def __call__(self, REQUEST, RESPONSE):
          title, = self.subpath
          last_bt = None, None
          for bt in self.aq_parent.searchFolder(title=title):
            bt = bt.getObject()
246 247 248
            modified = bt.getModificationDate()
            if last_bt[0] < modified and bt.getInstallationState() != 'deleted':
              last_bt = modified, bt
249 250 251 252 253 254 255 256 257 258 259
          if last_bt[1] is None:
            return RESPONSE.notFoundError(title)
          RESPONSE.setHeader('Content-type', 'application/data')
          RESPONSE.setHeader('Content-Disposition',
                             'inline;filename=%s-%s.zexp' % (title, last_bt[0]))
          if REQUEST['REQUEST_METHOD'] == 'GET':
            bt = last_bt[1]
            if bt.getBuildingState() != 'built':
              bt.build()
            return self.aq_parent.manage_exportObject(bt.getId(), download=1)
      return asRepository().__of__(self)
260

261
    security.declareProtected(Permissions.ManagePortal,
262 263
                              'getDefaultBusinessTemplateDownloadURL')
    def getDefaultBusinessTemplateDownloadURL(self):
264 265 266 267 268
      """Returns the default download URL for business templates.
      """
      return "file://%s/" % pathname2url(
                  os.path.join(getConfiguration().instancehome, 'bt5'))

Rafael Monnerat's avatar
Rafael Monnerat committed
269
    security.declareProtected('Import/Export objects', 'save')
270
    def save(self, business_template, REQUEST=None, RESPONSE=None):
Yoshinori Okuji's avatar
Yoshinori Okuji committed
271
      """
Vincent Pelletier's avatar
Vincent Pelletier committed
272
        Save the BusinessTemplate in the servers's filesystem.
Yoshinori Okuji's avatar
Yoshinori Okuji committed
273 274
      """
      cfg = getConfiguration()
Vincent Pelletier's avatar
Vincent Pelletier committed
275 276
      path = os.path.join(cfg.clienthome,
                          '%s' % (business_template.getTitle(),))
277
      path = pathname2url(path)
278
      business_template.export(path=path, local=True)
279
      if REQUEST is not None:
280
        psm = translateString('Saved in ${path} .',
281
                              mapping={'path':pathname2url(path)})
282
        ret_url = '%s/%s?portal_status_message=%s' % \
Vincent Pelletier's avatar
Vincent Pelletier committed
283
                  (business_template.absolute_url(),
284
                   REQUEST.get('form_id', 'view'), psm)
Vincent Pelletier's avatar
Vincent Pelletier committed
285 286 287
        if RESPONSE is None:
          RESPONSE = REQUEST.RESPONSE
        return REQUEST.RESPONSE.redirect( ret_url )
288 289

    security.declareProtected( 'Import/Export objects', 'export' )
290
    def export(self, business_template, REQUEST=None, RESPONSE=None, is_package=False):
291
      """
Vincent Pelletier's avatar
Vincent Pelletier committed
292 293
        Export the Business Template as a bt5 file and offer the user to
        download it.
294
      """
295
      export_string = business_template.export()
Aurel's avatar
Aurel committed
296
      try:
297 298
        if RESPONSE is not None:
          RESPONSE.setHeader('Content-type','tar/x-gzip')
299
          if not is_package:
300 301 302 303 304
            RESPONSE.setHeader('Content-Disposition', 'inline;filename=%s-%s.bt5'
              % (business_template.getTitle(), business_template.getVersion()))
          else:
            RESPONSE.setHeader('Content-Disposition', 'inline;filename=%s.bp5'
            % business_template.getTitle())
Aurel's avatar
Aurel committed
305 306 307
        return export_string.getvalue()
      finally:
        export_string.close()
Yoshinori Okuji's avatar
Yoshinori Okuji committed
308

309
    security.declareProtected( 'Import/Export objects', 'publish' )
310 311
    def publish(self, business_template, url, username=None, password=None):
      """
Vincent Pelletier's avatar
Vincent Pelletier committed
312
        Publish the given business template at the given URL.
313 314
      """
      business_template.build()
Vincent Pelletier's avatar
Vincent Pelletier committed
315
      export_string = self.manage_exportObject(id=business_template.getId(),
316
                                               download=True)
317
      bt = Resource(url, username=username, password=password)
Vincent Pelletier's avatar
Vincent Pelletier committed
318 319
      bt.put(file=export_string,
             content_type='application/x-erp5-business-template')
320
      business_template.setPublicationUrl(url)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
321

322
    security.declareProtected(Permissions.ManagePortal, 'update')
323 324
    def update(self, business_template):
      """
Vincent Pelletier's avatar
Vincent Pelletier committed
325
        Update an existing template from its publication URL.
326 327 328 329 330 331
      """
      url = business_template.getPublicationUrl()
      id = business_template.getId()
      bt = Resource(url)
      export_string = bt.get().get_body()
      self.deleteContent(id)
Aurel's avatar
Aurel committed
332
      self._importObjectFromFile(StringIO(export_string), id=id)
333

334

335
    security.declareProtected( Permissions.ManagePortal, 'manage_download' )
336 337
    def manage_download(self, url, id=None, REQUEST=None):
      """The management interface for download.
338
      """
339 340
      if REQUEST is None:
        REQUEST = getattr(self, 'REQUEST', None)
341

342
      bt = self.download(url, id=id)
343

344
      if REQUEST is not None:
345
        ret_url = bt.absolute_url()
Yusei Tahara's avatar
Yusei Tahara committed
346
        psm = translateString("Business template downloaded successfully.")
347
        REQUEST.RESPONSE.redirect("%s?portal_status_message=%s"
348
                                    % (ret_url, psm))
349

350
    def _download_local(self, path, bt_id, format_version=1):
351 352
      """Download Business Template from local directory or file
      """
353
      if format_version == 2:
354 355 356
        bp = self.newContent(bt_id, 'Business Package')
        bp.importFile(path)
        return bp
357 358 359 360
      elif format_version == 3:
        bm = self.newContent(bt_id, 'Business Manager')
        bm.importFile(path)
        return bm
361

362 363
      bt = self.newContent(bt_id, 'Business Template')
      bt.importFile(path)
364
      return bt
365 366 367 368

    def _download_url(self, url, bt_id):
      tempid, temppath = mkstemp()
      try:
369
        os.close(tempid) # Close the opened fd as soon as possible.
370
        file_path, headers = urlretrieve(url, temppath)
371
        if re.search(r'<title>.*Revision \d+:', open(file_path, 'r').read()):
372 373 374
          # this looks like a subversion repository, try to check it out
          LOG('ERP5', INFO, 'TemplateTool doing a svn checkout of %s' % url)
          return self._download_svn(url, bt_id)
Rafael Monnerat's avatar
Rafael Monnerat committed
375

376 377 378 379 380 381 382 383
        return self._download_local(file_path, bt_id)
      finally:
        os.remove(temppath)

    def _download_svn(self, url, bt_id):
      svn_checkout_tmp_dir = mkdtemp()
      svn_checkout_dir = os.path.join(svn_checkout_tmp_dir, 'bt')
      try:
384 385
        from Products.ERP5VCS.WorkingCopy import getVcsTool
        getVcsTool('svn').__of__(self).export(url, svn_checkout_dir)
386 387 388 389 390
        return self._download_local(svn_checkout_dir, bt_id)
      finally:
        shutil.rmtree(svn_checkout_tmp_dir)

    security.declareProtected( 'Import/Export objects', 'download' )
391
    def download(self, url, id=None, REQUEST=None):
392 393 394 395 396 397 398 399 400 401
      """
      Download Business Template from url, can be file or local directory
      """
      # For backward compatibility: If REQUEST is passed, it is likely that we
      # come from the management interface.
      if REQUEST is not None:
        return self.manage_download(url, id=id, REQUEST=REQUEST)
      if id is None:
        id = self.generateNewId()

402 403 404 405 406
      urltype, name = splittype(url)
      # Create a zexp path which would be used for Business Manager files
      zexp_path = name + '/' + name.split('/')[-1] + '.zexp'

      if WIN and urltype and '\\' in name:
407
        urltype = None
408
        path = url
409
      if urltype and urltype != 'file':
410
        if '/portal_templates/asRepository/' in url:
411 412 413 414 415
          # In this case, the downloaded BT is already built.
          bt = self._p_jar.importFile(urlopen(url))
          bt.id = id
          del bt.uid
          return self[self._setObject(id, bt)]
416
        bt = self._download_url(url, id)
417 418 419 420
      elif os.path.exists(zexp_path):
        # If the path exists, we create a Business Manager object after
        # downloading it from zexp path
        bt = self._download_local(os.path.normpath(zexp_path), id, format_version=3)
421
      else:
422 423
        template_version_path_list = [
                                      name+'/bp/template_format_version',
424
                                      name+'/bt/template_format_version',
425 426 427 428
                                     ]

        for path in template_version_path_list:
          try:
429
            file = open(os.path.normpath(path))
430
          except IOError:
431 432 433 434 435 436 437
            continue
        try:
          format_version = int(file.read())
          file.close()
        except UnboundLocalError:
          # In case none of the above paths do have template_format_version
          format_version = 1
438
        # XXX: Download only needed in case the file is in directory
439
        bt = self._download_local(os.path.normpath(name), id, format_version)
440

441
      bt.build(no_action=True)
442
      return bt
Jean-Paul Smets's avatar
Jean-Paul Smets committed
443

444
    security.declareProtected('Import/Export objects', 'importBase64EncodedText')
445
    def importBase64EncodedText(self, file_data=None, id=None, REQUEST=None,
446
                                batch_mode=False, **kw):
447
      """
448 449 450
        Import Business Template from passed base64 encoded text.
      """
      import_file = StringIO(decodestring(file_data))
451
      return self.importFile(import_file = import_file, id = id, REQUEST = REQUEST,
452 453
                             batch_mode = batch_mode, **kw)

454
    security.declareProtected('Import/Export objects', 'importFile')
455
    def importFile(self, import_file=None, id=None, REQUEST=None,
456
                   batch_mode=False, **kw):
457
      """
Vincent Pelletier's avatar
Vincent Pelletier committed
458
        Import Business Template from one file
459
      """
460 461
      if REQUEST is None:
        REQUEST = getattr(self, 'REQUEST', None)
462

463 464 465 466 467
      if id is None:
        id = self.generateNewId()

      if (import_file is None) or (len(import_file.read()) == 0):
        if REQUEST is not None:
Yusei Tahara's avatar
Yusei Tahara committed
468
          psm = translateString('No file or an empty file was specified.')
469 470
          REQUEST.RESPONSE.redirect("%s?portal_status_message=%s"
                                    % (self.absolute_url(), psm))
Alexandre Boeglin's avatar
Alexandre Boeglin committed
471 472
          return
        else :
473
          raise RuntimeError, 'No file or an empty file was specified'
Aurel's avatar
Aurel committed
474
      # copy to a temp location
Alexandre Boeglin's avatar
Alexandre Boeglin committed
475
      import_file.seek(0) #Rewind to the beginning of file
476
      tempid, temppath = mkstemp()
477 478
      try:
        os.close(tempid) # Close the opened fd as soon as possible
479
        with open(temppath, 'wb') as tempfile:
480
          tempfile.write(import_file.read())
481
        bt = self._download_local(temppath, id)
482 483
      finally:
        os.remove(temppath)
484
      bt.build(no_action=True)
Aurel's avatar
Aurel committed
485
      bt.reindexObject()
486

487
      if not batch_mode and \
488
         (REQUEST is not None):
489
        ret_url = bt.absolute_url()
Yusei Tahara's avatar
Yusei Tahara committed
490
        psm = translateString("Business templates imported successfully.")
491 492
        REQUEST.RESPONSE.redirect("%s?portal_status_message=%s"
                                  % (ret_url, psm))
493
      elif batch_mode:
494
        return bt
495

496
    security.declareProtected(Permissions.ManagePortal, 'getDiffFilterScriptList')
497 498 499 500
    def getDiffFilterScriptList(self):
      """
      Return list of scripts usable to filter diff
      """
501
      # XXX, the "or ()" should not be there, the preference tool is
502 503
      # inconsistent, the called method should not return None when
      # nothing is selected
504
      portal = self.getPortalObject()
505 506 507 508 509 510 511 512
      script_list = []
      for script_id in portal.portal_preferences\
         .getPreferredDiffFilterScriptIdList() or ():
        try:
          script_list.append(getattr(portal, script_id))
        except AttributeError:
          LOG("TemplateTool", WARNING, "Unable to find %r script" % script_id)
      return script_list
513

514
    security.declareProtected(Permissions.ManagePortal, 'getFilteredDiffAsHTML')
515 516 517 518 519 520
    def getFilteredDiffAsHTML(self, diff):
      """
      Return the diff filtered by python scripts into html format
      """
      return self.getFilteredDiff(diff).toHTML()

521
    def _cleanUpTemplateFolder(self, folder_path):
522 523
      file_object_list = [x for x in os.listdir(folder_path)]
      for file_object in file_object_list:
524 525 526 527 528 529
        file_object_path = os.path.join(folder_path, file_object)
        if os.path.isfile(file_object_path):
          os.unlink(file_object_path)
        else:
          shutil.rmtree(file_object_path)

530 531
    security.declareProtected( 'Import/Export objects', 'importAndReExportBusinessTemplateFromPath' )
    def importAndReExportBusinessTemplateFromPath(self, template_path):
532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553
      """
        Imports the template that is in the template_path and exports it to the
        same path.

        We want to clean this directory, i.e. remove all files before
        the export. Because this is called as activity though, it could cause
        the following problem:
        - Activity imports the template
        - Activity removes all files from template_path
        - Activity fails in export.
        Then the folder contents will be changed, so when retrying the
        activity may succeed without the user understanding that files were
        erased. For this reason export is done in 3 steps:
        - First to a temporary directory
        - If there was no error delete contents of template_path
        - Copy the contents of the temporary directory to the template_path
      """
      import_template = self.download(url=template_path)
      export_dir = mkdtemp()
      try:
        import_template.export(path=export_dir, local=True)
        self._cleanUpTemplateFolder(template_path)
554 555 556 557 558
        file_name_list = [x for x in os.listdir(export_dir)]
        for file_name in file_name_list:
          temp_file_path = os.path.join(export_dir, file_name)
          destination_file_path = os.path.join(template_path, file_name)
          shutil.move(temp_file_path, destination_file_path)
559 560 561 562 563
      except:
        raise
      finally:
        shutil.rmtree(export_dir)

564 565
    security.declareProtected( 'Import/Export objects', 'importAndReExportBusinessTemplateListFromPath' )
    def importAndReExportBusinessTemplateListFromPath(self, repository_list, REQUEST=None, **kw):
566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590
      """
        Migrate business templates to new format where files like .py or .html
        are exported seprately than the xml.
      """
      repository_list = filter(bool, repository_list)

      if REQUEST is None:
        REQUEST = getattr(self, 'REQUEST', None)
        
      if len(repository_list) == 0 and REQUEST:
        ret_url = self.absolute_url()
        REQUEST.RESPONSE.redirect("%s?portal_status_message=%s"
                                  % (ret_url, 'No repository was defined'))
                                    
      for repository in repository_list:
        repository = repository.rstrip('\n')
        repository = repository.rstrip('\r')
        for business_template_id in os.listdir(repository):
          template_path = os.path.join(repository, business_template_id)
          if os.path.isfile(template_path):
            LOG(business_template_id,0,'is file, so it is skipped')
          else:
            if not os.path.exists((os.path.join(template_path, 'bt'))):
              LOG(business_template_id,0,'has no bt sub-folder, so it is skipped')
            else:
591 592
              self.activate(activity='SQLQueue').\
                importAndReExportBusinessTemplateFromPath(template_path)
593

594
    security.declareProtected( 'Import/Export objects', 'migrateBTToBM')
595
    def migrateBTToBM(self, template_path, isReduced=False, REQUEST=None, **kw):
596
      """
597 598
        Migrate business template repository to Business Manager repo.
        Business Manager completely rely only on BusinessItem and to show
599 600 601 602
        the difference between both of them

        So, the steps should be:
        1. Install the business template which is going to be migrated
603
        2. Create a new Business Manager with random id and title
604 605
        3. Add the path, build and export the template
        4. Remove the business template from the directory and add the business
606 607 608
        manager there instead
        5. Change the ID and title of the business manager
        6. Export the business manager to the directory, leaving anything in
609
        the installed erp5 unchanged
610 611
      """
      import_template = self.download(url=template_path)
612
      if import_template.getPortalType() == 'Business Manager':
613 614 615
        LOG(import_template.getTitle(),0,'Already migrated')
        return

616 617
      export_dir = mkdtemp()

618 619 620
      removable_property = {}
      removable_sub_object_path = []

621
      installed_bt_list = self.getInstalledBusinessTemplatesList()
622 623
      installed_bt_title_list = [bt.title for bt in installed_bt_list]

624
      is_installed = False
625 626 627
      if import_template.getTitle() not in installed_bt_title_list:
        # Install the business template
        import_template.install(**kw)
628
        is_installed = True
629

630
      # Make list of object paths which needs to be added in the bm5
631 632 633 634 635 636 637 638 639
      # This can be decided by looping over every type of items we do have in
      # bt5 and checking if there have been any changes being made to it via this
      # bt5 installation or not.
      # For ex:
      # CatalogTempalteMethodItem, CatalogResultsKeyItem, etc. do make some
      # changes in erp5_mysql_innodb(by adding properties, by adding sub-objects),
      # so we need to add portal_catalog/erp5_mysql_innodb everytime we find
      # a bt5 making changes in any of these items.

640 641
      portal_path = self.getPortalObject()
      template_path_list = []
642
      property_path_list = []
643 644

      # For modules, we don't need to create path for the module
645
      module_list = import_template.getTemplateModuleIdList()
646
      for path in module_list:
647
        template_path_list.append(path)
648

649
      # For portal_types, we have to add path and subobjects
650 651
      portal_type_id_list = import_template.getTemplatePortalTypeIdList()
      portal_type_path_list = []
652
      portal_type_workflow_chain_path_list = []
653
      for id in portal_type_id_list:
654
        portal_type_path_list.append('portal_types/'+id)
655 656 657 658 659 660
        # Add type_worklow list separately in path
        portal_type_workflow_chain_path_list.append('portal_types/'+id+'#type_workflow_list')
        # Remove type_workflow_list from the objects, so that we don't end up in
        # conflict
        portal_type_path = 'portal_types/' + id
        removable_property[portal_type_path] = ['type_workflow_list']
661
      template_path_list.extend(portal_type_path_list)
662
      template_path_list.extend(portal_type_workflow_chain_path_list)
663

664
      # For categories, we create path for category objects as well as the subcategories
665
      category_list = import_template.getTemplateBaseCategoryList()
666 667
      category_path_list = []
      for base_category in category_list:
668
        category_path_list.append('portal_categories/'+base_category)
669
        #category_path_list.append('portal_categories/'+base_category+'/**')
670 671
      template_path_list.extend(category_path_list)

672 673 674 675
      # Adding tools
      template_tool_id_list = import_template.getTemplateToolIdList()
      tool_id_list = []
      for tool_id in template_tool_id_list:
676
        tool_id_list.append(tool_id)
677 678
      template_path_list.extend(tool_id_list)

679 680 681 682 683 684 685
      # Adding business template skin selection property on the portal_tempaltes
      template_skin_selection_list = import_template.getTemplateRegisteredSkinSelectionList()
      selection_list = []
      for selection in template_skin_selection_list:
        skin, selection = selection.split(' | ')
        selection_list.append('portal_skins/%s#business_template_registered_skin_selections'%skin)

686
      # For portal_skins, we export the folder
687
      portal_skin_list = import_template.getTemplateSkinIdList()
688 689
      portal_skin_path_list = []
      for skin in portal_skin_list:
690
        portal_skin_path_list.append('portal_skins/'+skin)
691
        #portal_skin_path_list.append('portal_skins/'+skin+'/**')
692 693 694 695 696 697 698 699 700 701 702 703
      template_path_list.extend(portal_skin_path_list)

      # For workflow chains,
      # We have 2 objects in the Business Template design where we deal with
      # workflow objects, we deal with the installation separately:
      # 1. Workflow_id : We export the whole workflow objects in this case
      # 2. Portal Workflow chain: It is already being exported via portal_types
      # XXX: CHECK For 2, keeping in mind the migration of workflow would be merged
      # before this part where we make workflow_list as property of portal_type
      workflow_id_list = import_template.getTemplateWorkflowIdList()
      workflow_path_list = []
      for workflow in workflow_id_list:
704
        workflow_path_list.append('portal_workflow/' + workflow)
705
        #workflow_path_list.append('portal_workflow/' + workflow + '/**')
706 707
      template_path_list.extend(workflow_path_list)

708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728
      # For tests in portal components add them with portal_components head
      test_id_list = import_template.getTemplateTestIdList()
      test_path_list = []
      for path in test_id_list:
        test_path_list.append('portal_components/' + path)
      template_path_list.extend(test_path_list)

      # For documents in portal components add them with portal_components head
      document_id_list = import_template.getTemplateDocumentIdList()
      document_path_list = []
      for path in document_id_list:
        document_path_list.append('portal_components/' + path)
      template_path_list.extend(document_path_list)

      # For extensions in portal components add them with portal_components head
      extension_id_list = import_template.getTemplateExtensionIdList()
      extension_path_list = []
      for path in extension_id_list:
        extension_path_list.append('portal_components/' + path)
      template_path_list.extend(extension_path_list)

729
      # For paths, we add them directly to the path list
730 731
      path_list = import_template.getTemplatePathList()
      for path in path_list:
732
        template_path_list.append(path)
733

734
      # Catalog methods would be added as sub objects
735 736 737
      catalog_method_item_list = import_template.getTemplateCatalogMethodIdList()
      catalog_method_path_list = []
      for method in catalog_method_item_list:
738
        catalog_method_path_list.append('portal_catalog/' + method)
739
      template_path_list.extend(catalog_method_path_list)
740 741

      # For catalog objects, we check if there is any catalog object, and then
742
      # add catalog object also in the path if there is
743 744 745 746 747 748 749 750 751 752 753 754 755 756 757
      template_catalog_datetime_key   = import_template.getTemplateCatalogDatetimeKeyList()
      template_catalog_full_text_key  = import_template.getTemplateCatalogFullTextKeyList()
      template_catalog_keyword_key    = import_template.getTemplateCatalogKeywordKeyList()
      template_catalog_local_role_key = import_template.getTemplateCatalogLocalRoleKeyList()
      template_catalog_multivalue_key = import_template.getTemplateCatalogMultivalueKeyList()
      template_catalog_related_key    = import_template.getTemplateCatalogRelatedKeyList()
      template_catalog_request_key    = import_template.getTemplateCatalogRequestKeyList()
      template_catalog_result_key     = import_template.getTemplateCatalogResultKeyList()
      template_catalog_result_table   = import_template.getTemplateCatalogResultTableList()
      template_catalog_role_key       = import_template.getTemplateCatalogRoleKeyList()
      template_catalog_scriptable_key = import_template.getTemplateCatalogScriptableKeyList()
      template_catalog_search_key     = import_template.getTemplateCatalogSearchKeyList()
      template_catalog_security_uid_column = import_template.getTemplateCatalogSecurityUidColumnList()
      template_catalog_topic_key      = import_template.getTemplateCatalogTopicKeyList()

758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789
      catalog_property_list = [
        template_catalog_datetime_key,
        template_catalog_full_text_key,
        template_catalog_keyword_key,
        template_catalog_local_role_key,
        template_catalog_multivalue_key,
        template_catalog_related_key,
        template_catalog_request_key,
        template_catalog_result_key,
        template_catalog_result_table,
        template_catalog_role_key,
        template_catalog_scriptable_key,
        template_catalog_search_key,
        template_catalog_security_uid_column,
        template_catalog_topic_key,
        ]
      is_property_added = any(catalog_property_list)

      properties_removed = [
        'sql_catalog_datetime_search_keys_list',
        'sql_catalog_full_text_search_keys_list',
        'sql_catalog_keyword_search_keys_list',
        'sql_catalog_local_role_keys_list',
        'sql_catalog_multivalue_keys_list',
        'sql_catalog_related_keys_list',
        'sql_catalog_request_keys_list',
        'sql_search_result_keys_list',
        'sql_search_tables_list',
        'sql_catalog_role_keys_list',
        'sql_catalog_scriptable_keys_list',
        'sql_catalog_search_keys_list',
        'sql_catalog_security_uid_columns_list',
790
        'sql_catalog_topic_search_keys_list',
791 792 793 794 795 796 797
        ]

      if is_property_added:
        if catalog_method_path_list:
          catalog_path = catalog_method_path_list[0].rsplit('/', 1)[0]
        else:
          catalog_path = 'portal_catalog/erp5_mysql_innodb'
798
          removable_sub_object_path.append(catalog_path)
799
        template_path_list.append(catalog_path)
800 801
        removable_property[catalog_path] = properties_removed
        for prop in properties_removed:
802
            property_path_list.append('%s#%s' % (catalog_path, prop))
803

804 805 806
      # Add these catalog items in the object_property instead of adding
      # dummy path item for them
      if import_template.getTitle() == 'erp5_mysql_innodb_catalog':
807
        template_path_list.append('portal_catalog/erp5_mysql_innodb')
808

809 810 811 812
      # Add portal_property_sheets
      property_sheet_id_list = import_template.getTemplatePropertySheetIdList()
      property_sheet_path_list = []
      for property_sheet in property_sheet_id_list:
813
        property_sheet_path_list.append('portal_property_sheets/' + property_sheet)
814
        #property_sheet_path_list.append('portal_property_sheets/' + property_sheet + '/**')
815
      template_path_list.extend(property_sheet_path_list)
816

817
      # Create new objects for business package
818 819
      migrated_bm = self.newContent(
                                    portal_type='Business Manager',
820
                                    title=import_template.getTitle()
821
                                    )
822

823
      template_path_list.extend(property_path_list)
824
      template_path_list.extend(selection_list)
825
      template_path_list = self.cleanTemplatePathList(template_path_list)
826 827 828 829

      # XXX: Add layer=1 and sign=1 for default for all paths
      template_path_list = [l + ' | 1 | 1' for l in template_path_list]

830 831 832 833 834
      def reduceDependencyList(bt, template_path_list):
        """
          Used for recursive udpation of layer for dependency in a BT
        """
        dependency_list = bt.getDependencyList()
835 836 837
        # XXX: Do not return template_path_list of the new BM incase there is no
        # dependency_list, instead look for the latest updated version of
        # new_template_path_list
838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872
        if not dependency_list:
          return template_path_list
        else:
          # Copy of the initial template list to be used to update the layer
          new_template_path_list = list(template_path_list)
          for item in dependency_list:
            dependency = item.split(' ', 1)
            if len(dependency) > 1:
              version = dependency[1]
              if version:
                version = version[1:-1]
              base_bt = self.getLastestBTOnRepos(dependency[0], version)
            else:
              try:
                base_bt = self.getLastestBTOnRepos(dependency[0])
              except BusinessTemplateIsMeta:
                bt_list = self.getProviderList(dependency[0])
                # We explicilty use the Business Template which is used the most
                # while dealing with provision list
                repository_list = self.getRepositoryList()
                if dependency[0] == 'erp5_full_text_catalog':
                  base_bt = [repository_list[1], 'erp5_full_text_mroonga_catalog']
                if dependency[0] == 'erp5_view_style':
                  base_bt = [repository_list[0], 'erp5_xhtml_style']
                if dependency[0] == 'erp5_catalog':
                  base_bt = [repository_list[0], 'erp5_mysql_innodb_catalog']
                # XXX: Create path for the BT(s) here

            # Download the base_bt
            base_bt_path = os.path.join(base_bt[0], base_bt[1])
            base_bt = self.download(base_bt_path)

            # Check for the item list and if the BT is Business Manager,
            # if BM, then compare and update layer and if not run migration and
            # then do it again
873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900
            if base_bt.getPortalType() != 'Business Manager':
              # If the base_bt is not Business Manager, run the migration on the
              # base_bt
              base_bt = self.migrateBTToBM(base_bt_path, isReduced=True)

            # Check for item path which also exists in base_bt
            base_path_list = base_bt.getPathList()

            copy_of_template_path_list = new_template_path_list[:]
            # Loop through all the paths in the new_template_path_list and
            # check for their existence in base_path_list
            for idx, path in enumerate(new_template_path_list):
              path_list = path.split(' | ')
              item_path = path_list[0]
              item_layer = path_list[2]
              if item_path in base_path_list:
                # TODO: Increase the layer of the path item by +1 and save it
                # back at updated_template_path_list
                item_layer = int(item_layer) + 1
                updated_path = item_path + ' | 1 | ' + str(item_layer)
                copy_of_template_path_list[idx] = updated_path
            new_template_path_list = copy_of_template_path_list

            if base_bt.getPortalType() != 'Business Manager':
              # Recursively reduce the base Business Templatem no need to do
              # this for Business Manager(s) as it had already been migrated
              # with taking care of layer
              reduceDependencyList(base_bt, new_template_path_list)
901 902 903 904 905 906 907

          return new_template_path_list

      # Take care about the the dependency_list also and then update the layer
      # accordingly for the path(s) that already exists in the dependencies.
      template_path_list = reduceDependencyList(import_template, template_path_list)

908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927
      # Create new sub-objects instead based on template_path_list
      for path in template_path_list:

        path_list = path.split(' | ')

        # Create Business Property Item for objects with property in path
        if '#' in path_list[0]:
          migrated_bm.newContent(
                portal_type='Business Property Item',
                item_path=path_list[0],
                item_sign=path_list[1],
                item_layer=path_list[2],
                )
        else:
          migrated_bm.newContent(
                portal_type='Business Item',
                item_path=path_list[0],
                item_sign=path_list[1],
                item_layer=path_list[2],
                )
928 929

      kw['removable_property'] = removable_property
930 931
      kw['removable_sub_object_path'] = removable_sub_object_path
      migrated_bm.build(**kw)
932

933 934
      # Commit transaction to generate all oids before exporting
      transaction.commit()
935
      # Export the newly built business package to the export directory
936 937
      migrated_bm.export(path=export_dir, local=True)

938
      if is_installed:
939
        import_template.uninstall()
940

941 942 943
      if isReduced:
        return migrated_bm

944 945 946 947 948 949
    def cleanTemplatePathList(self, path_list):
      """
      Remove redundant paths and sub-objects' path if the object path already
      exist.
      """
      # Split path into list
950
      a2 = [l.split('/') for l in path_list]
951
      # Create new list for paths with **
952
      a3 = [l for l in a2 if l[-1] in ('**', '*')]
953
      # Create new list for paths without **
954
      a4 = [l for l in a2 if l[-1] not in ('**', '*')]
955 956 957 958 959 960 961 962
      # Remove ** from paths in a3
      reserved_id = ('portal_transforms', 'portal_ids')
      a3 = [l[:-1] for l in a3 if l[0] not in reserved_id] + [l for l in a3 if l[0] in reserved_id]
      # Create new final path list
      a2 = a3+a4
      # Join the path list
      a2 = [('/').join(l) for l in a2]
      # Remove the redundant paths
963 964
      seen = set()
      seen_add = seen.add
965 966
      # XXX: What about redundant signs with different layers
      # Maybe we will end up reducing them
967
      a2 = [x for x in a2 if not (x in seen or seen_add(x))]
968 969 970 971 972

      return a2

    security.declareProtected( 'Import/Export objects', 'migrateBTListToBM')
    def migrateBTListToBM(self, repository_list, REQUEST=None, **kw):
973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997
      """
      Run migration for BT5 one by one in a given repository. This will be done
      via activities.
      """
      repository_list = filter(bool, repository_list)

      if REQUEST is None:
        REQUEST = getattr(self, 'REQUEST', None)

      if len(repository_list) == 0 and REQUEST:
        ret_url = self.absolute_url()
        REQUEST.RESPONSE.redirect("%s?portal_status_message=%s"
                                  % (ret_url, 'No repository was defined'))

      for repository in repository_list:
        repository = repository.rstrip('\n')
        repository = repository.rstrip('\r')
        for business_template_id in os.listdir(repository):
          template_path = os.path.join(repository, business_template_id)
          if os.path.isfile(template_path):
            LOG(business_template_id,0,'is file, so it is skipped')
          else:
            if not os.path.exists((os.path.join(template_path, 'bt'))):
              LOG(business_template_id,0,'has no bt sub-folder, so it is skipped')
            else:
998
              self.migrateBTToBM(template_path)
999 1000 1001
              #self.activate(activity='SQLQueue').\
              #  migrateBTToBP(template_path)

1002
    security.declareProtected(Permissions.ManagePortal, 'getFilteredDiff')
1003 1004 1005 1006 1007 1008
    def getFilteredDiff(self, diff):
      """
      Filter the diff using python scripts
      """
      diff_file_object = DiffFile(diff)
      diff_block_list = diff_file_object.getModifiedBlockList()
1009 1010 1011 1012
      if diff_block_list:
        script_list = self.getDiffFilterScriptList()
        for block, line_tuple in diff_block_list:
          for script in script_list:
1013 1014
            if script(line_tuple[0], line_tuple[1]):
              diff_file_object.children.remove(block)
1015
              break
1016 1017 1018 1019
      # XXX-Aurel : this method should return a text diff but
      # DiffFile does not provide yet such feature
      return diff_file_object

1020
    security.declareProtected(Permissions.ManagePortal, 'diffObjectAsHTML')
1021 1022 1023
    def diffObjectAsHTML(self, REQUEST, **kw):
      """
        Convert diff into a HTML format before reply
1024
        This is compatible with ERP5VCS look and feel but
1025 1026 1027 1028
        it is preferred in future we use more difflib python library.
      """
      return DiffFile(self.diffObject(REQUEST, **kw)).toHTML()

1029
    security.declareProtected(Permissions.ManagePortal, 'diffObject')
1030
    def diffObject(self, REQUEST, **kw):
Aurel's avatar
Aurel committed
1031
      """
Vincent Pelletier's avatar
Vincent Pelletier committed
1032 1033
        Make diff between two objects, whose paths are stored in values bt1
        and bt2 in the REQUEST object.
Aurel's avatar
Aurel committed
1034
      """
1035 1036
      bt1_id = getattr(REQUEST, 'bt1', None)
      bt2_id = getattr(REQUEST, 'bt2', None)
1037 1038 1039 1040 1041 1042 1043
      if bt1_id is not None and bt2_id is not None:
        bt1 = self._getOb(bt1_id)
        bt2 = self._getOb(bt2_id)
        if self.compareVersions(bt1.getVersion(), bt2.getVersion()) < 0:
          return bt2.diffObject(REQUEST, compare_with=bt1_id)
        else:
          return bt1.diffObject(REQUEST, compare_with=bt2_id)
Aurel's avatar
Aurel committed
1044
      else:
1045 1046 1047 1048 1049
        object_id = getattr(REQUEST, 'object_id', None)
        bt1_id = object_id.split('|')[0]
        bt1 = self._getOb(bt1_id)
        REQUEST.set('object_id', object_id.split('|')[1])
        return bt1.diffObject(REQUEST)
1050

Vincent Pelletier's avatar
Vincent Pelletier committed
1051 1052 1053 1054
    security.declareProtected( 'Import/Export objects',
                               'updateRepositoryBusinessTemplateList' )

    def updateRepositoryBusinessTemplateList(self, repository_list,
1055
        REQUEST=None, RESPONSE=None, genbt5list=0, **kw):
Vincent Pelletier's avatar
Vincent Pelletier committed
1056 1057
      """
        Update the information on Business Templates from repositories.
1058

1059 1060 1061 1062
      For local repositories, genbt5list > 0 enables automatic generation
      of bt5list, without saving it on disk:
      - genbt5list=1: only if bt5list is missing
      - genbt5list>1: always
1063 1064
      """
      self.repository_dict = PersistentMapping()
1065
      property_list = ('title', 'version', 'revision', 'description', 'license',
1066 1067
                       'dependency', 'test_dependency', 'provision', 'copyright',
                       'force_install')
Vincent Pelletier's avatar
Vincent Pelletier committed
1068 1069
      #LOG('updateRepositoryBusiessTemplateList', 0,
      #    'repository_list = %r' % (repository_list,))
1070
      for repository in repository_list:
1071 1072 1073 1074 1075 1076 1077
        urltype, url = splittype(repository)
        if WIN and urltype and '\\' in url:
          urltype = None
          url = repository
        if urltype and urltype != 'file':
          f = urlopen(repository + '/bt5list')
        else:
1078
          url = os.path.expanduser(url)
1079 1080 1081 1082 1083 1084
          bt5list = os.path.join(url, 'bt5list')
          if genbt5list > os.path.exists(bt5list):
            f = generateInformation(url)
            f.seek(0)
          else:
            f = open(bt5list, 'rb')
1085
        try:
1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096
          try:
            doc = parse(f)
          except ExpatError:
            if REQUEST is not None:
              psm = translateString('Invalid repository: ${repo}',
                                    mapping={'repo':repository})
              REQUEST.RESPONSE.redirect("%s?portal_status_message=%s"
                                       % (self.absolute_url(), psm))
              return
            else:
              raise RuntimeError, 'Invalid repository: %s' % repository
1097
          try:
1098
            property_dict_list = []
1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118
            root = doc.documentElement
            for template in root.getElementsByTagName("template"):
              id = template.getAttribute('id')
              if type(id) == type(u''):
                id = id.encode('utf-8')
              temp_property_dict = {}
              for node in template.childNodes:
                if node.nodeName in property_list:
                  value = ''
                  for text in node.childNodes:
                    if text.nodeType == text.TEXT_NODE:
                      value = text.data
                      if type(value) == type(u''):
                        value = value.encode('utf-8')
                      break
                  temp_property_dict.setdefault(node.nodeName, []).append(value)

              property_dict = {}
              property_dict['id'] = id
              property_dict['title'] = temp_property_dict.get('title', [''])[0]
Vincent Pelletier's avatar
Vincent Pelletier committed
1119 1120
              property_dict['version'] = \
                  temp_property_dict.get('version', [''])[0]
Jérome Perrin's avatar
Jérome Perrin committed
1121 1122
              property_dict['revision'] = \
                  temp_property_dict.get('revision', [''])[0]
Vincent Pelletier's avatar
Vincent Pelletier committed
1123 1124 1125 1126 1127 1128
              property_dict['description'] = \
                  temp_property_dict.get('description', [''])[0]
              property_dict['license'] = \
                  temp_property_dict.get('license', [''])[0]
              property_dict['dependency_list'] = \
                  temp_property_dict.get('dependency', ())
1129 1130
              property_dict['test_dependency_list'] = \
                  temp_property_dict.get('test_dependency', ())
1131 1132
              property_dict['provision_list'] = \
                  temp_property_dict.get('provision', ())
Vincent Pelletier's avatar
Vincent Pelletier committed
1133 1134
              property_dict['copyright_list'] = \
                  temp_property_dict.get('copyright', ())
1135 1136
              property_dict['force_install'] = \
                  int(temp_property_dict.get('force_install', [0])[0])
1137

1138 1139 1140 1141 1142
              property_dict_list.append(property_dict)
          finally:
            doc.unlink()
        finally:
          f.close()
1143

1144
        #XXX: Hardcoding 'erp5_core_proxy_field_legacy' BP in the list
1145
        bp_dict_1 ={
1146 1147 1148 1149
          'copyright_list': ['Copyright (c) 2001-2017 Nexedi SA'],
          'dependency_list': [],
          'description': '',
          'force_install': 0,
1150
          'id': 'erp5_core_proxy_field_legacy',
1151 1152 1153
          'license': 'GPL',
          'revision': '',
          'test_dependency_list': [],
1154 1155
          'provision_list': [],
          'title': 'erp5_core_proxy_field_legacy',
1156
          'version': '5.4.7'}
1157 1158 1159

        bp_dict_2 ={
          'copyright_list': ['Copyright (c) 2001-2017 Nexedi SA'],
1160
          'dependency_list': ['erp5_base',],
1161 1162
          'description': '',
          'force_install': 0,
1163
          'id': 'erp5_pdm',
1164 1165 1166 1167
          'license': 'GPL',
          'revision': '',
          'test_dependency_list': [],
          'provision_list': [],
1168
          'title': 'erp5_pdm',
1169
          'version': '5.4.7'}
1170

1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181
        bp_dict_3 ={
          'copyright_list': ['Copyright (c) 2001-2017 Nexedi SA'],
          'dependency_list': ['erp5_ui_test',],
          'description': '',
          'force_install': 0,
          'id': 'erp5_performance_test',
          'license': 'GPL',
          'revision': '',
          'test_dependency_list': [],
          'provision_list': [],
          'title': 'erp5_performance_test',
1182
          'version': '5.4.7'}
1183

1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194
        bp_dict_4 ={
          'copyright_list': ['Copyright (c) 2001-2017 Nexedi SA'],
          'dependency_list': ['erp5_ui_test_core',],
          'description': '',
          'force_install': 0,
          'id': 'erp5_ui_test',
          'license': 'GPL',
          'revision': '',
          'test_dependency_list': [],
          'provision_list': [],
          'title': 'erp5_ui_test',
1195
          'version': '5.4.7'}
1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207

        bp_dict_5 ={
          'copyright_list': ['Copyright (c) 2001-2017 Nexedi SA'],
          'dependency_list': ['erp5_core', 'erp5_xhtml_style',],
          'description': '',
          'force_install': 0,
          'id': 'erp5_ui_test_core',
          'license': 'GPL',
          'revision': '',
          'test_dependency_list': [],
          'provision_list': [],
          'title': 'erp5_ui_test_core',
1208
          'version': '5.4.7'}
1209

1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224
        bp_dict_6 ={
          'copyright_list': ['Copyright (c) 2001-2017 Nexedi SA'],
          'dependency_list': ['erp5_core (>= 1.0rc12)',
                              'erp5_full_text_catalog'
                              'erp5_core_proxy_field_legacy',],
          'description': '',
          'force_install': 0,
          'id': 'erp5_base',
          'license': 'GPL',
          'revision': '',
          'test_dependency_list': [],
          'provision_list': [],
          'title': 'erp5_base',
          'version': '5.4.7'}

1225
        if repository.endswith('/bt5'):
1226
          property_dict_list.append(bp_dict_1)
1227
          property_dict_list.append(bp_dict_2)
1228
          property_dict_list.append(bp_dict_3)
1229 1230
          property_dict_list.append(bp_dict_4)
          property_dict_list.append(bp_dict_5)
1231
          property_dict_list.append(bp_dict_6)
1232

1233
        bm_dict_1 ={
1234 1235 1236 1237 1238 1239 1240 1241 1242 1243
          'copyright_list': ['Copyright (c) 2001-2017 Nexedi SA'],
          'dependency_list': [],
          'description': '',
          'force_install': 0,
          'id': 'erp5_mysql_innodb_catalog',
          'license': 'GPL',
          'revision': '',
          'test_dependency_list': [],
          'provision_list': ['erp5_catalog'],
          'title': 'erp5_mysql_innodb_catalog',
1244
          'version': '5.4.7'}
1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256

        bm_dict_2 ={
          'copyright_list': ['Copyright (c) 2001-2017 Nexedi SA'],
          'dependency_list': ['erp5_core'],
          'description': '',
          'force_install': 0,
          'id': 'erp5_xhtml_style',
          'license': 'GPL',
          'revision': '',
          'test_dependency_list': [],
          'provision_list': ['erp5_view_style'],
          'title': 'erp5_xhtml_style',
1257
          'version': '5.4.7'}
1258

1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269
        bm_dict_3 ={
          'copyright_list': ['Copyright (c) 2001-2017 Nexedi SA'],
          'dependency_list': [],
          'description': '',
          'force_install': 0,
          'id': 'erp5_mysql_ndb_catalog',
          'license': 'GPL',
          'revision': '',
          'test_dependency_list': [],
          'provision_list': ['erp5_catalog'],
          'title': 'erp5_mysql_ndb_catalog',
1270
          'version': '5.4.7'}
1271

1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282
        bm_dict_4 ={
          'copyright_list': ['Copyright (c) 2001-2017 Nexedi SA'],
          'dependency_list': ['erp5_view_style',],
          'description': '',
          'force_install': 0,
          'id': 'erp5_jquery',
          'license': 'GPL',
          'revision': '',
          'test_dependency_list': [],
          'provision_list': [],
          'title': 'erp5_jquery',
1283
          'version': '5.4.7'}
1284

1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295
        bm_dict_5 ={
          'copyright_list': ['Copyright (c) 2001-2017 Nexedi SA'],
          'dependency_list': [],
          'description': '',
          'force_install': 0,
          'id': 'erp5_property_sheets',
          'license': 'GPL',
          'revision': '',
          'test_dependency_list': [],
          'provision_list': [],
          'title': 'erp5_property_sheets',
1296
          'version': '5.4.7'}
1297

1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311
        bm_dict_6 ={
          'copyright_list': ['Copyright (c) 2001-2017 Nexedi SA'],
          'dependency_list': ['erp5_catalog (>= 1.1)',
                              'erp5_core_proxy_field_legacy',
                              'erp5_property_sheets'],
          'description': '',
          'force_install': 0,
          'id': 'erp5_core',
          'license': 'GPL',
          'revision': '',
          'test_dependency_list': ['erp5_full_text_mroonga_catalog',
                                    'erp5_base'],
          'provision_list': ['erp5_auto_logout',],
          'title': 'erp5_core',
1312
          'version': '5.4.7'}
1313

1314
        if repository.endswith('/bootstrap'):
1315
          property_dict_list.append(bm_dict_1)
1316 1317
          property_dict_list.append(bm_dict_2)
          property_dict_list.append(bm_dict_3)
1318
          property_dict_list.append(bm_dict_4)
1319
          property_dict_list.append(bm_dict_5)
1320
          property_dict_list.append(bm_dict_6)
1321

1322
        self.repository_dict[repository] = tuple(property_dict_list)
1323

1324
      if REQUEST is not None:
1325
        ret_url = self.absolute_url() + '/' + REQUEST.get('dialog_id', 'view')
Yusei Tahara's avatar
Yusei Tahara committed
1326
        psm = translateString("Business templates updated successfully.")
1327 1328
        REQUEST.RESPONSE.redirect("%s?cancel_url=%s&portal_status_message=%s&dialog_category=object_exchange&selection_name=business_template_selection"
                                  % (ret_url, REQUEST.form.get('cancel_url', ''), psm))
1329

Vincent Pelletier's avatar
Vincent Pelletier committed
1330 1331
    security.declareProtected( Permissions.AccessContentsInformation,
                               'getRepositoryList' )
1332
    def getRepositoryList(self):
Vincent Pelletier's avatar
Vincent Pelletier committed
1333 1334
      """
        Get the list of repositories.
1335 1336
      """
      return self.repository_dict.keys()
1337

1338 1339
    security.declarePublic( 'decodeRepositoryBusinessTemplateUid' )
    def decodeRepositoryBusinessTemplateUid(self, uid):
Vincent Pelletier's avatar
Vincent Pelletier committed
1340 1341 1342
      """
        Decode the uid of a business template from a repository.
        Return a repository and an id.
1343
      """
1344
      return cPickle.loads(b64decode(uid))
1345

1346 1347 1348 1349 1350 1351 1352 1353
    security.declarePublic( 'encodeRepositoryBusinessTemplateUid' )
    def encodeRepositoryBusinessTemplateUid(self, repository, id):
      """
        encode the repository and the id of a business template.
        Return an uid.
      """
      return b64encode(cPickle.dumps((repository, id)))

1354
    security.declarePublic('compareVersionStrings')
1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382
    def compareVersionStrings(self, version, comparing_string):
      """
       comparing_string is like "<= 0.2" | "operator version"
       operators supported: '<=', '<' or '<<', '>' or '>>', '>=', '=' or '=='
      """
      operator, comp_version = comparing_string.split(' ')
      diff_version = self.compareVersions(version, comp_version)
      if operator == '<' or operator == '<<':
        if diff_version < 0:
          return True;
        return False;
      if operator == '<=':
        if diff_version <= 0:
          return True;
        return False;
      if operator == '>' or operator == '>>':
        if diff_version > 0:
          return True;
        return False;
      if operator == '>=':
        if diff_version >= 0:
          return True;
        return False;
      if operator == '=' or operator == '==':
        if diff_version == 0:
          return True;
        return False;
      raise UnsupportedComparingOperator, 'Unsupported comparing operator: %s'%(operator,)
1383

1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397
    security.declareProtected(Permissions.AccessContentsInformation,
                              'IsOneProviderInstalled')
    def IsOneProviderInstalled(self, title):
      """
        return true if a business template that
        provides the bt with the given title is
        installed
      """
      installed_bt_list = self.getInstalledBusinessTemplatesList()
      for bt in installed_bt_list:
        provision_list = bt.getProvisionList()
        if title in provision_list:
          return True
      return False
1398

1399 1400 1401 1402 1403
    security.declareProtected(Permissions.AccessContentsInformation,
                               'getLastestBTOnRepos')
    def getLastestBTOnRepos(self, title, version_restriction=None):
      """
       It's possible we have different versions of the same BT
1404
       available on various repositories or on the same repository.
1405 1406 1407 1408 1409 1410
       This function returns the latest one that meet the version_restriction
       (i.e "<= 0.2") in the following form :
       tuple (repository, id)
      """
      result = None
      for repository, property_dict_list in self.repository_dict.items():
Jérome Perrin's avatar
Jérome Perrin committed
1411
        for property_dict in property_dict_list:
1412 1413 1414
          provision_list = property_dict.get('provision_list', [])
          if title in provision_list:
            raise BusinessTemplateIsMeta, 'Business Template %s is provided by another one'%(title,)
Jérome Perrin's avatar
Jérome Perrin committed
1415
          if title == property_dict['title']:
1416 1417
            if (version_restriction is None) or (self.compareVersionStrings(property_dict['version'], version_restriction)):
              if (result is None) or (self.compareVersions(property_dict['version'], result[2]) > 0):
Rafael Monnerat's avatar
Rafael Monnerat committed
1418
                result = (repository, property_dict['id'], property_dict['version'])
1419 1420 1421 1422
      if result is not None:
        return (result[0], result[1])
      else:
        raise BusinessTemplateUnknownError, 'Business Template %s (%s) could not be found in the repositories'%(title, version_restriction or '')
1423

1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437
    security.declareProtected(Permissions.AccessContentsInformation,
                              'getProviderList')
    def getProviderList(self, title):
      """
       return a list of business templates that provides
       the given business template
      """
      result_list = []
      for repository, property_dict_list in self.repository_dict.items():
        for property_dict in property_dict_list:
          provision_list = property_dict['provision_list']
          if (title in provision_list) and (property_dict['title'] not in result_list):
            result_list.append(property_dict['title'])
      return result_list
1438

1439 1440
    security.declareProtected(Permissions.AccessContentsInformation,
                               'getDependencyList')
1441 1442 1443
    @transactional_cached(lambda self, bt, with_test_dependency_list=False:
                          (bt, with_test_dependency_list))
    def getDependencyList(self, bt, with_test_dependency_list=False):
1444 1445 1446 1447
      """
       Return the list of missing dependencies for a business
       template, given a tuple : (repository, id)
      """
1448 1449 1450
      # We do not take into consideration the dependencies
      # for meta business templates
      if bt[0] != 'meta':
1451 1452 1453 1454 1455
        result_list = []
        for repository, property_dict_list in self.repository_dict.items():
          if repository == bt[0]:
            for property_dict in property_dict_list:
              if property_dict['id'] == bt[1]:
1456 1457 1458 1459 1460
                dependency_list = [q.strip() for q in
                                   property_dict['dependency_list'] if q]
                if with_test_dependency_list:
                  dependency_list.extend([q.strip() for q in
                                          property_dict['test_dependency_list'] if q])
1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483
                for dependency_couple in dependency_list:
                  # dependency_couple is like "erp5_xhtml_style (>= 0.2)"
                  dependency_couple_list = dependency_couple.split(' ', 1)
                  dependency = dependency_couple_list[0]
                  version_restriction = None
                  if len(dependency_couple_list) > 1:
                    version_restriction = dependency_couple_list[1]
                    if version_restriction.startswith('('):
                      # Something like "(>= 1.0rc6)".
                      version_restriction = version_restriction[1:-1]
                  require_update = False
                  if dependency not in result_list:
                    # Get the lastest version of the dependency on the
                    # repository that meet the version restriction
                    provider_installed = False
                    bt_dep = None
                    try:
                      bt_dep = self.getLastestBTOnRepos(dependency, version_restriction)
                    except BusinessTemplateUnknownError:
                      raise BusinessTemplateMissingDependency, 'While analysing %s the following dependency could not be satisfied: %s (%s)\nReason: Business Template could not be found in the repositories'%(bt[1], dependency, version_restriction or '')
                    except BusinessTemplateIsMeta:
                      provider_list = self.getProviderList(dependency)
                      for provider in provider_list:
1484
                        if self.getInstalledBusinessTemplate(provider) is not None:
1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495
                          bt_dep = self.getLastestBTOnRepos(provider)
                          break
                      if bt_dep is None:
                        bt_dep = ('meta', dependency)
                    sub_dep_list = self.getDependencyList(bt_dep)
                    for sub_dep in sub_dep_list:
                      if sub_dep not in result_list:
                        result_list.append(sub_dep)
                    result_list.append(bt_dep)
                return result_list
        raise BusinessTemplateUnknownError, 'The Business Template %s could not be found on repository %s'%(bt[1], bt[0])
1496
      return []
1497

1498 1499
    security.declareProtected(Permissions.ManagePortal,
                              'findProviderInBTList')
1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510
    def findProviderInBTList(self, provider_list, bt_list):
      """
       Find one provider in provider_list which is present in
       bt_list and returns the found tuple (repository, id)
       in bt_list.
      """
      for provider in provider_list:
        for repository, id in bt_list:
          if id.startswith(provider):
            return (repository, id)
      raise BusinessTemplateUnknownError, 'Provider not found in bt_list'
1511

1512 1513 1514 1515
    security.declareProtected(Permissions.AccessContentsInformation,
                              'sortBusinessTemplateList')
    def sortBusinessTemplateList(self, bt_list):
      """
1516 1517 1518 1519 1520 1521
      Sort a list of business template in repositories according to
      dependencies

      bt_list : list of (repository, id) tuple.
      """
      sorted_bt_list = []
1522
      title_id_mapping = {}
1523 1524 1525 1526 1527 1528 1529

      # Calculate the dependency graph
      dependency_dict = {}
      provition_dict = {}
      repository_dict = {}
      undependent_list = []

1530 1531 1532
      for repository, bt_id in bt_list:
        bt = [x for x in self.repository_dict[repository] \
              if x['id'] == bt_id][0]
1533 1534 1535 1536 1537 1538
        bt_title = bt['title']
        repository_dict[bt_title] = repository
        dependency_dict[bt_title] = [x.split(' ')[0] for x in bt['dependency_list']]
        title_id_mapping[bt_title] = bt_id
        if not dependency_dict[bt_title]:
          del dependency_dict[bt_title]
1539
        for provision in list(bt['provision_list']):
1540 1541
          provition_dict[provision] = bt_title
        undependent_list.append(bt_title)
1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570

      # Calculate the reverse dependency graph
      reverse_dependency_dict = {}
      for bt_id, dependency_id_list in dependency_dict.items():
        update_dependency_id_list = []
        for dependency_id in dependency_id_list:

          # Get ride of provision id
          if dependency_id in provition_dict:
            dependency_id = provition_dict[dependency_id]
          update_dependency_id_list.append(dependency_id)

          # Fill incoming edge dict
          if dependency_id in reverse_dependency_dict:
            reverse_dependency_dict[dependency_id].append(bt_id)
          else:
            reverse_dependency_dict[dependency_id] = [bt_id]

          # Remove from free node list
          try:
            undependent_list.remove(dependency_id)
          except ValueError:
            pass

        dependency_dict[bt_id] = update_dependency_id_list

      # Let's sort the bt5!
      while undependent_list:
        bt_id = undependent_list.pop(0)
1571 1572 1573
        if bt_id not in repository_dict:
          continue
        sorted_bt_list.insert(0, (repository_dict[bt_id], title_id_mapping[bt_id]))
1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588
        for dependency_id in dependency_dict.get(bt_id, []):

          local_dependency_list = reverse_dependency_dict[dependency_id]
          local_dependency_list.remove(bt_id)
          if local_dependency_list:
            reverse_dependency_dict[dependency_id] = local_dependency_list
          else:
            del reverse_dependency_dict[dependency_id]
            undependent_list.append(dependency_id)

      if len(sorted_bt_list) != len(bt_list):
        raise NotImplementedError, \
          "Circular dependencies on %s" % reverse_dependency_dict.keys()
      else:
        return sorted_bt_list
1589

1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619
    security.declareProtected(Permissions.AccessContentsInformation,
                              'sortDownloadedBusinessTemplateList')
    def sortDownloadedBusinessTemplateList(self, id_list):
      """
      Sort a list of already downloaded business templates according to
      dependencies

      id_list : list of business template's id in portal_templates.
      """
      def isDepend(a, b):
        # return True if a depends on b.
        dependency_list = [x.split(' ')[0] for x in a.getDependencyList()]
        provision_list = list(b.getProvisionList()) + [b.getTitle()]
        for i in provision_list:
          if i in dependency_list:
            return True
          return False

      sorted_bt_list = []
      for bt_id in id_list:
        bt = self._getOb(bt_id)
        for j in range(len(sorted_bt_list)):
          if isDepend(sorted_bt_list[j], bt):
            sorted_bt_list.insert(j, bt)
            break
        else:
           sorted_bt_list.append(bt)
      sorted_bt_list = [bt.getId() for bt in sorted_bt_list]
      return sorted_bt_list

Vincent Pelletier's avatar
Vincent Pelletier committed
1620 1621
    security.declareProtected( Permissions.AccessContentsInformation,
                               'getRepositoryBusinessTemplateList' )
1622
    def getRepositoryBusinessTemplateList(self, update_only=False,
1623
             template_list=None, **kw):
1624
      """Get the list of Business Templates in repositories.
1625 1626 1627

         update_only: return only bt that needs to be updated
         template_list: only returns bt within the given list
1628 1629
      """
      from Products.ERP5Type.Document import newTempBusinessTemplate
1630 1631 1632 1633
      result_list = []
      template_set = None
      if template_list is not None:
        template_set = set(template_list)
1634 1635

      template_item_list = []
1636 1637 1638 1639 1640 1641 1642 1643 1644 1645
      # First of all, filter Business Templates in repositories.
      template_item_dict = {}
      for repository, property_dict_list in self.repository_dict.items():
        for property_dict in property_dict_list:
          title = property_dict['title']
          if template_set and not(title in template_set):
            continue
          if not update_only:
            template_item_list.append((repository, property_dict))
          else:
1646
            if title not in template_item_dict:
Vincent Pelletier's avatar
Vincent Pelletier committed
1647 1648
              # If this is the first time to see this business template,
              # insert it.
1649 1650
              template_item_dict[title] = (repository, property_dict)
            else:
Vincent Pelletier's avatar
Vincent Pelletier committed
1651 1652 1653 1654
              # If this business template has been seen before, insert it only
              # if this business template is newer.
              previous_repository, previous_property_dict = \
                  template_item_dict[title]
1655 1656
              if self.compareVersions(previous_property_dict['version'],
                                      property_dict['version']) < 0:
1657
                template_item_dict[title] = (repository, property_dict)
1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668 1669
      # Next, select only updated business templates.
      if update_only:
        for repository, property_dict in template_item_dict.values():
          installed_bt = \
              self.getInstalledBusinessTemplate(property_dict['title'], strict=True)
          if installed_bt is not None:
            diff_version = self.compareVersions(installed_bt.getVersion(),
                                                property_dict['version'])
            if diff_version < 0:
              template_item_list.append((repository, property_dict))
            elif diff_version == 0 \
                  and property_dict['revision'] \
1670
                  and installed_bt.getRevision() != property_dict['revision']:
1671 1672
                    template_item_list.append((repository, property_dict))
          elif template_list is not None:
1673 1674 1675 1676 1677
            template_item_list.append((repository, property_dict))

      # Create temporary Business Template objects for displaying.
      for repository, property_dict in template_item_list:
        property_dict = property_dict.copy()
1678
        id = filename = property_dict.pop('id')
1679 1680 1681 1682
        installed_bt = \
            self.getInstalledBusinessTemplate(property_dict['title'])
        if installed_bt is not None:
          installed_version = installed_bt.getVersion()
1683 1684
          installed_revision = installed_bt.getShortRevision()
          if installed_bt.getRevision() == property_dict['revision']:
1685
            version_state = 'present'
1686 1687
          else:
            version_state = 'different'
1688 1689 1690
        else:
          installed_version = ''
          installed_revision = ''
1691
          version_state = 'new'
1692
        uid = self.encodeRepositoryBusinessTemplateUid(repository, id)
1693 1694
        obj = newTempBusinessTemplate(self, 'temp_' + uid,
                                      version_state = version_state,
1695
                                      version_state_title=version_state.title(),
1696
                                      filename = filename,
1697 1698
                                      installed_version = installed_version,
                                      installed_revision = installed_revision,
1699 1700
                                      repository = repository, **property_dict)
        obj.setUid(uid)
1701 1702 1703
        result_list.append(obj)
      result_list.sort(key=lambda x: x.getTitle())
      return result_list
1704

Vincent Pelletier's avatar
Vincent Pelletier committed
1705 1706
    security.declareProtected( Permissions.AccessContentsInformation,
                               'getUpdatedRepositoryBusinessTemplateList' )
1707 1708 1709 1710
    def getUpdatedRepositoryBusinessTemplateList(self, **kw):
      """Get the list of updated Business Templates in repositories.
      """
      #LOG('getUpdatedRepositoryBusinessTemplateList', 0, 'kw = %r' % (kw,))
1711
      return self.getRepositoryBusinessTemplateList(update_only=True, **kw)
1712

1713
    security.declarePublic('compareVersions')
1714
    def compareVersions(self, version1, version2):
Vincent Pelletier's avatar
Vincent Pelletier committed
1715 1716 1717
      """
        Return negative if version1 < version2, 0 if version1 == version2,
        positive if version1 > version2.
1718 1719

      Here is the algorithm:
Vincent Pelletier's avatar
Vincent Pelletier committed
1720 1721
        - Non-alphanumeric characters are not significant, besides the function
          of delimiters.
1722 1723 1724 1725
        - If a level of a version number is missing, it is assumed to be zero.
        - An alphabetical character is less than any numerical value.
        - Numerical values are compared as integers.

Vincent Pelletier's avatar
Vincent Pelletier committed
1726
      This implements the following predicates:
1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 1748 1749 1750
        - 1.0 < 1.0.1
        - 1.0rc1 < 1.0
        - 1.0a < 1.0.1
        - 1.1 < 2.0
        - 1.0.0 = 1.0
      """
      r = re.compile('(\d+|[a-zA-Z])')
      v1 = r.findall(version1)
      v2 = r.findall(version2)

      def convert(v, i):
        """Convert the ith element of v to an interger for a comparison.
        """
        #LOG('convert', 0, 'v = %r, i = %r' % (v, i))
        try:
          e = v[i]
          try:
            e = int(e)
          except ValueError:
            # ASCII code is one byte, so this produces negative.
            e = struct.unpack('b', e)[0] - 0x200
        except IndexError:
          e = 0
        return e
1751

1752 1753 1754 1755 1756 1757 1758 1759
      for i in xrange(max(len(v1), len(v2))):
        e1 = convert(v1, i)
        e2 = convert(v2, i)
        result = cmp(e1, e2)
        if result != 0:
          return result

      return 0
1760

1761
    def _getBusinessTemplateUrlDict(self):
1762
      business_template_url_dict = {}
1763
      for bt in self.getRepositoryBusinessTemplateList():
1764
        url, name = self.decodeRepositoryBusinessTemplateUid(bt.getUid())
1765 1766 1767
        if name.endswith('.bt5'):
          name = name[:-4]
        business_template_url_dict[name] = {
Rafael Monnerat's avatar
Rafael Monnerat committed
1768
          'url': '%s/%s' % (url, bt.filename),
1769 1770 1771 1772 1773
          'revision': bt.getRevision()
          }
      return business_template_url_dict

    security.declareProtected(Permissions.ManagePortal,
Rafael Monnerat's avatar
Rafael Monnerat committed
1774
        'installBusinessTemplatesFromRepositories')
1775
    def installBusinessTemplatesFromRepositories(self, *args, **kw):
1776 1777
      """Deprecated.
      """
1778
      DeprecationWarning('installBusinessTemplatesFromRepositories is deprecated; Use self.installBusinessTemplateListFromRepository instead.', DeprecationWarning)
1779
      return self.installBusinessTemplateListFromRepository(*args, **kw)
1780

1781 1782
    security.declareProtected(Permissions.ManagePortal,
         'resolveBusinessTemplateListDependency')
1783 1784 1785
    def resolveBusinessTemplateListDependency(self,
                                              template_title_list,
                                              with_test_dependency_list=False):
1786
      available_bt5_list = self.getRepositoryBusinessTemplateList()
1787
      template_title_list = set(template_title_list)
1788
      installed_bt5_title_list = self.getInstalledBusinessTemplateTitleList()
1789
      bt5_set = set()
1790 1791
      for available_bt5 in available_bt5_list:
        if available_bt5.title in template_title_list:
1792
          template_title_list.remove(available_bt5.title)
1793 1794
          bt5 = self.decodeRepositoryBusinessTemplateUid(available_bt5.uid)
          bt5_set.add(bt5)
1795
          meta_dependency_set = set()
1796 1797 1798
          for dep_repository, dep_id in self.getDependencyList(
              bt5,
              with_test_dependency_list):
1799 1800 1801
            if dep_repository != 'meta':
              bt5_set.add((dep_repository, dep_id))
            else:
1802 1803 1804 1805 1806 1807 1808 1809 1810 1811
              meta_dependency_set.add((dep_repository, dep_id))
          for dep_repository, dep_id in meta_dependency_set:
            provider_list = self.getProviderList(dep_id)
            provider_installed = False
            provider_title = None
            for provider in provider_list:
              if provider in [i[1].replace(".bt5", "") for i in bt5_set] or \
                    provider in installed_bt5_title_list or \
                    provider in template_title_list:
                provider_title = provider
1812
                for candidate in available_bt5_list:
1813
                  if candidate.title == provider:
1814 1815 1816
                    bt5_set.add(\
                      self.decodeRepositoryBusinessTemplateUid(
                          candidate.uid))
1817
                    break
1818 1819 1820 1821 1822 1823 1824 1825 1826 1827 1828 1829 1830 1831 1832
                break
            if provider_title is None and len(provider_list) == 1:
              provider_title = provider_list[0]
            LOG('resolveBT, provider_title', 0, provider_title)
            if provider_title:
              for candidate in available_bt5_list:
                if candidate.title == provider_title:
                  bt5_set.add(\
                    self.decodeRepositoryBusinessTemplateUid(
                        candidate.uid))
                  break
            else:
              raise BusinessTemplateMissingDependency,\
                "Unable to resolve dependencies for %s, options are %s" \
                    % (dep_id, provider_list)
1833 1834 1835 1836

      if len(template_title_list) > 0:
         raise BusinessTemplateUnknownError, 'The Business Template %s could not be found on repositories %s' % \
             (list(template_title_list), self.getRepositoryList())
1837 1838
      return self.sortBusinessTemplateList(list(bt5_set))

1839 1840 1841
    security.declareProtected(Permissions.ManagePortal,
        'installBusinessTemplateListFromRepository')
    def installBusinessTemplateListFromRepository(self, template_list,
1842
        only_different=True, update_catalog=False, activate=False,
1843
        install_dependency=False):
1844 1845 1846 1847
      """Installs template_list from configured repositories by default only newest"""
      # XXX-Luke: This method could replace
      # TemplateTool_installRepositoryBusinessTemplateList while still being
      # possible to reuse by external callers
1848

1849 1850
      operation_log = []
      resolved_template_list = self.resolveBusinessTemplateListDependency(
1851
                   template_list)
1852 1853
      installed_bt5_dict = {x.getTitle(): x.getRevision()
        for x in self.getInstalledBusinessTemplateList()}
1854 1855
      if only_different:
        template_url_dict = self._getBusinessTemplateUrlDict()
1856 1857

      def checkAvailability(bt_title):
1858
        return bt_title in template_list or bt_title in installed_bt5_dict
1859 1860 1861 1862 1863 1864 1865
      missing_dependency_list = [i for i in resolved_template_list
                                 if not checkAvailability(i[1].replace(".bt5", ""))]

      if not install_dependency and len(missing_dependency_list) > 0:
        raise BusinessTemplateMissingDependency,\
            "Impossible to install, please install the following dependencies before: %s" \
            % [x[1] for x in missing_dependency_list]
1866 1867

      activate_kw =  dict(activity="SQLQueue", tag="start_%s" % (time.time()))
1868
      for repository, bt_id in resolved_template_list:
1869 1870 1871
        if only_different:
          bt = template_url_dict.get(bt_id)
          if bt is not None and bt['revision'] == installed_bt5_dict.get(bt_id):
1872
            continue
1873
        bt_url = '%s/%s' % (repository, bt_id)
1874
        param_dict = dict(download_url=bt_url, only_different=only_different)
1875
        param_dict["update_catalog"] = update_catalog
1876 1877 1878 1879 1880 1881 1882

        if activate:
          self.activate(**activate_kw).\
                updateBusinessTemplateFromUrl(**param_dict)
          activate_kw["after_tag"] = activate_kw["tag"]
          activate_kw["tag"] = bt_id
          operation_log.append('Installed %s using activities' % (bt_id))
1883
        else:
1884 1885
          document = self.updateBusinessTemplateFromUrl(**param_dict)
          operation_log.append('Installed %s with revision %s' % (
1886
              document.getTitle(), document.getShortRevision()))
1887 1888

      return operation_log
1889

1890 1891 1892
    security.declareProtected(Permissions.ManagePortal,
            'updateBusinessTemplateFromUrl')
    def updateBusinessTemplateFromUrl(self, download_url, id=None,
1893 1894 1895
                                         keep_original_list=None,
                                         before_triggered_bt5_id_list=None,
                                         after_triggered_bt5_id_list=None,
1896
                                         update_catalog=False,
1897
                                         reinstall=False,
1898
                                         active_process=None,
Rafael Monnerat's avatar
Rafael Monnerat committed
1899
                                         force_keep_list=None,
1900
                                         only_different=True):
Rafael Monnerat's avatar
Rafael Monnerat committed
1901
      """
1902
        This method download and install a bt5, from a URL.
1903 1904 1905 1906 1907

        keep_original_list can be used to make paths not touched at all

        force_keep_list can be used to force path to be modified or removed
        even if template system proposes not touching it
1908
      """
1909 1910 1911 1912 1913 1914 1915 1916
      if keep_original_list is None:
        keep_original_list = []
      if before_triggered_bt5_id_list is None:
        before_triggered_bt5_id_list = []
      if after_triggered_bt5_id_list is None:
        after_triggered_bt5_id_list = []
      if force_keep_list is None:
        force_keep_list = []
1917 1918 1919 1920 1921 1922 1923 1924 1925 1926 1927 1928 1929
      if active_process is None:
        installed_dict = {}
        def log(msg):
          LOG('TemplateTool.updateBusinessTemplateFromUrl', INFO, msg)
      else:
        active_process = self.unrestrictedTraverse(active_process)
        if getattr(aq_base(active_process), 'installed_dict', None) is None:
          active_process.installed_dict = PersistentMapping()
        installed_dict = active_process.installed_dict
        message_list = []
        log = message_list.append

      log("Installing %s ..." % download_url)
1930
      imported_bt5 = self.download(url = download_url, id = id)
1931 1932
      bt_title = imported_bt5.getTitle()

1933 1934 1935
      if reinstall:
        install_kw = None
      else:
1936 1937 1938 1939 1940 1941 1942
        if only_different:
          previous_bt5 = self.getInstalledBusinessTemplate(bt_title)
          if previous_bt5 and \
             imported_bt5.getRevision() == previous_bt5.getRevision():
            log("%s is already installed with revision %s"
                % (bt_title, imported_bt5.getShortRevision()))
            return imported_bt5
1943 1944 1945

        install_kw = {}
        for listbox_line in imported_bt5.BusinessTemplate_getModifiedObject():
1946 1947
          item = listbox_line.object_id
          state = listbox_line.object_state
1948
          if state.startswith('Removed'):
1949 1950 1951 1952 1953 1954 1955 1956
            # The following condition could not be used to automatically decide
            # if an item must be kept or not. For example, this would not work
            # for items installed by PortalTypeWorkflowChainTemplateItem.
            maybe_moved = installed_dict.get(listbox_line.object_id, '')
            log('%s: %s%s' % (state, item,
              maybe_moved and ' (moved to %s ?)' % maybe_moved))
          else:
            installed_dict[item] = bt_title
1957 1958 1959

          # For actions which suggest that item shall be kept and item is not
          # explicitely forced, keep the default -- do nothing
1960 1961
          # XXX: 'force_keep_list' variable is misnamed.
          should_keep = item not in force_keep_list and state in (
1962 1963
            'Modified but should be kept', 'Removed but should be kept')
          # If item is forced to be untouched, do not touch it
1964 1965
          if item in keep_original_list or should_keep:
            if not should_keep:
1966 1967 1968
              log('Item %r is in force_keep_list and keep_original_list,'
                  ' as keep_original_list has precedence item is NOT MODIFIED'
                  % item)
1969 1970 1971
            install_kw[item] = 'nothing'
          else:
            install_kw[item] = listbox_line.choice_item_list[0][1]
1972

1973 1974
      # Run before script list
      for before_triggered_bt5_id in before_triggered_bt5_id_list:
1975 1976 1977
        log('Execute %r' % before_triggered_bt5_id)
        imported_bt5.unrestrictedTraverse(before_triggered_bt5_id)()

1978 1979 1980 1981 1982 1983 1984
      # Note: CATALOG_UPDATABLE should only be used in eceptional cases
      #       where the caller installs several bts and does not know
      #       which ones need to update catalog. Handling catalog should be
      #       usually done at upgrader level.
      if update_catalog is CATALOG_UPDATABLE and install_kw != {}:
        update_catalog = imported_bt5.isCatalogUpdatable()

1985 1986
      imported_bt5.install(object_to_update=install_kw,
                           update_catalog=update_catalog)
1987

1988 1989
      # Run After script list
      for after_triggered_bt5_id in after_triggered_bt5_id_list:
1990 1991 1992 1993 1994 1995 1996 1997
        log('Execute %r' % after_triggered_bt5_id)
        imported_bt5.unrestrictedTraverse(after_triggered_bt5_id)()
      if active_process is not None:
        active_process.postResult(ActiveResult(
          '%03u. %s' % (len(active_process.getResultList()) + 1, bt_title),
          detail='\n'.join(message_list)))
      else:
        log("Updated %s from %s" % (bt_title, download_url))
1998

1999 2000
      return imported_bt5

2001 2002 2003 2004 2005 2006 2007 2008 2009 2010 2011 2012 2013 2014 2015 2016 2017 2018 2019 2020
    security.declareProtected(Permissions.ManagePortal,
            'installMultipleBusinessPackage')
    def installMultipleBusinessPackage(self, bp5_list):
      """
      Install multiple Business Package at the same time
      """
      # XXX: Compare before calling install on path object property items
      from Products.ERP5.Document.BusinessPackage import \
                    ObjectPropertyTemplatePackageItem, PathTemplatePackageItem

      final_path_item = bp5_list[0]._path_item
      final_prop_item = bp5_list[0]._object_property_item

      for bp5 in bp5_list:
        final_path_item += bp5._path_item
        final_prop_item += bp5._object_property_item

      final_path_item.install()
      final_prop_item.install()

2021
    security.declareProtected(Permissions.ManagePortal,
2022 2023
            'installBusinessManager')
    def installBusinessManager(self, bm):
2024
      """
2025
      Run installation on flattened Business Manager
2026
      """
2027 2028 2029 2030 2031 2032
      # Run install on separate Business Item one by one
      for path_item in bm._path_item_list:
        path_item.install(self)

      bm.setStatus('installed')

2033 2034 2035 2036 2037 2038 2039 2040 2041 2042 2043 2044 2045 2046 2047 2048 2049 2050 2051 2052 2053 2054 2055 2056 2057 2058 2059 2060 2061 2062 2063 2064 2065 2066 2067 2068 2069 2070 2071 2072 2073 2074 2075 2076 2077 2078 2079 2080 2081 2082 2083 2084 2085 2086 2087 2088 2089 2090 2091 2092 2093 2094 2095 2096 2097 2098 2099 2100 2101 2102
    def rebuildBusinessManager(self, bm):
      """
      Compare the sub-objects in the Business Manager to the previous built
      state to give user powet to decide on which item to rebuild.
      """
      checkNeeded = True
      changed_path_list = []

      if bm.getBuildingState() not in  ['built', 'modified']:
        # In case the building_state is not built, we build the BM without
        # comparing anything
        checkNeeded = False
        return checkNeeded, changed_path_list

      portal = self.getPortalObject()
      for item in bm.objectValues():
        # Check for the change compared to old building state, i.e, if there is
        # some change made at ZODB state(it also count changes made due to
        # change while installation of other BM)
        path = item.getProperty('item_path')

        try:
          if item.isProperty:
            # Get the value for Business Property Item
            value = item.getProperty('item_property_value')
            # Get the value at ZODB
            relative_url, property_id = path.split('#')
            obj = portal.restrictedTraverse(relative_url)
            property_value = obj.getProperty(property_id)

            # If the value at ZODB for the property is none, raise KeyError
            # This is important to have compatibility between the way we check
            # path as well as property. Otherwise, if we install a new property,
            # we are always be getting an Error that there is change made at
            # ZODB for this property
            if not property_value:
              raise KeyError

            obj = property_value
          else:
            # Get the value of the Business Path Item
            value_list = item.objectValues()
            if value_list:
              value = value_list[0]
            else:
              # If there is no value, it means the path_item is new, thus no
              # need to comapre hash and check anything
              changed_path_list.append((path, 'New'))
              continue

            # Get the object at ZODB
            obj = portal.restrictedTraverse(path)

          # Calculate hash for value at ZODB
          obj_sha = self.calculateComparableHash(obj, item.isProperty)
          # Calculate hash for value at property_value
          item_sha = self.calculateComparableHash(value, item.isProperty)

          # Compare the hash with the item hash
          if obj_sha != item_sha:
            changed_path_list.append((path, 'Changed'))
          else:
            changed_path_list.append((path, 'Unchanged'))

        # KeyError is raised in case the value/object has been deleted at ZODB
        except KeyError:
          changed_path_list.append((path, 'Deleted'))

      return checkNeeded, changed_path_list

2103 2104
    security.declareProtected(Permissions.ManagePortal,
            'updateInstallationState')
2105
    def compareInstallationState(self, bm_list):
2106 2107 2108 2109 2110 2111 2112 2113 2114 2115 2116 2117 2118 2119 2120
      """
      Run installation after comparing combined Business Manager status

      Steps:
      1. Create combinedBM for the bm_list
      2. Get the old combinedBM by checking the 'installed' status for it or
      by checking timestamp (?? which is better)
      CombinedBM: Collection of all Business item(s) whether installed or
      uninstalled
      3. Build BM from the filesystem
      4. Compare the combinedBM state to the last combinedBM state
      5. Compare the installation state to the OFS state
      6. If conflict while comaprison at 3, raise the error
      7. In all other case, install the BM List
      """
2121

2122
      # Create old installation state from Installed Business Manager
2123 2124 2125
      installed_bm_list = self.getInstalledBusinessManagerList()
      combined_installed_path_item = [item for bm
                                      in installed_bm_list
2126
                                      for item in bm.objectValues()]
2127 2128 2129 2130 2131

      # Create BM for old installation state and update its path item list
      old_installation_state = self.newContent(
                                  portal_type='Business Manager',
                                  title='Old Installation State',
2132
                                  temp_object=True,
2133
                                  )
2134

2135
      for item in combined_installed_path_item:
2136
        item.isIndexable = ConstantGetter('isIndexable', value=False)
2137 2138 2139
        # Better to use new ids so that we don't end up in conflicts
        new_id = old_installation_state.generateNewId()
        old_installation_state._setObject(new_id, aq_base(item),
2140
                                          suppress_events=True)
2141 2142 2143 2144 2145 2146 2147 2148 2149 2150 2151 2152

      forbidden_bm_title_list = ['Old Installation State',]
      for bm in bm_list:
        forbidden_bm_title_list.append(bm.title)

      new_installed_bm_list = [l for l
                               in self.getInstalledBusinessManagerList()
                               if l.title not in forbidden_bm_title_list]
      new_installed_bm_list.extend(bm_list)

      combined_new_path_item = [item for bm
                                in new_installed_bm_list
2153
                                for item in bm.objectValues()]
2154 2155 2156 2157 2158

      # Create BM for new installation state and update its path item list
      new_installation_state = self.newContent(
                                  portal_type='Business Manager',
                                  title='New Installation State',
2159
                                  temp_object=True,
2160
                                  )
2161

2162
      for item in combined_new_path_item:
2163
        item.isIndexable = ConstantGetter('isIndexable', value=False)
2164 2165
        new_id = new_installation_state.generateNewId()
        new_installation_state._setObject(new_id, aq_base(item),
2166
                                          suppress_events=True)
2167 2168 2169 2170 2171 2172

      # Create installation process, which have the changes to be made in the
      # OFS during installation. Importantly, it should also be a Business Manager
      installation_process = self.newContent(
                                  portal_type='Business Manager',
                                  title='Installation Process',
2173
                                  temp_object=True,
2174 2175 2176 2177 2178 2179 2180 2181 2182 2183 2184 2185 2186 2187 2188 2189
                                  )

      # Get path list for old and new states
      old_state_path_list = old_installation_state.getPathList()
      new_state_path_list = new_installation_state.getPathList()

      to_install_path_item_list = []

      # Get the path which has been removed in new installation_state
      removed_path_list = [path for path
                           in old_state_path_list
                           if path not in new_state_path_list]

      # Add the removed path with negative sign in the to_install_path_item_list
      for path in removed_path_list:
        old_item = old_installation_state.getBusinessItemByPath(path)
2190
        old_item.setProperty('item_sign', '-1')
2191 2192
        to_install_path_item_list.append(old_item)

2193 2194
      # XXX: At this point, we expect all the Business Manager objects as 'reduced',
      # thus all the BusinessItem sub-objects should have single value
2195
      # Update hashes of item in old state before installation
2196
      for item in old_installation_state.objectValues():
2197
        if item.isProperty:
2198
          value = item.getProperty('item_property_value')
2199 2200
        else:
          value_list = item.objectValues()
2201 2202
          if value_list:
            value = value_list[0]
2203 2204
          else:
            value = ''
2205
        if value:
2206
          item.setProperty('item_sha', self.calculateComparableHash(
2207
                                                              value,
2208 2209
                                                              item.isProperty,
                                                              ))
2210

2211 2212
      # Path Item List for installation_process should be the difference between
      # old and new installation state
2213
      for item in new_installation_state.objectValues():
2214
        # If the path has been removed, then add it with sign = -1
2215 2216
        old_item = old_installation_state.getBusinessItemByPath(item.getProperty('item_path'))
        # Calculate sha for the items in new_insatallation_state
2217
        if item.isProperty:
2218
          value = item.getProperty('item_property_value')
2219
        else:
2220 2221 2222 2223 2224 2225 2226
          value_list = item.objectValues()
          if value_list:
            value = value_list[0]
          else:
            value = ''
        if value:
          item.setProperty('item_sha', self.calculateComparableHash(
2227 2228 2229
                                                                value,
                                                                item.isProperty,
                                                                ))
2230 2231 2232
        if old_item:
          # If the old_item exists, we match the hashes and if it differs, then
          # add the new item
2233
          if old_item.getProperty('item_sha') != item.getProperty('item_sha'):
2234 2235 2236 2237
            to_install_path_item_list.append(item)
        else:
          to_install_path_item_list.append(item)

2238
      for item in to_install_path_item_list:
2239
        item.isIndexable = ConstantGetter('isIndexable', value=False)
2240 2241
        new_id = new_installation_state.generateNewId()
        installation_process._setObject(new_id, aq_base(item),
2242
                                        suppress_events=True)
2243

2244
      change_list = self.compareOldStateToOFS(installation_process, old_installation_state)
2245

2246 2247
      if change_list:
        change_list = [(l[0].item_path, l[1]) for l in change_list]
2248

2249
      return change_list
2250

2251 2252 2253 2254 2255 2256 2257 2258 2259 2260 2261 2262 2263
    def updateInstallationState(self, bm_list, force=1):
      """
      First compare installation state and then install the final value
      """
      change_list = self.compareInstallationState(bm_list)

      if force:
        to_install_path_list = [l[0] for l in change_list]
        to_install_path_list = self.sortPathList(to_install_path_list)

        # Install the path items with bm_list as context
        self.installBusinessItemList(bm_list, to_install_path_list)

2264 2265
    installMultipleBusinessManager = updateInstallationState

2266 2267 2268 2269 2270 2271 2272 2273 2274 2275 2276 2277 2278 2279 2280 2281 2282 2283 2284 2285 2286 2287 2288 2289 2290 2291 2292 2293 2294 2295 2296 2297 2298 2299 2300 2301 2302 2303 2304 2305
    def installBusinessItemList(self, manager_list, item_path_list):
      """
      Install Business Item/Business Property Item from the current Installation
      Process given the change_list which carries the list of paths to be
      installed
      """
      LOG('INFO', 0, '%s' % [item_path_list])

      # Create BM for new installation state and update its path item list
      new_installation_state = self.newContent(
                                  portal_type='Business Manager',
                                  title='Final Installation State',
                                  temp_object=True,
                                  )
      combined_new_path_item_list = [item for bm
                                in manager_list
                                for item in bm.objectValues()]

      for item in combined_new_path_item_list:
        item.isIndexable = ConstantGetter('isIndexable', value=False)
        new_id = new_installation_state.generateNewId()
        new_installation_state._setObject(new_id, aq_base(item),
                                        suppress_events=True)

      for path in item_path_list:
        item = new_installation_state.getBusinessItemByPath(path)
        if item is None:
          raise ValueError("Couldn't find path in current Installation State")
        item.install(new_installation_state)

      # Update workflow history of the installed Business Manager(s)
      # Get the 'business_manager_installation_workflow' as it is already
      # bootstrapped and installed
      portal_workflow = self.getPortalObject().portal_workflow
      wf = portal_workflow._getOb('business_manager_installation_workflow')

      # Change the installation state for all the BM(s) in manager_list.
      for manager in manager_list:
        wf._executeMetaTransition(manager, 'installed')

2306
    def calculateComparableHash(self, object, isProperty=False):
2307 2308
      """
      Remove some attributes before comparing hashses
Ayush Tiwari's avatar
Ayush Tiwari committed
2309 2310
      and return hash of the comparable object dict, in case the object is
      an erp5 object.
2311 2312 2313 2314 2315

      Use shallow copy of the dict of the object at ZODB after removing
      attributes which changes at small updation, like workflow_history,
      uid, volatile attributes(which starts with _v)
      """
2316
      if isProperty:
Ayush Tiwari's avatar
Ayush Tiwari committed
2317
        obj_dict = object
2318 2319 2320 2321
        # Have compatibilty between tuples and list while comparing as we face
        # this situation a lot especially for list type properties
        if isinstance(obj_dict, list):
          obj_dict = tuple(obj_dict)
Ayush Tiwari's avatar
Ayush Tiwari committed
2322
      else:
2323

2324 2325
        klass = object.__class__
        classname = klass.__name__
Ayush Tiwari's avatar
Ayush Tiwari committed
2326
        obj_dict = object.__dict__.copy()
2327 2328 2329 2330 2331 2332 2333

        # If the dict is empty, do calculate hash of None as it stays same on
        # one platform and in any case its impossiblt to move live python
        # objects from one seeion to another
        if not bool(obj_dict):
          return hash(None)

2334
        attr_set = {'_dav_writelocks', '_filepath', '_owner', '_related_index',
2335
                    'last_id', 'uid', '_mt_index', '_count', '_tree',
2336 2337
                    '__ac_local_roles__', '__ac_local_roles_group_id_dict__',
                    'workflow_history',}
2338 2339 2340 2341 2342 2343 2344 2345 2346 2347 2348 2349 2350 2351 2352 2353 2354 2355

        attr_set.update(('isIndexable',))

        if classname in ('File', 'Image'):
          attr_set.update(('_EtagSupport__etag', 'size'))
        elif classname == 'Types Tool' and klass.__module__ == 'erp5.portal_type':
          attr_set.add('type_provider_list')

        for attr in object.__dict__.keys():
          if attr in attr_set or attr.startswith('_cache_cookie_') or attr.startswith('_v'):
            try:
              del obj_dict[attr]
            except AttributeError:
              # XXX: Continue in cases where we want to delete some properties which
              # are not in attribute list
              # Raise an error
              continue

2356 2357 2358 2359 2360 2361 2362 2363 2364 2365 2366 2367
          # Special case for configuration instance attributes
          if attr in ['_config', '_config_metadata']:
            import collections
            # Order the dictionary so that comparison can be correct
            obj_dict[attr] = collections.OrderedDict(sorted(obj_dict[attr].items()))
            if 'valid_tags' in obj_dict[attr]:
              try:
                obj_dict[attr]['valid_tags'] = collections.OrderedDict(sorted(obj_dict[attr]['valid_tags'].items()))
              except AttributeError:
                # This can occur in case the valid_tag object is PersistentList
                pass

2368
        if 'data' in obj_dict:
2369
          try:
2370 2371 2372
            obj_dict['data'] = obj_dict.get('data').__dict__
          except AttributeError:
            pass
2373 2374 2375 2376

      obj_sha = hash(pprint.pformat(obj_dict))
      return obj_sha

2377 2378 2379 2380 2381
    def sortPathList(self, path_list):
      """
      Custom sort for path_list according to the priorities of paths
      """
      def comparePath(path):
2382
        split_path_list = path.split('/')
2383 2384 2385 2386
        # Paths with property item should have the least priority as they should
        # be installed after installing the object only
        if '#' in path:
          return 11
2387 2388
        if len(split_path_list) == 2 and split_path_list[0] in ('portal_types', 'portal_categories'):
          return 1
2389 2390 2391 2392
        # portal_transforms objects needs portal_components installed first so
        # as to register the modules
        if len(split_path_list) == 2 and split_path_list[0] == 'portal_transforms':
          return 12
2393
        if len(split_path_list) > 2:
2394
          return 10
2395 2396 2397
        if len(split_path_list) == 1:
          return 2
        return 5
2398 2399 2400

      return sorted(path_list, key=comparePath)

2401 2402 2403 2404 2405 2406
    def compareOldStateToOFS(self, installation_process, old_state):

      # Get the paths about which we are concerned about
      to_update_path_list = installation_process.getPathList()
      portal = self.getPortalObject()

2407 2408 2409
      # List to store what changes will be done to which path. Here we compare
      # with all the states (old version, new version and state of object at ZODB)
      change_list = []
2410

2411 2412
      to_update_path_list = self.sortPathList(to_update_path_list)

2413 2414
      for path in to_update_path_list:
        try:
Ayush Tiwari's avatar
Ayush Tiwari committed
2415
          if '#' in str(path):
2416
            isProperty = True
Ayush Tiwari's avatar
Ayush Tiwari committed
2417 2418 2419 2420 2421 2422 2423 2424 2425 2426 2427 2428
            relative_url, property_id = path.split('#')
            obj = portal.restrictedTraverse(relative_url)
            property_value = obj.getProperty(property_id)

            # If the value at ZODB for the property is none, raise KeyError
            # This is important to have compatibility between the way we check
            # path as well as property. Otherwise, if we install a new property,
            # we are always be getting an Error that there is change made at
            # ZODB for this property
            if not property_value:
              raise KeyError
            property_type = obj.getPropertyType(property_id)
2429
            obj = property_value
Ayush Tiwari's avatar
Ayush Tiwari committed
2430
          else:
2431
            isProperty = False
2432 2433 2434 2435 2436 2437 2438 2439 2440 2441 2442
            # XXX: Hardcoding because of problem with 'resource' trying to access
            # the resource via acqusition. Should be removed completely before
            # merging (DONT PUSH THIS)
            if path == 'portal_categories/resource':
              path_list = path.split('/')
              container_path = path_list[:-1]
              object_id = path_list[-1]
              container = portal.restrictedTraverse(container_path)
              obj = container._getOb(object_id)
            else:
              obj = portal.restrictedTraverse(path)
Ayush Tiwari's avatar
Ayush Tiwari committed
2443

2444
          obj_sha = self.calculateComparableHash(obj, isProperty)
2445

2446
          # Get item at old state
2447
          old_item = old_state.getBusinessItemByPath(path)
2448 2449 2450 2451 2452
          # Check if there is an object at old state at this path

          if old_item:
            # Compare hash with ZODB

2453
            if old_item.getProperty('item_sha') == obj_sha:
2454 2455 2456 2457
              # No change at ZODB on old item, so get the new item
              new_item = installation_process.getBusinessItemByPath(path)
              # Compare new item hash with ZODB

2458 2459
              if new_item.getProperty('item_sha') == obj_sha:
                if int(new_item.getProperty('item_sign')) == -1:
2460
                  # If the sign is negative, remove the value from the path
2461
                  change_list.append((new_item, 'Removing'))
2462 2463 2464
                else:
                  # If same hash, and +1 sign, do nothing
                  continue
2465 2466 2467

              else:
                # Install the new_item
2468
                change_list.append((new_item, 'Adding'))
2469 2470 2471 2472 2473 2474

            else:
              # Change at ZODB, so get the new item
              new_item = installation_process.getBusinessItemByPath(path)
              # Compare new item hash with ZODB

2475
              if new_item.getProperty('item_sha') == obj_sha:
2476 2477 2478 2479
                # If same hash, do nothing
                continue

              else:
2480 2481
                # Trying to update change at ZODB
                change_list.append((new_item, 'Updating'))
2482 2483 2484 2485 2486 2487

          else:
            # Object created at ZODB by the user
            # Compare with the new_item

            new_item = installation_process.getBusinessItemByPath(path)
2488
            if new_item.getProperty('item_sha') == obj_sha:
2489 2490 2491 2492
              # If same hash, do nothing
              continue

            else:
2493 2494
              # Trying to update change at ZODB
              change_list.append((new_item, 'Updating'))
2495

2496
        except (AttributeError, KeyError) as e:
2497 2498 2499 2500 2501 2502 2503 2504 2505 2506
          # Get item at old state
          old_item = old_state.getBusinessItemByPath(path)
          # Check if there is an object at old state at this path

          if old_item:
            # This means that the user had removed the object at this path
            # Check what the sign is for the new_item
            new_item = installation_process.getBusinessItemByPath(path)
            # Check sign of new_item

2507
            if int(new_item.getProperty('item_sign')) == 1:
2508 2509
              # Object at ZODB has been removed by the user
              change_item.append((new_item, 'Adding'))
2510 2511 2512 2513

          else:
            # If there is  no item at old state, install the new_item
            new_item = installation_process.getBusinessItemByPath(path)
2514
            # XXX: Hack for not trying to install the sub-objects from zexp,
2515
            # This should rather be implemented while exporting the object,
2516
            # where we shouldn't export sub-objects in the zexp
2517 2518 2519 2520 2521
            if not isProperty:
              try:
                value =  new_item.objectValues()[0]
              except IndexError:
                continue
2522 2523
            # Installing a new item
            change_list.append((new_item, 'Adding'))
2524

2525
      return change_list
2526

2527 2528
    def getInstalledBusinessManagerList(self):
      bm_list = self.objectValues(portal_type='Business Manager')
2529 2530
      installed_bm_list = [bm for bm in bm_list
                          if bm.getInstallationState() == 'installed']
2531 2532 2533 2534 2535 2536 2537 2538 2539
      return installed_bm_list

    def getInstalledBusinessManagerTitleList(self):
      installed_bm_list = self.getInstalledBusinessManagerList()
      if not len(installed_bm_list):
        return []
      installed_bm_title_list = [bm.title for bm in installed_bm_list]
      return installed_bm_title_list

2540 2541 2542 2543 2544 2545 2546
    security.declareProtected(Permissions.ManagePortal,
            'getBusinessTemplateUrl')
    def getBusinessTemplateUrl(self, base_url_list, bt5_title):
      """
        This method verify if the business template are available
        into one url (repository).
      """
2547 2548
      if base_url_list is None:
        base_url_list = self.getRepositoryList()
2549 2550 2551 2552 2553
      # This list could be preconfigured at some properties or
      # at preferences.
      for base_url in base_url_list:
        url = "%s/%s" % (base_url, bt5_title)
        if base_url == "INSTANCE_HOME_REPOSITORY":
Rafael Monnerat's avatar
Rafael Monnerat committed
2554
          url = "file://%s/bt5/%s" % (getConfiguration().instancehome,
2555 2556 2557 2558 2559 2560 2561 2562 2563 2564 2565 2566 2567 2568 2569
                                      bt5_title)
          LOG('ERP5', INFO, "TemplateTool: INSTANCE_HOME_REPOSITORY is %s." \
              % url)
        try:
          urllib2.urlopen(url)
          return url
        except (urllib2.HTTPError, OSError):
          # XXX Try again with ".bt5" in case the folder format be used
          # Instead tgz one.
          url = "%s.bt5" % url
          try:
            urllib2.urlopen(url)
            return url
          except (urllib2.HTTPError, OSError):
            pass
Rafael Monnerat's avatar
Rafael Monnerat committed
2570
      LOG('ERP5', INFO, 'TemplateTool: %s was not found into the url list: '
2571 2572 2573
                        '%s.' % (bt5_title, base_url_list))
      return None

2574 2575 2576
    security.declareProtected(Permissions.ManagePortal,
        'upgradeSite')
    def upgradeSite(self, bt5_list, deprecated_after_script_dict=None,
2577 2578
                    deprecated_reinstall_set=None, dry_run=False,
                    delete_orphaned=False,
2579 2580
                    keep_bt5_id_set=[],
                    update_catalog=False):
2581 2582 2583 2584 2585 2586 2587
      """
      Upgrade many business templates at a time. bt5_list should
      contains only final business templates, then all dependencies
      are calculated, and missing business templates will be added,
      old business templates will be updated, and orphelin business
      templates will be deleted

2588 2589 2590
      keep_bt5_id_set: business template that should not be deleted.
                       This is useful if we want to keep an old business
                       template without updating it and without removing it
2591

2592 2593
      deprecated_reinstall_set: this parameter is obsolete, please set
                                force_install property at business template level
2594 2595
                                It list all business templates who needs
                                reinstall
2596 2597 2598 2599 2600 2601

      update_catalog: handling catalog should be handled outside upgradeSite.
                      This option only exists for the case where it is not
                      known which bts need catalog update. In this case one
                      can pass CATALOG_UPDATABLE which will be propagated to
                      updateBusinessTemplateFromUrl.
2602
      """
2603
      # make sure that we updated information on repository
2604 2605 2606 2607 2608 2609 2610 2611 2612 2613 2614 2615 2616
      self.updateRepositoryBusinessTemplateList(self.getRepositoryList())
      # do upgrade
      message_list = []
      deprecated_reinstall_set = deprecated_reinstall_set or set()
      def append(message):
        message_list.append(message)
        LOG('upgradeSite', 0, message)
      dependency_list = [x[1] for x in \
        self.resolveBusinessTemplateListDependency(bt5_list)]
      update_bt5_list = self.getRepositoryBusinessTemplateList(
        template_list=dependency_list)
      update_bt5_list.sort(key=lambda x: dependency_list.index(x.title))
      for bt5 in update_bt5_list:
2617
        reinstall = bt5.title in deprecated_reinstall_set or bt5.force_install
2618 2619
        if (not(reinstall) and bt5.version_state == 'present') or \
            bt5.title in keep_bt5_id_set:
2620 2621 2622 2623 2624
          continue
        append("Update %s business template in state %s%s" % \
          (bt5.title, bt5.version_state, (reinstall and ' (reinstall)') or ''))
        if not(dry_run):
          bt5_url = "%s/%s" % (bt5.repository, bt5.title)
2625 2626
          self.updateBusinessTemplateFromUrl(bt5_url, reinstall=reinstall,
                                             update_catalog=update_catalog)
2627 2628 2629 2630 2631 2632 2633 2634 2635 2636 2637 2638 2639 2640
      if delete_orphaned:
        if keep_bt5_id_set is None:
          keep_bt5_id_set = set()
        to_remove_bt5_list = [x for x in self.getInstalledBusinessTemplateList()
                              if x.title not in dependency_list]
        sorted_to_remove_bt5_id_list = self.sortDownloadedBusinessTemplateList(
                                  [x.id for x in to_remove_bt5_list])
        sorted_to_remove_bt5_id_list.reverse()
        to_remove_bt5_list.sort(
          key=lambda x: sorted_to_remove_bt5_id_list.index(x.id))
        for bt in to_remove_bt5_list:
          if bt.title in keep_bt5_id_set:
            continue
          append("Uninstall business template %s" % bt.title)
2641
          if not(dry_run):
2642 2643 2644
            # XXX Here is missing parameters to really remove stuff
            bt.uninstall()

2645 2646
      return message_list

Jean-Paul Smets's avatar
Jean-Paul Smets committed
2647
InitializeClass(TemplateTool)