1 ##############################################################################
3 # Copyright (c) 2005 TINY SPRL. (http://tiny.be) All Rights Reserved.
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
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.
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.
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.
26 ##############################################################################
33 from osv import fields, osv, orm
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*)$")
40 def vercmp(ver1, ver2):
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")
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
56 match1 = ver_regexp.match(ver1)
57 match2 = ver_regexp.match(ver2)
59 if not match1 or not match1.groups():
61 if not match2 or not match2.groups():
64 list1 = [int(match1.group(1))]
65 list2 = [int(match2.group(1))]
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:
76 list2.append(int(vlist2[i]))
77 elif len(vlist2) <= i or len(vlist2[i]) == 0:
78 list1.append(int(vlist1[i]))
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
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)))
94 for i in range(0, max(len(list1), len(list2))):
99 elif list1[i] != list2[i]:
100 return list1[i] - list2[i]
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:]
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
111 s1 = suffix_regexp.match(list1[i]).groups()
115 s2 = suffix_regexp.match(list2[i]).groups()
117 return suffix_value[s1[0]] - suffix_value[s2[0]]
119 # it's possible that the s(1|2)[1] == ''
120 # in such a case, fudge it.
132 # the suffix part is equal to, so finally check the revision
134 r1 = int(match1.group(9))
138 r2 = int(match2.group(9))
144 class module_repository(osv.osv):
145 _name = "ir.module.repository"
146 _description = "Module Repository"
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.'),
158 'sequence': lambda *a: 5,
159 '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)"',
164 class module_category(osv.osv):
165 _name = "ir.module.category"
166 _description = "Module Category"
168 def _module_nbr(self,cr,uid, ids, prop, unknow_none,context):
169 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')
170 result = dict(cr.fetchall())
172 cr.execute('select id from ir_module_category where parent_id=%d', (id,))
173 childs = [c for c, in cr.fetchall()]
174 result[id] = reduce(lambda x,y:x+y, [result.get(c, 0) for c in childs], result.get(id, 0))
178 'name': fields.char("Name", size=128, required=True),
179 'parent_id': fields.many2one('ir.module.category', 'Parent Category', select=True),
180 'child_ids': fields.one2many('ir.module.category', 'parent_id', 'Parent Category'),
181 'module_nr': fields.function(_module_nbr, method=True, string='# of Modules', type='integer')
186 class module(osv.osv):
187 _name = "ir.module.module"
188 _description = "Module"
190 def get_module_info(self, name):
192 f = tools.file_open(os.path.join(tools.config['addons_path'], name, '__terp__.py'))
195 if name == 'version':
196 info = release.version.rsplit('.', 1)[0] + '.' + info
202 def _get_installed_version(self, cr, uid, ids, field_name=None, arg=None, context={}):
204 for m in self.browse(cr, uid, ids):
205 if m.state in ('installed', 'to upgrade', 'to remove'):
206 res[m.id] = release.version.rsplit('.', 1)[0] + '.' + \
207 self.get_module_info(m.name).get('version', False)
213 'name': fields.char("Name", size=128, readonly=True, required=True),
214 'category_id': fields.many2one('ir.module.category', 'Category', readonly=True),
215 'shortdesc': fields.char('Short description', size=256, readonly=True),
216 'description': fields.text("Description", readonly=True),
217 'author': fields.char("Author", size=128, readonly=True),
218 'website': fields.char("Website", size=256, readonly=True),
219 'installed_version': fields.function(_get_installed_version, method=True,
220 string='Installed version', type='char'),
221 'latest_version': fields.char('Latest version', size=64, readonly=True),
222 'url': fields.char('URL', size=128),
223 'dependencies_id': fields.one2many('ir.module.module.dependency',
224 'module_id', 'Dependencies', readonly=True),
225 'state': fields.selection([
226 ('uninstallable','Uninstallable'),
227 ('uninstalled','Not Installed'),
228 ('installed','Installed'),
229 ('to upgrade','To be upgraded'),
230 ('to remove','To be removed'),
231 ('to install','To be installed')
232 ], string='State', readonly=True),
233 'demo': fields.boolean('Demo data'),
234 'license': fields.selection([('GPL-2', 'GPL-2'),
235 ('Other proprietary', 'Other proprietary')], string='License',
240 'state': lambda *a: 'uninstalled',
241 'demo': lambda *a: False,
242 'license': lambda *a: 'GPL-2',
247 ('name_uniq', 'unique (name)', 'The name of the module must be unique !')
250 def unlink(self, cr, uid, ids, context=None):
253 if isinstance(ids, (int, long)):
255 for mod in self.read(cr, uid, ids, ['state'], context):
256 if mod['state'] in ('installed', 'to upgrade', 'to remove', 'to install'):
257 raise orm.except_orm('Error',
258 'You try to remove a module that is installed or will be installed')
259 return super(module, self).unlink(cr, uid, ids, context=context)
261 def state_change(self, cr, uid, ids, newstate, context={}, level=50):
263 raise 'Recursion error in modules dependencies !'
265 for module in self.browse(cr, uid, ids):
267 for dep in module.dependencies_id:
268 ids2 = self.search(cr, uid, [('name','=',dep.name)])
269 mdemo = self.state_change(cr, uid, ids2, newstate, context, level-1,)\
271 if not module.dependencies_id:
273 if module.state == 'uninstalled':
274 self.write(cr, uid, [module.id], {'state': newstate, 'demo':mdemo})
275 demo = demo and mdemo
278 def state_upgrade(self, cr, uid, ids, newstate, context=None, level=50):
279 dep_obj = self.pool.get('ir.module.module.dependency')
281 raise 'Recursion error in modules dependencies !'
282 for module in self.browse(cr, uid, ids):
283 dep_ids = dep_obj.search(cr, uid, [('name', '=', module.name)])
286 for dep in dep_obj.browse(cr, uid, dep_ids):
287 if dep.module_id.state != 'to upgrade':
288 ids2.append(dep.module_id.id)
289 self.state_upgrade(cr, uid, ids2, newstate, context, level)
290 if module.state == 'installed':
291 self.write(cr, uid, module.id, {'state': newstate})
294 def button_install(self, cr, uid, ids, context={}):
295 return self.state_change(cr, uid, ids, 'to install', context)
297 def button_install_cancel(self, cr, uid, ids, context={}):
298 self.write(cr, uid, ids, {'state': 'uninstalled', 'demo':False})
301 def button_uninstall(self, cr, uid, ids, context={}):
302 for module in self.browse(cr, uid, ids):
303 cr.execute('''select m.state,m.name
305 ir_module_module_dependency d
307 ir_module_module m on (d.module_id=m.id)
310 m.state not in ('uninstalled','uninstallable','to remove')''', (module.name,))
313 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)))
314 self.write(cr, uid, ids, {'state': 'to remove'})
317 def button_uninstall_cancel(self, cr, uid, ids, context={}):
318 self.write(cr, uid, ids, {'state': 'installed'})
320 def button_upgrade(self, cr, uid, ids, context=None):
321 return self.state_upgrade(cr, uid, ids, 'to upgrade', context)
322 def button_upgrade_cancel(self, cr, uid, ids, context={}):
323 self.write(cr, uid, ids, {'state': 'installed'})
325 def button_update_translations(self, cr, uid, ids, context={}):
326 cr.execute('select code from res_lang where translatable=TRUE')
327 langs = [l[0] for l in cr.fetchall()]
328 modules = self.read(cr, uid, ids, ['name'])
329 for module in modules:
330 files = self.get_module_info(module['name']).get('translations', {})
332 if files.has_key(lang):
333 filepath = files[lang]
334 # if filepath does not contain :// we prepend the path of the module
335 if filepath.find('://') == -1:
336 filepath = os.path.join(tools.config['addons_path'], module['name'], filepath)
337 tools.trans_load(filepath, lang)
340 # update the list of available packages
341 def update_list(self, cr, uid, context={}):
342 robj = self.pool.get('ir.module.repository')
343 adp = tools.config['addons_path']
344 res = [0, 0] # [update, add]
346 # iterate through installed modules and mark them as being so
347 for name in os.listdir(adp):
349 if name[-4:]=='.zip':
351 ids = self.search(cr, uid, [('name','=',mod_name)])
354 mod = self.browse(cr, uid, id)
355 terp = self.get_module_info(mod_name)
356 if terp.get('installable', True) and mod.state == 'uninstallable':
357 self.write(cr, uid, id, {'state': 'uninstalled'})
358 if vercmp(terp.get('version', ''), mod.latest_version) > 0:
359 self.write(cr, uid, id, {
360 'latest_version': terp.get('version')})
362 self.write(cr, uid, id, {
363 'description': terp.get('description', ''),
364 'shortdesc': terp.get('name', ''),
365 'author': terp.get('author', 'Unknown'),
366 'website': terp.get('website', ''),
367 'license': terp.get('license', 'GPL-2'),
369 cr.execute('DELETE FROM ir_module_module_dependency\
370 WHERE module_id = %d', (id,))
371 self._update_dependencies(cr, uid, ids[0], terp.get('depends',
373 self._update_category(cr, uid, ids[0], terp.get('category',
376 terp_file = os.path.join(adp, name, '__terp__.py')
377 mod_path = os.path.join(adp, name)
378 if os.path.isdir(mod_path) or os.path.islink(mod_path) or zipfile.is_zipfile(mod_path):
379 terp = self.get_module_info(mod_name)
380 if not terp or not terp.get('installable', True):
382 if not os.path.isfile(os.path.join(adp, mod_name+'.zip')):
384 # XXX must restrict to only addons paths
385 imp.load_module(name, *imp.find_module(mod_name))
388 mod_path = os.path.join(adp, mod_name+'.zip')
389 zimp = zipimport.zipimporter(mod_path)
390 zimp.load_module(mod_name)
391 id = self.create(cr, uid, {
393 'state': 'uninstalled',
394 'description': terp.get('description', ''),
395 'shortdesc': terp.get('name', ''),
396 'author': terp.get('author', 'Unknown'),
397 'website': terp.get('website', ''),
398 'latest_version': terp.get('version', ''),
399 'license': terp.get('license', 'GPL-2'),
402 self._update_dependencies(cr, uid, id, terp.get('depends', []))
403 self._update_category(cr, uid, id, terp.get('category', 'Uncategorized'))
405 for repository in robj.browse(cr, uid, robj.search(cr, uid, [])):
406 index_page = urllib.urlopen(repository.url).read()
407 modules = re.findall(repository.filter, index_page, re.I+re.M)
413 if version == 'x': # 'x' version was a mistake
416 if vercmp(version, mod_sort[name][0]) <= 0:
418 mod_sort[name] = [version, extension]
419 for name in mod_sort.keys():
420 version, extension = mod_sort[name]
421 url = repository.url+'/'+name+'-'+version+extension
422 ids = self.search(cr, uid, [('name','=',name)])
424 self.create(cr, uid, {
426 'latest_version': version,
428 'state': 'uninstalled',
433 latest_version = self.read(cr, uid, id, ['latest_version'])\
435 if latest_version == 'x': # 'x' version was a mistake
437 c = vercmp(version, latest_version)
439 self.write(cr, uid, id,
440 {'latest_version': version, 'url': url})
444 def download(self, cr, uid, ids, context=None):
445 adp = tools.config['addons_path']
446 for mod in self.browse(cr, uid, ids, context=context):
449 match = re.search('-([a-zA-Z0-9\._-]+)(\.zip)', mod.url, re.I)
452 version = match.group(1)
453 if vercmp(mod.installed_version or '0', version) >= 0:
455 zipfile = urllib.urlopen(mod.url).read()
456 fname = os.path.join(adp, mod.name+'.zip')
458 fp = file(fname, 'wb')
462 raise orm.except_orm('Error', 'Can not create the module file:\n %s'
465 def _update_dependencies(self, cr, uid, id, depends=[]):
467 cr.execute('INSERT INTO ir_module_module_dependency (module_id, name) values (%d, %s)', (id, d))
469 def _update_category(self, cr, uid, id, category='Uncategorized'):
470 categs = category.split('/')
474 cr.execute('select id from ir_module_category where name=%s and parent_id=%d', (categs[0], p_id))
476 cr.execute('select id from ir_module_category where name=%s and parent_id is NULL', (categs[0],))
479 cr.execute('select nextval(\'ir_module_category_id_seq\')')
480 c_id = cr.fetchone()[0]
481 cr.execute('insert into ir_module_category (id, name, parent_id) values (%d, %s, %d)', (c_id, categs[0], p_id))
486 self.write(cr, uid, [id], {'category_id': p_id})
489 class module_dependency(osv.osv):
490 _name = "ir.module.module.dependency"
491 _description = "Module dependency"
493 def _state(self, cr, uid, ids, name, args, context={}):
495 mod_obj = self.pool.get('ir.module.module')
496 for md in self.browse(cr, uid, ids):
497 ids = mod_obj.search(cr, uid, [('name', '=', md.name)])
499 result[md.id] = mod_obj.read(cr, uid, [ids[0]], ['state'])[0]['state']
501 result[md.id] = 'unknown'
505 'name': fields.char('Name', size=128),
506 'module_id': fields.many2one('ir.module.module', 'Module', select=True),
507 'state': fields.function(_state, method=True, type='selection', selection=[
508 ('uninstallable','Uninstallable'),
509 ('uninstalled','Not Installed'),
510 ('installed','Installed'),
511 ('to upgrade','To be upgraded'),
512 ('to remove','To be removed'),
513 ('to install','To be installed'),
514 ('unknown', 'Unknown'),
515 ], string='State', readonly=True),