TemplateTool.py 24.8 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)
182 183 184 185
      # XXX Why is it necessary to create a temporary directory?
      tmpdir_path = mkdtemp() 
      # XXX not thread safe
      current_directory = os.getcwd() 
186
      os.chdir(tmpdir_path)
Aurel's avatar
Aurel committed
187
      export_string = business_template.export(path=path)
188
      os.chdir(current_directory)
189
      if RESPONSE is not None:
190
        RESPONSE.setHeader('Content-type','tar/x-gzip')
191
        RESPONSE.setHeader('Content-Disposition',
192
                           'inline;filename=%s-%s.bt5' % \
193
                               (path, 
194
                                business_template.getVersion()))
Aurel's avatar
Aurel committed
195 196 197 198
      try:
        return export_string.getvalue()
      finally:
        export_string.close()
Yoshinori Okuji's avatar
Yoshinori Okuji committed
199

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

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

Aurel's avatar
Aurel committed
224 225
    def _importBT(self, path=None, id=id):
      """
Vincent Pelletier's avatar
Vincent Pelletier committed
226
        Import template from a temp file (as uploaded by the user)
Aurel's avatar
Aurel committed
227 228
      """
      file = open(path, 'r')
229 230 231 232 233 234 235
      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
236 237 238 239
      if magic == '<?xml': # old version
        self._importObjectFromFile(path, id=id)
        bt = self[id]
        bt.id = id # Make sure id is consistent
240
        bt.setProperty('template_format_version', 0, type='int')
Aurel's avatar
Aurel committed
241
      else: # new version
Vincent Pelletier's avatar
Vincent Pelletier committed
242 243
        # XXX: should really check for a magic and offer a falback if it
        # doens't correspond to anything handled.
Aurel's avatar
Aurel committed
244
        tar = tarfile.open(path, 'r:gz')
245 246
        try:
          # create bt object
247
          bt = self.newContent(portal_type='Business Template', id=id)
248 249
          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):
      """
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

352 353
    def importFile(self, import_file=None, id=None, REQUEST=None, 
                   batch_mode=0, **kw):
354
      """
Vincent Pelletier's avatar
Vincent Pelletier committed
355
        Import Business Template from one file
356
      """
357 358
      if REQUEST is None:
        REQUEST = getattr(self, 'REQUEST', None)
359 360 361 362 363 364 365 366 367
      
      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
368 369
          return
        else :
370
          raise RuntimeError, 'No file or an empty file was specified'
Aurel's avatar
Aurel committed
371
      # copy to a temp location
Alexandre Boeglin's avatar
Alexandre Boeglin committed
372
      import_file.seek(0) #Rewind to the beginning of file
373
      tempid, temppath = mkstemp()
374 375 376 377 378 379 380 381 382 383
      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)
384
      bt.build(no_action=1)
Aurel's avatar
Aurel committed
385
      bt.reindexObject()
386

387 388
      if (batch_mode == 0) and \
         (REQUEST is not None):
389 390 391 392
        ret_url = bt.absolute_url() + '/view'
        psm = N_("Business+Templates+Imported+Successfully")
        REQUEST.RESPONSE.redirect("%s?portal_status_message=%s"
                                  % (ret_url, psm))
393 394
      elif (batch_mode == 1):
        return bt
395

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

    def diffObject(self, REQUEST, **kw):
Aurel's avatar
Aurel committed
407
      """
Vincent Pelletier's avatar
Vincent Pelletier committed
408 409
        Make diff between two objects, whose paths are stored in values bt1
        and bt2 in the REQUEST object.
Aurel's avatar
Aurel committed
410
      """
411 412 413 414
      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
415
      if self.compareVersions(bt1.getVersion(), bt2.getVersion()) < 0:
416
        return bt2.diffObject(REQUEST, compare_with=bt1_id)
Aurel's avatar
Aurel committed
417
      else:
418
        return bt1.diffObject(REQUEST, compare_with=bt2_id)
419

Vincent Pelletier's avatar
Vincent Pelletier committed
420 421 422 423 424 425 426
    security.declareProtected( 'Import/Export objects',
                               'updateRepositoryBusinessTemplateList' )

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

      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
522 523
              # If this is the first time to see this business template,
              # insert it.
524 525
              template_item_dict[title] = (repository, property_dict)
            else:
Vincent Pelletier's avatar
Vincent Pelletier committed
526 527 528 529
              # 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]
530 531 532
	      diff_version = self.compareVersions(previous_property_dict['version'],
                                                  property_dict['version'])
              if diff_version < 0:
533
                template_item_dict[title] = (repository, property_dict)
534 535 536 537 538
              elif diff_version == 0 \
	           and previous_property_dict['revision'] \
	           and property_dict['revision'] \
		   and previous_property_dict['revision'] < property_dict['revision'] :
		      template_item_dict[title] = (repository, property_dict)
539 540
        # Next, select only updated business templates.
        for repository, property_dict in template_item_dict.values():
Vincent Pelletier's avatar
Vincent Pelletier committed
541 542
          installed_bt = \
              self.getInstalledBusinessTemplate(property_dict['title'])
543
          if installed_bt is not None:
544 545 546
	    diff_version = self.compareVersions(installed_bt.getVersion(),
                                                property_dict['version'])
            if diff_version < 0:
547
              template_item_list.append((repository, property_dict))
548 549 550 551 552
	    elif diff_version == 0 \
	         and installed_bt.getRevision() \
	         and property_dict['revision'] \
		 and installed_bt.getRevision() < property_dict['revision'] :
		   template_item_list.append((repository, property_dict))
553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573
        # 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]
574
        uid = b64encode(cPickle.dumps((repository, id)))
575 576 577 578 579 580
        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)
581
      template_list.sort(lambda x,y:cmp(x.getTitle(), y.getTitle()))
582 583
      return template_list

Vincent Pelletier's avatar
Vincent Pelletier committed
584 585
    security.declareProtected( Permissions.AccessContentsInformation,
                               'getUpdatedRepositoryBusinessTemplateList' )
586 587 588 589 590 591 592
    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
593 594 595
      """
        Return negative if version1 < version2, 0 if version1 == version2,
        positive if version1 > version2.
596 597

      Here is the algorithm:
Vincent Pelletier's avatar
Vincent Pelletier committed
598 599
        - Non-alphanumeric characters are not significant, besides the function
          of delimiters.
600 601 602 603
        - 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
604
      This implements the following predicates:
605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638
        - 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
639
InitializeClass(TemplateTool)