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