diff --git a/.bzrignore b/.bzrignore index 1e0564801ab..e7618c7b0ce 100644 --- a/.bzrignore +++ b/.bzrignore @@ -1,14 +1,21 @@ -.* -*.egg-info -*.orig -*.vim +.*.swp +.bzrignore +.idea +.project +.pydevproject +.ropeproject +.settings +.DS_Store +openerp/addons/* +openerp/filestore* +.Python +*.pyc +*.pyo +bin/* build/ -RE:^bin/ -RE:^dist/ -RE:^include/ - -RE:^share/ -RE:^man/ -RE:^lib/ - -RE:^addons/\w+/doc/_build/ +include/ +lib/ +share/ +doc/_build/* +win32/*.bat +win32/meta.py diff --git a/openerp/addons/base/__openerp__.py b/openerp/addons/base/__openerp__.py index d400b28a729..5976b23ebb8 100644 --- a/openerp/addons/base/__openerp__.py +++ b/openerp/addons/base/__openerp__.py @@ -3,7 +3,7 @@ # # OpenERP, Open Source Management Solution # Copyright (C) 2004-2009 Tiny SPRL (). -# Copyright (C) 2010, 2012 OpenERP s.a. (). +# Copyright (C) 2010-2012 OpenERP s.a. (). # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -102,5 +102,8 @@ The kernel of OpenERP, needed for all installation. 'installable': True, 'auto_install': True, 'css': ['static/src/css/modules.css'], + 'js': [ + 'static/src/js/apps.js', + ], } # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/addons/base/module/module.py b/openerp/addons/base/module/module.py index a10b38cf586..47a4cfc8174 100644 --- a/openerp/addons/base/module/module.py +++ b/openerp/addons/base/module/module.py @@ -24,12 +24,22 @@ from docutils.transforms import Transform, writer_aux from docutils.writers.html4css1 import Writer import imp import logging +import os import re +import shutil +import tempfile import urllib +import urllib2 +import zipfile import zipimport +try: + from cStringIO import StringIO +except ImportError: + from StringIO import StringIO # NOQA + import openerp -from openerp import modules, pooler, release, tools, addons +from openerp import modules, pooler, tools, addons from openerp.modules.db import create_categories from openerp.tools.parse_version import parse_version from openerp.tools.translate import _ @@ -43,14 +53,29 @@ ACTION_DICT = { 'res_model': 'base.module.upgrade', 'target': 'new', 'type': 'ir.actions.act_window', - 'nodestroy':True, + 'nodestroy': True, } +def backup(path, raise_exception=True): + path = os.path.normpath(path) + if not os.path.exists(path): + if not raise_exception: + return None + raise OSError('path does not exists') + cnt = 1 + while True: + bck = '%s~%d' % (path, cnt) + if not os.path.exists(bck): + shutil.move(path, bck) + return bck + cnt += 1 + + class module_category(osv.osv): _name = "ir.module.category" _description = "Application" - def _module_nbr(self,cr,uid, ids, prop, unknow_none, context): + def _module_nbr(self, cr, uid, ids, prop, unknow_none, context): cr.execute('SELECT category_id, COUNT(*) \ FROM ir_module_module \ WHERE category_id IN %(ids)s \ @@ -58,7 +83,7 @@ class module_category(osv.osv): FROM ir_module_category \ WHERE parent_id IN %(ids)s) \ GROUP BY category_id', {'ids': tuple(ids)} - ) + ) result = dict(cr.fetchall()) for id in ids: cr.execute('select id from ir_module_category where parent_id=%s', (id,)) @@ -71,16 +96,16 @@ class module_category(osv.osv): 'parent_id': fields.many2one('ir.module.category', 'Parent Application', select=True), 'child_ids': fields.one2many('ir.module.category', 'parent_id', 'Child Applications'), 'module_nr': fields.function(_module_nbr, string='Number of Modules', type='integer'), - 'module_ids' : fields.one2many('ir.module.module', 'category_id', 'Modules'), - 'description' : fields.text("Description", translate=True), - 'sequence' : fields.integer('Sequence'), - 'visible' : fields.boolean('Visible'), + 'module_ids': fields.one2many('ir.module.module', 'category_id', 'Modules'), + 'description': fields.text("Description", translate=True), + 'sequence': fields.integer('Sequence'), + 'visible': fields.boolean('Visible'), 'xml_id': fields.function(osv.osv.get_external_id, type='char', size=128, string="External ID"), } _order = 'name' _defaults = { - 'visible' : 1, + 'visible': 1, } class MyFilterMessages(Transform): @@ -119,7 +144,6 @@ class module(osv.osv): info = {} try: info = modules.load_information_from_description_file(name) - info['version'] = release.major_version + '.' + info['version'] except Exception: _logger.debug('Error when trying to fetch informations for ' 'module %s', name, exc_info=True) @@ -134,9 +158,10 @@ class module(osv.osv): return res def _get_latest_version(self, cr, uid, ids, field_name=None, arg=None, context=None): - res = dict.fromkeys(ids, '') + default_version = modules.adapt_version('1.0') + res = dict.fromkeys(ids, default_version) for m in self.browse(cr, uid, ids): - res[m.id] = self.get_module_info(m.name).get('version', '') + res[m.id] = self.get_module_info(m.name).get('version', default_version) return res def _get_views(self, cr, uid, ids, field_name=None, arg=None, context=None): @@ -167,9 +192,11 @@ class module(osv.osv): continue # then, search and group ir.model.data records - imd_models = dict( [(m,[]) for m in dmodels]) - imd_ids = model_data_obj.search(cr,uid,[('module','=', module_rec.name), - ('model','in',tuple(dmodels))]) + imd_models = dict([(m, []) for m in dmodels]) + imd_ids = model_data_obj.search(cr, uid, [ + ('module', '=', module_rec.name), + ('model', 'in', tuple(dmodels)) + ]) for imd_res in model_data_obj.read(cr, uid, imd_ids, ['model', 'res_id'], context=context): imd_models[imd_res['model']].append(imd_res['res_id']) @@ -181,7 +208,7 @@ class module(osv.osv): view_ids = imd_models.get('ir.ui.view', []) for v in view_obj.browse(cr, uid, view_ids, context=context): aa = v.inherit_id and '* INHERIT ' or '' - res_mod_dic['views_by_module'].append(aa + v.name + '('+v.type+')') + res_mod_dic['views_by_module'].append('%s%s (%s)' % (aa, v.name, v.type)) report_ids = imd_models.get('ir.actions.report.xml', []) for rx in report_obj.browse(cr, uid, report_ids, context=context): @@ -191,15 +218,13 @@ class module(osv.osv): for um in menu_obj.browse(cr, uid, menu_ids, context=context): res_mod_dic['menus_by_module'].append(um.complete_name) except KeyError, e: - _logger.warning( - 'Data not found for items of %s', module_rec.name) + _logger.warning('Data not found for items of %s', module_rec.name) except AttributeError, e: - _logger.warning( - 'Data not found for items of %s %s', module_rec.name, str(e)) + _logger.warning('Data not found for items of %s %s', module_rec.name, str(e)) except Exception, e: - _logger.warning('Unknown error while fetching data of %s', - module_rec.name, exc_info=True) - for key, _ in res.iteritems(): + _logger.warning('Unknown error while fetching data of %s', module_rec.name, exc_info=True) + + for key in res.iterkeys(): for k, v in res[key].iteritems(): res[key][k] = "\n".join(sorted(v)) return res @@ -232,36 +257,34 @@ class module(osv.osv): # installed_version refer the latest version (the one on disk) # latest_version refer the installed version (the one in database) # published_version refer the version available on the repository - 'installed_version': fields.function(_get_latest_version, - string='Latest Version', type='char'), + 'installed_version': fields.function(_get_latest_version, string='Latest Version', type='char'), 'latest_version': fields.char('Installed Version', size=64, readonly=True), 'published_version': fields.char('Published Version', size=64, readonly=True), 'url': fields.char('URL', size=128, readonly=True), 'sequence': fields.integer('Sequence'), - 'dependencies_id': fields.one2many('ir.module.module.dependency', - 'module_id', 'Dependencies', readonly=True), + 'dependencies_id': fields.one2many('ir.module.module.dependency', 'module_id', 'Dependencies', readonly=True), 'auto_install': fields.boolean('Automatic Installation', - help='An auto-installable module is automatically installed by the ' - 'system when all its dependencies are satisfied. ' - 'If the module has no dependency, it is always installed.'), + help='An auto-installable module is automatically installed by the ' + 'system when all its dependencies are satisfied. ' + 'If the module has no dependency, it is always installed.'), 'state': fields.selection([ - ('uninstallable','Not Installable'), - ('uninstalled','Not Installed'), - ('installed','Installed'), - ('to upgrade','To be upgraded'), - ('to remove','To be removed'), - ('to install','To be installed') + ('uninstallable', 'Not Installable'), + ('uninstalled', 'Not Installed'), + ('installed', 'Installed'), + ('to upgrade', 'To be upgraded'), + ('to remove', 'To be removed'), + ('to install', 'To be installed') ], string='Status', readonly=True, select=True), 'demo': fields.boolean('Demo Data', readonly=True), 'license': fields.selection([ - ('GPL-2', 'GPL Version 2'), - ('GPL-2 or any later version', 'GPL-2 or later version'), - ('GPL-3', 'GPL Version 3'), - ('GPL-3 or any later version', 'GPL-3 or later version'), - ('AGPL-3', 'Affero GPL-3'), - ('Other OSI approved licence', 'Other OSI Approved Licence'), - ('Other proprietary', 'Other Proprietary') + ('GPL-2', 'GPL Version 2'), + ('GPL-2 or any later version', 'GPL-2 or later version'), + ('GPL-3', 'GPL Version 3'), + ('GPL-3 or any later version', 'GPL-3 or later version'), + ('AGPL-3', 'Affero GPL-3'), + ('Other OSI approved licence', 'Other OSI Approved Licence'), + ('Other proprietary', 'Other Proprietary') ], string='License', readonly=True), 'menus_by_module': fields.function(_get_views, string='Menus', type='text', multi="meta", store=True), 'reports_by_module': fields.function(_get_views, string='Reports', type='text', multi="meta", store=True), @@ -283,7 +306,7 @@ class module(osv.osv): return _('The name of the module must be unique !') _sql_constraints = [ - ('name_uniq', 'UNIQUE (name)',_name_uniq_msg ), + ('name_uniq', 'UNIQUE (name)', _name_uniq_msg), ] def unlink(self, cr, uid, ids, context=None): @@ -292,10 +315,9 @@ class module(osv.osv): if isinstance(ids, (int, long)): ids = [ids] mod_names = [] - for mod in self.read(cr, uid, ids, ['state','name'], context): + for mod in self.read(cr, uid, ids, ['state', 'name'], context): if mod['state'] in ('installed', 'to upgrade', 'to remove', 'to install'): - raise orm.except_orm(_('Error'), - _('You try to remove a module that is installed or will be installed')) + raise orm.except_orm(_('Error'), _('You try to remove a module that is installed or will be installed')) mod_names.append(mod['name']) #Removing the entry from ir_model_data #ids_meta = self.pool.get('ir.model.data').search(cr, uid, [('name', '=', 'module_meta_information'), ('module', 'in', mod_names)]) @@ -340,7 +362,7 @@ class module(osv.osv): raise orm.except_orm(_('Error'), msg % (module_name, e.args[0])) def state_update(self, cr, uid, ids, newstate, states_to_update, context=None, level=100): - if level<1: + if level < 1: raise orm.except_orm(_('Error'), _('Recursion error in modules dependencies !')) demo = False for module in self.browse(cr, uid, ids, context=context): @@ -348,9 +370,9 @@ class module(osv.osv): for dep in module.dependencies_id: if dep.state == 'unknown': 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,)) - ids2 = self.search(cr, uid, [('name','=',dep.name)]) + ids2 = self.search(cr, uid, [('name', '=', dep.name)]) if dep.state != newstate: - mdemo = self.state_update(cr, uid, ids2, newstate, states_to_update, context, level-1,) or mdemo + mdemo = self.state_update(cr, uid, ids2, newstate, states_to_update, context, level - 1) or mdemo else: od = self.browse(cr, uid, ids2)[0] mdemo = od.demo or mdemo @@ -359,7 +381,7 @@ class module(osv.osv): if not module.dependencies_id: mdemo = module.demo if module.state in states_to_update: - self.write(cr, uid, [module.id], {'state': newstate, 'demo':mdemo}) + self.write(cr, uid, [module.id], {'state': newstate, 'demo': mdemo}) demo = demo or mdemo return demo @@ -403,7 +425,7 @@ class module(osv.osv): return self._button_immediate_function(cr, uid, ids, self.button_install, context=context) def button_install_cancel(self, cr, uid, ids, context=None): - self.write(cr, uid, ids, {'state': 'uninstalled', 'demo':False}) + self.write(cr, uid, ids, {'state': 'uninstalled', 'demo': False}) return True def module_uninstall(self, cr, uid, ids, context=None): @@ -421,12 +443,13 @@ class module(osv.osv): return True def downstream_dependencies(self, cr, uid, ids, known_dep_ids=None, - exclude_states=['uninstalled','uninstallable','to remove'], + exclude_states=['uninstalled', 'uninstallable', 'to remove'], context=None): """Return the ids of all modules that directly or indirectly depend on the given module `ids`, and that satisfy the `exclude_states` filter""" - if not ids: return [] + if not ids: + return [] known_dep_ids = set(known_dep_ids or []) cr.execute('''SELECT DISTINCT m.id FROM @@ -437,13 +460,13 @@ class module(osv.osv): d.name IN (SELECT name from ir_module_module where id in %s) AND m.state NOT IN %s AND m.id NOT IN %s ''', - (tuple(ids),tuple(exclude_states), tuple(known_dep_ids or ids))) + (tuple(ids), tuple(exclude_states), tuple(known_dep_ids or ids))) new_dep_ids = set([m[0] for m in cr.fetchall()]) missing_mod_ids = new_dep_ids - known_dep_ids known_dep_ids |= new_dep_ids if missing_mod_ids: known_dep_ids |= set(self.downstream_dependencies(cr, uid, list(missing_mod_ids), - known_dep_ids, exclude_states,context)) + known_dep_ids, exclude_states, context)) return list(known_dep_ids) def _button_immediate_function(self, cr, uid, ids, function, context=None): @@ -460,9 +483,9 @@ class module(osv.osv): menu_obj = self.pool.get('ir.ui.menu') menu_ids = menu_obj.search(cr, uid, [('parent_id', '=', False)], context=context) return { - 'type' : 'ir.actions.client', - 'tag' : 'reload', - 'params' : {'menu_id' : menu_ids and menu_ids[0] or False} + 'type': 'ir.actions.client', + 'tag': 'reload', + 'params': {'menu_id': menu_ids and menu_ids[0] or False} } def button_immediate_uninstall(self, cr, uid, ids, context=None): @@ -496,20 +519,19 @@ class module(osv.osv): self.update_list(cr, uid) i = 0 - while i parse_version(mod.latest_version or ''): + if parse_version(terp.get('version', default_version)) > parse_version(mod.latest_version or default_version): res[0] += 1 if updated_values: self.write(cr, uid, mod.id, updated_values) @@ -587,24 +610,32 @@ class module(osv.osv): self._update_dependencies(cr, uid, mod, terp.get('depends', [])) self._update_category(cr, uid, mod, terp.get('category', 'Uncategorized')) + # Trigger load_addons if new module have been discovered it exists on + # wsgi handlers, so they can react accordingly + if tuple(res) != (0, 0): + for handler in openerp.service.wsgi_server.module_handlers: + if hasattr(handler,'load_addons'): + handler.load_addons() + return res def download(self, cr, uid, ids, download=True, context=None): res = [] + default_version = modules.adapt_version('1.0') for mod in self.browse(cr, uid, ids, context=context): if not mod.url: continue match = re.search('-([a-zA-Z0-9\._-]+)(\.zip)', mod.url, re.I) - version = '0' + version = default_version if match: version = match.group(1) - if parse_version(mod.installed_version or '0') >= parse_version(version): + if parse_version(mod.installed_version) >= parse_version(version): continue res.append(mod.url) if not download: continue zip_content = urllib.urlopen(mod.url).read() - fname = modules.get_module_path(str(mod.name)+'.zip', downloaded=True) + fname = modules.get_module_path(str(mod.name) + '.zip', downloaded=True) try: with open(fname, 'wb') as fp: fp.write(zip_content) @@ -614,17 +645,77 @@ class module(osv.osv): raise orm.except_orm(_('Error'), _('Can not create the module file:\n %s') % (fname,)) terp = self.get_module_info(mod.name) self.write(cr, uid, mod.id, self.get_values_from_terp(terp)) - cr.execute('DELETE FROM ir_module_module_dependency ' \ - 'WHERE module_id = %s', (mod.id,)) - self._update_dependencies(cr, uid, mod, terp.get('depends', - [])) - self._update_category(cr, uid, mod, terp.get('category', - 'Uncategorized')) + cr.execute('DELETE FROM ir_module_module_dependency WHERE module_id = %s', (mod.id,)) + self._update_dependencies(cr, uid, mod, terp.get('depends', [])) + self._update_category(cr, uid, mod, terp.get('category', 'Uncategorized')) # Import module zimp = zipimport.zipimporter(fname) zimp.load_module(mod.name) return res + def install_from_urls(self, cr, uid, urls, context=None): + OPENERP = 'openerp' + tmp = tempfile.mkdtemp() + try: + for module_name, url in urls.items(): + if not url: + continue # nothing to download, local version is already the last one + try: + content = urllib2.urlopen(url).read() + except Exception, e: + _logger.exception('ggr') + raise osv.except_osv('grrr', e) + else: + zipfile.ZipFile(StringIO(content)).extractall(tmp) + assert os.path.isdir(os.path.join(tmp, module_name)) + + for module_name in urls: + if module_name == OPENERP: + continue # special case. handled below + module_path = modules.get_module_path(module_name, downloaded=True, display_warning=False) + bck = backup(module_path, False) + shutil.move(os.path.join(tmp, module_name), module_path) + if bck: + shutil.rmtree(bck) + + if urls.get(OPENERP, None): + # special case. it containt the server and the base module. + # extract path is not the same + + base_path = os.path.dirname(modules.get_module_path('base')) + + # copy all modules in the SERVER/openerp/addons directory to the new "openerp" module (except base itself) + for d in os.listdir(base_path): + if d != 'base' and os.path.isdir(os.path.join(base_path, d)): + destdir = os.path.join(tmp, OPENERP, 'addons', d) # XXX 'openerp' subdirectory ? + shutil.copytree(os.path.join(base_path, d), destdir) + + # then replace the server by the new "base" module + server_dir = openerp.tools.config['root_path'] # XXX or dirname() + bck = backup(server_dir) + shutil.move(os.path.join(tmp, OPENERP), server_dir) + #if bck: + # shutil.rmtree(bck) + + self.update_list(cr, uid, context=context) + + ids = self.search(cr, uid, [('name', 'in', urls.keys())], context=context) + if self.search_count(cr, uid, [('id', 'in', ids), ('state', '=', 'installed')], context=context): + # if any to update + cr.commit() + openerp.service.restart_server() + return { + 'type': 'ir.actions.client', + 'tag': 'home', + 'params': {'wait': True}, + } + return self.button_immediate_install(cr, uid, ids, context=context) + finally: + shutil.rmtree(tmp) + + def install_by_names(self, cr, uid, names, context=None): + raise NotImplementedError('# TODO') + def _update_dependencies(self, cr, uid, mod_browse, depends=None): if depends is None: depends = [] @@ -679,20 +770,20 @@ class module_dependency(osv.osv): _columns = { # The dependency name - 'name': fields.char('Name', size=128, select=True), + 'name': fields.char('Name', size=128, select=True), # The module that depends on it 'module_id': fields.many2one('ir.module.module', 'Module', select=True, ondelete='cascade'), 'state': fields.function(_state, type='selection', selection=[ - ('uninstallable','Uninstallable'), - ('uninstalled','Not Installed'), - ('installed','Installed'), - ('to upgrade','To be upgraded'), - ('to remove','To be removed'), - ('to install','To be installed'), + ('uninstallable', 'Uninstallable'), + ('uninstalled', 'Not Installed'), + ('installed', 'Installed'), + ('to upgrade', 'To be upgraded'), + ('to remove', 'To be removed'), + ('to install', 'To be installed'), ('unknown', 'Unknown'), - ], string='Status', readonly=True, select=True), + ], string='Status', readonly=True, select=True), } # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/addons/base/module/module_view.xml b/openerp/addons/base/module/module_view.xml index 5f33a437f08..3a43a2591ba 100644 --- a/openerp/addons/base/module/module_view.xml +++ b/openerp/addons/base/module/module_view.xml @@ -1,9 +1,7 @@ - - ir.module.category.form ir.module.category @@ -32,7 +30,6 @@ - ir.module.module.list.select ir.module.module @@ -43,7 +40,7 @@ - + @@ -53,37 +50,6 @@ - - - Modules Kanban - ir.module.module - - - - - - - - -
- - -
-

-

-
- -

- - -
-
-
-
-
-
-
- Modules ir.module.module @@ -100,7 +66,6 @@ - ir.module.module.form ir.module.module @@ -176,20 +141,74 @@ + + Modules Kanban + ir.module.module + + + + + + + + +
+ + +
+

+

+
+ +

+ + +
+
+
+
+
+
+
- Install a Module + + Modules ir.module.module form kanban,tree,form - {'search_default_app':1} +

No module found!

You should try others search criteria.

- + + + + + + Apps + apps + + + + + + Updates + apps.updates + {} + + +
diff --git a/openerp/addons/base/module/wizard/base_module_import_view.xml b/openerp/addons/base/module/wizard/base_module_import_view.xml index 192bf6c2a92..d9d6bb6ee78 100644 --- a/openerp/addons/base/module/wizard/base_module_import_view.xml +++ b/openerp/addons/base/module/wizard/base_module_import_view.xml @@ -43,7 +43,7 @@ id="menu_view_base_module_import" parent="menu_management" groups="base.group_no_one" - sequence="1"/> + sequence="6"/> --> diff --git a/openerp/addons/base/module/wizard/base_module_update_view.xml b/openerp/addons/base/module/wizard/base_module_update_view.xml index 75d2b1316a6..cb9b9090294 100644 --- a/openerp/addons/base/module/wizard/base_module_update_view.xml +++ b/openerp/addons/base/module/wizard/base_module_update_view.xml @@ -18,17 +18,17 @@
-
-
- + Module Update @@ -45,7 +45,7 @@ id="menu_view_base_module_update" groups="base.group_no_one" parent="menu_management" - sequence="2" + sequence="40" icon="STOCK_CONVERT"/> diff --git a/openerp/addons/base/module/wizard/base_module_upgrade_view.xml b/openerp/addons/base/module/wizard/base_module_upgrade_view.xml index afd3a2e17b1..1a1225befcb 100644 --- a/openerp/addons/base/module/wizard/base_module_upgrade_view.xml +++ b/openerp/addons/base/module/wizard/base_module_upgrade_view.xml @@ -35,7 +35,7 @@ groups="base.group_no_one" id="menu_view_base_module_upgrade" parent="menu_management" - sequence="3"/> + sequence="50"/> Module Upgrade Install diff --git a/openerp/addons/base/res/res_config.py b/openerp/addons/base/res/res_config.py index 9513f48a27e..e9c813c19c9 100644 --- a/openerp/addons/base/res/res_config.py +++ b/openerp/addons/base/res/res_config.py @@ -466,7 +466,8 @@ class res_config_settings(osv.osv_memory): groups.append((name, ref(field_group), ref(field.implied_group))) elif name.startswith('module_') and isinstance(field, fields.boolean): mod_ids = ir_module.search(cr, uid, [('name', '=', name[7:])]) - modules.append((name, ir_module.browse(cr, uid, mod_ids[0], context))) + record = ir_module.browse(cr, uid, mod_ids[0], context) if mod_ids else None + modules.append((name, record)) else: others.append(name) @@ -490,7 +491,7 @@ class res_config_settings(osv.osv_memory): # modules: which modules are installed/to install for name, module in classified['module']: - res[name] = module.state in ('installed', 'to install', 'to upgrade') + res[name] = module and module.state in ('installed', 'to install', 'to upgrade') # other fields: call all methods that start with 'get_default_' for method in dir(self): @@ -501,9 +502,7 @@ class res_config_settings(osv.osv_memory): def execute(self, cr, uid, ids, context=None): ir_values = self.pool.get('ir.values') - ir_model_data = self.pool.get('ir.model.data') ir_module = self.pool.get('ir.module.module') - res_groups = self.pool.get('res.groups') classified = self._get_classified_fields(cr, uid, context) config = self.browse(cr, uid, ids[0], context) @@ -526,17 +525,26 @@ class res_config_settings(osv.osv_memory): getattr(self, method)(cr, uid, ids, context) # module fields: install/uninstall the selected modules - to_install_ids = [] + to_install_names = [] to_uninstall_ids = [] + lm = len('module_') for name, module in classified['module']: if config[name]: - if module.state == 'uninstalled': to_install_ids.append(module.id) + if not module or module.state == 'uninstalled': + to_install_names.append(name[lm:]) else: - if module.state in ('installed','upgrade'): to_uninstall_ids.append(module.id) + if module and module.state in ('installed', 'to upgrade'): + to_uninstall_ids.append(module.id) - if to_install_ids or to_uninstall_ids: - ir_module.button_uninstall(cr, uid, to_uninstall_ids, context=context) - ir_module.button_immediate_install(cr, uid, to_install_ids, context=context) + if to_uninstall_ids: + ir_module.button_immediate_uninstall(cr, uid, to_uninstall_ids, context=context) + + if to_install_names: + return { + 'type': 'ir.actions.client', + 'tag': 'apps', + 'params': {'modules': to_install_names}, + } config = self.pool.get('res.config').next(cr, uid, [], context=context) or {} if config.get('type') not in ('ir.actions.act_window_close',): diff --git a/openerp/addons/base/static/src/js/apps.js b/openerp/addons/base/static/src/js/apps.js new file mode 100644 index 00000000000..397f028c1fd --- /dev/null +++ b/openerp/addons/base/static/src/js/apps.js @@ -0,0 +1,117 @@ +openerp.base = function(instance) { + + instance.base.apps_remote = null; + instance.base.apps_client = null; + + instance.base.Apps = instance.web.Widget.extend({ + template: 'EmptyComponent', + remote_action_id: 'loempia.action_embed', + failback_action_id: 'base.open_module_tree', + + init: function(parent, action) { + this._super(parent, action); + var options = action.params || {}; + + if (options.apps_user) { + sessionStorage.setItem('apps.login', options.apps_user); + } + if (options.apps_access_token) { + sessionStorage.setItem('apps.access_token', options.apps_access_token); + } + + this.params = options; // NOTE read by embedded client action + }, + + get_client: function() { + // return the client via a deferred, resolved or rejected depending if the remote host is available or not. + var check_client_available = function(client) { + var d = $.Deferred(); + var i = new Image(); + i.onerror = function() { + d.reject(client); + }; + i.onload = function() { + client.session.session_bind(client.origin).then(function() { + // check if client can authenticate + client.authenticate().then( + function() { /* done */ + d.resolve(client); + }, function() { /* fail */ + if (client.login === 'anonymous') { + d.reject(client); + } else { + sessionStorage.removeItem('apps.login'); + sessionStorage.removeItem('apps.access_token'); + client.bind_crendentials(client.dbname, 'anonymous', 'anonymous'); + client.authenticate().then( + function() { /* done */ + d.resolve(client); + }, function() { /* fail */ + d.reject(client); + }); + } + }); + }); + + }; + i.src = _.str.sprintf('%s/web/static/src/img/sep-a.gif', client.origin); + return d.promise(); + }; + if (instance.base.apps_client) { + return check_client_available(instance.base.apps_client); + } else { + var ICP = new instance.web.Model('ir.config_parameter'); + return ICP.call('get_param', ['apps.server', 'https://apps.openerp.com/apps']).then(function(u) { + var link = $(_.str.sprintf('', u))[0]; + var host = _.str.sprintf('%s//%s', link.protocol, link.host); + var dbname = link.pathname; + if (dbname[0] === '/') { + dbname = dbname.substr(1); + } + var login = (sessionStorage ? sessionStorage.getItem('apps.login') : null) || 'anonymous'; + var passwd = (sessionStorage ? sessionStorage.getItem('apps.access_token') : null) || 'anonymous'; + if (_.isNull(instance.base.apps_remote)) { + instance.base.apps_remote = new openerp.init(); + } + var client = new instance.base.apps_remote.web.EmbeddedClient(null, host, dbname, login, passwd); + instance.base.apps_client = client; + return check_client_available(client); + }); + } + }, + + destroy: function() { + if (instance.base.apps_client) { + instance.base.apps_client.destroy(); + } + return this._super(); + }, + + start: function() { + var self = this; + return self.get_client(). + done(function(client) { + client.replace(self.$el). + done(function() { + client.$el.removeClass('openerp'); + client.do_action(self.remote_action_id); + }); + }). + fail(function(client) { + self.do_warn('Apps Server not reachable.', 'Showing local modules.', true); + self.rpc('/web/action/load', {action_id: self.failback_action_id}).done(function(action) { + self.do_action(action); + instance.webclient.menu.open_action(action.id); + }); + }); + }, + }); + + instance.base.AppsUpdates = instance.base.Apps.extend({ + remote_action_id: 'loempia.action_embed_updates' + }); + + instance.web.client_actions.add("apps", "instance.base.Apps"); + instance.web.client_actions.add("apps.updates", "instance.base.AppsUpdates"); + +}; diff --git a/openerp/cli/server.py b/openerp/cli/server.py index d97d4c81571..bc83b735624 100644 --- a/openerp/cli/server.py +++ b/openerp/cli/server.py @@ -207,10 +207,15 @@ def quit_on_signals(): pass config = openerp.tools.config + openerp.service.stop_services() + + if getattr(openerp, 'phoenix', False): + # like the phoenix, reborn from ashes... + openerp.service._reexec() + return + if config['pidfile']: os.unlink(config['pidfile']) - - openerp.service.stop_services() sys.exit(0) def configure_babel_localedata_path(): diff --git a/openerp/modules/__init__.py b/openerp/modules/__init__.py index 56265e353a2..feaf014f568 100644 --- a/openerp/modules/__init__.py +++ b/openerp/modules/__init__.py @@ -3,7 +3,7 @@ # # OpenERP, Open Source Management Solution # Copyright (C) 2004-2009 Tiny SPRL (). -# Copyright (C) 2010-2011 OpenERP s.a. (). +# Copyright (C) 2010-2012 OpenERP s.a. (). # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -32,7 +32,8 @@ from openerp.modules.module import \ load_information_from_description_file, \ get_module_resource, zip_directory, \ get_module_path, initialize_sys_path, \ - load_openerp_module, init_module_models + load_openerp_module, init_module_models, \ + adapt_version from openerp.modules.loading import load_modules diff --git a/openerp/modules/module.py b/openerp/modules/module.py index bb4ff481fb9..98955aff492 100644 --- a/openerp/modules/module.py +++ b/openerp/modules/module.py @@ -3,7 +3,7 @@ # # OpenERP, Open Source Management Solution # Copyright (C) 2004-2009 Tiny SPRL (). -# Copyright (C) 2010-2011 OpenERP s.a. (). +# Copyright (C) 2010-2012 OpenERP s.a. (). # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -329,7 +329,7 @@ def load_information_from_description_file(module): 'license': 'AGPL-3', 'name': False, 'post_load': None, - 'version': '0.0.0', + 'version': '1.0', 'web': False, 'website': '', 'sequence': 100, @@ -349,6 +349,7 @@ def load_information_from_description_file(module): # 'active' has been renamed 'auto_install' info['auto_install'] = info['active'] + info['version'] = adapt_version(info['version']) return info #TODO: refactor the logger in this file to follow the logging guidelines @@ -447,15 +448,22 @@ def get_modules(): def get_modules_with_version(): modules = get_modules() - res = {} + res = dict.fromkeys(modules, adapt_version('1.0')) for module in modules: try: info = load_information_from_description_file(module) - res[module] = "%s.%s" % (release.major_version, info['version']) - except Exception, e: + res[module] = info['version'] + except Exception: continue return res +def adapt_version(version): + serie = release.major_version + if version == serie or not version.startswith(serie + '.'): + version = '%s.%s' % (serie, version) + return version + + def get_test_modules(module, submodule, explode): """ Return a list of submodules containing tests. diff --git a/openerp/release.py b/openerp/release.py index 50d9b8ac760..e66d924906b 100644 --- a/openerp/release.py +++ b/openerp/release.py @@ -50,4 +50,6 @@ author = 'OpenERP S.A.' author_email = 'info@openerp.com' license = 'AGPL-3' +nt_service_name = "openerp-server-" + serie + # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/service/__init__.py b/openerp/service/__init__.py index f8752732cdf..307ee4a6526 100644 --- a/openerp/service/__init__.py +++ b/openerp/service/__init__.py @@ -21,6 +21,10 @@ ############################################################################## import logging +import os +import signal +import subprocess +import sys import threading import time @@ -33,6 +37,7 @@ import wsgi_server import openerp.modules import openerp.netsvc import openerp.osv +from openerp.release import nt_service_name import openerp.tools #.apidoc title: RPC Services @@ -97,27 +102,58 @@ def stop_services(): _logger.info("Initiating shutdown") _logger.info("Hit CTRL-C again or send a second signal to force the shutdown.") - logging.shutdown() # Manually join() all threads before calling sys.exit() to allow a second signal # to trigger _force_quit() in case some non-daemon threads won't exit cleanly. # threading.Thread.join() should not mask signals (at least in python 2.5). me = threading.currentThread() + _logger.debug('current thread: %r', me) for thread in threading.enumerate(): + _logger.debug('process %r (%r)', thread, thread.isDaemon()) if thread != me and not thread.isDaemon() and thread.ident != main_thread_id: while thread.isAlive(): + _logger.debug('join and sleep') # Need a busyloop here as thread.join() masks signals # and would prevent the forced shutdown. thread.join(0.05) time.sleep(0.05) + _logger.debug('--') openerp.modules.registry.RegistryManager.delete_all() + logging.shutdown() def start_services_workers(): import openerp.service.workers openerp.multi_process = True - openerp.service.workers.Multicorn(openerp.service.wsgi_server.application).run() +def _reexec(): + """reexecute openerp-server process with (nearly) the same arguments""" + if openerp.tools.osutil.is_running_as_nt_service(): + subprocess.call('sc stop {0} && sc start {0}'.format(nt_service_name), shell=True) + exe = os.path.basename(sys.executable) + strip_args = ['-d', '-u'] + a = sys.argv[:] + args = [x for i, x in enumerate(a) if x not in strip_args and a[max(i - 1, 0)] not in strip_args] + if not args or args[0] != exe: + args.insert(0, exe) + os.execv(sys.executable, args) + +def restart_server(): + if openerp.multi_process: + raise NotImplementedError("Multicorn is not supported (but gunicorn was)") + pid = openerp.wsgi.core.arbiter_pid + os.kill(pid, signal.SIGHUP) + else: + if os.name == 'nt': + def reborn(): + stop_services() + _reexec() + + # run in a thread to let the current thread return response to the caller. + threading.Thread(target=reborn).start() + else: + openerp.phoenix = True + os.kill(os.getpid(), signal.SIGINT) # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/service/web_services.py b/openerp/service/web_services.py index 5b88e0fd880..bd5b21bb5d5 100644 --- a/openerp/service/web_services.py +++ b/openerp/service/web_services.py @@ -54,7 +54,12 @@ from openerp import SUPERUSER_ID _logger = logging.getLogger(__name__) -RPC_VERSION_1 = {'server_version': '6.1', 'protocol_version': 1} +RPC_VERSION_1 = { + 'server_version': release.version, + 'server_version_info': release.version_info, + 'server_serie': release.serie, + 'protocol_version': 1, +} # This should be moved to openerp.modules.db, along side initialize(). def _initialize_db(serv, id, db_name, demo, lang, user_password): diff --git a/openerp/service/wsgi_server.py b/openerp/service/wsgi_server.py index f6d7a0582f7..67436a1c0bf 100644 --- a/openerp/service/wsgi_server.py +++ b/openerp/service/wsgi_server.py @@ -442,5 +442,6 @@ def stop_service(): """ if httpd: httpd.shutdown() + openerp.netsvc.close_socket(httpd.socket) # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/tools/osutil.py b/openerp/tools/osutil.py index 67c586ad008..b5cff6b2646 100644 --- a/openerp/tools/osutil.py +++ b/openerp/tools/osutil.py @@ -26,6 +26,12 @@ Some functions related to the os and os.path module import os from os.path import join as opj +if os.name == 'nt': + import ctypes + import win32service as ws + import win32serviceutil as wsu + + def listdir(dir, recursive=False): """Allow to recursively get the file listing""" dir = os.path.normpath(dir) @@ -56,6 +62,59 @@ def walksymlinks(top, topdown=True, onerror=None): yield dirpath, dirnames, filenames +if os.name != 'nt': + getppid = os.getppid + is_running_as_nt_service = lambda: False +else: + # based on http://mail.python.org/pipermail/python-win32/2007-June/006174.html + _TH32CS_SNAPPROCESS = 0x00000002 + class _PROCESSENTRY32(ctypes.Structure): + _fields_ = [("dwSize", ctypes.c_ulong), + ("cntUsage", ctypes.c_ulong), + ("th32ProcessID", ctypes.c_ulong), + ("th32DefaultHeapID", ctypes.c_ulong), + ("th32ModuleID", ctypes.c_ulong), + ("cntThreads", ctypes.c_ulong), + ("th32ParentProcessID", ctypes.c_ulong), + ("pcPriClassBase", ctypes.c_ulong), + ("dwFlags", ctypes.c_ulong), + ("szExeFile", ctypes.c_char * 260)] + + def getppid(): + CreateToolhelp32Snapshot = ctypes.windll.kernel32.CreateToolhelp32Snapshot + Process32First = ctypes.windll.kernel32.Process32First + Process32Next = ctypes.windll.kernel32.Process32Next + CloseHandle = ctypes.windll.kernel32.CloseHandle + hProcessSnap = CreateToolhelp32Snapshot(_TH32CS_SNAPPROCESS, 0) + current_pid = os.getpid() + try: + pe32 = _PROCESSENTRY32() + pe32.dwSize = ctypes.sizeof(_PROCESSENTRY32) + if not Process32First(hProcessSnap, ctypes.byref(pe32)): + raise OSError('Failed getting first process.') + while True: + if pe32.th32ProcessID == current_pid: + return pe32.th32ParentProcessID + if not Process32Next(hProcessSnap, ctypes.byref(pe32)): + return None + finally: + CloseHandle(hProcessSnap) + + from contextlib import contextmanager + from openerp.release import nt_service_name + + def is_running_as_nt_service(): + @contextmanager + def close_srv(srv): + try: + yield srv + finally: + ws.CloseServiceHandle(srv) + + with close_srv(ws.OpenSCManager(None, None, ws.SC_MANAGER_ALL_ACCESS)) as hscm: + with close_srv(wsu.SmartOpenService(hscm, nt_service_name, ws.SERVICE_ALL_ACCESS)) as hs: + info = ws.QueryServiceStatusEx(hs) + return info['ProcessId'] == getppid() if __name__ == '__main__': from pprint import pprint as pp diff --git a/win32/OpenERPServerService.py b/win32/OpenERPServerService.py index bf7c83e74a4..8b3c117c03d 100644 --- a/win32/OpenERPServerService.py +++ b/win32/OpenERPServerService.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- ############################################################################## -# +# # OpenERP, Open Source Management Solution # Copyright (C) 2004-2009 Tiny SPRL (). # @@ -15,14 +15,12 @@ # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . +# along with this program. If not, see . # ############################################################################## -# Win32 python extensions modules import win32serviceutil import win32service -import win32event import win32api import win32process import servicemanager @@ -30,35 +28,32 @@ import servicemanager import sys import subprocess import os -import thread + +try: + import meta +except ImportError: + if hasattr(sys, 'frozen'): + raise + from setup import generate_files + generate_files() + import meta # noqa class OpenERPServerService(win32serviceutil.ServiceFramework): # required info - _svc_name_ = "openerp-server-6.1" - _svc_display_name_ = "OpenERP Server 6.1" - # optionnal info - _svc_description_ = "OpenERP Server 6.1 service" + _svc_name_ = meta.nt_service_name + _svc_display_name_ = "%s %s" % (meta.description, meta.serie) def __init__(self, args): win32serviceutil.ServiceFramework.__init__(self, args) - # Create an event which we will use to wait on. - # The "service stop" request will set this event. - self.hWaitStop = win32event.CreateEvent(None, 0, 0, None) # a reference to the server's process self.terpprocess = None - # info if the service terminates correctly or if the server crashed - self.stopping = False - def SvcStop(self): # Before we do anything, tell the SCM we are starting the stop process. self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING) - # stop the running TERP Server: say it's a normal exit + # stop the running OpenERP Server: say it's a normal exit win32api.TerminateProcess(int(self.terpprocess._handle), 0) servicemanager.LogInfoMsg("OpenERP Server stopped correctly") - # And set my event. - win32event.SetEvent(self.hWaitStop) - def StartTERP(self): # The server finds now its configuration automatically on Windows @@ -69,29 +64,19 @@ class OpenERPServerService(win32serviceutil.ServiceFramework): server_path = os.path.join(server_dir, 'server', 'openerp-server.exe') self.terpprocess = subprocess.Popen([server_path], cwd=server_dir, creationflags=win32process.CREATE_NO_WINDOW) - - def StartControl(self,ws): - # this listens to the Service Manager's events - win32event.WaitForSingleObject(ws, win32event.INFINITE) - self.stopping = True - def SvcDoRun(self): - # Start OpenERP Server itself self.StartTERP() - # start the loop waiting for the Service Manager's stop signal - thread.start_new_thread(self.StartControl, (self.hWaitStop,)) - # Log a info message that the server is running servicemanager.LogInfoMsg("OpenERP Server up and running") - # verification if the server is really running, else quit with an error - self.terpprocess.wait() - if not self.stopping: - sys.exit("OpenERP Server check: server not running, check the logfile for more info") + # exit with same exit code as OpenERP process + sys.exit(self.terpprocess.wait()) +def option_handler(opts): + # configure the service to auto restart on failures... + subprocess.call(['sc', 'failure', meta.nt_service_name, 'reset=', '0', 'actions=', 'restart/0/restart/0/restart/0']) -if __name__=='__main__': +if __name__ == '__main__': # Do with the service whatever option is passed in the command line - win32serviceutil.HandleCommandLine(OpenERPServerService) + win32serviceutil.HandleCommandLine(OpenERPServerService, customOptionHandler=option_handler) # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: - diff --git a/win32/setup.py b/win32/setup.py index 2dfb8ef3493..0feedce01e0 100644 --- a/win32/setup.py +++ b/win32/setup.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- ############################################################################## -# +# # OpenERP, Open Source Management Solution # Copyright (C) 2004-2010 Tiny SPRL (). # @@ -15,7 +15,7 @@ # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . +# along with this program. If not, see . # ############################################################################## @@ -30,15 +30,46 @@ def datas(): r.append(("Microsoft.VC90.CRT", glob.glob('C:\Microsoft.VC90.CRT\*.*'))) return r -setup(service=["OpenERPServerService"], - options={"py2exe":{"excludes":["Tkconstants","Tkinter","tcl", - "_imagingtk","PIL._imagingtk", - "ImageTk", "PIL.ImageTk", - "FixTk"], - "skip_archive": 1, - "optimize": 2,}}, +meta = {} +execfile(os.path.join(os.path.dirname(__file__), '..', 'openerp', 'release.py'), meta) + +def generate_files(): + actions = { + 'start': ['stop', 'start'], + 'stop': ['stop'], + } + + files = [] + for action, steps in actions.items(): + fname = action + '.bat' + files.append(fname) + with open(fname, 'w') as fp: + fp.write('@PATH=%WINDIR%\system32;%WINDIR%;%WINDIR%\System32\Wbem;.\n') + for step in steps: + fp.write('@net %s %s\n' % (step, meta['nt_service_name'])) + + files.append('meta.py') + with open('meta.py', 'w') as fp: + for m in 'description serie nt_service_name'.split(): + fp.write("%s = %r\n" % (m, meta[m],)) + + return files + +excludes = "Tkconstants Tkinter tcl _imagingtk PIL._imagingtk ImageTk PIL.ImageTk FixTk".split() + +setup(service = ["OpenERPServerService"], + version = meta['version'], + license = meta['license'], + url = meta['url'], + author = meta['author'], + author_email = meta['author_email'], + data_files = generate_files(), + options = {"py2exe": { + "excludes": excludes, + "skip_archive": 1, + "optimize": 2, + }}, data_files=datas(), ) # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: - diff --git a/win32/start.bat b/win32/start.bat deleted file mode 100644 index ff3c49b1397..00000000000 --- a/win32/start.bat +++ /dev/null @@ -1,7 +0,0 @@ -@PATH=%WINDIR%\system32;%WINDIR%;%WINDIR%\System32\Wbem;. - -@net stop openerp-server-6.1 - -@net start openerp-server-6.1 - -cls diff --git a/win32/stop.bat b/win32/stop.bat deleted file mode 100644 index 76a35b7e91f..00000000000 --- a/win32/stop.bat +++ /dev/null @@ -1,5 +0,0 @@ -@PATH=%WINDIR%\system32;%WINDIR%;%WINDIR%\System32\Wbem;. - -@net stop openerp-server-6.1 - -cls