[FIX] setup.py: added docutils dependency introduced at revision 4341.3.1.
[odoo/odoo.git] / openerp / addons / base / module / module.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2012 OpenERP S.A. (<http://openerp.com>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 import base64
23 from docutils.core import publish_string
24 import imp
25 import logging
26 import re
27 import urllib
28 import zipimport
29
30 from openerp import modules, pooler, release, tools, addons
31 from openerp.tools.parse_version import parse_version
32 from openerp.tools.translate import _
33 from openerp.osv import fields, osv, orm
34
35 _logger = logging.getLogger(__name__)
36
37 ACTION_DICT = {
38     'view_type': 'form',
39     'view_mode': 'form',
40     'res_model': 'base.module.upgrade',
41     'target': 'new',
42     'type': 'ir.actions.act_window',
43     'nodestroy':True,
44 }
45
46 class module_category(osv.osv):
47     _name = "ir.module.category"
48     _description = "Application"
49
50     def _module_nbr(self,cr,uid, ids, prop, unknow_none, context):
51         cr.execute('SELECT category_id, COUNT(*) \
52                       FROM ir_module_module \
53                      WHERE category_id IN %(ids)s \
54                         OR category_id IN (SELECT id \
55                                              FROM ir_module_category \
56                                             WHERE parent_id IN %(ids)s) \
57                      GROUP BY category_id', {'ids': tuple(ids)}
58                     )
59         result = dict(cr.fetchall())
60         for id in ids:
61             cr.execute('select id from ir_module_category where parent_id=%s', (id,))
62             result[id] = sum([result.get(c, 0) for (c,) in cr.fetchall()],
63                              result.get(id, 0))
64         return result
65
66     _columns = {
67         'name': fields.char("Name", size=128, required=True, translate=True, select=True),
68         'parent_id': fields.many2one('ir.module.category', 'Parent Application', select=True),
69         'child_ids': fields.one2many('ir.module.category', 'parent_id', 'Child Applications'),
70         'module_nr': fields.function(_module_nbr, string='Number of Modules', type='integer'),
71         'module_ids' : fields.one2many('ir.module.module', 'category_id', 'Modules'),
72         'description' : fields.text("Description", translate=True),
73         'sequence' : fields.integer('Sequence'),
74         'visible' : fields.boolean('Visible'),
75         'xml_id': fields.function(osv.osv.get_external_id, type='char', size=128, string="External ID"),
76     }
77     _order = 'name'
78
79     _defaults = {
80         'visible' : 1,
81     }
82
83 class module(osv.osv):
84     _name = "ir.module.module"
85     _rec_name = "shortdesc"
86     _description = "Module"
87
88     @classmethod
89     def get_module_info(cls, name):
90         info = {}
91         try:
92             info = modules.load_information_from_description_file(name)
93             info['version'] = release.major_version + '.' + info['version']
94         except Exception:
95             _logger.debug('Error when trying to fetch informations for '
96                           'module %s', name, exc_info=True)
97         return info
98
99     def _get_desc(self, cr, uid, ids, field_name=None, arg=None, context=None):
100         res = dict.fromkeys(ids, '')
101         for module in self.browse(cr, uid, ids, context=context):
102             desc = self.get_module_info(module.name).get('description', '')
103             overrides = dict(embed_stylesheet=False, doctitle_xform=False, output_encoding='unicode')
104             output = publish_string(source=desc, writer_name='html', settings_overrides=overrides)
105             res[module.id] = output
106         return res
107
108     def _get_latest_version(self, cr, uid, ids, field_name=None, arg=None, context=None):
109         res = dict.fromkeys(ids, '')
110         for m in self.browse(cr, uid, ids):
111             res[m.id] = self.get_module_info(m.name).get('version', '')
112         return res
113
114     def _get_views(self, cr, uid, ids, field_name=None, arg=None, context=None):
115         res = {}
116         model_data_obj = self.pool.get('ir.model.data')
117         view_obj = self.pool.get('ir.ui.view')
118         report_obj = self.pool.get('ir.actions.report.xml')
119         menu_obj = self.pool.get('ir.ui.menu')
120
121         dmodels = []
122         if field_name is None or 'views_by_module' in field_name:
123             dmodels.append('ir.ui.view')
124         if field_name is None or 'reports_by_module' in field_name:
125             dmodels.append('ir.actions.report.xml')
126         if field_name is None or 'menus_by_module' in field_name:
127             dmodels.append('ir.ui.menu')
128         assert dmodels, "no models for %s" % field_name
129
130         for module_rec in self.browse(cr, uid, ids, context=context):
131             res[module_rec.id] = {
132                 'menus_by_module': [],
133                 'reports_by_module': [],
134                 'views_by_module': []
135             }
136
137             # Skip uninstalled modules below, no data to find anyway.
138             if module_rec.state not in ('installed', 'to upgrade', 'to remove'):
139                 continue
140
141             # then, search and group ir.model.data records
142             imd_models = dict( [(m,[]) for m in dmodels])
143             imd_ids = model_data_obj.search(cr,uid,[('module','=', module_rec.name),
144                 ('model','in',tuple(dmodels))])
145
146             for imd_res in model_data_obj.read(cr, uid, imd_ids, ['model', 'res_id'], context=context):
147                 imd_models[imd_res['model']].append(imd_res['res_id'])
148
149             # For each one of the models, get the names of these ids.
150             # We use try except, because views or menus may not exist.
151             try:
152                 res_mod_dic = res[module_rec.id]
153                 view_ids = imd_models.get('ir.ui.view', [])
154                 for v in view_obj.browse(cr, uid, view_ids, context=context):
155                     aa = v.inherit_id and '* INHERIT ' or ''
156                     res_mod_dic['views_by_module'].append(aa + v.name + '('+v.type+')')
157
158                 report_ids = imd_models.get('ir.actions.report.xml', [])
159                 for rx in report_obj.browse(cr, uid, report_ids, context=context):
160                     res_mod_dic['reports_by_module'].append(rx.name)
161
162                 menu_ids = imd_models.get('ir.ui.menu', [])
163                 for um in menu_obj.browse(cr, uid, menu_ids, context=context):
164                     res_mod_dic['menus_by_module'].append(um.complete_name)
165             except KeyError, e:
166                 _logger.warning(
167                       'Data not found for items of %s', module_rec.name)
168             except AttributeError, e:
169                 _logger.warning(
170                       'Data not found for items of %s %s', module_rec.name, str(e))
171             except Exception, e:
172                 _logger.warning('Unknown error while fetching data of %s',
173                       module_rec.name, exc_info=True)
174         for key, _ in res.iteritems():
175             for k, v in res[key].iteritems():
176                 res[key][k] = "\n".join(sorted(v))
177         return res
178
179     def _get_icon_image(self, cr, uid, ids, field_name=None, arg=None, context=None):
180         res = dict.fromkeys(ids, '')
181         for module in self.browse(cr, uid, ids, context=context):
182             path = addons.get_module_resource(module.name, 'static', 'src', 'img', 'icon.png')
183             if path:
184                 image_file = tools.file_open(path, 'rb')
185                 try:
186                     res[module.id] = image_file.read().encode('base64')
187                 finally:
188                     image_file.close()
189         return res
190
191     _columns = {
192         'name': fields.char("Technical Name", size=128, readonly=True, required=True, select=True),
193         'category_id': fields.many2one('ir.module.category', 'Category', readonly=True, select=True),
194         'shortdesc': fields.char('Module Name', size=64, readonly=True, translate=True),
195         'summary': fields.char('Summary', size=64, readonly=True, translate=True),
196         'description': fields.function(_get_desc, string='Description', type='html', method=True, readonly=True, store=True),
197         'author': fields.char("Author", size=128, readonly=True),
198         'maintainer': fields.char('Maintainer', size=128, readonly=True),
199         'contributors': fields.text('Contributors', readonly=True),
200         'website': fields.char("Website", size=256, readonly=True),
201
202         # attention: Incorrect field names !!
203         #   installed_version refer the latest version (the one on disk)
204         #   latest_version refer the installed version (the one in database)
205         #   published_version refer the version available on the repository
206         'installed_version': fields.function(_get_latest_version,
207             string='Latest Version', type='char'),
208         'latest_version': fields.char('Installed Version', size=64, readonly=True),
209         'published_version': fields.char('Published Version', size=64, readonly=True),
210
211         'url': fields.char('URL', size=128, readonly=True),
212         'sequence': fields.integer('Sequence'),
213         'dependencies_id': fields.one2many('ir.module.module.dependency',
214             'module_id', 'Dependencies', readonly=True),
215         'auto_install': fields.boolean('Automatic Installation',
216             help='An auto-installable module is automatically installed by the '
217             'system when all its dependencies are satisfied. '
218             'If the module has no dependency, it is always installed.'),
219         'state': fields.selection([
220             ('uninstallable','Not Installable'),
221             ('uninstalled','Not Installed'),
222             ('installed','Installed'),
223             ('to upgrade','To be upgraded'),
224             ('to remove','To be removed'),
225             ('to install','To be installed')
226         ], string='State', readonly=True, select=True),
227         'demo': fields.boolean('Demo Data', readonly=True),
228         'license': fields.selection([
229                 ('GPL-2', 'GPL Version 2'),
230                 ('GPL-2 or any later version', 'GPL-2 or later version'),
231                 ('GPL-3', 'GPL Version 3'),
232                 ('GPL-3 or any later version', 'GPL-3 or later version'),
233                 ('AGPL-3', 'Affero GPL-3'),
234                 ('Other OSI approved licence', 'Other OSI Approved Licence'),
235                 ('Other proprietary', 'Other Proprietary')
236             ], string='License', readonly=True),
237         'menus_by_module': fields.function(_get_views, string='Menus', type='text', multi="meta", store=True),
238         'reports_by_module': fields.function(_get_views, string='Reports', type='text', multi="meta", store=True),
239         'views_by_module': fields.function(_get_views, string='Views', type='text', multi="meta", store=True),
240         'certificate' : fields.char('Quality Certificate', size=64, readonly=True),
241         'application': fields.boolean('Application', readonly=True),
242         'icon': fields.char('Icon URL', size=128),
243         'icon_image': fields.function(_get_icon_image, string='Icon', type="binary"),
244     }
245
246     _defaults = {
247         'state': 'uninstalled',
248         'sequence': 100,
249         'demo': False,
250         'license': 'AGPL-3',
251     }
252     _order = 'sequence,name'
253
254     def _name_uniq_msg(self, cr, uid, ids, context=None):
255         return _('The name of the module must be unique !')
256     def _certificate_uniq_msg(self, cr, uid, ids, context=None):
257         return _('The certificate ID of the module must be unique !')
258
259     _sql_constraints = [
260         ('name_uniq', 'UNIQUE (name)',_name_uniq_msg ),
261         ('certificate_uniq', 'UNIQUE (certificate)',_certificate_uniq_msg )
262     ]
263
264     def unlink(self, cr, uid, ids, context=None):
265         if not ids:
266             return True
267         if isinstance(ids, (int, long)):
268             ids = [ids]
269         mod_names = []
270         for mod in self.read(cr, uid, ids, ['state','name'], context):
271             if mod['state'] in ('installed', 'to upgrade', 'to remove', 'to install'):
272                 raise orm.except_orm(_('Error'),
273                         _('You try to remove a module that is installed or will be installed'))
274             mod_names.append(mod['name'])
275         #Removing the entry from ir_model_data
276         #ids_meta = self.pool.get('ir.model.data').search(cr, uid, [('name', '=', 'module_meta_information'), ('module', 'in', mod_names)])
277
278         #if ids_meta:
279         #    self.pool.get('ir.model.data').unlink(cr, uid, ids_meta, context)
280
281         return super(module, self).unlink(cr, uid, ids, context=context)
282
283     @staticmethod
284     def _check_external_dependencies(terp):
285         depends = terp.get('external_dependencies')
286         if not depends:
287             return
288         for pydep in depends.get('python', []):
289             parts = pydep.split('.')
290             parts.reverse()
291             path = None
292             while parts:
293                 part = parts.pop()
294                 try:
295                     _, path, _ = imp.find_module(part, path and [path] or None)
296                 except ImportError:
297                     raise ImportError('No module named %s' % (pydep,))
298
299         for binary in depends.get('bin', []):
300             if tools.find_in_path(binary) is None:
301                 raise Exception('Unable to find %r in path' % (binary,))
302
303     @classmethod
304     def check_external_dependencies(cls, module_name, newstate='to install'):
305         terp = cls.get_module_info(module_name)
306         try:
307             cls._check_external_dependencies(terp)
308         except Exception, e:
309             if newstate == 'to install':
310                 msg = _('Unable to install module "%s" because an external dependency is not met: %s')
311             elif newstate == 'to upgrade':
312                 msg = _('Unable to upgrade module "%s" because an external dependency is not met: %s')
313             else:
314                 msg = _('Unable to process module "%s" because an external dependency is not met: %s')
315             raise orm.except_orm(_('Error'), msg % (module_name, e.args[0]))
316
317     def state_update(self, cr, uid, ids, newstate, states_to_update, context=None, level=100):
318         if level<1:
319             raise orm.except_orm(_('Error'), _('Recursion error in modules dependencies !'))
320         demo = False
321         for module in self.browse(cr, uid, ids, context=context):
322             mdemo = False
323             for dep in module.dependencies_id:
324                 if dep.state == 'unknown':
325                     raise orm.except_orm(_('Error'), _("You try to install module '%s' that depends on module '%s'.\nBut the latter module is not available in your system.") % (module.name, dep.name,))
326                 ids2 = self.search(cr, uid, [('name','=',dep.name)])
327                 if dep.state != newstate:
328                     mdemo = self.state_update(cr, uid, ids2, newstate, states_to_update, context, level-1,) or mdemo
329                 else:
330                     od = self.browse(cr, uid, ids2)[0]
331                     mdemo = od.demo or mdemo
332
333             self.check_external_dependencies(module.name, newstate)
334             if not module.dependencies_id:
335                 mdemo = module.demo
336             if module.state in states_to_update:
337                 self.write(cr, uid, [module.id], {'state': newstate, 'demo':mdemo})
338             demo = demo or mdemo
339         return demo
340
341     def button_install(self, cr, uid, ids, context=None):
342
343         # Mark the given modules to be installed.
344         self.state_update(cr, uid, ids, 'to install', ['uninstalled'], context)
345
346         # Mark (recursively) the newly satisfied modules to also be installed:
347
348         # Select all auto-installable (but not yet installed) modules.
349         domain = [('state', '=', 'uninstalled'), ('auto_install', '=', True),]
350         uninstalled_ids = self.search(cr, uid, domain, context=context)
351         uninstalled_modules = self.browse(cr, uid, uninstalled_ids, context=context)
352
353         # Keep those with all their dependencies satisfied.
354         def all_depencies_satisfied(m):
355             return all(x.state in ('to install', 'installed', 'to upgrade') for x in m.dependencies_id)
356         to_install_modules = filter(all_depencies_satisfied, uninstalled_modules)
357         to_install_ids = map(lambda m: m.id, to_install_modules)
358
359         # Mark them to be installed.
360         if to_install_ids:
361             self.button_install(cr, uid, to_install_ids, context=context)
362         return dict(ACTION_DICT, name=_('Install'))
363
364     def button_immediate_install(self, cr, uid, ids, context=None):
365         """ Installs the selected module(s) immediately and fully,
366         returns the next res.config action to execute
367
368         :param ids: identifiers of the modules to install
369         :returns: next res.config item to execute
370         :rtype: dict[str, object]
371         """
372         return self._button_immediate_function(cr, uid, ids, self.button_install, context=context)
373
374     def button_install_cancel(self, cr, uid, ids, context=None):
375         self.write(cr, uid, ids, {'state': 'uninstalled', 'demo':False})
376         return True
377
378     def module_uninstall(self, cr, uid, ids, context=None):
379         """Perform the various steps required to uninstall a module completely
380         including the deletion of all database structures created by the module:
381         tables, columns, constraints, etc."""
382         ir_model_data = self.pool.get('ir.model.data')
383         ir_model_constraint = self.pool.get('ir.model.constraint')
384         modules_to_remove = [m.name for m in self.browse(cr, uid, ids, context)]
385         constraint_ids = ir_model_constraint.search(cr, uid, [('module', 'in', modules_to_remove)])
386         ir_model_constraint._module_data_uninstall(cr, uid, constraint_ids, context)
387         ir_model_data._module_data_uninstall(cr, uid, modules_to_remove, context)
388         self.write(cr, uid, ids, {'state': 'uninstalled'})
389         return True
390
391     def downstream_dependencies(self, cr, uid, ids, known_dep_ids=None,
392                                 exclude_states=['uninstalled','uninstallable','to remove'],
393                                 context=None):
394         """Return the ids of all modules that directly or indirectly depend
395         on the given module `ids`, and that satisfy the `exclude_states`
396         filter"""
397         if not ids: return []
398         known_dep_ids = set(known_dep_ids or [])
399         cr.execute('''SELECT DISTINCT m.id
400                         FROM
401                             ir_module_module_dependency d
402                         JOIN
403                             ir_module_module m ON (d.module_id=m.id)
404                         WHERE
405                             d.name IN (SELECT name from ir_module_module where id in %s) AND
406                             m.state NOT IN %s AND
407                             m.id NOT IN %s ''',
408                    (tuple(ids),tuple(exclude_states), tuple(known_dep_ids or ids)))
409         new_dep_ids = set([m[0] for m in cr.fetchall()])
410         missing_mod_ids = new_dep_ids - known_dep_ids
411         known_dep_ids |= new_dep_ids
412         if missing_mod_ids:
413             known_dep_ids |= set(self.downstream_dependencies(cr, uid, list(missing_mod_ids),
414                                                           known_dep_ids, exclude_states,context))
415         return list(known_dep_ids)
416
417     def _button_immediate_function(self, cr, uid, ids, function, context=None):
418         function(cr, uid, ids, context=context)
419
420         cr.commit()
421         _, pool = pooler.restart_pool(cr.dbname, update_module=True)
422
423         config = pool.get('res.config').next(cr, uid, [], context=context) or {}
424         if config.get('type') not in ('ir.actions.reload', 'ir.actions.act_window_close'):
425             return config
426
427         # reload the client; open the first available root menu
428         menu_obj = self.pool.get('ir.ui.menu')
429         menu_ids = menu_obj.search(cr, uid, [('parent_id', '=', False)], context=context)
430
431         return {
432             'type' : 'ir.actions.client',
433             'tag' : 'reload',
434             'params' : {'menu_id' : menu_ids and menu_ids[0] or False}
435         }
436
437     def button_immediate_uninstall(self, cr, uid, ids, context=None):
438         """
439         Uninstall the selected module(s) immediately and fully,
440         returns the next res.config action to execute
441         """
442         return self._button_immediate_function(cr, uid, ids, self.button_uninstall, context=context)
443
444     def button_uninstall(self, cr, uid, ids, context=None):
445         if any(m.name == 'base' for m in self.browse(cr, uid, ids, context=context)):
446             raise orm.except_orm(_('Error'), _("The `base` module cannot be uninstalled"))
447         dep_ids = self.downstream_dependencies(cr, uid, ids, context=context)
448         self.write(cr, uid, ids + dep_ids, {'state': 'to remove'})
449         return dict(ACTION_DICT, name=_('Uninstall'))
450
451     def button_uninstall_cancel(self, cr, uid, ids, context=None):
452         self.write(cr, uid, ids, {'state': 'installed'})
453         return True
454
455     def button_immediate_upgrade(self, cr, uid, ids, context=None):
456         """
457         Upgrade the selected module(s) immediately and fully,
458         return the next res.config action to execute
459         """
460         return self._button_immediate_function(cr, uid, ids, self.button_upgrade, context=context)
461
462     def button_upgrade(self, cr, uid, ids, context=None):
463         depobj = self.pool.get('ir.module.module.dependency')
464         todo = self.browse(cr, uid, ids, context=context)
465         self.update_list(cr, uid)
466
467         i = 0
468         while i<len(todo):
469             mod = todo[i]
470             i += 1
471             if mod.state not in ('installed','to upgrade'):
472                 raise orm.except_orm(_('Error'),
473                         _("Can not upgrade module '%s'. It is not installed.") % (mod.name,))
474             self.check_external_dependencies(mod.name, 'to upgrade')
475             iids = depobj.search(cr, uid, [('name', '=', mod.name)], context=context)
476             for dep in depobj.browse(cr, uid, iids, context=context):
477                 if dep.module_id.state=='installed' and dep.module_id not in todo:
478                     todo.append(dep.module_id)
479
480         ids = map(lambda x: x.id, todo)
481         self.write(cr, uid, ids, {'state':'to upgrade'}, context=context)
482
483         to_install = []
484         for mod in todo:
485             for dep in mod.dependencies_id:
486                 if dep.state == 'unknown':
487                     raise orm.except_orm(_('Error'), _('You try to upgrade a module that depends on the module: %s.\nBut this module is not available in your system.') % (dep.name,))
488                 if dep.state == 'uninstalled':
489                     ids2 = self.search(cr, uid, [('name','=',dep.name)])
490                     to_install.extend(ids2)
491
492         self.button_install(cr, uid, to_install, context=context)
493         return dict(ACTION_DICT, name=_('Apply Schedule Upgrade'))
494
495     def button_upgrade_cancel(self, cr, uid, ids, context=None):
496         self.write(cr, uid, ids, {'state': 'installed'})
497         return True
498
499     def button_update_translations(self, cr, uid, ids, context=None):
500         self.update_translations(cr, uid, ids)
501         return True
502
503     @staticmethod
504     def get_values_from_terp(terp):
505         return {
506             'description': terp.get('description', ''),
507             'shortdesc': terp.get('name', ''),
508             'author': terp.get('author', 'Unknown'),
509             'maintainer': terp.get('maintainer', False),
510             'contributors': ', '.join(terp.get('contributors', [])) or False,
511             'website': terp.get('website', ''),
512             'license': terp.get('license', 'AGPL-3'),
513             'certificate': terp.get('certificate') or False,
514             'sequence': terp.get('sequence', 100),
515             'application': terp.get('application', False),
516             'auto_install': terp.get('auto_install', False),
517             'icon': terp.get('icon', False),
518             'summary': terp.get('summary', ''),
519         }
520
521     # update the list of available packages
522     def update_list(self, cr, uid, context=None):
523         res = [0, 0] # [update, add]
524
525         known_mods = self.browse(cr, uid, self.search(cr, uid, []))
526         known_mods_names = dict([(m.name, m) for m in known_mods])
527
528         # iterate through detected modules and update/create them in db
529         for mod_name in modules.get_modules():
530             mod = known_mods_names.get(mod_name)
531             terp = self.get_module_info(mod_name)
532             values = self.get_values_from_terp(terp)
533
534             if mod:
535                 updated_values = {}
536                 for key in values:
537                     old = getattr(mod, key)
538                     updated = isinstance(values[key], basestring) and tools.ustr(values[key]) or values[key]
539                     if not old == updated:
540                         updated_values[key] = values[key]
541                 if terp.get('installable', True) and mod.state == 'uninstallable':
542                     updated_values['state'] = 'uninstalled'
543                 if parse_version(terp.get('version', '')) > parse_version(mod.latest_version or ''):
544                     res[0] += 1
545                 if updated_values:
546                     self.write(cr, uid, mod.id, updated_values)
547             else:
548                 mod_path = modules.get_module_path(mod_name)
549                 if not mod_path:
550                     continue
551                 if not terp or not terp.get('installable', True):
552                     continue
553                 id = self.create(cr, uid, dict(name=mod_name, state='uninstalled', **values))
554                 mod = self.browse(cr, uid, id)
555                 res[1] += 1
556
557             self._update_dependencies(cr, uid, mod, terp.get('depends', []))
558             self._update_category(cr, uid, mod, terp.get('category', 'Uncategorized'))
559
560         return res
561
562     def download(self, cr, uid, ids, download=True, context=None):
563         res = []
564         for mod in self.browse(cr, uid, ids, context=context):
565             if not mod.url:
566                 continue
567             match = re.search('-([a-zA-Z0-9\._-]+)(\.zip)', mod.url, re.I)
568             version = '0'
569             if match:
570                 version = match.group(1)
571             if parse_version(mod.installed_version or '0') >= parse_version(version):
572                 continue
573             res.append(mod.url)
574             if not download:
575                 continue
576             zip_content = urllib.urlopen(mod.url).read()
577             fname = modules.get_module_path(str(mod.name)+'.zip', downloaded=True)
578             try:
579                 with open(fname, 'wb') as fp:
580                     fp.write(zip_content)
581             except Exception:
582                 _logger.exception('Error when trying to create module '
583                                   'file %s', fname)
584                 raise orm.except_orm(_('Error'), _('Can not create the module file:\n %s') % (fname,))
585             terp = self.get_module_info(mod.name)
586             self.write(cr, uid, mod.id, self.get_values_from_terp(terp))
587             cr.execute('DELETE FROM ir_module_module_dependency ' \
588                     'WHERE module_id = %s', (mod.id,))
589             self._update_dependencies(cr, uid, mod, terp.get('depends',
590                 []))
591             self._update_category(cr, uid, mod, terp.get('category',
592                 'Uncategorized'))
593             # Import module
594             zimp = zipimport.zipimporter(fname)
595             zimp.load_module(mod.name)
596         return res
597
598     def _update_dependencies(self, cr, uid, mod_browse, depends=None):
599         if depends is None:
600             depends = []
601         existing = set(x.name for x in mod_browse.dependencies_id)
602         needed = set(depends)
603         for dep in (needed - existing):
604             cr.execute('INSERT INTO ir_module_module_dependency (module_id, name) values (%s, %s)', (mod_browse.id, dep))
605         for dep in (existing - needed):
606             cr.execute('DELETE FROM ir_module_module_dependency WHERE module_id = %s and name = %s', (mod_browse.id, dep))
607
608     def _update_category(self, cr, uid, mod_browse, category='Uncategorized'):
609         current_category = mod_browse.category_id
610         current_category_path = []
611         while current_category:
612             current_category_path.insert(0, current_category.name)
613             current_category = current_category.parent_id
614
615         categs = category.split('/')
616         if categs != current_category_path:
617             p_id = None
618             while categs:
619                 if p_id is not None:
620                     cr.execute('SELECT id FROM ir_module_category WHERE name=%s AND parent_id=%s', (categs[0], p_id))
621                 else:
622                     cr.execute('SELECT id FROM ir_module_category WHERE name=%s AND parent_id is NULL', (categs[0],))
623                 c_id = cr.fetchone()
624                 if not c_id:
625                     cr.execute('INSERT INTO ir_module_category (name, parent_id) VALUES (%s, %s) RETURNING id', (categs[0], p_id))
626                     c_id = cr.fetchone()[0]
627                 else:
628                     c_id = c_id[0]
629                 p_id = c_id
630                 categs = categs[1:]
631             self.write(cr, uid, [mod_browse.id], {'category_id': p_id})
632
633     def update_translations(self, cr, uid, ids, filter_lang=None, context=None):
634         if context is None:
635             context = {}
636         if not filter_lang:
637             pool = pooler.get_pool(cr.dbname)
638             lang_obj = pool.get('res.lang')
639             lang_ids = lang_obj.search(cr, uid, [('translatable', '=', True)])
640             filter_lang = [lang.code for lang in lang_obj.browse(cr, uid, lang_ids)]
641         elif not isinstance(filter_lang, (list, tuple)):
642             filter_lang = [filter_lang]
643
644         for mod in self.browse(cr, uid, ids):
645             if mod.state != 'installed':
646                 continue
647             modpath = modules.get_module_path(mod.name)
648             if not modpath:
649                 # unable to find the module. we skip
650                 continue
651             for lang in filter_lang:
652                 iso_lang = tools.get_iso_codes(lang)
653                 f = modules.get_module_resource(mod.name, 'i18n', iso_lang + '.po')
654                 context2 = context and context.copy() or {}
655                 if f and '_' in iso_lang:
656                     iso_lang2 = iso_lang.split('_')[0]
657                     f2 = modules.get_module_resource(mod.name, 'i18n', iso_lang2 + '.po')
658                     if f2:
659                         _logger.info('module %s: loading base translation file %s for language %s', mod.name, iso_lang2, lang)
660                         tools.trans_load(cr, f2, lang, verbose=False, context=context)
661                         context2['overwrite'] = True
662                 # Implementation notice: we must first search for the full name of
663                 # the language derivative, like "en_UK", and then the generic,
664                 # like "en".
665                 if (not f) and '_' in iso_lang:
666                     iso_lang = iso_lang.split('_')[0]
667                     f = modules.get_module_resource(mod.name, 'i18n', iso_lang + '.po')
668                 if f:
669                     _logger.info('module %s: loading translation file (%s) for language %s', mod.name, iso_lang, lang)
670                     tools.trans_load(cr, f, lang, verbose=False, context=context2)
671                 elif iso_lang != 'en':
672                     _logger.warning('module %s: no translation for language %s', mod.name, iso_lang)
673
674     def check(self, cr, uid, ids, context=None):
675         for mod in self.browse(cr, uid, ids, context=context):
676             if not mod.description:
677                 _logger.warning('module %s: description is empty !', mod.name)
678
679             if not mod.certificate or not mod.certificate.isdigit():
680                 _logger.info('module %s: no quality certificate', mod.name)
681             else:
682                 val = long(mod.certificate[2:]) % 97 == 29
683                 if not val:
684                     _logger.critical('module %s: invalid quality certificate: %s', mod.name, mod.certificate)
685                     raise osv.except_osv(_('Error'), _('Module %s: Invalid Quality Certificate') % (mod.name,))
686
687 class module_dependency(osv.osv):
688     _name = "ir.module.module.dependency"
689     _description = "Module dependency"
690
691     def _state(self, cr, uid, ids, name, args, context=None):
692         result = {}
693         mod_obj = self.pool.get('ir.module.module')
694         for md in self.browse(cr, uid, ids):
695             ids = mod_obj.search(cr, uid, [('name', '=', md.name)])
696             if ids:
697                 result[md.id] = mod_obj.read(cr, uid, [ids[0]], ['state'])[0]['state']
698             else:
699                 result[md.id] = 'unknown'
700         return result
701
702     _columns = {
703         # The dependency name
704         'name': fields.char('Name',  size=128, select=True),
705
706         # The module that depends on it
707         'module_id': fields.many2one('ir.module.module', 'Module', select=True, ondelete='cascade'),
708
709         'state': fields.function(_state, type='selection', selection=[
710             ('uninstallable','Uninstallable'),
711             ('uninstalled','Not Installed'),
712             ('installed','Installed'),
713             ('to upgrade','To be upgraded'),
714             ('to remove','To be removed'),
715             ('to install','To be installed'),
716             ('unknown', 'Unknown'),
717             ], string='State', readonly=True, select=True),
718     }
719
720 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: