TemplateTool.py 23.9 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 31
from Products.CMFCore.utils import UniqueObject

Yoshinori Okuji's avatar
Yoshinori Okuji committed
32
from App.config import getConfiguration
Aurel's avatar
Aurel committed
33
import os, tarfile, string, commands, OFS
Yoshinori Okuji's avatar
Yoshinori Okuji committed
34

35
from Acquisition import Implicit, aq_base
Jean-Paul Smets's avatar
Jean-Paul Smets committed
36
from AccessControl import ClassSecurityInfo
37
from Globals import InitializeClass, DTMLFile, PersistentMapping
Jean-Paul Smets's avatar
Jean-Paul Smets committed
38
from Products.ERP5Type.Tool.BaseTool import BaseTool
Jean-Paul Smets's avatar
Jean-Paul Smets committed
39
from Products.ERP5Type import Permissions
Aurel's avatar
Aurel committed
40
from Products.ERP5.Document.BusinessTemplate import TemplateConditionError
41
from tempfile import mkstemp, mkdtemp
Jean-Paul Smets's avatar
Jean-Paul Smets committed
42
from Products.ERP5 import _dtmldir
Aurel's avatar
Aurel committed
43 44 45
from OFS.Traversable import NotFound
from difflib import unified_diff
from cStringIO import StringIO
Jean-Paul Smets's avatar
Jean-Paul Smets committed
46
from zLOG import LOG
47 48 49 50 51
from urllib import pathname2url, urlopen, splittype, urlretrieve
import re
from xml.dom.minidom import parse
import struct
import cPickle
52 53 54 55
try:
  from base64 import b64encode, b64decode
except ImportError:
  from base64 import encodestring as b64encode, decodestring as b64decode
56 57
from Products.ERP5Type.Message import Message
N_ = lambda msgid, **kw: Message('ui', msgid, **kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
58

59 60
class LocalConfiguration(Implicit):
  """
Vincent Pelletier's avatar
Vincent Pelletier committed
61
    Contains local configuration information
62 63 64 65 66 67 68
  """
  def __init__(self, **kw):
    self.__dict__.update(kw)

  def update(self, **kw):
    self.__dict__.update(kw)

Jean-Paul Smets's avatar
Jean-Paul Smets committed
69
class TemplateTool (BaseTool):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
70
    """
71
      TemplateTool manages Business Templates.
Jean-Paul Smets's avatar
Jean-Paul Smets committed
72

73 74 75 76 77 78
      TemplateTool provides some methods to deal with Business Templates:
        - download
        - publish
        - install
        - update
        - save
Jean-Paul Smets's avatar
Jean-Paul Smets committed
79 80
    """
    id = 'portal_templates'
81
    title = 'Template Tool'
Jean-Paul Smets's avatar
Jean-Paul Smets committed
82
    meta_type = 'ERP5 Template Tool'
Jean-Paul Smets's avatar
Jean-Paul Smets committed
83
    portal_type = 'Template Tool'
84
    allowed_types = ( 'ERP5 Business Template',)
85 86 87
    
    # This stores information on repositories.
    repository_dict = {}
Jean-Paul Smets's avatar
Jean-Paul Smets committed
88 89 90 91 92

    # Declarative Security
    security = ClassSecurityInfo()

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

95 96
    def getInstalledBusinessTemplate(self, title, **kw):
      """
Vincent Pelletier's avatar
Vincent Pelletier committed
97
        Return an installed version of business template of a certain title.
98 99
      """
      # This can be slow if, say, 10000 business templates are present.
Vincent Pelletier's avatar
Vincent Pelletier committed
100 101 102
      # 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.
103 104 105 106
      for bt in self.contentValues(filter={'portal_type':'Business Template'}):
        if bt.getInstallationState() == 'installed' and bt.getTitle() == title:
          return bt
      return None
107
        
108
    # Christophe Dumez <christophe@nexedi.com>
109 110 111 112 113 114 115 116
    def getInstalledBusinessTemplatesList(self):
      """Get list of installed business templates
      """
      installed_bts = []
      for bt in self.contentValues(filter={'portal_type':'Business Template'}):
        if bt.getInstallationState() == 'installed':
          installed_bts.append(bt)
      return installed_bts
117 118 119 120 121 122 123 124 125 126
        
    # Christophe Dumez <christophe@nexedi.com>
    def getBuiltBusinessTemplatesList(self):
      """Get list of built and not_installed business templates
      """
      built_bts = []
      for bt in self.contentValues(filter={'portal_type':'Business Template'}):
        if bt.getInstallationState() == 'not_installed' and bt.getBuildingState() == 'built':
          built_bts.append(bt)
      return built_bts
127

128
    def updateLocalConfiguration(self, template, **kw):
Vincent Pelletier's avatar
Vincent Pelletier committed
129 130 131 132
      """
        Call the update method on the configuration, create if it doesn't
        exists.
      """
133
      template_id = template.getId()
Vincent Pelletier's avatar
Vincent Pelletier committed
134 135
      if getattr(self, '_local_configuration', None) is None:
        self._local_configuration = PersistentMapping()
136 137 138 139 140 141
      if not self._local_configuration.has_key(template_id):
        self._local_configuration[template_id] = LocalConfiguration(**kw)
      else:
        self._local_configuration[template_id].update(**kw)

    def getLocalConfiguration(self, template):
Vincent Pelletier's avatar
Vincent Pelletier committed
142 143 144 145
      """
        Return the configuration for the given business template, or None if
        it's not defined.
      """
146
      template_id = template.getId()
Vincent Pelletier's avatar
Vincent Pelletier committed
147 148
      if getattr(self, '_local_configuration', None) is None:
        self._local_configuration = PersistentMapping()
149 150
      local_configuration = self._local_configuration.get(template_id, None)
      if local_configuration is not None:
151
        return local_configuration.__of__(template)
152 153
      return None

154 155
    security.declareProtected( 'Import/Export objects', 'save' )
    def save(self, business_template, REQUEST=None, RESPONSE=None):
Yoshinori Okuji's avatar
Yoshinori Okuji committed
156
      """
Vincent Pelletier's avatar
Vincent Pelletier committed
157
        Save the BusinessTemplate in the servers's filesystem.
Yoshinori Okuji's avatar
Yoshinori Okuji committed
158 159
      """
      cfg = getConfiguration()
Vincent Pelletier's avatar
Vincent Pelletier committed
160 161
      path = os.path.join(cfg.clienthome,
                          '%s' % (business_template.getTitle(),))
162
      path = pathname2url(path)
Aurel's avatar
Aurel committed
163
      business_template.export(path=path, local=1)
164
      if REQUEST is not None:
165 166 167
        psm = N_('Saved+in+${path}+.',
                  mapping={'path': pathname2url(path)})
        ret_url = '%s/%s?portal_status_message=%s' % \
Vincent Pelletier's avatar
Vincent Pelletier committed
168
                  (business_template.absolute_url(),
169
                   REQUEST.get('form_id', 'view'), psm)
Vincent Pelletier's avatar
Vincent Pelletier committed
170 171 172
        if RESPONSE is None:
          RESPONSE = REQUEST.RESPONSE
        return REQUEST.RESPONSE.redirect( ret_url )
173 174 175 176

    security.declareProtected( 'Import/Export objects', 'export' )
    def export(self, business_template, REQUEST=None, RESPONSE=None):
      """
Vincent Pelletier's avatar
Vincent Pelletier committed
177 178
        Export the Business Template as a bt5 file and offer the user to
        download it.
179
      """
180
      path = business_template.getTitle()
181
      path = pathname2url(path)
Vincent Pelletier's avatar
Vincent Pelletier committed
182 183 184
      tmpdir_path = mkdtemp() # XXX Why is it necessary to create a temporary
                              # directory?
      current_directory = os.getcwd() # XXX not thread safe
185
      os.chdir(tmpdir_path)
Aurel's avatar
Aurel committed
186
      export_string = business_template.export(path=path)
187
      os.chdir(current_directory)
188
      if RESPONSE is not None:
189
        RESPONSE.setHeader('Content-type','tar/x-gzip')
190
        RESPONSE.setHeader('Content-Disposition',
191
                           'inline;filename=%s-%s.bt5' % \
192
                               (path, 
193
                                business_template.getVersion()))
Aurel's avatar
Aurel committed
194 195 196 197
      try:
        return export_string.getvalue()
      finally:
        export_string.close()
Yoshinori Okuji's avatar
Yoshinori Okuji committed
198

199
    security.declareProtected( 'Import/Export objects', 'publish' )
200 201
    def publish(self, business_template, url, username=None, password=None):
      """
Vincent Pelletier's avatar
Vincent Pelletier committed
202
        Publish the given business template at the given URL.
203 204
      """
      business_template.build()
Vincent Pelletier's avatar
Vincent Pelletier committed
205 206
      export_string = self.manage_exportObject(id=business_template.getId(),
                                               download=1)
207
      bt = Resource(url, username=username, password=password)
Vincent Pelletier's avatar
Vincent Pelletier committed
208 209
      bt.put(file=export_string,
             content_type='application/x-erp5-business-template')
210
      business_template.setPublicationUrl(url)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
211

212 213
    def update(self, business_template):
      """
Vincent Pelletier's avatar
Vincent Pelletier committed
214
        Update an existing template from its publication URL.
215 216 217 218 219 220
      """
      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
221
      self._importObjectFromFile(StringIO(export_string), id=id)
222

Aurel's avatar
Aurel committed
223 224
    def _importBT(self, path=None, id=id):
      """
Vincent Pelletier's avatar
Vincent Pelletier committed
225
        Import template from a temp file (as uploaded by the user)
Aurel's avatar
Aurel committed
226 227
      """
      file = open(path, 'r')
228 229 230 231 232 233 234
      try:
        # read magic key to determine wich kind of bt we use
        file.seek(0)
        magic = file.read(5)
      finally:
        file.close()
        
Aurel's avatar
Aurel committed
235 236 237 238
      if magic == '<?xml': # old version
        self._importObjectFromFile(path, id=id)
        bt = self[id]
        bt.id = id # Make sure id is consistent
239
        bt.setProperty('template_format_version', 0, type='int')
Aurel's avatar
Aurel committed
240
      else: # new version
Vincent Pelletier's avatar
Vincent Pelletier committed
241 242
        # XXX: should really check for a magic and offer a falback if it
        # doens't correspond to anything handled.
Aurel's avatar
Aurel committed
243
        tar = tarfile.open(path, 'r:gz')
244 245 246 247 248 249
        try:
          # create bt object
          self.newContent(portal_type='Business Template', id=id)
          bt = self._getOb(id)
          prop_dict = {}
          for prop in bt.propertyMap():
Aurel's avatar
Aurel committed
250
            prop_type = prop['type']
251 252 253 254 255 256 257
            pid = prop['id']
            prop_path = os.path.join(tar.members[0].name, 'bt', pid)
            try:
              info = tar.getmember(prop_path)
            except KeyError:
              continue
            value = tar.extractfile(info).read()
Jean-Paul Smets's avatar
Jean-Paul Smets committed
258 259
            if prop_type == 'text' or prop_type == 'string' \
                                   or prop_type == 'int':
260
              prop_dict[pid] = value
Aurel's avatar
Aurel committed
261
            elif prop_type == 'lines' or prop_type == 'tokens':
262 263 264 265 266
              prop_dict[pid[:-5]] = value.split(str(os.linesep))
          prop_dict.pop('id', '')
          bt.edit(**prop_dict)
          # import all other files from bt
          fobj = open(path, 'r')
267
          try:
268 269 270 271 272
            bt.importFile(file=fobj)
          finally:
            fobj.close()
        finally:
          tar.close()
Aurel's avatar
Aurel committed
273 274
      return bt

275
    security.declareProtected( Permissions.ManagePortal, 'manage_download' )
276 277
    def manage_download(self, url, id=None, REQUEST=None):
      """The management interface for download.
278
      """
279 280
      if REQUEST is None:
        REQUEST = getattr(self, 'REQUEST', None)
281

282
      bt = self.download(url, id=id)
283 284
            
      if REQUEST is not None:
285 286 287 288
        ret_url = bt.absolute_url() + '/view'
        psm = N_("Business+Template+Downloaded+Successfully")
        REQUEST.RESPONSE.redirect("%s?portal_status_message=%s" 
                                    % (ret_url, psm))
289 290 291 292

    security.declareProtected( 'Import/Export objects', 'download' )
    def download(self, url, id=None, REQUEST=None):
      """
Vincent Pelletier's avatar
Vincent Pelletier committed
293
        Download Business Template from url, can be file or local directory
294
      """
Vincent Pelletier's avatar
Vincent Pelletier committed
295 296
      # For backward compatibility: If REQUEST is passed, it is likely that we
      # come from the management interface.
297
      if REQUEST is not None:
298
        return self.manage_download(url, id=id, REQUEST=REQUEST)
299 300 301
      
      if id is None:
        id = self.generateNewId()
302

Aurel's avatar
Aurel committed
303
      urltype, name = splittype(url)
Vincent Pelletier's avatar
Vincent Pelletier committed
304 305
      if os.path.isdir(name): # new version of business template in plain
                              # format (folder)
306 307
        file_list = []
        def callback(arg, directory, files):
Vincent Pelletier's avatar
Vincent Pelletier committed
308 309
          if 'CVS' not in directory and '.svn' not in directory: # XXX:
                                                        # possible side-effects
310 311
            for file in files:
              file_list.append(os.path.join(directory, file))
312

Yoshinori Okuji's avatar
Yoshinori Okuji committed
313
        os.path.walk(name, callback, None)
Aurel's avatar
Aurel committed
314 315
        file_list.sort()
        # import bt object
316 317
        bt = self.newContent(portal_type='Business Template', id=id)
        id = bt.getId()
Aurel's avatar
Aurel committed
318 319 320
        bt_path = os.path.join(name, 'bt')

        # import properties
321
        prop_dict = {}
Aurel's avatar
Aurel committed
322
        for prop in bt.propertyMap():
Aurel's avatar
Aurel committed
323
          prop_type = prop['type']
Aurel's avatar
Aurel committed
324
          pid = prop['id']
325 326 327
          prop_path = os.path.join('.', bt_path, pid)
          if not os.path.exists(prop_path):
            continue          
Aurel's avatar
Aurel committed
328
          value = open(prop_path, 'r').read()
Aurel's avatar
Aurel committed
329
          if prop_type in ('text', 'string', 'int', 'boolean'):
330
            prop_dict[pid] = value
Aurel's avatar
Aurel committed
331
          elif prop_type in ('lines', 'tokens'):
332
            prop_dict[pid[:-5]] = value.split(str(os.linesep))
333
        prop_dict.pop('id', '')
334
        bt.edit(**prop_dict)
Aurel's avatar
Aurel committed
335 336 337
        # import all others objects
        bt.importFile(dir=1, file=file_list, root_path=name)
      else:
338 339 340 341 342 343 344 345 346
        tempid, temppath = mkstemp()
        try:
          os.close(tempid) # Close the opened fd as soon as possible.    
          file, headers = urlretrieve(url, temppath)
          if id is None:
            id = str(self.generateNewId())
          bt = self._importBT(temppath, id)
        finally:
          os.remove(temppath)
347
      bt.build(no_action=1)
348
      bt.reindexObject()
Jean-Paul Smets's avatar
Jean-Paul Smets committed
349

350
      return bt
Jean-Paul Smets's avatar
Jean-Paul Smets committed
351

Alexandre Boeglin's avatar
Alexandre Boeglin committed
352
    def importFile(self, import_file=None, id=None, REQUEST=None, **kw):
353
      """
Vincent Pelletier's avatar
Vincent Pelletier committed
354
        Import Business Template from one file
355
      """
356 357
      if REQUEST is None:
        REQUEST = getattr(self, 'REQUEST', None)
358 359 360 361 362 363 364 365 366
      
      if id is None:
        id = self.generateNewId()

      if (import_file is None) or (len(import_file.read()) == 0):
        if REQUEST is not None:
          psm = N_('No+file+or+an+empty+file+was+specified')
          REQUEST.RESPONSE.redirect("%s?portal_status_message=%s"
                                    % (self.absolute_url(), psm))
Alexandre Boeglin's avatar
Alexandre Boeglin committed
367 368
          return
        else :
369
          raise RuntimeError, 'No file or an empty file was specified'
Aurel's avatar
Aurel committed
370
      # copy to a temp location
Alexandre Boeglin's avatar
Alexandre Boeglin committed
371
      import_file.seek(0) #Rewind to the beginning of file
372
      tempid, temppath = mkstemp()
373 374 375 376 377 378 379 380 381 382
      try:
        os.close(tempid) # Close the opened fd as soon as possible
        tempfile = open(temppath, 'w')
        try:
          tempfile.write(import_file.read())
        finally:
          tempfile.close()
        bt = self._importBT(temppath, id)
      finally:
        os.remove(temppath)
383
      bt.build(no_action=1)
Aurel's avatar
Aurel committed
384
      bt.reindexObject()
385 386

      if REQUEST is not None:
387 388 389 390
        ret_url = bt.absolute_url() + '/view'
        psm = N_("Business+Templates+Imported+Successfully")
        REQUEST.RESPONSE.redirect("%s?portal_status_message=%s"
                                  % (ret_url, psm))
391

Vincent Pelletier's avatar
Vincent Pelletier committed
392
    def runUnitTestList(self, test_list=[], **kwd):
393 394 395
      """
        Runs Unit Tests related to this Business Template
      """
Vincent Pelletier's avatar
Vincent Pelletier committed
396 397
      # XXX: should check for file presence before trying to execute.
      # XXX: should check if the unit test file is configured in the BT
398
      from Products.ERP5Type.tests.runUnitTest import getUnitTestFile
Vincent Pelletier's avatar
Vincent Pelletier committed
399 400
      return os.popen('/usr/bin/python %s %s 2>&1'
                      % (getUnitTestFile(), ' '.join(test_list))).read()
401 402

    def diffObject(self, REQUEST, **kw):
Aurel's avatar
Aurel committed
403
      """
Vincent Pelletier's avatar
Vincent Pelletier committed
404 405
        Make diff between two objects, whose paths are stored in values bt1
        and bt2 in the REQUEST object.
Aurel's avatar
Aurel committed
406
      """
407 408 409 410
      bt1_id = getattr(REQUEST, 'bt1', None)
      bt2_id = getattr(REQUEST, 'bt2', None)
      bt1 = self._getOb(bt1_id)
      bt2 = self._getOb(bt2_id)
Vincent Pelletier's avatar
Vincent Pelletier committed
411
      if self.compareVersions(bt1.getVersion(), bt2.getVersion()) < 0:
412
        return bt2.diffObject(REQUEST, compare_with=bt1_id)
Aurel's avatar
Aurel committed
413
      else:
414
        return bt1.diffObject(REQUEST, compare_with=bt2_id)
415

Vincent Pelletier's avatar
Vincent Pelletier committed
416 417 418 419 420 421 422
    security.declareProtected( 'Import/Export objects',
                               'updateRepositoryBusinessTemplateList' )

    def updateRepositoryBusinessTemplateList(self, repository_list,
                                             REQUEST=None, RESPONSE=None, **kw):
      """
        Update the information on Business Templates from repositories.
423 424
      """
      self.repository_dict = PersistentMapping()
Vincent Pelletier's avatar
Vincent Pelletier committed
425 426 427 428
      property_list = ('title', 'version', 'description', 'license',
                       'dependency', 'copyright')
      #LOG('updateRepositoryBusiessTemplateList', 0,
      #    'repository_list = %r' % (repository_list,))
429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455
      for repository in repository_list:
        url = '/'.join([repository, 'bt5list'])
        f = urlopen(url)
        property_dict_list = []
        try:
          doc = parse(f)
          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
456 457 458 459 460 461 462 463 464 465
              property_dict['version'] = \
                  temp_property_dict.get('version', [''])[0]
              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', ())
              property_dict['copyright_list'] = \
                  temp_property_dict.get('copyright', ())
466 467 468 469 470 471 472 473 474 475 476
              
              property_dict_list.append(property_dict)
          finally:
            doc.unlink()
        finally:
          f.close()
        
        self.repository_dict[repository] = tuple(property_dict_list)
        
      if REQUEST is not None:
        ret_url = self.absolute_url() + '/' + REQUEST.get('form_id', 'view')
477 478 479
        psm = N_("Business+Templates+Updated+Successfully")
        REQUEST.RESPONSE.redirect("%s?portal_status_message=%s"
                                  % (ret_url, psm))
480
                
Vincent Pelletier's avatar
Vincent Pelletier committed
481 482
    security.declareProtected( Permissions.AccessContentsInformation,
                               'getRepositoryList' )
483
    def getRepositoryList(self):
Vincent Pelletier's avatar
Vincent Pelletier committed
484 485
      """
        Get the list of repositories.
486 487 488 489 490
      """
      return self.repository_dict.keys()
      
    security.declarePublic( 'decodeRepositoryBusinessTemplateUid' )
    def decodeRepositoryBusinessTemplateUid(self, uid):
Vincent Pelletier's avatar
Vincent Pelletier committed
491 492 493
      """
        Decode the uid of a business template from a repository.
        Return a repository and an id.
494
      """
495
      return cPickle.loads(b64decode(uid))
496
      
Vincent Pelletier's avatar
Vincent Pelletier committed
497 498
    security.declareProtected( Permissions.AccessContentsInformation,
                               'getRepositoryBusinessTemplateList' )
499 500 501
    def getRepositoryBusinessTemplateList(self, update_only=0, **kw):
      """Get the list of Business Templates in repositories.
      """
Vincent Pelletier's avatar
Vincent Pelletier committed
502 503
      version_state_title_dict = { 'new' : 'New', 'present' : 'Present',
                                   'old' : 'Old' }
504 505 506 507 508 509 510 511 512 513 514 515

      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
516 517
              # If this is the first time to see this business template,
              # insert it.
518 519
              template_item_dict[title] = (repository, property_dict)
            else:
Vincent Pelletier's avatar
Vincent Pelletier committed
520 521 522 523 524 525
              # 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]
              if self.compareVersions(previous_property_dict['version'],
                                      property_dict['version']) < 0:
526 527 528
                template_item_dict[title] = (repository, property_dict)
        # Next, select only updated business templates.
        for repository, property_dict in template_item_dict.values():
Vincent Pelletier's avatar
Vincent Pelletier committed
529 530
          installed_bt = \
              self.getInstalledBusinessTemplate(property_dict['title'])
531
          if installed_bt is not None:
Vincent Pelletier's avatar
Vincent Pelletier committed
532 533
            if self.compareVersions(installed_bt.getVersion(),
                                    property_dict['version']) < 0:
534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555
              template_item_list.append((repository, property_dict))
        # FIXME: resolve dependencies
      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'
        for bt in self.searchFolder(title = property_dict['title']):
          result = self.compareVersions(version, bt.getObject().getVersion())
          if result == 0:
            version_state = 'present'
            break
          elif result < 0:
            version_state = 'old'
        version_state_title = version_state_title_dict[version_state]
556
        uid = b64encode(cPickle.dumps((repository, id)))
557 558 559 560 561 562
        obj = newTempBusinessTemplate(self, 'temp_' + uid,
                                      version_state = version_state,
                                      version_state_title = version_state_title,
                                      repository = repository, **property_dict)
        obj.setUid(uid)
        template_list.append(obj)
563
      template_list.sort(lambda x,y:cmp(x.getTitle(), y.getTitle()))
564 565
      return template_list

Vincent Pelletier's avatar
Vincent Pelletier committed
566 567
    security.declareProtected( Permissions.AccessContentsInformation,
                               'getUpdatedRepositoryBusinessTemplateList' )
568 569 570 571 572 573 574
    def getUpdatedRepositoryBusinessTemplateList(self, **kw):
      """Get the list of updated Business Templates in repositories.
      """
      #LOG('getUpdatedRepositoryBusinessTemplateList', 0, 'kw = %r' % (kw,))
      return self.getRepositoryBusinessTemplateList(update_only=1, **kw)
      
    def compareVersions(self, version1, version2):
Vincent Pelletier's avatar
Vincent Pelletier committed
575 576 577
      """
        Return negative if version1 < version2, 0 if version1 == version2,
        positive if version1 > version2.
578 579

      Here is the algorithm:
Vincent Pelletier's avatar
Vincent Pelletier committed
580 581
        - Non-alphanumeric characters are not significant, besides the function
          of delimiters.
582 583 584 585
        - 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
586
      This implements the following predicates:
587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620
        - 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
        
      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
      
Jean-Paul Smets's avatar
Jean-Paul Smets committed
621
InitializeClass(TemplateTool)