Launchpad automatic translations update.
[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         'complexity': fields.selection([('easy','Easy'), ('normal','Normal'), ('expert','Expert')],
201             string='Complexity', readonly=True,
202             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.'),
203     }
204
205     _defaults = {
206         'state': 'uninstalled',
207         'demo': False,
208         'license': 'AGPL-3',
209         'web': False,
210     }
211     _order = 'name'
212
213     def _name_uniq_msg(self, cr, uid, ids, context=None):
214         return _('The name of the module must be unique !')
215     def _certificate_uniq_msg(self, cr, uid, ids, context=None):
216         return _('The certificate ID of the module must be unique !')
217
218     _sql_constraints = [
219         ('name_uniq', 'UNIQUE (name)',_name_uniq_msg ),
220         ('certificate_uniq', 'UNIQUE (certificate)',_certificate_uniq_msg )
221     ]
222
223     def unlink(self, cr, uid, ids, context=None):
224         if not ids:
225             return True
226         if isinstance(ids, (int, long)):
227             ids = [ids]
228         mod_names = []
229         for mod in self.read(cr, uid, ids, ['state','name'], context):
230             if mod['state'] in ('installed', 'to upgrade', 'to remove', 'to install'):
231                 raise orm.except_orm(_('Error'),
232                         _('You try to remove a module that is installed or will be installed'))
233             mod_names.append(mod['name'])
234         #Removing the entry from ir_model_data
235         ids_meta = self.pool.get('ir.model.data').search(cr, uid, [('name', '=', 'module_meta_information'), ('module', 'in', mod_names)])
236
237         if ids_meta:
238             self.pool.get('ir.model.data').unlink(cr, uid, ids_meta, context)
239
240         return super(module, self).unlink(cr, uid, ids, context=context)
241
242     @staticmethod
243     def _check_external_dependencies(terp):
244         depends = terp.get('external_dependencies')
245         if not depends:
246             return
247         for pydep in depends.get('python', []):
248             parts = pydep.split('.')
249             parts.reverse()
250             path = None
251             while parts:
252                 part = parts.pop()
253                 try:
254                     f, path, descr = imp.find_module(part, path and [path] or None)
255                 except ImportError:
256                     raise ImportError('No module named %s' % (pydep,))
257
258         for binary in depends.get('bin', []):
259             if tools.find_in_path(binary) is None:
260                 raise Exception('Unable to find %r in path' % (binary,))
261
262     @classmethod
263     def check_external_dependencies(cls, module_name, newstate='to install'):
264         terp = cls.get_module_info(module_name)
265         try:
266             cls._check_external_dependencies(terp)
267         except Exception, e:
268             if newstate == 'to install':
269                 msg = _('Unable to install module "%s" because an external dependency is not met: %s')
270             elif newstate == 'to upgrade':
271                 msg = _('Unable to upgrade module "%s" because an external dependency is not met: %s')
272             else:
273                 msg = _('Unable to process module "%s" because an external dependency is not met: %s')
274             raise orm.except_orm(_('Error'), msg % (module_name, e.args[0]))
275
276     def state_update(self, cr, uid, ids, newstate, states_to_update, context=None, level=100):
277         if level<1:
278             raise orm.except_orm(_('Error'), _('Recursion error in modules dependencies !'))
279         demo = False
280         for module in self.browse(cr, uid, ids):
281             mdemo = False
282             for dep in module.dependencies_id:
283                 if dep.state == 'unknown':
284                     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,))
285                 ids2 = self.search(cr, uid, [('name','=',dep.name)])
286                 if dep.state != newstate:
287                     mdemo = self.state_update(cr, uid, ids2, newstate, states_to_update, context, level-1,) or mdemo
288                 else:
289                     od = self.browse(cr, uid, ids2)[0]
290                     mdemo = od.demo or mdemo
291
292             self.check_external_dependencies(module.name, newstate)
293             if not module.dependencies_id:
294                 mdemo = module.demo
295             if module.state in states_to_update:
296                 self.write(cr, uid, [module.id], {'state': newstate, 'demo':mdemo})
297             demo = demo or mdemo
298         return demo
299
300     def button_install(self, cr, uid, ids, context=None):
301         self.state_update(cr, uid, ids, 'to install', ['uninstalled'], context)
302         return dict(ACTION_DICT, name=_('Install'))
303         
304
305     def button_install_cancel(self, cr, uid, ids, context=None):
306         self.write(cr, uid, ids, {'state': 'uninstalled', 'demo':False})
307         return True
308
309     def button_uninstall(self, cr, uid, ids, context=None):
310         for module in self.browse(cr, uid, ids):
311             cr.execute('''select m.state,m.name
312                 from
313                     ir_module_module_dependency d
314                 join
315                     ir_module_module m on (d.module_id=m.id)
316                 where
317                     d.name=%s and
318                     m.state not in ('uninstalled','uninstallable','to remove')''', (module.name,))
319             res = cr.fetchall()
320             if res:
321                 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)))
322         self.write(cr, uid, ids, {'state': 'to remove'})
323         return dict(ACTION_DICT, name=_('Uninstall'))
324
325     def button_uninstall_cancel(self, cr, uid, ids, context=None):
326         self.write(cr, uid, ids, {'state': 'installed'})
327         return True
328
329     def button_upgrade(self, cr, uid, ids, context=None):
330         depobj = self.pool.get('ir.module.module.dependency')
331         todo = self.browse(cr, uid, ids, context=context)
332         self.update_list(cr, uid)
333
334         i = 0
335         while i<len(todo):
336             mod = todo[i]
337             i += 1
338             if mod.state not in ('installed','to upgrade'):
339                 raise orm.except_orm(_('Error'),
340                         _("Can not upgrade module '%s'. It is not installed.") % (mod.name,))
341             self.check_external_dependencies(mod.name, 'to upgrade')
342             iids = depobj.search(cr, uid, [('name', '=', mod.name)], context=context)
343             for dep in depobj.browse(cr, uid, iids, context=context):
344                 if dep.module_id.state=='installed' and dep.module_id not in todo:
345                     todo.append(dep.module_id)
346
347         ids = map(lambda x: x.id, todo)
348         self.write(cr, uid, ids, {'state':'to upgrade'}, context=context)
349
350         to_install = []
351         for mod in todo:
352             for dep in mod.dependencies_id:
353                 if dep.state == 'unknown':
354                     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,))
355                 if dep.state == 'uninstalled':
356                     ids2 = self.search(cr, uid, [('name','=',dep.name)])
357                     to_install.extend(ids2)
358
359         self.button_install(cr, uid, to_install, context=context)
360         return dict(ACTION_DICT, name=_('Upgrade'))
361
362     def button_upgrade_cancel(self, cr, uid, ids, context=None):
363         self.write(cr, uid, ids, {'state': 'installed'})
364         return True
365
366     def button_update_translations(self, cr, uid, ids, context=None):
367         self.update_translations(cr, uid, ids)
368         return True
369
370     @staticmethod
371     def get_values_from_terp(terp):
372         return {
373             'description': terp.get('description', ''),
374             'shortdesc': terp.get('name', ''),
375             'author': terp.get('author', 'Unknown'),
376             'maintainer': terp.get('maintainer', False),
377             'contributors': ', '.join(terp.get('contributors', [])) or False,
378             'website': terp.get('website', ''),
379             'license': terp.get('license', 'AGPL-3'),
380             'certificate': terp.get('certificate') or False,
381             'web': terp.get('web') or False,
382             'complexity': terp.get('complexity', ''),
383         }
384
385     # update the list of available packages
386     def update_list(self, cr, uid, context={}):
387         res = [0, 0] # [update, add]
388
389         known_mods = self.browse(cr, uid, self.search(cr, uid, []))
390         known_mods_names = dict([(m.name, m) for m in known_mods])
391
392         # iterate through detected modules and update/create them in db
393         for mod_name in addons.get_modules():
394             mod = known_mods_names.get(mod_name)
395             terp = self.get_module_info(mod_name)
396             values = self.get_values_from_terp(terp)
397
398             if mod:
399                 updated_values = {}
400                 for key in values:
401                     old = getattr(mod, key)
402                     updated = isinstance(values[key], basestring) and tools.ustr(values[key]) or values[key] 
403                     if not old == updated:
404                         updated_values[key] = values[key]
405                 if terp.get('installable', True) and mod.state == 'uninstallable':
406                     updated_values['state'] = 'uninstalled'
407                 if parse_version(terp.get('version', '')) > parse_version(mod.latest_version or ''):
408                     res[0] += 1
409                 if updated_values:
410                     self.write(cr, uid, mod.id, updated_values)
411             else:
412                 mod_path = addons.get_module_path(mod_name)
413                 if not mod_path:
414                     continue
415                 if not terp or not terp.get('installable', True):
416                     continue
417                 id = self.create(cr, uid, dict(name=mod_name, state='uninstalled', **values))
418                 mod = self.browse(cr, uid, id)
419                 res[1] += 1
420
421             self._update_dependencies(cr, uid, mod, terp.get('depends', []))
422             self._update_category(cr, uid, mod, terp.get('category', 'Uncategorized'))
423
424         return res
425
426     def download(self, cr, uid, ids, download=True, context=None):
427         res = []
428         for mod in self.browse(cr, uid, ids, context=context):
429             if not mod.url:
430                 continue
431             match = re.search('-([a-zA-Z0-9\._-]+)(\.zip)', mod.url, re.I)
432             version = '0'
433             if match:
434                 version = match.group(1)
435             if parse_version(mod.installed_version or '0') >= parse_version(version):
436                 continue
437             res.append(mod.url)
438             if not download:
439                 continue
440             zipfile = urllib.urlopen(mod.url).read()
441             fname = addons.get_module_path(str(mod.name)+'.zip', downloaded=True)
442             try:
443                 fp = file(fname, 'wb')
444                 fp.write(zipfile)
445                 fp.close()
446             except Exception:
447                 self.__logger.exception('Error when trying to create module '
448                                         'file %s', fname)
449                 raise orm.except_orm(_('Error'), _('Can not create the module file:\n %s') % (fname,))
450             terp = self.get_module_info(mod.name)
451             self.write(cr, uid, mod.id, self.get_values_from_terp(terp))
452             cr.execute('DELETE FROM ir_module_module_dependency ' \
453                     'WHERE module_id = %s', (mod.id,))
454             self._update_dependencies(cr, uid, mod, terp.get('depends',
455                 []))
456             self._update_category(cr, uid, mod, terp.get('category',
457                 'Uncategorized'))
458             # Import module
459             zimp = zipimport.zipimporter(fname)
460             zimp.load_module(mod.name)
461         return res
462
463     def _update_dependencies(self, cr, uid, mod_browse, depends=None):
464         if depends is None:
465             depends = []
466         existing = set(x.name for x in mod_browse.dependencies_id)
467         needed = set(depends)
468         for dep in (needed - existing):
469             cr.execute('INSERT INTO ir_module_module_dependency (module_id, name) values (%s, %s)', (mod_browse.id, dep))
470         for dep in (existing - needed):
471             cr.execute('DELETE FROM ir_module_module_dependency WHERE module_id = %s and name = %s', (mod_browse.id, dep))
472
473     def _update_category(self, cr, uid, mod_browse, category='Uncategorized'):
474         current_category = mod_browse.category_id
475         current_category_path = []
476         while current_category:
477             current_category_path.insert(0, current_category.name)
478             current_category = current_category.parent_id
479
480         categs = category.split('/')
481         if categs != current_category_path:
482             p_id = None
483             while categs:
484                 if p_id is not None:
485                     cr.execute('SELECT id FROM ir_module_category WHERE name=%s AND parent_id=%s', (categs[0], p_id))
486                 else:
487                     cr.execute('SELECT id FROM ir_module_category WHERE name=%s AND parent_id is NULL', (categs[0],))
488                 c_id = cr.fetchone()
489                 if not c_id:
490                     cr.execute('INSERT INTO ir_module_category (name, parent_id) VALUES (%s, %s) RETURNING id', (categs[0], p_id))
491                     c_id = cr.fetchone()[0]
492                 else:
493                     c_id = c_id[0]
494                 p_id = c_id
495                 categs = categs[1:]
496             self.write(cr, uid, [mod_browse.id], {'category_id': p_id})
497
498     def update_translations(self, cr, uid, ids, filter_lang=None, context=None):
499         if context is None:
500             context = {}
501         logger = logging.getLogger('i18n')
502         if not filter_lang:
503             pool = pooler.get_pool(cr.dbname)
504             lang_obj = pool.get('res.lang')
505             lang_ids = lang_obj.search(cr, uid, [('translatable', '=', True)])
506             filter_lang = [lang.code for lang in lang_obj.browse(cr, uid, lang_ids)]
507         elif not isinstance(filter_lang, (list, tuple)):
508             filter_lang = [filter_lang]
509
510         for mod in self.browse(cr, uid, ids):
511             if mod.state != 'installed':
512                 continue
513             modpath = addons.get_module_path(mod.name)
514             if not modpath:
515                 # unable to find the module. we skip
516                 continue
517             for lang in filter_lang:
518                 iso_lang = tools.get_iso_codes(lang)
519                 f = addons.get_module_resource(mod.name, 'i18n', iso_lang + '.po')
520                 context2 = context and context.copy() or {}
521                 if f and '_' in iso_lang:
522                     iso_lang2 = iso_lang.split('_')[0]
523                     f2 = addons.get_module_resource(mod.name, 'i18n', iso_lang2 + '.po')
524                     if f2:
525                         logger.info('module %s: loading base translation file %s for language %s', mod.name, iso_lang2, lang)
526                         tools.trans_load(cr, f2, lang, verbose=False, context=context)
527                         context2['overwrite'] = True
528                 # Implementation notice: we must first search for the full name of
529                 # the language derivative, like "en_UK", and then the generic,
530                 # like "en".
531                 if (not f) and '_' in iso_lang:
532                     iso_lang = iso_lang.split('_')[0]
533                     f = addons.get_module_resource(mod.name, 'i18n', iso_lang + '.po')
534                 if f:
535                     logger.info('module %s: loading translation file (%s) for language %s', mod.name, iso_lang, lang)
536                     tools.trans_load(cr, f, lang, verbose=False, context=context2)
537                 elif iso_lang != 'en':
538                     logger.warning('module %s: no translation for language %s', mod.name, iso_lang)
539         tools.trans_update_res_ids(cr)
540
541     def check(self, cr, uid, ids, context=None):
542         logger = logging.getLogger('init')
543         for mod in self.browse(cr, uid, ids, context=context):
544             if not mod.description:
545                 logger.warn('module %s: description is empty !', mod.name)
546
547             if not mod.certificate or not mod.certificate.isdigit():
548                 logger.info('module %s: no quality certificate', mod.name)
549             else:
550                 val = long(mod.certificate[2:]) % 97 == 29
551                 if not val:
552                     logger.critical('module %s: invalid quality certificate: %s', mod.name, mod.certificate)
553                     raise osv.except_osv(_('Error'), _('Module %s: Invalid Quality Certificate') % (mod.name,))
554
555     def list_web(self, cr, uid, context=None):
556         """ list_web(cr, uid, context) -> [(module_name, module_version)]
557         Lists all the currently installed modules with a web component.
558
559         Returns a list of a tuple of addon names and addon versions.
560         """
561         return [
562             (module['name'], module['installed_version'])
563             for module in self.browse(cr, uid,
564                 self.search(cr, uid,
565                     [('web', '=', True),
566                      ('state', 'in', ['installed','to upgrade','to remove'])],
567                     context=context),
568                 context=context)]
569     def _web_dependencies(self, cr, uid, module, context=None):
570         for dependency in module.dependencies_id:
571             (parent,) = self.browse(cr, uid, self.search(cr, uid,
572                 [('name', '=', dependency.name)], context=context),
573                                  context=context)
574             if parent.web:
575                 yield parent.name
576             else:
577                 self._web_dependencies(
578                     cr, uid, parent, context=context)
579
580     def _translations_subdir(self, module):
581         """ Returns the path to the subdirectory holding translations for the
582         module files, or None if it can't find one
583
584         :param module: a module object
585         :type module: browse(ir.module.module)
586         """
587         subdir = addons.get_module_resource(module.name, 'po')
588         if subdir: return subdir
589         # old naming convention
590         subdir = addons.get_module_resource(module.name, 'i18n')
591         if subdir: return subdir
592         return None
593
594     def _add_translations(self, module, web_data):
595         """ Adds translation data to a zipped web module
596
597         :param module: a module descriptor
598         :type module: browse(ir.module.module)
599         :param web_data: zipped data of a web module
600         :type web_data: bytes
601         """
602         # cStringIO.StringIO is either read or write, not r/w
603         web_zip = StringIO.StringIO(web_data)
604         web_archive = zipfile.ZipFile(web_zip, 'a')
605
606         # get the contents of the i18n or po folder and move them to the
607         # po/messages subdirectory of the web module.
608         # The POT file will be incorrectly named, but that should not
609         # matter since the web client is not going to use it, only the PO
610         # files.
611         translations_file = cStringIO.StringIO(
612             addons.zip_directory(self._translations_subdir(module), False))
613         translations_archive = zipfile.ZipFile(translations_file)
614
615         for path in translations_archive.namelist():
616             web_path = os.path.join(
617                 'web', 'po', 'messages', os.path.basename(path))
618             web_archive.writestr(
619                 web_path,
620                 translations_archive.read(path))
621
622         translations_archive.close()
623         translations_file.close()
624
625         web_archive.close()
626         try:
627             return web_zip.getvalue()
628         finally:
629             web_zip.close()
630
631     def get_web(self, cr, uid, names, context=None):
632         """ get_web(cr, uid, [module_name], context) -> [{name, depends, content}]
633
634         Returns the web content of all the named addons.
635
636         The toplevel directory of the zipped content is called 'web',
637         its final naming has to be managed by the client
638         """
639         modules = self.browse(cr, uid,
640             self.search(cr, uid, [('name', 'in', names)], context=context),
641                               context=context)
642         if not modules: return []
643         self.__logger.info('Sending web content of modules %s '
644                            'to web client', names)
645
646         modules_data = []
647         for module in modules:
648             web_data = addons.zip_directory(
649                 addons.get_module_resource(module.name, 'web'), False)
650             if self._translations_subdir(module):
651                 web_data = self._add_translations(module, web_data)
652             modules_data.append({
653                 'name': module.name,
654                 'version': module.installed_version,
655                 'depends': list(self._web_dependencies(
656                     cr, uid, module, context=context)),
657                 'content': base64.encodestring(web_data)
658             })
659         return modules_data
660
661 module()
662
663 class module_dependency(osv.osv):
664     _name = "ir.module.module.dependency"
665     _description = "Module dependency"
666
667     def _state(self, cr, uid, ids, name, args, context=None):
668         result = {}
669         mod_obj = self.pool.get('ir.module.module')
670         for md in self.browse(cr, uid, ids):
671             ids = mod_obj.search(cr, uid, [('name', '=', md.name)])
672             if ids:
673                 result[md.id] = mod_obj.read(cr, uid, [ids[0]], ['state'])[0]['state']
674             else:
675                 result[md.id] = 'unknown'
676         return result
677
678     _columns = {
679         'name': fields.char('Name',  size=128, select=True),
680         'module_id': fields.many2one('ir.module.module', 'Module', select=True, ondelete='cascade'),
681         'state': fields.function(_state, method=True, type='selection', selection=[
682             ('uninstallable','Uninstallable'),
683             ('uninstalled','Not Installed'),
684             ('installed','Installed'),
685             ('to upgrade','To be upgraded'),
686             ('to remove','To be removed'),
687             ('to install','To be installed'),
688             ('unknown', 'Unknown'),
689             ], string='State', readonly=True, select=True),
690     }
691 module_dependency()