#This file is part of Tryton. The COPYRIGHT file at the top level of #this repository contains the full copyright notices and license terms. "Module" import os from trytond.model import ModelView, ModelSQL, fields import trytond.tools as tools from trytond.modules import MODULES_PATH, create_graph, get_module_list from trytond.wizard import Wizard from trytond.backend import Database from trytond.pool import Pool class Module(ModelSQL, ModelView): "Module" _name = "ir.module.module" _description = __doc__ name = fields.Char("Name", readonly=True, required=True) shortdesc = fields.Char('Short description', readonly=True, translate=True) description = fields.Text("Description", readonly=True, translate=True) author = fields.Char("Author", readonly=True) website = fields.Char("Website", readonly=True) version = fields.Function(fields.Char('Version'), 'get_version') dependencies = fields.One2Many('ir.module.module.dependency', 'module', 'Dependencies', readonly=True) state = fields.Selection([ ('uninstalled', 'Not Installed'), ('installed', 'Installed'), ('to upgrade', 'To be upgraded'), ('to remove', 'To be removed'), ('to install', 'To be installed'), ], string='State', readonly=True) def __init__(self): super(Module, self).__init__() self._sql_constraints = [ ('name_uniq', 'unique (name)', 'The name of the module must be unique!'), ] self._order.insert(0, ('name', 'ASC')) self._rpc.update({ 'button_install': True, 'button_install_cancel': True, 'button_uninstall': True, 'button_uninstall_cancel': True, 'button_upgrade': True, 'button_upgrade_cancel': True, 'on_write': False, }) self._error_messages.update({ 'delete_state': 'You can not remove a module that is installed ' \ 'or will be installed', 'missing_dep': 'Missing dependencies %s for module "%s"', 'uninstall_dep': 'The modules you are trying to uninstall ' \ 'depends on installed modules:', }) def default_state(self, cursor, user, context=None): return 'uninstalled' @staticmethod def get_module_info(name): "Return the content of the __tryton__.py" try: if name in ['ir', 'workflow', 'res', 'webdav']: file_p = tools.file_open(os.path.join(name, '__tryton__.py')) else: file_p = tools.file_open(os.path.join(name, '__tryton__.py')) data = file_p.read() info = tools.safe_eval(data) file_p.close() except: return {} return info def get_version(self, cursor, user, ids, name, context=None): res = {} for module in self.browse(cursor, user, ids, context=context): res[module.id] = Module.get_module_info( module.name).get('version', '') return res def delete(self, cursor, user, ids, context=None): if not ids: return True if isinstance(ids, (int, long)): ids = [ids] for module in self.browse(cursor, user, ids, context=context): if module.state in ( 'installed', 'to upgrade', 'to remove', 'to install', ): self.raise_user_error(cursor, 'delete_state', context=context) return super(Module, self).delete(cursor, user, ids, context=context) def on_write(self, cursor, user, ids, context=None): if not ids: return res = [] graph, packages, later = create_graph(get_module_list()) for module in self.browse(cursor, user, ids, context=context): if module.name not in graph: continue def get_parents(name, graph): parents = set() for node in graph: if graph[name] in node.childs: parents.add(node.name) for parent in parents.copy(): parents.update(get_parents(parent, graph)) return parents dependencies = get_parents(module.name, graph) def get_childs(name, graph): childs = set(x.name for x in graph[name].childs) childs2 = set() for child in childs: childs2.update(get_childs(child, graph)) childs.update(childs2) return childs dependencies.update(get_childs(module.name, graph)) res += self.search(cursor, user, [ ('name', 'in', list(dependencies)), ], context=context) return list({}.fromkeys(res)) def state_install(self, cursor, user, ids, context=None): graph, packages, later = create_graph(get_module_list()) for module in self.browse(cursor, user, ids, context=context): if module.name not in graph: missings = [] for package, deps, datas in packages: if package == module.name: missings = [x for x in deps if x not in graph] self.raise_user_error(cursor, 'missing_dep', (missings, module.name), context=context) def get_parents(name, graph): parents = set() for node in graph: if graph[name] in node.childs: parents.add(node.name) for parent in parents.copy(): parents.update(get_parents(parent, graph)) return parents dependencies = list(get_parents(module.name, graph)) module_install_ids = self.search(cursor, user, [ ('name', 'in', dependencies), ('state', '=', 'uninstalled'), ], context=context) self.write(cursor, user, module_install_ids + [module.id], { 'state': 'to install', }, context=context) def state_upgrade(self, cursor, user, ids, context=None): graph, packages, later = create_graph(get_module_list()) for module in self.browse(cursor, user, ids): if module.name not in graph: missings = [] for package, deps, datas in packages: if package == module.name: missings = [x for x in deps if x not in graph] self.raise_user_error(cursor, user, 'missing_dep', (missings, module.name), context=context) def get_childs(name, graph): childs = set(x.name for x in graph[name].childs) childs2 = set() for child in childs: childs2.update(get_childs(child, graph)) childs.update(childs2) return childs dependencies = list(get_childs(module.name, graph)) module_installed_ids = self.search(cursor, user, [ ('name', 'in', dependencies), ('state', '=', 'installed'), ], context=context) self.write(cursor, user, module_installed_ids + [module.id], { 'state': 'to upgrade', }, context=context) def button_install(self, cursor, user, ids, context=None): return self.state_install(cursor, user, ids, context=context) def button_install_cancel(self, cursor, user, ids, context=None): self.write(cursor, user, ids, { 'state': 'uninstalled', }, context=context) return True def button_uninstall(self, cursor, user, ids, context=None): for module in self.browse(cursor, user, ids, context=context): cursor.execute('SELECT m.state, m.name ' \ 'FROM ir_module_module_dependency d ' \ 'JOIN ir_module_module m on (d.module = m.id) ' \ 'WHERE d.name = %s ' \ 'AND m.state not in ' \ '(\'uninstalled\', \'to remove\')', (module.name,)) res = cursor.fetchall() if res: self.raise_user_error(cursor, 'uninstall_dep', error_description='\n'.join( '\t%s: %s' % (x[0], x[1]) for x in res), context=context) self.write(cursor, user, ids, {'state': 'to remove'}) return True def button_uninstall_cancel(self, cursor, user, ids, context=None): self.write(cursor, user, ids, {'state': 'installed'}, context=context) return True def button_upgrade(self, cursor, user, ids, context=None): return self.state_upgrade(cursor, user, ids, context) def button_upgrade_cancel(self, cursor, user, ids, context=None): self.write(cursor, user, ids, {'state': 'installed'}, context=context) return True # update the list of available packages def update_list(self, cursor, user, context=None): lang_obj = self.pool.get('ir.lang') if context is None: context = {} res = 0 context = context.copy() if 'language' in context: del context['language'] lang_ids = lang_obj.search(cursor, user, [ ('translatable', '=', True), ], context=context) lang_codes = [x.code for x in lang_obj.browse(cursor, user, lang_ids, context=context)] module_names = get_module_list() module_ids = self.search(cursor, user, [], context=context) modules = self.browse(cursor, user, module_ids, context=context) name2module = {} for module in modules: name2module.setdefault(module.name, {}) name2module[module.name]['en_US'] = module for code in lang_codes: ctx = context.copy() ctx['language'] = code modules = self.browse(cursor, user, module_ids, context=ctx) for module in modules: name2module[module.name][code] = module # iterate through installed modules and mark them as being so for name in module_names: mod_name = name if mod_name in name2module.keys(): mod = name2module[mod_name]['en_US'] tryton = Module.get_module_info(mod_name) if mod.description != tryton.get('description', '').decode('utf-8', 'ignore') \ or mod.shortdesc != tryton.get('name', '').decode('utf-8', 'ignore') \ or mod.author != tryton.get('author', '').decode('utf-8', 'ignore') \ or mod.website != tryton.get('website', '').decode('utf-8', 'ignore'): self.write(cursor, user, mod.id, { 'description': tryton.get('description', ''), 'shortdesc': tryton.get('name', ''), 'author': tryton.get('author', ''), 'website': tryton.get('website', ''), }, context=context) for code in lang_codes: mod2 = name2module[mod_name][code] if mod2.description != \ tryton.get('description_' + code, tryton.get('description', '') ).decode('utf-8', 'ignore') \ or mod2.shortdesc != \ tryton.get('name_' + code, tryton.get('name', '') ).decode('utf-8', 'ignore'): ctx = context.copy() ctx['language'] = code self.write(cursor, user, mod.id, { 'description': tryton.get('description_' + code, ''), 'shortdesc': tryton.get('name_' + code, ''), }, context=ctx) self._update_dependencies(cursor, user, mod, tryton.get('depends', []), context=context) continue if name in ['ir', 'workflow', 'res', 'webdav']: mod_path = os.path.join( os.path.dirname(MODULES_PATH), name) else: mod_path = os.path.join(MODULES_PATH, name) tryton = Module.get_module_info(mod_name) if not tryton: continue new_id = self.create(cursor, user, { 'name': mod_name, 'state': 'uninstalled', 'description': tryton.get('description', ''), 'shortdesc': tryton.get('name', ''), 'author': tryton.get('author', 'Unknown'), 'website': tryton.get('website', ''), }, context=context) for code in lang_codes: ctx = context.copy() ctx['language'] = code self.write(cursor, user, new_id, { 'description': tryton.get('description_' + code, ''), 'shortdesc': tryton.get('name_' + code, ''), }, context=ctx) res += 1 name2module.setdefault(mod_name, {}) name2module[mod_name]['en_US'] = self.browse(cursor, user, new_id, context=context) self._update_dependencies(cursor, user, name2module[mod_name]['en_US'], tryton.get('depends', []), context=context) return res def _update_dependencies(self, cursor, user, module, depends=None, context=None): dependency_obj = self.pool.get('ir.module.module.dependency') dependency_obj.delete(cursor, user, [x.id for x in module.dependencies if x.name not in depends], context=context) if depends is None: depends = [] dependency_names = [x.name for x in module.dependencies] for depend in depends: if depend not in dependency_names: dependency_obj.create(cursor, user, { 'module': module.id, 'name': depend, }, context=context) Module() class ModuleDependency(ModelSQL, ModelView): "Module dependency" _name = "ir.module.module.dependency" _description = __doc__ name = fields.Char('Name') module = fields.Many2One('ir.module.module', 'Module', select=1, ondelete='CASCADE', required=True) state = fields.Function(fields.Selection([ ('uninstalled','Not Installed'), ('installed','Installed'), ('to upgrade','To be upgraded'), ('to remove','To be removed'), ('to install','To be installed'), ('unknown', 'Unknown'), ], 'State', readonly=True), 'get_state') def __init__(self): super(ModuleDependency, self).__init__() self._sql_constraints += [ ('name_module_uniq', 'UNIQUE(name, module)', 'Dependency must be unique by module!'), ] def get_state(self, cursor, user, ids, name, context=None): result = {} module_obj = self.pool.get('ir.module.module') for dependency in self.browse(cursor, user, ids): ids = module_obj.search(cursor, user, [ ('name', '=', dependency.name), ], context=context) if ids: result[dependency.id] = module_obj.browse(cursor, user, ids[0], context=context).state else: result[dependency.id] = 'unknown' return result ModuleDependency() class ModuleConfigWizardItem(ModelSQL, ModelView): "Config wizard to run after installing module" _name = 'ir.module.module.config_wizard.item' _description = __doc__ name = fields.Char('Name', required=True, readonly=True) sequence= fields.Integer('Sequence') state = fields.Selection([ ('open', 'Open'), ('done', 'Done'), ], string='State', required=True, select=1) def __init__(self): super(ModuleConfigWizardItem, self).__init__() self._order.insert(0, ('sequence', 'ASC')) def default_state(self, cursor, user, context=None): return 'open' def default_sequence(self, cursor, user, context=None): return 10 ModuleConfigWizardItem() class ModuleConfigWizardFirst(ModelView): 'Module Config Wizard First' _name = 'ir.module.module.config_wizard.first' _description = __doc__ ModuleConfigWizardFirst() class ModuleConfigWizard(Wizard): 'Run config wizards' _name = 'ir.module.module.config_wizard' states = { 'init': { 'result': { 'type': 'choice', 'next_state': '_first', }, }, 'first': { 'result': { 'type': 'form', 'object': 'ir.module.module.config_wizard.first', 'state': [ ('end', 'Cancel', 'tryton-cancel'), ('wizard', 'Ok', 'tryton-ok', True), ], }, }, 'wizard': { 'result': { 'type': 'action', 'action': '_action_wizard', 'state': 'next', }, }, 'next': { 'result': { 'type': 'choice', 'next_state': '_next', }, }, } def _first(self, cursor, user, data, context=None): res = self._next(cursor, user, data, context=context) if res == 'wizard': return 'first' return res def _action_wizard(self, cursor, user, data, context=None): item_obj = self.pool.get('ir.module.module.config_wizard.item') item_ids = item_obj.search(cursor, user, [ ('state', '=', 'open'), ], limit=1, context=context) if item_ids: item = item_obj.browse(cursor, user, item_ids[0], context=context) item_obj.write(cursor, user, item.id, { 'state': 'done', }, context=context) return { 'type': 'ir.action.wizard', 'wiz_name': item.name, } return {} def _next(self, cursor, user, data, context=None): item_obj = self.pool.get('ir.module.module.config_wizard.item') item_ids = item_obj.search(cursor, user, [ ('state', '=', 'open'), ], context=context) if item_ids: return 'wizard' return 'end' ModuleConfigWizard() class ModuleInstallUpgradeInit(ModelView): 'Module Install Upgrade Init' _name = 'ir.module.module.install_upgrade.init' _description = __doc__ module_info = fields.Text('Modules to update', readonly=True) ModuleInstallUpgradeInit() class ModuleInstallUpgradeStart(ModelView): 'Module Install Upgrade Start' _name = 'ir.module.module.install_upgrade.start' _description = __doc__ ModuleInstallUpgradeStart() class ModuleInstallUpgrade(Wizard): "Install / Upgrade modules" _name = 'ir.module.module.install_upgrade' def _get_install(self, cursor, user, data, context): module_obj = self.pool.get('ir.module.module') module_ids = module_obj.search(cursor, user, [ ('state', 'in', ['to upgrade', 'to remove', 'to install']), ], context=context) modules = module_obj.browse(cursor, user, module_ids, context=context) return { 'module_info': '\n'.join(x.name + ': ' + x.state \ for x in modules), } def _upgrade_module(self, cursor, user, data, context): module_obj = self.pool.get('ir.module.module') lang_obj = self.pool.get('ir.lang') dbname = cursor.dbname db = Database(dbname).connect() cursor = db.cursor() try: module_ids = module_obj.search(cursor, user, [ ('state', 'in', ['to upgrade', 'to remove', 'to install']), ], context=context) lang_ids = lang_obj.search(cursor, user, [ ('translatable', '=', True), ], context=context) lang = [x.code for x in lang_obj.browse(cursor, user, lang_ids, context=context)] finally: cursor.commit() cursor.close() if module_ids: pool = Pool(dbname) pool.init(update=True, lang=lang) new_wizard = pool.get('ir.module.module.install_upgrade', type='wizard') new_wizard._lock.acquire() new_wizard._datas[data['_wiz_id']] = self._datas[data['_wiz_id']] new_wizard._lock.release() return {} states = { 'init': { 'actions': ['_get_install'], 'result': { 'type': 'form', 'object': 'ir.module.module.install_upgrade.init', 'state': [ ('end', 'Cancel', 'tryton-cancel'), ('start', 'Start Upgrade', 'tryton-ok', True), ], }, }, 'start': { 'actions': ['_upgrade_module'], 'result': { 'type': 'form', 'object': 'ir.module.module.install_upgrade.start', 'state': [ ('menu', 'Ok', 'tryton-ok', True), ], }, }, 'menu': { 'result': { 'type': 'action', 'action': '_menu', 'state': 'config', }, }, 'config': { 'result': { 'type': 'action', 'action': '_config', 'state': 'end', }, }, } def _menu(self, cursor, user, data, context=None): model_data_obj = self.pool.get('ir.model.data') act_window_obj = self.pool.get('ir.action.act_window') act_window_id = model_data_obj.get_id(cursor, user, 'ir', 'act_menu_tree', context=context) res = act_window_obj.read(cursor, user, act_window_id, context=context) return res def _config(self, cursor, user, data, context=None): return { 'type': 'ir.action.wizard', 'wiz_name': 'ir.module.module.config_wizard', } ModuleInstallUpgrade() class ModuleConfig(Wizard): 'Configure Modules' _name = 'ir.module.module.config' states = { 'init': { 'result': { 'type': 'action', 'action': '_action_open', 'state': 'end', }, }, } def _action_open(self, cursor, user, datas, context=None): model_data_obj = self.pool.get('ir.model.data') act_window_obj = self.pool.get('ir.action.act_window') act_window_id = model_data_obj.get_id(cursor, user, 'ir', 'act_module_form', context=context) res = act_window_obj.read(cursor, user, act_window_id, context=context) return res ModuleConfig()