TemplateTool.py 26.3 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
Jean-Paul Smets's avatar
Jean-Paul Smets committed
56

57 58 59 60 61 62 63 64 65 66
class LocalConfiguration(Implicit):
  """
    Holds local configuration information
  """
  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
67
class TemplateTool (BaseTool):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
68
    """
69
      TemplateTool manages Business Templates.
Jean-Paul Smets's avatar
Jean-Paul Smets committed
70

71
      TemplateTool provides some methods to deal with Business Templates:
Jean-Paul Smets's avatar
Jean-Paul Smets committed
72

73
        - download
Jean-Paul Smets's avatar
Jean-Paul Smets committed
74

75
        - publish
Jean-Paul Smets's avatar
Jean-Paul Smets committed
76

77
        - install
Jean-Paul Smets's avatar
Jean-Paul Smets committed
78

79
        - update
Jean-Paul Smets's avatar
Jean-Paul Smets committed
80

81
        - save
Jean-Paul Smets's avatar
Jean-Paul Smets committed
82 83
    """
    id = 'portal_templates'
84
    title = 'Template Tool'
Jean-Paul Smets's avatar
Jean-Paul Smets committed
85
    meta_type = 'ERP5 Template Tool'
Jean-Paul Smets's avatar
Jean-Paul Smets committed
86
    portal_type = 'Template Tool'
87
    allowed_types = ( 'ERP5 Business Template',)
88 89 90
    
    # This stores information on repositories.
    repository_dict = {}
Jean-Paul Smets's avatar
Jean-Paul Smets committed
91 92 93 94 95

    # Declarative Security
    security = ClassSecurityInfo()

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

98 99 100 101 102 103 104 105 106 107 108 109
    def getInstalledBusinessTemplate(self, title, **kw):
      """
        Return a installed business template if any.
      """
      # This can be slow if, say, 10000 business templates are present.
      # 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.
      for bt in self.contentValues(filter={'portal_type':'Business Template'}):
        if bt.getInstallationState() == 'installed' and bt.getTitle() == title:
          return bt
      return None
110

111 112 113 114 115 116 117 118 119 120 121 122 123
    def updateLocalConfiguration(self, template, **kw):
      template_id = template.getId()
      if not hasattr(self, '_local_configuration'): self._local_configuration = PersistentMapping()
      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):
      template_id = template.getId()
      if not hasattr(self, '_local_configuration'): self._local_configuration = PersistentMapping()
      local_configuration = self._local_configuration.get(template_id, None)
      if local_configuration is not None:
124
        return local_configuration.__of__(template)
125 126
      return None

127 128
    security.declareProtected( 'Import/Export objects', 'save' )
    def save(self, business_template, REQUEST=None, RESPONSE=None):
Yoshinori Okuji's avatar
Yoshinori Okuji committed
129
      """
Aurel's avatar
Aurel committed
130
        Save BT in folder format
Yoshinori Okuji's avatar
Yoshinori Okuji committed
131 132
      """
      cfg = getConfiguration()
Aurel's avatar
Aurel committed
133
      path = os.path.join(cfg.clienthome, '%s' % (business_template.getTitle(),))
134
      path = pathname2url(path)
Aurel's avatar
Aurel committed
135
      business_template.export(path=path, local=1)
136 137
      if REQUEST is not None:
        ret_url = business_template.absolute_url() + '/' + REQUEST.get('form_id', 'view')
138
        qs = '?portal_status_message=Saved+in+%s+.' % pathname2url(path)
139 140 141 142 143 144
        if RESPONSE is None: RESPONSE = REQUEST.RESPONSE
        return REQUEST.RESPONSE.redirect( ret_url + qs )

    security.declareProtected( 'Import/Export objects', 'export' )
    def export(self, business_template, REQUEST=None, RESPONSE=None):
      """
Aurel's avatar
Aurel committed
145
        Export BT in tarball format 
146
      """
147
      path = business_template.getTitle()
148
      path = pathname2url(path)
149 150
      tmpdir_path = mkdtemp() # XXXXXXXXXXXXXXX Why is it necessary to create a temporary directory?
      current_directory = os.getcwd() # XXXXXXXXXXXXXXXXXX not thread safe
151
      os.chdir(tmpdir_path)
Aurel's avatar
Aurel committed
152
      export_string = business_template.export(path=path)
153
      os.chdir(current_directory)
154
      if RESPONSE is not None:
155
        RESPONSE.setHeader('Content-type','tar/x-gzip')
156
        RESPONSE.setHeader('Content-Disposition',
157
                           'inline;filename=%s-%s.bt5' % \
158
                               (path, 
159
                                business_template.getVersion()))
Aurel's avatar
Aurel committed
160 161 162 163
      try:
        return export_string.getvalue()
      finally:
        export_string.close()
Yoshinori Okuji's avatar
Yoshinori Okuji committed
164

165
    security.declareProtected( 'Import/Export objects', 'publish' )
166 167 168 169 170 171 172
    def publish(self, business_template, url, username=None, password=None):
      """
        Publish in a format or another
      """
      business_template.build()
      export_string = self.manage_exportObject(id=business_template.getId(), download=1)
      bt = Resource(url, username=username, password=password)
173
      bt.put(file=export_string, content_type='application/x-erp5-business-template')
174
      business_template.setPublicationUrl(url)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
175

176 177 178 179 180 181 182 183 184
    def update(self, business_template):
      """
        Update an existing template
      """
      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
185
      self._importObjectFromFile(StringIO(export_string), id=id)
186

Aurel's avatar
Aurel committed
187 188 189 190 191
    def _importBT(self, path=None, id=id):
      """
        Import template from a temp file
      """
      file = open(path, 'r')
192 193 194 195 196 197 198
      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
199 200 201 202
      if magic == '<?xml': # old version
        self._importObjectFromFile(path, id=id)
        bt = self[id]
        bt.id = id # Make sure id is consistent
203
        bt.setProperty('template_format_version', 0, type='int')
Aurel's avatar
Aurel committed
204 205
      else: # new version
        tar = tarfile.open(path, 'r:gz')
206 207 208 209 210 211
        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
212
            prop_type = prop['type']
213 214 215 216 217 218 219
            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()
Aurel's avatar
Aurel committed
220
            if prop_type == 'text' or prop_type == 'string' or prop_type == 'int':
221
              prop_dict[pid] = value
Aurel's avatar
Aurel committed
222
            elif prop_type == 'lines' or prop_type == 'tokens':
223 224 225 226 227
              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')
228
          try:
229 230 231 232 233
            bt.importFile(file=fobj)
          finally:
            fobj.close()
        finally:
          tar.close()
Aurel's avatar
Aurel committed
234 235
      return bt

236
    security.declareProtected( Permissions.ManagePortal, 'manage_download' )
237 238
    def manage_download(self, url, id=None, REQUEST=None):
      """The management interface for download.
239
      """
240 241
      if REQUEST is None:
        REQUEST = getattr(self, 'REQUEST', None)
242

243 244 245 246 247 248 249 250 251 252 253 254 255 256
      self.download(url, id=id)
            
      if REQUEST is not None:
        ret_url = self.absolute_url() + '/' + REQUEST.get('form_id', 'view')
        REQUEST.RESPONSE.redirect("%s?portal_status_message=Business+Templates+Downloaded+Successfully"
                           % ret_url)

    security.declareProtected( 'Import/Export objects', 'download' )
    def download(self, url, id=None, REQUEST=None):
      """
        Download Business template, can be file or local directory
      """
      # For backward compatibility; If REQUEST is passed, it is likely from the management interface.
      if REQUEST is not None:
257 258
        return self.manage_download(url, id=id, REQUEST=REQUEST)

Aurel's avatar
Aurel committed
259
      urltype, name = splittype(url)
Aurel's avatar
Aurel committed
260
      if os.path.isdir(name): # new version of business template in plain format (folder)
261 262
        file_list = []
        def callback(arg, directory, files):
263 264 265
          if 'CVS' not in directory:
            for file in files:
              file_list.append(os.path.join(directory, file))
266 267

        os.path.walk(name, callback, None)        
Aurel's avatar
Aurel committed
268 269
        file_list.sort()
        # import bt object
270 271
        bt = self.newContent(portal_type='Business Template', id=id)
        id = bt.getId()
Aurel's avatar
Aurel committed
272 273 274
        bt_path = os.path.join(name, 'bt')

        # import properties
275
        prop_dict = {}
Aurel's avatar
Aurel committed
276
        for prop in bt.propertyMap():
Aurel's avatar
Aurel committed
277
          prop_type = prop['type']
Aurel's avatar
Aurel committed
278
          pid = prop['id']
279 280 281
          prop_path = os.path.join('.', bt_path, pid)
          if not os.path.exists(prop_path):
            continue          
Aurel's avatar
Aurel committed
282
          value = open(prop_path, 'r').read()
Aurel's avatar
Aurel committed
283
          if prop_type in ('text', 'string', 'int', 'boolean'):
284
            prop_dict[pid] = value
Aurel's avatar
Aurel committed
285
          elif prop_type in ('lines', 'tokens'):
286
            prop_dict[pid[:-5]] = value.split(str(os.linesep))
287
        prop_dict.pop('id', '')
288
        bt.edit(**prop_dict)
Aurel's avatar
Aurel committed
289 290 291
        # import all others objects
        bt.importFile(dir=1, file=file_list, root_path=name)
      else:
292 293 294 295 296 297 298 299 300
        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)
301
      bt.build(no_action=1)
302
      bt.reindexObject()
Jean-Paul Smets's avatar
Jean-Paul Smets committed
303

304
      return bt
Jean-Paul Smets's avatar
Jean-Paul Smets committed
305

Alexandre Boeglin's avatar
Alexandre Boeglin committed
306
    def importFile(self, import_file=None, id=None, REQUEST=None, **kw):
307
      """
Aurel's avatar
Aurel committed
308
        Import Business template from one file
309
      """
310 311 312
      if REQUEST is None:
        REQUEST = getattr(self, 'REQUEST', None)
        
Alexandre Boeglin's avatar
Alexandre Boeglin committed
313 314 315 316 317 318
      if (import_file is None) or (len(import_file.read()) == 0) :
        if REQUEST is not None :
          REQUEST.RESPONSE.redirect("%s?portal_status_message=No+file+or+an+empty+file+was+specified"
              % self.absolute_url())
          return
        else :
319
          raise RuntimeError, 'No file or an empty file was specified'
Aurel's avatar
Aurel committed
320
      # copy to a temp location
Alexandre Boeglin's avatar
Alexandre Boeglin committed
321
      import_file.seek(0) #Rewind to the beginning of file
322
      tempid, temppath = mkstemp()
323 324 325 326 327 328 329 330 331 332
      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)
333
      bt.build(no_action=1)
Aurel's avatar
Aurel committed
334
      bt.reindexObject()
335 336

      if REQUEST is not None:
337 338 339
        ret_url = self.absolute_url() + '/' + REQUEST.get('form_id', 'view')
        REQUEST.RESPONSE.redirect("%s?portal_status_message=Business+Templates+Imported+Successfully"
                           % ret_url)
340

341 342 343 344 345 346 347 348
    def runUnitTestList(self, test_list=[], **kwd) :
      """
        Runs Unit Tests related to this Business Template
      """
      
      from Products.ERP5Type.tests.runUnitTest import getUnitTestFile
      return os.popen('/usr/bin/python %s %s 2>&1' % (getUnitTestFile(), ' '.join(test_list))).read()

Aurel's avatar
Aurel committed
349 350 351 352 353 354 355 356 357 358 359 360 361
    def diff(self, **kw):
      """
      Make a diff between two Business Template
      """
      compare_to_installed = 0
      # get the business template      
      p = self.getPortalObject()
      portal_selections = p.portal_selections
      selection_name = 'business_template_selection' # harcoded because we can also get delete_selection
      uids = portal_selections.getSelectionCheckedUidsFor(selection_name)
      bt1 = self.portal_catalog.getObject(uids[0])
      if bt1.getBuildingState() != 'built':
        raise TemplateConditionError, 'Business Template must be built to make diff'
362 363
      if (getattr(bt1, 'template_format_version', 0)) != 1:
        raise TemplateConditionError, 'Business Template must be in new format'
Aurel's avatar
Aurel committed
364
      # check if there is a second bt or if we compare to installed one
Aurel's avatar
Aurel committed
365 366 367 368
      if len(uids) == 2:
        bt2 = self.portal_catalog.getObject(uids[1])
        if bt2.getBuildingState() != 'built':
          raise TemplateConditionError, 'Business Template must be built to make diff'
369 370
        if (getattr(bt2, 'template_format_version', 0)) != 1:
          raise TemplateConditionError, 'Business Template must be in new format'
Aurel's avatar
Aurel committed
371 372 373 374
      else:
        compare_to_installed = 1
        installed_bt = self.getInstalledBusinessTemplate(title=bt1.getTitle())
        if installed_bt is None:
375
          raise NotFound, 'Installed business template with title %s not found' %(bt1.getTitle(),)
Aurel's avatar
Aurel committed
376
        LOG('compare to installed bt', 0, str((installed_bt.getTitle(), installed_bt.getId())))
Aurel's avatar
Aurel committed
377 378 379
        # get a copy of the installed bt
        bt2 = self.manage_clone(ob=installed_bt, id='installed_bt')
        bt2.edit(description='tmp bt generated for diff')
380 381
        
      # separate item because somes are exported with zope exportXML and other with our own method
382
      # and others are just python code on filesystem
Aurel's avatar
Aurel committed
383
      diff_msg = 'Diff between %s-%s and %s-%s' %(bt1.getTitle(), bt1.getId(), bt2.getTitle(), bt2.getId())
384
      # for the one with zope exportXml
Aurel's avatar
Aurel committed
385 386
      item_list_1 = ['_product_item', '_workflow_item', '_portal_type_item', '_category_item', '_path_item',
                     '_skin_item', '_action_item']
387
      for item_name  in item_list_1:
Aurel's avatar
Aurel committed
388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410
        item1 = getattr(bt1, item_name)        
        # build current item if we compare to installed bt
        if compare_to_installed:
          getattr(bt2, item_name).build(bt2)
        item2 = getattr(bt2, item_name)
        for key in  item1._objects.keys():
          if item2._objects.has_key(key):
            object1 = item1._objects[key]
            object2 = item2._objects[key]
            f1 = StringIO()
            f2 = StringIO()
            OFS.XMLExportImport.exportXML(object1._p_jar, object1._p_oid, f1)
            OFS.XMLExportImport.exportXML(object2._p_jar, object2._p_oid, f2)
            obj1_xml = f1.getvalue()
            obj2_xml = f2.getvalue()
            f1.close()
            f2.close()
            ob1_xml_lines = obj1_xml.splitlines()
            ob2_xml_lines = obj2_xml.splitlines()
            diff_list = list(unified_diff(ob1_xml_lines, ob2_xml_lines, fromfile=bt1.getId(), tofile=bt2.getId(), lineterm=''))
            if len(diff_list) != 0:
              diff_msg += '\n\nObject %s diff :\n' %(key)
              diff_msg += '\n'.join(diff_list)
411

412
      # for our own way to generate xml
Aurel's avatar
Aurel committed
413 414 415 416 417
      item_list_2 = ['_site_property_item', '_module_item', '_catalog_result_key_item', '_catalog_related_key_item',
                     '_catalog_result_table_item', '_catalog_keyword_key_item', '_catalog_full_text_key_item',
                     '_catalog_request_key_item', '_catalog_multivalue_key_item', '_catalog_topic_key_item',
                     '_portal_type_allowed_content_type_item', '_portal_type_hidden_content_type_item',
                     '_portal_type_property_sheet_item', '_portal_type_base_category_item',]
418 419 420 421 422 423 424 425
      for item_name  in item_list_2:
        item1 = getattr(bt1, item_name)        
        # build current item if we compare to installed bt
        if compare_to_installed:
          getattr(bt2, item_name).build(bt2)
        item2 = getattr(bt2, item_name)
        for key in  item1._objects.keys():
          if item2._objects.has_key(key):
426 427
            obj1_xml = item1.generateXml(path=key)
            obj2_xml = item2.generateXml(path=key)
428 429 430 431 432 433
            ob1_xml_lines = obj1_xml.splitlines()
            ob2_xml_lines = obj2_xml.splitlines()
            diff_list = list(unified_diff(ob1_xml_lines, ob2_xml_lines, fromfile=bt1.getId(), tofile=bt2.getId(), lineterm=''))
            if len(diff_list) != 0:
              diff_msg += '\n\nObject %s diff :\n' %(key)
              diff_msg += '\n'.join(diff_list)
434
              
435
      # for document located on filesystem
436
      item_list_3 = ['_document_item', '_property_sheet_item', '_extension_item', '_test_item', '_message_translation_item']
437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452
      for item_name  in item_list_3:
        item1 = getattr(bt1, item_name)        
        # build current item if we compare to installed bt
        if compare_to_installed:
          getattr(bt2, item_name).build(bt2)
        item2 = getattr(bt2, item_name)
        for key in  item1._objects.keys():
          if item2._objects.has_key(key):
            obj1_code = item1._objects[key]
            obj2_code = item2._objects[key]
            ob1_lines = obj1_code.splitlines()
            ob2_lines = obj2_code.splitlines()
            diff_list = list(unified_diff(ob1_lines, ob2_lines, fromfile=bt1.getId(), tofile=bt2.getId(), lineterm=''))
            if len(diff_list) != 0:
              diff_msg += '\n\nObject %s diff :\n' %(key)
              diff_msg += '\n'.join(diff_list)
453
              
Aurel's avatar
Aurel committed
454 455 456
      if compare_to_installed:
        self.manage_delObjects(ids=['installed_bt'])
      return diff_msg
457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520

    security.declareProtected( 'Import/Export objects', 'updateRepositoryBusinessTemplateList' )
    def updateRepositoryBusinessTemplateList(self, repository_list, REQUEST=None, RESPONSE=None, **kw):
      """Update the information on Business Templates in repositories.
      """
      self.repository_dict = PersistentMapping()
      property_list = ('title', 'version', 'description', 'license', 'dependency', 'copyright')
      #LOG('updateRepositoryBusiessTemplateList', 0, 'repository_list = %r' % (repository_list,))
      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]
              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', ())
              
              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')
        REQUEST.RESPONSE.redirect("%s?portal_status_message=Business+Templates+Updated+Successfully"
                           % ret_url)
                
    security.declareProtected( Permissions.AccessContentsInformation, 'getRepositoryList' )
    def getRepositoryList(self):
      """Get the list of repositories.
      """
      return self.repository_dict.keys()
      
    security.declarePublic( 'decodeRepositoryBusinessTemplateUid' )
    def decodeRepositoryBusinessTemplateUid(self, uid):
      """Decode the uid of a business template in a repository. Return a repository and an id.
      """
521
      return cPickle.loads(b64decode(uid))
522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574
      
    security.declareProtected( Permissions.AccessContentsInformation, 'getRepositoryBusinessTemplateList' )
    def getRepositoryBusinessTemplateList(self, update_only=0, **kw):
      """Get the list of Business Templates in repositories.
      """
      version_state_title_dict = { 'new' : 'New', 'present' : 'Present', 'old' : 'Old' }

      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:
              # If this is the first time to see this business template, insert it.
              template_item_dict[title] = (repository, property_dict)
            else:
              # 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:
                template_item_dict[title] = (repository, property_dict)
        # Next, select only updated business templates.
        for repository, property_dict in template_item_dict.values():
          installed_bt = self.getInstalledBusinessTemplate(property_dict['title'])
          if installed_bt is not None:
            if self.compareVersions(installed_bt.getVersion(), property_dict['version']) < 0:
              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]
575
        uid = b64encode(cPickle.dumps((repository, id)))
576 577 578 579 580 581 582 583 584 585 586 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 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644
        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)
        
      return template_list

    security.declareProtected( Permissions.AccessContentsInformation, 'getUpdatedRepositoryBusinessTemplateList' )
    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):
      """Return negative if version1 < version2, 0 if version1 == version2, positive if version1 > version2.

      Here is the algorithm:

        - Non-alphanumeric characters are not significant, besides the function of delimiters.

        - 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.

      This archives the following predicates:

        - 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
645
InitializeClass(TemplateTool)