Terminologies
[odoo/odoo.git] / bin / addons / base / module / module.py
1 ##############################################################################
2 #
3 # Copyright (c) 2005 TINY SPRL. (http://tiny.be) All Rights Reserved.
4 #
5 # WARNING: This program as such is intended to be used by professional
6 # programmers who take the whole responsability of assessing all potential
7 # consequences resulting from its eventual inadequacies and bugs
8 # End users who are looking for a ready-to-use solution with commercial
9 # garantees and support are strongly adviced to contract a Free Software
10 # Service Company
11 #
12 # This program is Free Software; you can redistribute it and/or
13 # modify it under the terms of the GNU General Public License
14 # as published by the Free Software Foundation; either version 2
15 # of the License, or (at your option) any later version.
16 #
17 # This program is distributed in the hope that it will be useful,
18 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20 # GNU General Public License for more details.
21 #
22 # You should have received a copy of the GNU General Public License
23 # along with this program; if not, write to the Free Software
24 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
25 #
26 ##############################################################################
27
28 import tarfile
29 import re
30 import urllib
31 import os
32 import tools
33 from osv import fields, osv, orm
34 import zipfile
35 import release
36
37 ver_regexp = re.compile("^(\\d+)((\\.\\d+)*)([a-z]?)((_(pre|p|beta|alpha|rc)\\d*)*)(-r(\\d+))?$")
38 suffix_regexp = re.compile("^(alpha|beta|rc|pre|p)(\\d*)$")
39
40 def vercmp(ver1, ver2):
41         """
42         Compare two versions
43         Take from portage_versions.py
44         @param ver1: version to compare with
45         @type ver1: string (example "1.2-r3")
46         @param ver2: version to compare again
47         @type ver2: string (example "2.1-r1")
48         @rtype: None or float
49         @return:
50         1. position if ver1 is greater than ver2
51         2. negative if ver1 is less than ver2
52         3. 0 if ver1 equals ver2
53         4. None if ver1 or ver2 are invalid
54         """
55
56         match1 = ver_regexp.match(ver1)
57         match2 = ver_regexp.match(ver2)
58
59         if not match1 or not match1.groups():
60                 return None
61         if not match2 or not match2.groups():
62                 return None
63
64         list1 = [int(match1.group(1))]
65         list2 = [int(match2.group(1))]
66
67         if len(match1.group(2)) or len(match2.group(2)):
68                 vlist1 = match1.group(2)[1:].split(".")
69                 vlist2 = match2.group(2)[1:].split(".")
70                 for i in range(0, max(len(vlist1), len(vlist2))):
71                         # Implicit .0 is given -1, so 1.0.0 > 1.0
72                         # would be ambiguous if two versions that aren't literally equal
73                         # are given the same value (in sorting, for example).
74                         if len(vlist1) <= i or len(vlist1[i]) == 0:
75                                 list1.append(-1)
76                                 list2.append(int(vlist2[i]))
77                         elif len(vlist2) <= i or len(vlist2[i]) == 0:
78                                 list1.append(int(vlist1[i]))
79                                 list2.append(-1)
80                         # Let's make life easy and use integers unless we're forced to use floats
81                         elif (vlist1[i][0] != "0" and vlist2[i][0] != "0"):
82                                 list1.append(int(vlist1[i]))
83                                 list2.append(int(vlist2[i]))
84                         # now we have to use floats so 1.02 compares correctly against 1.1
85                         else:
86                                 list1.append(float("0."+vlist1[i]))
87                                 list2.append(float("0."+vlist2[i]))
88         # and now the final letter
89         if len(match1.group(4)):
90                 list1.append(ord(match1.group(4)))
91         if len(match2.group(4)):
92                 list2.append(ord(match2.group(4)))
93
94         for i in range(0, max(len(list1), len(list2))):
95                 if len(list1) <= i:
96                         return -1
97                 elif len(list2) <= i:
98                         return 1
99                 elif list1[i] != list2[i]:
100                         return list1[i] - list2[i]
101
102         # main version is equal, so now compare the _suffix part
103         list1 = match1.group(5).split("_")[1:]
104         list2 = match2.group(5).split("_")[1:]
105
106         for i in range(0, max(len(list1), len(list2))):
107                 # Implicit _p0 is given a value of -1, so that 1 < 1_p0
108                 if len(list1) <= i:
109                         s1 = ("p","-1")
110                 else:
111                         s1 = suffix_regexp.match(list1[i]).groups()
112                 if len(list2) <= i:
113                         s2 = ("p","-1")
114                 else:
115                         s2 = suffix_regexp.match(list2[i]).groups()
116                 if s1[0] != s2[0]:
117                         return suffix_value[s1[0]] - suffix_value[s2[0]]
118                 if s1[1] != s2[1]:
119                         # it's possible that the s(1|2)[1] == ''
120                         # in such a case, fudge it.
121                         try:
122                                 r1 = int(s1[1])
123                         except ValueError:
124                                 r1 = 0
125                         try:
126                                 r2 = int(s2[1])
127                         except ValueError:
128                                 r2 = 0
129                         if r1 - r2:
130                                 return r1 - r2
131
132         # the suffix part is equal to, so finally check the revision
133         if match1.group(9):
134                 r1 = int(match1.group(9))
135         else:
136                 r1 = 0
137         if match2.group(9):
138                 r2 = int(match2.group(9))
139         else:
140                 r2 = 0
141         return r1 - r2
142
143
144 class module_repository(osv.osv):
145         _name = "ir.module.repository"
146         _description = "Module Repository"
147         _columns = {
148                 'name': fields.char('Name', size=128),
149                 'url': fields.char('Url', size=256, required=True),
150                 'sequence': fields.integer('Sequence', required=True),
151                 'filter': fields.char('Filter', size=128, required=True,
152                         help='Regexp to search module on the repository webpage:\n'
153                         '- The first parenthesis must match the name of the module.\n'
154                         '- The second parenthesis must match all the version number.\n'
155                         '- The last parenthesis must match the extension of the module.'),
156                 'active': fields.boolean('Active'),
157         }
158         _defaults = {
159                 'sequence': lambda *a: 5,
160                 'filter': lambda *a: 'href="([a-zA-Z0-9_]+)-('+release.version.rsplit('.', 1)[0]+'.(\\d+)((\\.\\d+)*)([a-z]?)((_(pre|p|beta|alpha|rc)\\d*)*)(-r(\\d+))?)(\.zip)"',
161                 'active': lambda *a: 1,
162         }
163         _order = "sequence"
164 module_repository()
165
166 class module_category(osv.osv):
167         _name = "ir.module.category"
168         _description = "Module Category"
169         
170         def _module_nbr(self,cr,uid, ids, prop, unknow_none,context):
171                 cr.execute('select category_id,count(*) from ir_module_module where category_id in ('+','.join(map(str,ids))+') or category_id in (select id from ir_module_category where parent_id in ('+','.join(map(str,ids))+')) group by category_id')
172                 result = dict(cr.fetchall())
173                 for id in ids:
174                         cr.execute('select id from ir_module_category where parent_id=%d', (id,))
175                         childs = [c for c, in cr.fetchall()]
176                         result[id] = reduce(lambda x,y:x+y, [result.get(c, 0) for c in childs], result.get(id, 0))
177                 return result
178                 
179         _columns = {
180                 'name': fields.char("Name", size=128, required=True),
181                 'parent_id': fields.many2one('ir.module.category', 'Parent Category', select=True),
182                 'child_ids': fields.one2many('ir.module.category', 'parent_id', 'Parent Category'),
183                 'module_nr': fields.function(_module_nbr, method=True, string='# of Modules', type='integer')
184         }
185         _order = 'name'
186 module_category()
187
188 class module(osv.osv):
189         _name = "ir.module.module"
190         _description = "Module"
191
192         def get_module_info(self, name):
193                 try:
194                         f = tools.file_open(os.path.join(tools.config['addons_path'], name, '__terp__.py'))
195                         data = f.read()
196                         info = eval(data)
197                         if name == 'version':
198                                 info = release.version.rsplit('.', 1)[0] + '.' + info
199                         f.close()
200                 except:
201                         return {}
202                 return info
203
204         def _get_installed_version(self, cr, uid, ids, field_name=None, arg=None, context={}):
205                 res = {}
206                 for m in self.browse(cr, uid, ids):
207                         if m.state in ('installed', 'to upgrade', 'to remove'):
208                                 res[m.id] = release.version.rsplit('.', 1)[0] + '.' + \
209                                         self.get_module_info(m.name).get('version', False)
210                         else:
211                                 res[m.id] = ''
212                 return res
213
214         _columns = {
215                 'name': fields.char("Name", size=128, readonly=True, required=True),
216                 'category_id': fields.many2one('ir.module.category', 'Category', readonly=True),
217                 'shortdesc': fields.char('Short description', size=256, readonly=True),
218                 'description': fields.text("Description", readonly=True),
219                 'author': fields.char("Author", size=128, readonly=True),
220                 'website': fields.char("Website", size=256, readonly=True),
221                 'installed_version': fields.function(_get_installed_version, method=True,
222                         string='Installed version', type='char'),
223                 'latest_version': fields.char('Latest version', size=64, readonly=True),
224                 'url': fields.char('URL', size=128),
225                 'dependencies_id': fields.one2many('ir.module.module.dependency',
226                         'module_id', 'Dependencies', readonly=True),
227                 'state': fields.selection([
228                         ('uninstallable','Not Installable'),
229                         ('uninstalled','Not Installed'),
230                         ('installed','Installed'),
231                         ('to upgrade','To be upgraded'),
232                         ('to remove','To be removed'),
233                         ('to install','To be installed')
234                 ], string='State', readonly=True),
235                 'demo': fields.boolean('Demo data'),
236                 'license': fields.selection([('GPL-2', 'GPL-2'),
237                         ('Other proprietary', 'Other proprietary')], string='License',
238                         readonly=True),
239         }
240         
241         _defaults = {
242                 'state': lambda *a: 'uninstalled',
243                 'demo': lambda *a: False,
244                 'license': lambda *a: 'GPL-2',
245         }
246         _order = 'name'
247
248         _sql_constraints = [
249                 ('name_uniq', 'unique (name)', 'The name of the module must be unique !')
250         ]
251
252         def unlink(self, cr, uid, ids, context=None):
253                 if not ids:
254                         return True
255                 if isinstance(ids, (int, long)):
256                         ids = [ids]
257                 for mod in self.read(cr, uid, ids, ['state'], context):
258                         if mod['state'] in ('installed', 'to upgrade', 'to remove', 'to install'):
259                                 raise orm.except_orm('Error',
260                                                 'You try to remove a module that is installed or will be installed')
261                 return super(module, self).unlink(cr, uid, ids, context=context)
262
263         def state_change(self, cr, uid, ids, newstate, context={}, level=50):
264                 if level<1:
265                         raise Exception, 'Recursion error in modules dependencies !'
266                 demo = True
267                 for module in self.browse(cr, uid, ids):
268                         mdemo = True
269                         for dep in module.dependencies_id:
270                                 ids2 = self.search(cr, uid, [('name','=',dep.name)])
271                                 mdemo = self.state_change(cr, uid, ids2, newstate, context, level-1,)\
272                                                 and mdemo
273                         if not module.dependencies_id:
274                                 mdemo = module.demo
275                         if module.state == 'uninstalled':
276                                 self.write(cr, uid, [module.id], {'state': newstate, 'demo':mdemo})
277                         demo = demo and mdemo
278                 return demo
279
280         def state_upgrade(self, cr, uid, ids, newstate, context=None, level=50):
281                 dep_obj = self.pool.get('ir.module.module.dependency')
282                 if level<1:
283                         raise Exception, 'Recursion error in modules dependencies !'
284                 for module in self.browse(cr, uid, ids):
285                         dep_ids = dep_obj.search(cr, uid, [('name', '=', module.name)])
286                         if dep_ids:
287                                 ids2 = []
288                                 for dep in dep_obj.browse(cr, uid, dep_ids):
289                                         if dep.module_id.state != 'to upgrade':
290                                                 ids2.append(dep.module_id.id)
291                                 self.state_upgrade(cr, uid, ids2, newstate, context, level)
292                         if module.state == 'installed':
293                                 self.write(cr, uid, module.id, {'state': newstate})
294                 return True
295
296         def button_install(self, cr, uid, ids, context={}):
297                 return self.state_change(cr, uid, ids, 'to install', context)
298
299         def button_install_cancel(self, cr, uid, ids, context={}):
300                 self.write(cr, uid, ids, {'state': 'uninstalled', 'demo':False})
301                 return True
302
303         def button_uninstall(self, cr, uid, ids, context={}):
304                 for module in self.browse(cr, uid, ids):
305                         cr.execute('''select m.state,m.name
306                                 from
307                                         ir_module_module_dependency d 
308                                 join 
309                                         ir_module_module m on (d.module_id=m.id)
310                                 where
311                                         d.name=%s and
312                                         m.state not in ('uninstalled','uninstallable','to remove')''', (module.name,))
313                         res = cr.fetchall()
314                         if res:
315                                 raise orm.except_orm('Error', 'The module you are trying to remove depends on installed modules :\n' + '\n'.join(map(lambda x: '\t%s: %s' % (x[0], x[1]), res)))
316                 self.write(cr, uid, ids, {'state': 'to remove'})
317                 return True
318
319         def button_uninstall_cancel(self, cr, uid, ids, context={}):
320                 self.write(cr, uid, ids, {'state': 'installed'})
321                 return True
322         def button_upgrade(self, cr, uid, ids, context=None):
323                 return self.state_upgrade(cr, uid, ids, 'to upgrade', context)
324         def button_upgrade_cancel(self, cr, uid, ids, context={}):
325                 self.write(cr, uid, ids, {'state': 'installed'})
326                 return True
327         def button_update_translations(self, cr, uid, ids, context={}):
328                 cr.execute('select code from res_lang where translatable=TRUE')
329                 langs = [l[0] for l in cr.fetchall()]
330                 modules = self.read(cr, uid, ids, ['name'])
331                 for module in modules: 
332                         files = self.get_module_info(module['name']).get('translations', {})
333                         for lang in langs:
334                                 if files.has_key(lang):
335                                         filepath = files[lang]
336                                         # if filepath does not contain :// we prepend the path of the module
337                                         if filepath.find('://') == -1:
338                                                 filepath = os.path.join(tools.config['addons_path'], module['name'], filepath)
339                                         tools.trans_load(filepath, lang)
340                 return True
341
342         # update the list of available packages
343         def update_list(self, cr, uid, context={}):
344                 robj = self.pool.get('ir.module.repository')
345                 adp = tools.config['addons_path']
346                 res = [0, 0] # [update, add]
347
348                 # iterate through installed modules and mark them as being so
349                 for name in os.listdir(adp):
350                         mod_name = name
351                         if name[-4:]=='.zip':
352                                 mod_name=name[:-4]
353                         ids = self.search(cr, uid, [('name','=',mod_name)])
354                         if ids:
355                                 id = ids[0]
356                                 mod = self.browse(cr, uid, id)
357                                 terp = self.get_module_info(mod_name)
358                                 if terp.get('installable', True) and mod.state == 'uninstallable':
359                                         self.write(cr, uid, id, {'state': 'uninstalled'})
360                                 if vercmp(terp.get('version', ''), mod.latest_version) > 0:
361                                         self.write(cr, uid, id, {
362                                                 'latest_version': terp.get('version')})
363                                         res[0] += 1
364                                 self.write(cr, uid, id, {
365                                         'description': terp.get('description', ''),
366                                         'shortdesc': terp.get('name', ''),
367                                         'author': terp.get('author', 'Unknown'),
368                                         'website': terp.get('website', ''),
369                                         'license': terp.get('license', 'GPL-2'),
370                                         })
371                                 cr.execute('DELETE FROM ir_module_module_dependency\
372                                                 WHERE module_id = %d', (id,))
373                                 self._update_dependencies(cr, uid, ids[0], terp.get('depends',
374                                         []))
375                                 self._update_category(cr, uid, ids[0], terp.get('category',
376                                         'Uncategorized'))
377                                 continue
378                         terp_file = os.path.join(adp, name, '__terp__.py')
379                         mod_path = os.path.join(adp, name)
380                         if os.path.isdir(mod_path) or os.path.islink(mod_path) or zipfile.is_zipfile(mod_path):
381                                 terp = self.get_module_info(mod_name)
382                                 if not terp or not terp.get('installable', True):
383                                         continue
384                                 if not os.path.isfile(os.path.join(adp, mod_name+'.zip')):
385                                         import imp
386                                         # XXX must restrict to only addons paths
387                                         imp.load_module(name, *imp.find_module(mod_name))
388                                 else:
389                                         import zipimport
390                                         mod_path = os.path.join(adp, mod_name+'.zip')
391                                         zimp = zipimport.zipimporter(mod_path)
392                                         zimp.load_module(mod_name)
393                                 id = self.create(cr, uid, {
394                                         'name': mod_name,
395                                         'state': 'uninstalled',
396                                         'description': terp.get('description', ''),
397                                         'shortdesc': terp.get('name', ''),
398                                         'author': terp.get('author', 'Unknown'),
399                                         'website': terp.get('website', ''),
400                                         'latest_version': terp.get('version', ''),
401                                         'license': terp.get('license', 'GPL-2'),
402                                 })
403                                 res[1] += 1
404                                 self._update_dependencies(cr, uid, id, terp.get('depends', []))
405                                 self._update_category(cr, uid, id, terp.get('category', 'Uncategorized'))
406
407                 for repository in robj.browse(cr, uid, robj.search(cr, uid, [])):
408                         index_page = urllib.urlopen(repository.url).read()
409                         modules = re.findall(repository.filter, index_page, re.I+re.M)
410                         mod_sort = {}
411                         for m in modules:
412                                 name = m[0]
413                                 version = m[1]
414                                 extension = m[-1]
415                                 if version == 'x': # 'x' version was a mistake
416                                         version = '0'
417                                 if name in mod_sort:
418                                         if vercmp(version, mod_sort[name][0]) <= 0:
419                                                 continue
420                                 mod_sort[name] = [version, extension]
421                         for name in mod_sort.keys():
422                                 version, extension = mod_sort[name]
423                                 url = repository.url+'/'+name+'-'+version+extension
424                                 ids = self.search(cr, uid, [('name','=',name)])
425                                 if not ids:
426                                         self.create(cr, uid, {
427                                                 'name': name,
428                                                 'latest_version': version,
429                                                 'url': url,
430                                                 'state': 'uninstalled',
431                                         })
432                                         res[1] += 1
433                                 else:
434                                         id = ids[0]
435                                         latest_version = self.read(cr, uid, id, ['latest_version'])\
436                                                         ['latest_version']
437                                         if latest_version == 'x': # 'x' version was a mistake
438                                                 latest_version = '0'
439                                         c = vercmp(version, latest_version)
440                                         if c > 0:
441                                                 self.write(cr, uid, id,
442                                                                 {'latest_version': version, 'url': url})
443                                                 res[0] += 1
444                 return res
445
446         def download(self, cr, uid, ids, download=True, context=None):
447                 res = []
448                 adp = tools.config['addons_path']
449                 for mod in self.browse(cr, uid, ids, context=context):
450                         if not mod.url:
451                                 continue
452                         match = re.search('-([a-zA-Z0-9\._-]+)(\.zip)', mod.url, re.I)
453                         version = '0'
454                         if match:
455                                 version = match.group(1)
456                         if vercmp(mod.installed_version or '0', version) >= 0:
457                                 continue
458                         res.append(mod.url)
459                         if not download:
460                                 continue
461                         zipfile = urllib.urlopen(mod.url).read()
462                         fname = os.path.join(adp, mod.name+'.zip')
463                         try:
464                                 fp = file(fname, 'wb')
465                                 fp.write(zipfile)
466                                 fp.close()
467                         except IOError, e:
468                                 raise orm.except_orm('Error', 'Can not create the module file:\n %s'
469                                                 % (fname,))
470                 return res
471
472         def _update_dependencies(self, cr, uid, id, depends=[]):
473                 for d in depends:
474                         cr.execute('INSERT INTO ir_module_module_dependency (module_id, name) values (%d, %s)', (id, d))
475
476         def _update_category(self, cr, uid, id, category='Uncategorized'):
477                 categs = category.split('/')
478                 p_id = None
479                 while categs:
480                         if p_id is not None:
481                                 cr.execute('select id from ir_module_category where name=%s and parent_id=%d', (categs[0], p_id))
482                         else:
483                                 cr.execute('select id from ir_module_category where name=%s and parent_id is NULL', (categs[0],))
484                         c_id = cr.fetchone()
485                         if not c_id:
486                                 cr.execute('select nextval(\'ir_module_category_id_seq\')')
487                                 c_id = cr.fetchone()[0]
488                                 cr.execute('insert into ir_module_category (id, name, parent_id) values (%d, %s, %d)', (c_id, categs[0], p_id))
489                         else:
490                                 c_id = c_id[0]
491                         p_id = c_id
492                         categs = categs[1:]
493                 self.write(cr, uid, [id], {'category_id': p_id})
494 module()
495
496 class module_dependency(osv.osv):
497         _name = "ir.module.module.dependency"
498         _description = "Module dependency"
499
500         def _state(self, cr, uid, ids, name, args, context={}):
501                 result = {}
502                 mod_obj = self.pool.get('ir.module.module')
503                 for md in self.browse(cr, uid, ids):
504                         ids = mod_obj.search(cr, uid, [('name', '=', md.name)])
505                         if ids:
506                                 result[md.id] = mod_obj.read(cr, uid, [ids[0]], ['state'])[0]['state']
507                         else:
508                                 result[md.id] = 'unknown'
509                 return result
510
511         _columns = {
512                 'name': fields.char('Name',  size=128),
513                 'module_id': fields.many2one('ir.module.module', 'Module', select=True),
514                 'state': fields.function(_state, method=True, type='selection', selection=[
515                         ('uninstallable','Uninstallable'),
516                         ('uninstalled','Not Installed'),
517                         ('installed','Installed'),
518                         ('to upgrade','To be upgraded'),
519                         ('to remove','To be removed'),
520                         ('to install','To be installed'),
521                         ('unknown', 'Unknown'),
522                         ], string='State', readonly=True),
523         }
524 module_dependency()
525