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