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