TemplateTool.py 40 KB
Newer Older
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1 2 3
##############################################################################
#
# Copyright (c) 2002 Nexedi SARL and Contributors. All Rights Reserved.
Jean-Paul Smets's avatar
Jean-Paul Smets committed
4
#                    Jean-Paul Smets-Solanes <jp@nexedi.com>
Jean-Paul Smets's avatar
Jean-Paul Smets committed
5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
#
# 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.
#
##############################################################################

29
from webdav.client import Resource
Jean-Paul Smets's avatar
Jean-Paul Smets committed
30

Yoshinori Okuji's avatar
Yoshinori Okuji committed
31
from App.config import getConfiguration
32
import os
33
import shutil
34
import sys
Yoshinori Okuji's avatar
Yoshinori Okuji committed
35

36
from Acquisition import Implicit, Explicit
Jean-Paul Smets's avatar
Jean-Paul Smets committed
37
from AccessControl import ClassSecurityInfo
38
from Products.ERP5Type.Globals import InitializeClass, DTMLFile, PersistentMapping
39
from Products.ERP5Type.DiffUtils import DiffFile
Jean-Paul Smets's avatar
Jean-Paul Smets committed
40
from Products.ERP5Type.Tool.BaseTool import BaseTool
41
from Products.ERP5Type import Permissions, tarfile
42
from Products.ERP5.Document.BusinessTemplate import BusinessTemplateMissingDependency
43
from tempfile import mkstemp, mkdtemp
Jean-Paul Smets's avatar
Jean-Paul Smets committed
44
from Products.ERP5 import _dtmldir
Aurel's avatar
Aurel committed
45
from cStringIO import StringIO
46
from urllib import pathname2url, urlopen, splittype, urlretrieve
47 48
import re
from xml.dom.minidom import parse
49
from xml.parsers.expat import ExpatError
50 51
import struct
import cPickle
52
import posixpath
53
from base64 import b64encode, b64decode
54
from Products.ERP5Type.Message import translateString
55
from zLOG import LOG, INFO
56
from base64 import decodestring
57
import subprocess
58

Jean-Paul Smets's avatar
Jean-Paul Smets committed
59

60
WIN = os.name == 'nt'
61

62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
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

Jean-Paul Smets's avatar
Jean-Paul Smets committed
78
class TemplateTool (BaseTool):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
79
    """
80
      TemplateTool manages Business Templates.
Jean-Paul Smets's avatar
Jean-Paul Smets committed
81

82 83 84 85 86 87
      TemplateTool provides some methods to deal with Business Templates:
        - download
        - publish
        - install
        - update
        - save
Jean-Paul Smets's avatar
Jean-Paul Smets committed
88 89
    """
    id = 'portal_templates'
90
    title = 'Template Tool'
Jean-Paul Smets's avatar
Jean-Paul Smets committed
91
    meta_type = 'ERP5 Template Tool'
Jean-Paul Smets's avatar
Jean-Paul Smets committed
92
    portal_type = 'Template Tool'
93
    allowed_types = ( 'ERP5 Business Template',)
94

95 96
    # This stores information on repositories.
    repository_dict = {}
Jean-Paul Smets's avatar
Jean-Paul Smets committed
97 98 99 100 101

    # Declarative Security
    security = ClassSecurityInfo()

    security.declareProtected( Permissions.ManagePortal, 'manage_overview' )
Aurel's avatar
Aurel committed
102
    manage_overview = DTMLFile( 'explainTemplateTool', _dtmldir )
Jean-Paul Smets's avatar
Jean-Paul Smets committed
103

104 105
    def getInstalledBusinessTemplate(self, title, **kw):
      """
Vincent Pelletier's avatar
Vincent Pelletier committed
106
        Return an installed version of business template of a certain title.
107 108
      """
      # This can be slow if, say, 10000 business templates are present.
Vincent Pelletier's avatar
Vincent Pelletier committed
109 110 111
      # 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.
112 113 114 115
      for bt in self.contentValues(filter={'portal_type':'Business Template'}):
        if bt.getInstallationState() == 'installed' and bt.getTitle() == title:
          return bt
      return None
116

117
    def getInstalledBusinessTemplatesList(self):
118 119 120 121 122 123 124
      """Deprecated.
      """
      DeprecationWarning('getInstalledBusinessTemplatesList is deprecated; Use getInstalledBusinessTemplateList instead.', DeprecationWarning)
      return self.getInstalledBusinessTemplateList()

    def getInstalledBusinessTemplateList(self):
      """Get the list of installed business templates.
125 126
      """
      installed_bts = []
127
      for bt in self.contentValues(portal_type='Business Template'):
128 129 130
        if bt.getInstallationState() == 'installed':
          installed_bts.append(bt)
      return installed_bts
131

132
    def getBuiltBusinessTemplatesList(self):
133 134 135 136 137 138 139
      """Deprecated.
      """
      DeprecationWarning('getBuiltBusinessTemplatesList is deprecated; Use getBuiltBusinessTemplateList instead.', DeprecationWarning)
      return self.getBuiltBusinessTemplateList()

    def getBuiltBusinessTemplateList(self):
      """Get the list of built and not installed business templates.
140 141
      """
      built_bts = []
142
      for bt in self.contentValues(portal_type='Business Template'):
143 144 145
        if bt.getInstallationState() == 'not_installed' and bt.getBuildingState() == 'built':
          built_bts.append(bt)
      return built_bts
146

147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178
    @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
        revision of a business template with a URL like:
          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()
            revision = int(bt.getRevision())
            if last_bt[0] < revision and bt.getInstallationState() != 'deleted':
              last_bt = revision, bt
          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)
179

180
    security.declareProtected(Permissions.ManagePortal,
181 182
                              'getDefaultBusinessTemplateDownloadURL')
    def getDefaultBusinessTemplateDownloadURL(self):
183 184 185 186 187
      """Returns the default download URL for business templates.
      """
      return "file://%s/" % pathname2url(
                  os.path.join(getConfiguration().instancehome, 'bt5'))

188 189
    security.declareProtected( 'Import/Export objects', 'save' )
    def save(self, business_template, REQUEST=None, RESPONSE=None):
Yoshinori Okuji's avatar
Yoshinori Okuji committed
190
      """
Vincent Pelletier's avatar
Vincent Pelletier committed
191
        Save the BusinessTemplate in the servers's filesystem.
Yoshinori Okuji's avatar
Yoshinori Okuji committed
192 193
      """
      cfg = getConfiguration()
Vincent Pelletier's avatar
Vincent Pelletier committed
194 195
      path = os.path.join(cfg.clienthome,
                          '%s' % (business_template.getTitle(),))
196
      path = pathname2url(path)
197
      business_template.export(path=path, local=True)
198
      if REQUEST is not None:
199
        psm = translateString('Saved in ${path} .',
200
                              mapping={'path':pathname2url(path)})
201
        ret_url = '%s/%s?portal_status_message=%s' % \
Vincent Pelletier's avatar
Vincent Pelletier committed
202
                  (business_template.absolute_url(),
203
                   REQUEST.get('form_id', 'view'), psm)
Vincent Pelletier's avatar
Vincent Pelletier committed
204 205 206
        if RESPONSE is None:
          RESPONSE = REQUEST.RESPONSE
        return REQUEST.RESPONSE.redirect( ret_url )
207 208 209 210

    security.declareProtected( 'Import/Export objects', 'export' )
    def export(self, business_template, REQUEST=None, RESPONSE=None):
      """
Vincent Pelletier's avatar
Vincent Pelletier committed
211 212
        Export the Business Template as a bt5 file and offer the user to
        download it.
213
      """
214
      path = business_template.getTitle()
215
      path = pathname2url(path)
216
      # XXX Why is it necessary to create a temporary directory?
217
      tmpdir_path = mkdtemp()
218
      # XXX not thread safe
219
      current_directory = os.getcwd()
220
      os.chdir(tmpdir_path)
Aurel's avatar
Aurel committed
221
      export_string = business_template.export(path=path)
222
      os.chdir(current_directory)
223
      if RESPONSE is not None:
224
        RESPONSE.setHeader('Content-type','tar/x-gzip')
225
        RESPONSE.setHeader('Content-Disposition',
226
                           'inline;filename=%s-%s.bt5' % \
227
                               (path,
228
                                business_template.getVersion()))
Aurel's avatar
Aurel committed
229 230 231 232
      try:
        return export_string.getvalue()
      finally:
        export_string.close()
Yoshinori Okuji's avatar
Yoshinori Okuji committed
233

234
    security.declareProtected( 'Import/Export objects', 'publish' )
235 236
    def publish(self, business_template, url, username=None, password=None):
      """
Vincent Pelletier's avatar
Vincent Pelletier committed
237
        Publish the given business template at the given URL.
238 239
      """
      business_template.build()
Vincent Pelletier's avatar
Vincent Pelletier committed
240
      export_string = self.manage_exportObject(id=business_template.getId(),
241
                                               download=True)
242
      bt = Resource(url, username=username, password=password)
Vincent Pelletier's avatar
Vincent Pelletier committed
243 244
      bt.put(file=export_string,
             content_type='application/x-erp5-business-template')
245
      business_template.setPublicationUrl(url)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
246

247 248
    def update(self, business_template):
      """
Vincent Pelletier's avatar
Vincent Pelletier committed
249
        Update an existing template from its publication URL.
250 251 252 253 254 255
      """
      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
256
      self._importObjectFromFile(StringIO(export_string), id=id)
257

Aurel's avatar
Aurel committed
258 259
    def _importBT(self, path=None, id=id):
      """
Vincent Pelletier's avatar
Vincent Pelletier committed
260
        Import template from a temp file (as uploaded by the user)
Aurel's avatar
Aurel committed
261
      """
262
      file = open(path, 'rb')
263 264 265 266 267 268
      try:
        # read magic key to determine wich kind of bt we use
        file.seek(0)
        magic = file.read(5)
      finally:
        file.close()
269

Aurel's avatar
Aurel committed
270 271 272 273
      if magic == '<?xml': # old version
        self._importObjectFromFile(path, id=id)
        bt = self[id]
        bt.id = id # Make sure id is consistent
274
        bt.setProperty('template_format_version', 0, type='int')
Aurel's avatar
Aurel committed
275
      else: # new version
Vincent Pelletier's avatar
Vincent Pelletier committed
276 277
        # XXX: should really check for a magic and offer a falback if it
        # doens't correspond to anything handled.
Aurel's avatar
Aurel committed
278
        tar = tarfile.open(path, 'r:gz')
279 280
        try:
          # create bt object
281
          bt = self.newContent(portal_type='Business Template', id=id)
282 283
          prop_dict = {}
          for prop in bt.propertyMap():
Aurel's avatar
Aurel committed
284
            prop_type = prop['type']
285
            pid = prop['id']
286
            prop_path = posixpath.join(tar.members[0].name, 'bt', pid)
287 288 289 290 291
            try:
              info = tar.getmember(prop_path)
            except KeyError:
              continue
            value = tar.extractfile(info).read()
292 293 294 295 296
            if value is 'None':
              # At export time, we used to export non-existent properties:
              #   str(obj.getProperty('non-existing')) == 'None'
              # Discard them
              continue
Jean-Paul Smets's avatar
Jean-Paul Smets committed
297 298
            if prop_type == 'text' or prop_type == 'string' \
                                   or prop_type == 'int':
299
              prop_dict[pid] = value
Aurel's avatar
Aurel committed
300
            elif prop_type == 'lines' or prop_type == 'tokens':
301
              prop_dict[pid[:-5]] = value.splitlines()
302 303 304
          prop_dict.pop('id', '')
          bt.edit(**prop_dict)
          # import all other files from bt
305
          fobj = open(path, 'rb')
306
          try:
307 308 309 310 311
            bt.importFile(file=fobj)
          finally:
            fobj.close()
        finally:
          tar.close()
Aurel's avatar
Aurel committed
312 313
      return bt

314
    security.declareProtected( Permissions.ManagePortal, 'manage_download' )
315 316
    def manage_download(self, url, id=None, REQUEST=None):
      """The management interface for download.
317
      """
318 319
      if REQUEST is None:
        REQUEST = getattr(self, 'REQUEST', None)
320

321
      bt = self.download(url, id=id)
322

323
      if REQUEST is not None:
324
        ret_url = bt.absolute_url() + '/view'
Yusei Tahara's avatar
Yusei Tahara committed
325
        psm = translateString("Business template downloaded successfully.")
326
        REQUEST.RESPONSE.redirect("%s?portal_status_message=%s"
327
                                    % (ret_url, psm))
328

329 330 331 332 333
    def _download_local(self, path, bt_id):
      """Download Business Template from local directory or file
      """
      if os.path.isdir(os.path.normpath(path)):
        path = os.path.normpath(path)
334 335 336 337 338 339 340 341 342 343
        def callback(file_list, directory, files):
          for excluded_directory in ('CVS', '.svn'):
            try:
              files.remove(excluded_directory)
            except ValueError:
              pass
          for file in files:
            absolute_path = os.path.join(directory, file)
            if os.path.isfile(absolute_path):
              file_list.append(absolute_path)
344

345
        file_list = []
346
        os.path.walk(path, callback, file_list)
Aurel's avatar
Aurel committed
347 348
        file_list.sort()
        # import bt object
349 350
        bt = self.newContent(portal_type='Business Template', id=bt_id)
        bt_path = os.path.join(path, 'bt')
Aurel's avatar
Aurel committed
351 352

        # import properties
353
        prop_dict = {}
Aurel's avatar
Aurel committed
354
        for prop in bt.propertyMap():
Aurel's avatar
Aurel committed
355
          prop_type = prop['type']
Aurel's avatar
Aurel committed
356
          pid = prop['id']
357 358
          prop_path = os.path.join('.', bt_path, pid)
          if not os.path.exists(prop_path):
359
            continue
360
          value = open(prop_path, 'rb').read()
361 362 363 364 365
          if value is 'None':
            # At export time, we used to export non-existent properties:
            #   str(obj.getProperty('non-existing')) == 'None'
            # Discard them
            continue
Aurel's avatar
Aurel committed
366
          if prop_type in ('text', 'string', 'int', 'boolean'):
367
            prop_dict[pid] = value
Aurel's avatar
Aurel committed
368
          elif prop_type in ('lines', 'tokens'):
369
            prop_dict[pid[:-5]] = value.splitlines()
370
        prop_dict.pop('id', '')
371
        bt.edit(**prop_dict)
Aurel's avatar
Aurel committed
372
        # import all others objects
373
        bt.importFile(dir=True, file=file_list, root_path=path)
374
        return bt
Aurel's avatar
Aurel committed
375
      else:
376 377 378 379 380 381
        # this should be a file
        return self._importBT(path, bt_id)

    def _download_url(self, url, bt_id):
      tempid, temppath = mkstemp()
      try:
382
        os.close(tempid) # Close the opened fd as soon as possible.
383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402
        file_path, headers = urlretrieve(url, temppath)
        if re.search(r'<title>Revision \d+:', open(file_path, 'r').read()):
          # 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)

        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:
        import pysvn
        pysvn.Client().export(url, svn_checkout_dir)
        return self._download_local(svn_checkout_dir, bt_id)
      finally:
        shutil.rmtree(svn_checkout_tmp_dir)

403 404 405 406 407 408 409 410 411 412 413 414
    def assertBtPathExists(self, url):
      """
      Check if bt is present on the system
      """
      urltype, name = splittype(url)
      # Windows compatibility
      if WIN:
        if os.path.isdir(os.path.normpath(url)) or \
           os.path.isfile(os.path.normpath(url)):
          name = os.path.normpath(url)
      return os.path.exists(os.path.normpath(name))

415 416 417 418 419 420 421 422 423
    security.declareProtected( 'Import/Export objects', 'download' )
    def download(self, url, id=None, REQUEST=None):
      """
      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)
424

425 426 427 428 429 430 431 432 433 434
      if id is None:
        id = self.generateNewId()

      urltype, name = splittype(url)
      # Windows compatibility
      if WIN:
        if os.path.isdir(os.path.normpath(url)) or \
           os.path.isfile(os.path.normpath(url)):
          urltype = 'file'
          name = os.path.normpath(url)
435

436
      if urltype and urltype != 'file':
437
        if '/portal_templates/asRepository/' in url:
438 439 440 441 442
          # 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)]
443 444 445 446
        bt = self._download_url(url, id)
      else:
        bt = self._download_local(name, id)

447
      bt.build(no_action=True)
448
      return bt
Jean-Paul Smets's avatar
Jean-Paul Smets committed
449

450
    def importBase64EncodedText(self, file_data=None, id=None, REQUEST=None,
451
                                batch_mode=False, **kw):
452
      """
453 454 455
        Import Business Template from passed base64 encoded text.
      """
      import_file = StringIO(decodestring(file_data))
456
      return self.importFile(import_file = import_file, id = id, REQUEST = REQUEST,
457 458
                             batch_mode = batch_mode, **kw)

459
    def importFile(self, import_file=None, id=None, REQUEST=None,
460
                   batch_mode=False, **kw):
461
      """
Vincent Pelletier's avatar
Vincent Pelletier committed
462
        Import Business Template from one file
463
      """
464 465
      if REQUEST is None:
        REQUEST = getattr(self, 'REQUEST', None)
466

467 468 469 470 471
      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
472
          psm = translateString('No file or an empty file was specified.')
473 474
          REQUEST.RESPONSE.redirect("%s?portal_status_message=%s"
                                    % (self.absolute_url(), psm))
Alexandre Boeglin's avatar
Alexandre Boeglin committed
475 476
          return
        else :
477
          raise RuntimeError, 'No file or an empty file was specified'
Aurel's avatar
Aurel committed
478
      # copy to a temp location
Alexandre Boeglin's avatar
Alexandre Boeglin committed
479
      import_file.seek(0) #Rewind to the beginning of file
480
      tempid, temppath = mkstemp()
481 482
      try:
        os.close(tempid) # Close the opened fd as soon as possible
483
        tempfile = open(temppath, 'wb')
484 485 486 487 488 489 490
        try:
          tempfile.write(import_file.read())
        finally:
          tempfile.close()
        bt = self._importBT(temppath, id)
      finally:
        os.remove(temppath)
491
      bt.build(no_action=True)
Aurel's avatar
Aurel committed
492
      bt.reindexObject()
493

494
      if not batch_mode and \
495
         (REQUEST is not None):
496
        ret_url = bt.absolute_url() + '/view'
Yusei Tahara's avatar
Yusei Tahara committed
497
        psm = translateString("Business templates imported successfully.")
498 499
        REQUEST.RESPONSE.redirect("%s?portal_status_message=%s"
                                  % (ret_url, psm))
500
      elif batch_mode:
501
        return bt
502

503 504
    security.declareProtected(Permissions.ManagePortal, 'runUnitTestList')
    def runUnitTestList(self, test_list=[],
505
                        sql_connection_string='',
506 507
                        REQUEST=None, RESPONSE=None, **kwd):
      """Runs Unit Tests related to this Business Template
508
      """
Vincent Pelletier's avatar
Vincent Pelletier committed
509 510
      # XXX: should check for file presence before trying to execute.
      # XXX: should check if the unit test file is configured in the BT
511
      from Products.ERP5Type.tests.runUnitTest import getUnitTestFile
512 513 514 515 516 517 518 519
      if RESPONSE is not None:
        outfile = RESPONSE
      elif REQUEST is not None:
        outfile = RESPONSE = REQUEST.RESPONSE
      else:
        outfile =  StringIO()
      if RESPONSE is not None:
        RESPONSE.setHeader('Content-type', 'text/plain')
520
      test_cmd_args = [sys.executable, getUnitTestFile()]
521
      test_cmd_args += ['--erp5_sql_connection_string', sql_connection_string]
522 523 524 525 526
      test_cmd_args += test_list
      process = subprocess.Popen(test_cmd_args,
                                 stdout=subprocess.PIPE,
                                 stderr=subprocess.STDOUT)

527 528 529 530
      # "for line in process.stdout" is cleaner but is buffered,
      # see http://bugs.python.org/issue3907
      # We use this ugly construct to avoid waiting for test
      # termination before printing content
531 532 533 534 535 536
      while True:
        line = process.stdout.readline()
        if not line:
          break
        outfile.write(line)
        outfile.flush()
537

538 539
      if hasattr(outfile, 'getvalue'):
        return outfile.getvalue()
540

541 542 543 544 545 546 547 548
    def diffObjectAsHTML(self, REQUEST, **kw):
      """
        Convert diff into a HTML format before reply
        This is compatible with ERP5Subversion look and feel but
        it is preferred in future we use more difflib python library.
      """
      return DiffFile(self.diffObject(REQUEST, **kw)).toHTML()

549
    def diffObject(self, REQUEST, **kw):
Aurel's avatar
Aurel committed
550
      """
Vincent Pelletier's avatar
Vincent Pelletier committed
551 552
        Make diff between two objects, whose paths are stored in values bt1
        and bt2 in the REQUEST object.
Aurel's avatar
Aurel committed
553
      """
554 555
      bt1_id = getattr(REQUEST, 'bt1', None)
      bt2_id = getattr(REQUEST, 'bt2', None)
556 557 558 559 560 561 562
      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
563
      else:
564 565 566 567 568
        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)
569

Vincent Pelletier's avatar
Vincent Pelletier committed
570 571 572 573 574 575 576
    security.declareProtected( 'Import/Export objects',
                               'updateRepositoryBusinessTemplateList' )

    def updateRepositoryBusinessTemplateList(self, repository_list,
                                             REQUEST=None, RESPONSE=None, **kw):
      """
        Update the information on Business Templates from repositories.
577 578
      """
      self.repository_dict = PersistentMapping()
579
      property_list = ('title', 'version', 'revision', 'description', 'license',
580
                       'dependency', 'provision', 'copyright')
Vincent Pelletier's avatar
Vincent Pelletier committed
581 582
      #LOG('updateRepositoryBusiessTemplateList', 0,
      #    'repository_list = %r' % (repository_list,))
583 584 585 586 587
      for repository in repository_list:
        url = '/'.join([repository, 'bt5list'])
        f = urlopen(url)
        property_dict_list = []
        try:
588 589 590 591 592 593 594 595 596 597 598
          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
599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619
          try:
            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
620 621
              property_dict['version'] = \
                  temp_property_dict.get('version', [''])[0]
Jérome Perrin's avatar
Jérome Perrin committed
622 623
              property_dict['revision'] = \
                  temp_property_dict.get('revision', [''])[0]
Vincent Pelletier's avatar
Vincent Pelletier committed
624 625 626 627 628 629
              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', ())
630 631
              property_dict['provision_list'] = \
                  temp_property_dict.get('provision', ())
Vincent Pelletier's avatar
Vincent Pelletier committed
632 633
              property_dict['copyright_list'] = \
                  temp_property_dict.get('copyright', ())
634

635 636 637 638 639
              property_dict_list.append(property_dict)
          finally:
            doc.unlink()
        finally:
          f.close()
640

641
        self.repository_dict[repository] = tuple(property_dict_list)
642

643
      if REQUEST is not None:
644
        ret_url = self.absolute_url() + '/' + REQUEST.get('dialog_id', 'view')
Yusei Tahara's avatar
Yusei Tahara committed
645
        psm = translateString("Business templates updated successfully.")
646 647
        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))
648

Vincent Pelletier's avatar
Vincent Pelletier committed
649 650
    security.declareProtected( Permissions.AccessContentsInformation,
                               'getRepositoryList' )
651
    def getRepositoryList(self):
Vincent Pelletier's avatar
Vincent Pelletier committed
652 653
      """
        Get the list of repositories.
654 655
      """
      return self.repository_dict.keys()
656

657 658
    security.declarePublic( 'decodeRepositoryBusinessTemplateUid' )
    def decodeRepositoryBusinessTemplateUid(self, uid):
Vincent Pelletier's avatar
Vincent Pelletier committed
659 660 661
      """
        Decode the uid of a business template from a repository.
        Return a repository and an id.
662
      """
663
      return cPickle.loads(b64decode(uid))
664

665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700
    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)))

    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,)
701

702 703 704 705 706 707 708 709 710 711 712 713 714 715
    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
716

717 718 719 720 721
    security.declareProtected(Permissions.AccessContentsInformation,
                               'getLastestBTOnRepos')
    def getLastestBTOnRepos(self, title, version_restriction=None):
      """
       It's possible we have different versions of the same BT
722
       available on various repositories or on the same repository.
723 724 725 726 727 728
       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
729
        for property_dict in property_dict_list:
730 731 732
          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
733
          if title == property_dict['title']:
734 735 736 737 738 739 740
            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):
                result = (repository,  property_dict['id'], property_dict['version'])
      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 '')
741

742 743 744 745 746 747 748 749 750 751 752 753 754 755
    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
756

757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779
    security.declareProtected(Permissions.AccessContentsInformation,
                               'getDependencyList')
    def getDependencyList(self, bt):
      """
       Return the list of missing dependencies for a business
       template, given a tuple : (repository, id)
      """
      # We do not take into consideration the dependencies
      # for meta business templates
      if bt[0] == 'meta':
        return []
      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]:
              dependency_list = property_dict['dependency_list']
              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:
780 781 782 783
                  version_restriction = dependency_couple_list[1]
                  if version_restriction.startswith('('):
                    # Something like "(>= 1.0rc6)".
                    version_restriction = version_restriction[1:-1]
784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819
                require_update = False
                installed_bt = self.portal_templates.getInstalledBusinessTemplate(dependency)
                if version_restriction is not None:
                  if installed_bt is not None:
                    # Check if the installed version require an update
                    if not self.compareVersionStrings(installed_bt.getVersion(), version_restriction):
                      operator = version_restriction.split(' ')[0]
                      if operator in ('<', '<<', '<='):
                        raise BusinessTemplateMissingDependency, '%s (%s) is present but %s require: %s (%s)'%(dependency, installed_bt.getVersion(), property_dict['title'], dependency, version_restriction)
                      else:
                        require_update = True
                if (require_update or installed_bt is None) \
                  and dependency not in result_list:
                  # Get the lastest version of the dependency on the
                  # repository that meet the version restriction
                  provider_installed = False
                  try:
                    bt_dep = self.getLastestBTOnRepos(dependency, version_restriction)
                  except BusinessTemplateUnknownError:
                    raise BusinessTemplateMissingDependency, 'The following dependency could not be satisfied: %s (%s)\nReason: Business Template could not be found in the repositories'%(dependency, version_restriction or '')
                  except BusinessTemplateIsMeta:
                    provider_list = self.getProviderList(dependency)
                    for provider in provider_list:
                      if self.portal_templates.getInstalledBusinessTemplate(provider) is not None:
                        provider_installed = True
                        break
                    if not provider_installed:
                      bt_dep = ('meta', dependency)
                  if not provider_installed:
                    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])
820

821 822 823 824 825 826 827 828 829 830 831
    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'
832

833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849
    security.declareProtected(Permissions.AccessContentsInformation,
                              'sortBusinessTemplateList')
    def sortBusinessTemplateList(self, bt_list):
      """
       Sort a list of bt according to dependencies
      """
      result_list = []
      for repository, id in bt_list:
        dependency_list = self.getDependencyList((repository, id))
        dependency_list.append((repository, id))
        for dependency in dependency_list:
          if dependency[0] == 'meta':
            provider_list = self.getProviderList(dependency[1])
            dependency = self.findProviderInBTList(provider_list, bt_list)
          if dependency not in result_list:
            result_list.append(dependency)
      return result_list
850

Vincent Pelletier's avatar
Vincent Pelletier committed
851 852
    security.declareProtected( Permissions.AccessContentsInformation,
                               'getRepositoryBusinessTemplateList' )
853
    def getRepositoryBusinessTemplateList(self, update_only=False, **kw):
854 855
      """Get the list of Business Templates in repositories.
      """
Vincent Pelletier's avatar
Vincent Pelletier committed
856 857
      version_state_title_dict = { 'new' : 'New', 'present' : 'Present',
                                   'old' : 'Old' }
858 859 860 861 862 863 864 865 866 867 868 869

      from Products.ERP5Type.Document import newTempBusinessTemplate
      template_list = []

      template_item_list = []
      if update_only:
        # 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 title not in template_item_dict:
Vincent Pelletier's avatar
Vincent Pelletier committed
870 871
              # If this is the first time to see this business template,
              # insert it.
872 873
              template_item_dict[title] = (repository, property_dict)
            else:
Vincent Pelletier's avatar
Vincent Pelletier committed
874 875 876 877
              # 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]
Jérome Perrin's avatar
Jérome Perrin committed
878
              diff_version = self.compareVersions(previous_property_dict['version'],
879 880
                                                  property_dict['version'])
              if diff_version < 0:
881
                template_item_dict[title] = (repository, property_dict)
882
              elif diff_version == 0 \
Jérome Perrin's avatar
Jérome Perrin committed
883 884
                   and previous_property_dict['revision'] \
                   and property_dict['revision'] \
885
                   and int(previous_property_dict['revision']) < int(property_dict['revision']):
Jérome Perrin's avatar
Jérome Perrin committed
886
                      template_item_dict[title] = (repository, property_dict)
887 888
        # Next, select only updated business templates.
        for repository, property_dict in template_item_dict.values():
Vincent Pelletier's avatar
Vincent Pelletier committed
889 890
          installed_bt = \
              self.getInstalledBusinessTemplate(property_dict['title'])
891
          if installed_bt is not None:
Jérome Perrin's avatar
Jérome Perrin committed
892
            diff_version = self.compareVersions(installed_bt.getVersion(),
893 894
                                                property_dict['version'])
            if diff_version < 0:
895
              template_item_list.append((repository, property_dict))
Jérome Perrin's avatar
Jérome Perrin committed
896 897 898
            elif diff_version == 0 \
                 and installed_bt.getRevision() \
                 and property_dict['revision'] \
899
                 and int(installed_bt.getRevision()) < int(property_dict['revision']):
Jérome Perrin's avatar
Jérome Perrin committed
900
                   template_item_list.append((repository, property_dict))
901 902 903 904 905 906 907 908 909 910 911 912
      else:
        for repository, property_dict_list in self.repository_dict.items():
          for property_dict in property_dict_list:
            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()
        id = property_dict['id']
        del property_dict['id']
        version = property_dict['version']
        version_state = 'new'
913 914 915 916 917 918
        installed_bt = \
            self.getInstalledBusinessTemplate(property_dict['title'])
        if installed_bt is not None:
          installed_version = installed_bt.getVersion()
          installed_revision = installed_bt.getRevision()
          result = self.compareVersions(version, installed_version)
919 920 921 922
          if result == 0:
            version_state = 'present'
          elif result < 0:
            version_state = 'old'
923 924 925
        else:
          installed_version = ''
          installed_revision = ''
926
        version_state_title = version_state_title_dict[version_state]
927
        uid = b64encode(cPickle.dumps((repository, id)))
928 929 930
        obj = newTempBusinessTemplate(self, 'temp_' + uid,
                                      version_state = version_state,
                                      version_state_title = version_state_title,
931 932
                                      installed_version = installed_version,
                                      installed_revision = installed_revision,
933 934 935
                                      repository = repository, **property_dict)
        obj.setUid(uid)
        template_list.append(obj)
936
      template_list.sort(key=lambda x: x.getTitle())
937 938
      return template_list

Vincent Pelletier's avatar
Vincent Pelletier committed
939 940
    security.declareProtected( Permissions.AccessContentsInformation,
                               'getUpdatedRepositoryBusinessTemplateList' )
941 942 943 944
    def getUpdatedRepositoryBusinessTemplateList(self, **kw):
      """Get the list of updated Business Templates in repositories.
      """
      #LOG('getUpdatedRepositoryBusinessTemplateList', 0, 'kw = %r' % (kw,))
945
      return self.getRepositoryBusinessTemplateList(update_only=True, **kw)
946

947
    def compareVersions(self, version1, version2):
Vincent Pelletier's avatar
Vincent Pelletier committed
948 949 950
      """
        Return negative if version1 < version2, 0 if version1 == version2,
        positive if version1 > version2.
951 952

      Here is the algorithm:
Vincent Pelletier's avatar
Vincent Pelletier committed
953 954
        - Non-alphanumeric characters are not significant, besides the function
          of delimiters.
955 956 957 958
        - 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
959
      This implements the following predicates:
960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983
        - 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
984

985 986 987 988 989 990 991 992
      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
993

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