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