changeset 83757dc87808 moved the buttons from res to ir module but it missed the migration of the model data to allow updates from previous databases issue7186 review38031003
618 lines
21 KiB
Python
618 lines
21 KiB
Python
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
|
# this repository contains the full copyright notices and license terms.
|
|
from __future__ import division
|
|
|
|
from functools import wraps
|
|
|
|
from sql.operators import NotIn
|
|
|
|
from trytond.model import ModelView, ModelSQL, fields, Unique
|
|
from trytond.modules import create_graph, get_module_list, get_module_info
|
|
from trytond.wizard import Wizard, StateView, Button, StateTransition, \
|
|
StateAction
|
|
from trytond import backend
|
|
from trytond.pool import Pool
|
|
from trytond.transaction import Transaction
|
|
from trytond.pyson import Eval
|
|
from trytond.rpc import RPC
|
|
|
|
__all__ = [
|
|
'Module', 'ModuleDependency', 'ModuleConfigWizardItem',
|
|
'ModuleConfigWizardFirst', 'ModuleConfigWizardOther',
|
|
'ModuleConfigWizardDone', 'ModuleConfigWizard',
|
|
'ModuleActivateUpgradeStart', 'ModuleActivateUpgradeDone',
|
|
'ModuleActivateUpgrade', 'ModuleConfig',
|
|
]
|
|
|
|
|
|
def filter_state(state):
|
|
def filter(func):
|
|
@wraps(func)
|
|
def wrapper(cls, modules):
|
|
modules = [m for m in modules if m.state == state]
|
|
return func(cls, modules)
|
|
return wrapper
|
|
return filter
|
|
|
|
|
|
class Module(ModelSQL, ModelView):
|
|
"Module"
|
|
__name__ = "ir.module"
|
|
name = fields.Char("Name", readonly=True, required=True)
|
|
version = fields.Function(fields.Char('Version'), 'get_version')
|
|
dependencies = fields.One2Many('ir.module.dependency',
|
|
'module', 'Dependencies', readonly=True)
|
|
parents = fields.Function(fields.One2Many('ir.module', None, 'Parents'),
|
|
'get_parents')
|
|
childs = fields.Function(fields.One2Many('ir.module', None, 'Childs'),
|
|
'get_childs')
|
|
state = fields.Selection([
|
|
('not activated', 'Not Activated'),
|
|
('activated', 'Activated'),
|
|
('to upgrade', 'To be upgraded'),
|
|
('to remove', 'To be removed'),
|
|
('to activate', 'To be activated'),
|
|
], string='State', readonly=True)
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super(Module, cls).__setup__()
|
|
table = cls.__table__()
|
|
cls._sql_constraints = [
|
|
('name_uniq', Unique(table, table.name),
|
|
'The name of the module must be unique!'),
|
|
]
|
|
cls._order.insert(0, ('name', 'ASC'))
|
|
cls.__rpc__.update({
|
|
'on_write': RPC(instantiate=0),
|
|
})
|
|
cls._error_messages.update({
|
|
'delete_state': ('You can not remove a module that is activated '
|
|
'or will be activated'),
|
|
'missing_dep': 'Missing dependencies %s for module "%s"',
|
|
'deactivate_dep': ('Some activated modules depend on the ones '
|
|
'you are trying to deactivate:'),
|
|
})
|
|
cls._buttons.update({
|
|
'activate': {
|
|
'invisible': Eval('state') != 'not activated',
|
|
'depends': ['state'],
|
|
},
|
|
'activate_cancel': {
|
|
'invisible': Eval('state') != 'to activate',
|
|
'depends': ['state'],
|
|
},
|
|
'deactivate': {
|
|
'invisible': Eval('state') != 'activated',
|
|
'depends': ['state'],
|
|
},
|
|
'deactivate_cancel': {
|
|
'invisible': Eval('state') != 'to remove',
|
|
'depends': ['state'],
|
|
},
|
|
'upgrade': {
|
|
'invisible': Eval('state') != 'activated',
|
|
'depends': ['state'],
|
|
},
|
|
'upgrade_cancel': {
|
|
'invisible': Eval('state') != 'to upgrade',
|
|
'depends': ['state'],
|
|
},
|
|
})
|
|
|
|
@classmethod
|
|
def __register__(cls, module_name):
|
|
pool = Pool()
|
|
ModelData = pool.get('ir.model.data')
|
|
TableHandler = backend.get('TableHandler')
|
|
sql_table = cls.__table__()
|
|
model_data_sql_table = ModelData.__table__()
|
|
cursor = Transaction().connection.cursor()
|
|
|
|
# Migration from 3.6: remove double module
|
|
old_table = 'ir_module_module'
|
|
if TableHandler.table_exist(old_table):
|
|
TableHandler.table_rename(old_table, cls._table)
|
|
|
|
super(Module, cls).__register__(module_name)
|
|
|
|
# Migration from 4.0: rename installed to activated
|
|
cursor.execute(*sql_table.update(
|
|
[sql_table.state], ['activated'],
|
|
where=sql_table.state == 'installed'))
|
|
cursor.execute(*sql_table.update(
|
|
[sql_table.state], ['not activated'],
|
|
where=sql_table.state == 'uninstalled'))
|
|
|
|
# Migration from 4.6: register buttons on ir module
|
|
button_fs_ids = [
|
|
'module_activate_button',
|
|
'module_activate_cancel_button',
|
|
'module_deactivate_button',
|
|
'module_deactivate_cancel_button',
|
|
'module_upgrade_button',
|
|
'module_upgrade_cancel_button',
|
|
]
|
|
cursor.execute(*model_data_sql_table.update(
|
|
[model_data_sql_table.module], ['ir'],
|
|
where=((model_data_sql_table.module == 'res')
|
|
& (model_data_sql_table.fs_id.in_(button_fs_ids)))))
|
|
|
|
@staticmethod
|
|
def default_state():
|
|
return 'not activated'
|
|
|
|
def get_version(self, name):
|
|
return get_module_info(self.name).get('version', '')
|
|
|
|
@classmethod
|
|
def get_parents(cls, modules, name):
|
|
parent_names = list(set(d.name for m in modules
|
|
for d in m.dependencies))
|
|
parents = cls.search([
|
|
('name', 'in', parent_names),
|
|
])
|
|
name2id = dict((m.name, m.id) for m in parents)
|
|
return dict((m.id, [name2id[d.name] for d in m.dependencies])
|
|
for m in modules)
|
|
|
|
@classmethod
|
|
def get_childs(cls, modules, name):
|
|
child_ids = dict((m.id, []) for m in modules)
|
|
name2id = dict((m.name, m.id) for m in modules)
|
|
childs = cls.search([
|
|
('dependencies.name', 'in', name2id.keys()),
|
|
])
|
|
for child in childs:
|
|
for dep in child.dependencies:
|
|
if dep.name in name2id:
|
|
child_ids[name2id[dep.name]].append(child.id)
|
|
return child_ids
|
|
|
|
@classmethod
|
|
def delete(cls, records):
|
|
for module in records:
|
|
if module.state in (
|
|
'activated',
|
|
'to upgrade',
|
|
'to remove',
|
|
'to activate',
|
|
):
|
|
cls.raise_user_error('delete_state')
|
|
return super(Module, cls).delete(records)
|
|
|
|
@classmethod
|
|
def on_write(cls, modules):
|
|
dependencies = set()
|
|
|
|
def get_parents(module):
|
|
parents = set(p.id for p in module.parents)
|
|
for p in module.parents:
|
|
parents.update(get_parents(p))
|
|
return parents
|
|
|
|
def get_childs(module):
|
|
childs = set(c.id for c in module.childs)
|
|
for c in module.childs:
|
|
childs.update(get_childs(c))
|
|
return childs
|
|
|
|
for module in modules:
|
|
dependencies.update(get_parents(module))
|
|
dependencies.update(get_childs(module))
|
|
return list(dependencies)
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@filter_state('not activated')
|
|
def activate(cls, modules):
|
|
modules_activated = set(modules)
|
|
graph, packages, later = create_graph(get_module_list())
|
|
|
|
def get_parents(module):
|
|
parents = set(p for p in module.parents)
|
|
for p in module.parents:
|
|
parents.update(get_parents(p))
|
|
return parents
|
|
|
|
for module in modules:
|
|
if module.name not in graph:
|
|
missings = []
|
|
for package, deps, xdep, info in packages:
|
|
if package == module.name:
|
|
missings = [x for x in deps if x not in graph]
|
|
cls.raise_user_error('missing_dep', (missings, module.name))
|
|
|
|
modules_activated.update((m for m in get_parents(module)
|
|
if m.state == 'not activated'))
|
|
cls.write(list(modules_activated), {
|
|
'state': 'to activate',
|
|
})
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@filter_state('activated')
|
|
def upgrade(cls, modules):
|
|
modules_activated = set(modules)
|
|
graph, packages, later = create_graph(get_module_list())
|
|
|
|
def get_childs(module):
|
|
childs = set(c for c in module.childs)
|
|
for c in module.childs:
|
|
childs.update(get_childs(c))
|
|
return childs
|
|
|
|
for module in modules:
|
|
if module.name not in graph:
|
|
missings = []
|
|
for package, deps, xdep, info in packages:
|
|
if package == module.name:
|
|
missings = [x for x in deps if x not in graph]
|
|
cls.raise_user_error('missing_dep', (missings, module.name))
|
|
|
|
modules_activated.update((m for m in get_childs(module)
|
|
if m.state == 'activated'))
|
|
cls.write(list(modules_activated), {
|
|
'state': 'to upgrade',
|
|
})
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@filter_state('to activate')
|
|
def activate_cancel(cls, modules):
|
|
cls.write(modules, {
|
|
'state': 'not activated',
|
|
})
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@filter_state('activated')
|
|
def deactivate(cls, modules):
|
|
pool = Pool()
|
|
Module = pool.get('ir.module')
|
|
Dependency = pool.get('ir.module.dependency')
|
|
module_table = Module.__table__()
|
|
dep_table = Dependency.__table__()
|
|
cursor = Transaction().connection.cursor()
|
|
for module in modules:
|
|
cursor.execute(*dep_table.join(module_table,
|
|
condition=(dep_table.module == module_table.id)
|
|
).select(module_table.state, module_table.name,
|
|
where=(dep_table.name == module.name)
|
|
& NotIn(
|
|
module_table.state, ['not activated', 'to remove'])))
|
|
res = cursor.fetchall()
|
|
if res:
|
|
cls.raise_user_error('deactivate_dep',
|
|
error_description='\n'.join(
|
|
'\t%s: %s' % (x[0], x[1]) for x in res))
|
|
cls.write(modules, {'state': 'to remove'})
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@filter_state('to remove')
|
|
def deactivate_cancel(cls, modules):
|
|
cls.write(modules, {'state': 'not activated'})
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@filter_state('to upgrade')
|
|
def upgrade_cancel(cls, modules):
|
|
cls.write(modules, {'state': 'activated'})
|
|
|
|
@classmethod
|
|
def update_list(cls):
|
|
'Update the list of available packages'
|
|
count = 0
|
|
module_names = get_module_list()
|
|
|
|
modules = cls.search([])
|
|
name2module = dict((m.name, m) for m in modules)
|
|
cls.delete([m for m in modules
|
|
if m.state != 'activated' and m.name not in module_names])
|
|
|
|
# iterate through activated modules and mark them as being so
|
|
for name in module_names:
|
|
if name in name2module:
|
|
module = name2module[name]
|
|
tryton = get_module_info(name)
|
|
cls._update_dependencies(module, tryton.get('depends', []))
|
|
continue
|
|
|
|
tryton = get_module_info(name)
|
|
if not tryton:
|
|
continue
|
|
module, = cls.create([{
|
|
'name': name,
|
|
'state': 'not activated',
|
|
}])
|
|
count += 1
|
|
cls._update_dependencies(module, tryton.get('depends', []))
|
|
return count
|
|
|
|
@classmethod
|
|
def _update_dependencies(cls, module, depends=None):
|
|
pool = Pool()
|
|
Dependency = pool.get('ir.module.dependency')
|
|
Dependency.delete([x for x in module.dependencies
|
|
if x.name not in depends])
|
|
if depends is None:
|
|
depends = []
|
|
# Restart Browse Cache for deleted dependencies
|
|
module = cls(module.id)
|
|
dependency_names = [x.name for x in module.dependencies]
|
|
to_create = []
|
|
for depend in depends:
|
|
if depend not in dependency_names:
|
|
to_create.append({
|
|
'module': module.id,
|
|
'name': depend,
|
|
})
|
|
if to_create:
|
|
Dependency.create(to_create)
|
|
|
|
|
|
class ModuleDependency(ModelSQL, ModelView):
|
|
"Module dependency"
|
|
__name__ = "ir.module.dependency"
|
|
name = fields.Char('Name')
|
|
module = fields.Many2One('ir.module', 'Module', select=True,
|
|
ondelete='CASCADE', required=True)
|
|
state = fields.Function(fields.Selection([
|
|
('not activated', 'Not Activated'),
|
|
('activated', 'Activated'),
|
|
('to upgrade', 'To be upgraded'),
|
|
('to remove', 'To be removed'),
|
|
('to activate', 'To be activated'),
|
|
('unknown', 'Unknown'),
|
|
], 'State', readonly=True), 'get_state')
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super(ModuleDependency, cls).__setup__()
|
|
table = cls.__table__()
|
|
cls._sql_constraints += [
|
|
('name_module_uniq', Unique(table, table.name, table.module),
|
|
'Dependency must be unique by module!'),
|
|
]
|
|
|
|
@classmethod
|
|
def __register__(cls, module_name):
|
|
TableHandler = backend.get('TableHandler')
|
|
|
|
# Migration from 3.6: remove double module
|
|
old_table = 'ir_module_module_dependency'
|
|
if TableHandler.table_exist(old_table):
|
|
TableHandler.table_rename(old_table, cls._table)
|
|
|
|
super(ModuleDependency, cls).__register__(module_name)
|
|
|
|
def get_state(self, name):
|
|
pool = Pool()
|
|
Module = pool.get('ir.module')
|
|
dependencies = Module.search([
|
|
('name', '=', self.name),
|
|
])
|
|
if dependencies:
|
|
return dependencies[0].state
|
|
else:
|
|
return 'unknown'
|
|
|
|
|
|
class ModuleConfigWizardItem(ModelSQL, ModelView):
|
|
"Config wizard to run after activating a module"
|
|
__name__ = 'ir.module.config_wizard.item'
|
|
action = fields.Many2One('ir.action', 'Action', required=True,
|
|
readonly=True)
|
|
sequence = fields.Integer('Sequence', required=True)
|
|
state = fields.Selection([
|
|
('open', 'Open'),
|
|
('done', 'Done'),
|
|
], string='State', required=True, select=True)
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super(ModuleConfigWizardItem, cls).__setup__()
|
|
cls._order.insert(0, ('sequence', 'ASC'))
|
|
|
|
@classmethod
|
|
def __register__(cls, module_name):
|
|
TableHandler = backend.get('TableHandler')
|
|
cursor = Transaction().connection.cursor()
|
|
pool = Pool()
|
|
ModelData = pool.get('ir.model.data')
|
|
model_data = ModelData.__table__()
|
|
|
|
# Migration from 3.6: remove double module
|
|
old_table = 'ir_module_module_config_wizard_item'
|
|
if TableHandler.table_exist(old_table):
|
|
TableHandler.table_rename(old_table, cls._table)
|
|
cursor.execute(*model_data.update(
|
|
columns=[model_data.model],
|
|
values=[cls.__name__],
|
|
where=(model_data.model ==
|
|
'ir.module.module.config_wizard.item')))
|
|
|
|
table = TableHandler(cls, module_name)
|
|
|
|
# Migrate from 2.2 remove name
|
|
table.drop_column('name')
|
|
|
|
super(ModuleConfigWizardItem, cls).__register__(module_name)
|
|
|
|
@staticmethod
|
|
def default_state():
|
|
return 'open'
|
|
|
|
@staticmethod
|
|
def default_sequence():
|
|
return 10
|
|
|
|
|
|
class ModuleConfigWizardFirst(ModelView):
|
|
'Module Config Wizard First'
|
|
__name__ = 'ir.module.config_wizard.first'
|
|
|
|
|
|
class ModuleConfigWizardOther(ModelView):
|
|
'Module Config Wizard Other'
|
|
__name__ = 'ir.module.config_wizard.other'
|
|
|
|
percentage = fields.Float('Percentage', readonly=True)
|
|
|
|
@staticmethod
|
|
def default_percentage():
|
|
pool = Pool()
|
|
Item = pool.get('ir.module.config_wizard.item')
|
|
done = Item.search([
|
|
('state', '=', 'done'),
|
|
], count=True)
|
|
all = Item.search([], count=True)
|
|
return done / all
|
|
|
|
|
|
class ModuleConfigWizardDone(ModelView):
|
|
'Module Config Wizard Done'
|
|
__name__ = 'ir.module.config_wizard.done'
|
|
|
|
|
|
class ModuleConfigWizard(Wizard):
|
|
'Run config wizards'
|
|
__name__ = 'ir.module.config_wizard'
|
|
|
|
class ConfigStateAction(StateAction):
|
|
|
|
def __init__(self):
|
|
StateAction.__init__(self, None)
|
|
|
|
def get_action(self):
|
|
pool = Pool()
|
|
Item = pool.get('ir.module.config_wizard.item')
|
|
Action = pool.get('ir.action')
|
|
items = Item.search([
|
|
('state', '=', 'open'),
|
|
], limit=1)
|
|
if items:
|
|
item = items[0]
|
|
Item.write([item], {
|
|
'state': 'done',
|
|
})
|
|
return Action.get_action_values(item.action.type,
|
|
[item.action.id])[0]
|
|
|
|
start = StateTransition()
|
|
first = StateView('ir.module.config_wizard.first',
|
|
'ir.module_config_wizard_first_view_form', [
|
|
Button('Cancel', 'end', 'tryton-cancel'),
|
|
Button('OK', 'action', 'tryton-ok', default=True),
|
|
])
|
|
other = StateView('ir.module.config_wizard.other',
|
|
'ir.module_config_wizard_other_view_form', [
|
|
Button('Cancel', 'end', 'tryton-cancel'),
|
|
Button('Next', 'action', 'tryton-go-next', default=True),
|
|
])
|
|
action = ConfigStateAction()
|
|
done = StateView('ir.module.config_wizard.done',
|
|
'ir.module_config_wizard_done_view_form', [
|
|
Button('OK', 'end', 'tryton-ok', default=True),
|
|
])
|
|
|
|
def transition_start(self):
|
|
res = self.transition_action()
|
|
if res == 'other':
|
|
return 'first'
|
|
return res
|
|
|
|
def transition_action(self):
|
|
pool = Pool()
|
|
Item = pool.get('ir.module.config_wizard.item')
|
|
items = Item.search([
|
|
('state', '=', 'open'),
|
|
])
|
|
if items:
|
|
return 'other'
|
|
return 'done'
|
|
|
|
def end(self):
|
|
return 'reload menu'
|
|
|
|
|
|
class ModuleActivateUpgradeStart(ModelView):
|
|
'Module Activate Upgrade Start'
|
|
__name__ = 'ir.module.activate_upgrade.start'
|
|
module_info = fields.Text('Modules to update', readonly=True)
|
|
|
|
|
|
class ModuleActivateUpgradeDone(ModelView):
|
|
'Module Activate Upgrade Done'
|
|
__name__ = 'ir.module.activate_upgrade.done'
|
|
|
|
|
|
class ModuleActivateUpgrade(Wizard):
|
|
"Activate / Upgrade modules"
|
|
__name__ = 'ir.module.activate_upgrade'
|
|
|
|
start = StateView('ir.module.activate_upgrade.start',
|
|
'ir.module_activate_upgrade_start_view_form', [
|
|
Button('Cancel', 'end', 'tryton-cancel'),
|
|
Button('Start Upgrade', 'upgrade', 'tryton-ok', default=True),
|
|
])
|
|
upgrade = StateTransition()
|
|
done = StateView('ir.module.activate_upgrade.done',
|
|
'ir.module_activate_upgrade_done_view_form', [
|
|
Button('OK', 'config', 'tryton-ok', default=True),
|
|
])
|
|
config = StateAction('ir.act_module_config_wizard')
|
|
|
|
@classmethod
|
|
def check_access(cls):
|
|
# Use new transaction to prevent lock when activating modules
|
|
with Transaction().new_transaction():
|
|
super(ModuleActivateUpgrade, cls).check_access()
|
|
|
|
@staticmethod
|
|
def default_start(fields):
|
|
pool = Pool()
|
|
Module = pool.get('ir.module')
|
|
modules = Module.search([
|
|
('state', 'in', ['to upgrade', 'to remove', 'to activate']),
|
|
])
|
|
return {
|
|
'module_info': '\n'.join(x.name + ': ' + x.state
|
|
for x in modules),
|
|
}
|
|
|
|
def __init__(self, session_id):
|
|
pass
|
|
|
|
def _save(self):
|
|
pass
|
|
|
|
def transition_upgrade(self):
|
|
pool = Pool()
|
|
Module = pool.get('ir.module')
|
|
Lang = pool.get('ir.lang')
|
|
with Transaction().new_transaction():
|
|
modules = Module.search([
|
|
('state', 'in', ['to upgrade', 'to remove', 'to activate']),
|
|
])
|
|
update = [m.name for m in modules]
|
|
langs = Lang.search([
|
|
('translatable', '=', True),
|
|
])
|
|
lang = [x.code for x in langs]
|
|
if update:
|
|
pool.init(update=update, lang=lang)
|
|
return 'done'
|
|
|
|
|
|
class ModuleConfig(Wizard):
|
|
'Configure Modules'
|
|
__name__ = 'ir.module.config'
|
|
|
|
start = StateAction('ir.act_module_form')
|
|
|
|
@staticmethod
|
|
def transition_start():
|
|
return 'end'
|