When updating session with data from the client, the states must be mark as dirty to be saved in the database at the end of the process. issue2421
366 lines
12 KiB
Python
366 lines
12 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.
|
|
|
|
__all__ = ['Wizard', 'StateView', 'StateTransition', 'StateAction', 'Button',
|
|
'Session']
|
|
|
|
try:
|
|
import simplejson as json
|
|
except ImportError:
|
|
import json
|
|
|
|
from trytond.pool import Pool
|
|
from trytond.transaction import Transaction
|
|
from trytond.error import WarningErrorMixin
|
|
from trytond.url import URLMixin
|
|
from trytond.protocols.jsonrpc import object_hook, JSONEncoder
|
|
from trytond.model.fields import states_validate
|
|
from trytond.pyson import PYSONEncoder
|
|
|
|
|
|
class Button(object):
|
|
'''
|
|
Define a button on wizard.
|
|
'''
|
|
|
|
def __init__(self, string, state, icon='', default=False, states=None):
|
|
self.string = string
|
|
self.state = state
|
|
self.icon = icon
|
|
self.default = bool(default)
|
|
self.__states = None
|
|
self.states = states or {}
|
|
|
|
@property
|
|
def states(self):
|
|
return self.__states
|
|
|
|
@states.setter
|
|
def states(self, value):
|
|
states_validate(value)
|
|
self.__states = value
|
|
|
|
|
|
class State(object):
|
|
'''
|
|
A State of a wizard.
|
|
'''
|
|
|
|
|
|
class StateView(State):
|
|
'''
|
|
A view state of a wizard.
|
|
'''
|
|
|
|
def __init__(self, model_name, view, buttons):
|
|
'''
|
|
model_name is the name of the model
|
|
view is the xml id of the view
|
|
buttons is a list of Button
|
|
'''
|
|
self.model_name = model_name
|
|
self.view = view
|
|
self.buttons = buttons
|
|
assert len(self.buttons) == len(set(b.state for b in self.buttons))
|
|
assert len([b for b in self.buttons if b.default]) <= 1
|
|
|
|
def get_view(self):
|
|
'''
|
|
Returns the view definition
|
|
'''
|
|
model_obj = Pool().get(self.model_name)
|
|
model_data_obj = Pool().get('ir.model.data')
|
|
module, fs_id = self.view.split('.')
|
|
view_id = model_data_obj.get_id(module, fs_id)
|
|
return model_obj.fields_view_get(view_id=view_id, view_type='form')
|
|
|
|
def get_defaults(self, wizard, session, state_name, fields):
|
|
'''
|
|
Returns defaults values for the fields
|
|
'''
|
|
model_obj = Pool().get(self.model_name)
|
|
defaults = model_obj.default_get(fields)
|
|
default = getattr(wizard, 'default_%s' % state_name, None)
|
|
if default:
|
|
defaults.update(default(session, fields))
|
|
return defaults
|
|
|
|
def get_buttons(self, wizard, state_name):
|
|
'''
|
|
Returns button definitions translated
|
|
'''
|
|
translation_obj = Pool().get('ir.translation')
|
|
def translation_key(button):
|
|
return (','.join((wizard._name, state_name, button.state)),
|
|
'wizard_button', Transaction().language, button.string)
|
|
translation_keys = [translation_key(button) for button in self.buttons]
|
|
translations = translation_obj.get_sources(translation_keys)
|
|
encoder = PYSONEncoder()
|
|
result = []
|
|
for button in self.buttons:
|
|
result.append({
|
|
'state': button.state,
|
|
'icon': button.icon,
|
|
'default': button.default,
|
|
'string': (translations.get(translation_key(button))
|
|
or button.string),
|
|
'states': encoder.encode(button.states),
|
|
})
|
|
return result
|
|
|
|
|
|
class StateTransition(State):
|
|
'''
|
|
A transition state of a wizard.
|
|
'''
|
|
|
|
class StateAction(StateTransition):
|
|
'''
|
|
An action state of a wizard.
|
|
'''
|
|
|
|
def __init__(self, action_id):
|
|
'''
|
|
action_id is a string containing ``module.xml_id``
|
|
'''
|
|
super(StateAction, self).__init__()
|
|
self.action_id = action_id
|
|
|
|
def get_action(self):
|
|
"Returns action definition"
|
|
pool = Pool()
|
|
model_data_obj = pool.get('ir.model.data')
|
|
action_obj = pool.get('ir.action')
|
|
module, fs_id = self.action_id.split('.')
|
|
action_id = action_obj.get_action_id(
|
|
model_data_obj.get_id(module, fs_id))
|
|
action = action_obj.browse(action_id)
|
|
return action_obj.get_action_values(action.type, action.id)
|
|
|
|
|
|
class _SessionRecord(object):
|
|
'''
|
|
A record of a wizard form.
|
|
'''
|
|
# Declared in class to prevent:
|
|
# 'maximum recursion depth exceeded in __subclasscheck__'
|
|
_model = None
|
|
_data = None
|
|
__cache = None
|
|
|
|
def __init__(self, model, data):
|
|
self._model = model
|
|
self._data = data
|
|
self.__cache = {}
|
|
self.dirty = False
|
|
|
|
def __getattr__(self, name):
|
|
if name in self.__cache:
|
|
return self.__cache[name]
|
|
field = self._model._columns[name]
|
|
data = self._data.get(name, False)
|
|
if data:
|
|
target_obj = None
|
|
if hasattr(field, 'model_name'):
|
|
target_obj = Pool().get(field.model_name)
|
|
elif hasattr(field, 'get_target'):
|
|
target_obj = field.get_target()
|
|
if target_obj:
|
|
def instance(data):
|
|
if isinstance(data, dict):
|
|
return _SessionRecord(target_obj, data)
|
|
return target_obj.browse(data)
|
|
if isinstance(data, list):
|
|
data = [instance(x) for x in data]
|
|
else:
|
|
data = instance(data)
|
|
self.__cache[name] = data
|
|
return data
|
|
|
|
def __setattr__(self, name, value):
|
|
if (self._model is not None
|
|
and (name in self._model._columns
|
|
or name in self._model._inherit_fields)):
|
|
self.__cache.pop(name, None)
|
|
self._data[name] = value
|
|
self.dirty = True
|
|
else:
|
|
super(_SessionRecord, self).__setattr__(name, value)
|
|
|
|
|
|
class Session(object):
|
|
'''
|
|
A wizard session.
|
|
'''
|
|
|
|
def __init__(self, wizard, session_id):
|
|
pool = Pool()
|
|
session_obj = pool.get('ir.session.wizard')
|
|
self._session = session_obj.browse(session_id)
|
|
self.data = json.loads(self._session.data.encode('utf-8'),
|
|
object_hook=object_hook)
|
|
for state_name, state in wizard.states.iteritems():
|
|
if isinstance(state, StateView):
|
|
model = pool.get(state.model_name)
|
|
self.data.setdefault(state_name, {})
|
|
setattr(self, state_name,
|
|
_SessionRecord(model, self.data[state_name]))
|
|
|
|
@property
|
|
def dirty(self):
|
|
return any(getattr(self, state_name).dirty
|
|
for state_name in self.data)
|
|
|
|
def save(self):
|
|
"Save the session in database"
|
|
session_obj = Pool().get('ir.session.wizard')
|
|
if self.dirty:
|
|
session_obj.write(self._session.id, {
|
|
'data': json.dumps(self.data, cls=JSONEncoder),
|
|
})
|
|
|
|
|
|
class Wizard(WarningErrorMixin, URLMixin):
|
|
_name = ""
|
|
start_state = 'start'
|
|
end_state = 'end'
|
|
|
|
def __new__(cls):
|
|
Pool.register(cls, type='wizard')
|
|
|
|
def __init__(self):
|
|
super(Wizard, self).__init__()
|
|
self._rpc = {
|
|
'create': True,
|
|
'delete': True,
|
|
'execute': True,
|
|
}
|
|
self._error_messages = {}
|
|
|
|
@property
|
|
def states(self):
|
|
result = {}
|
|
for attr in dir(self):
|
|
if attr == 'states':
|
|
continue
|
|
if isinstance(getattr(self, attr), State):
|
|
result[attr] = getattr(self, attr)
|
|
return result
|
|
|
|
def init(self, module_name):
|
|
pool = Pool()
|
|
translation_obj = pool.get('ir.translation')
|
|
cursor = Transaction().cursor
|
|
for state_name, state in self.states.iteritems():
|
|
if isinstance(state, StateView):
|
|
for button in state.buttons:
|
|
cursor.execute('SELECT id, name, src '
|
|
'FROM ir_translation '
|
|
'WHERE module = %s '
|
|
'AND lang = %s '
|
|
'AND type = %s '
|
|
'AND name = %s',
|
|
(module_name, 'en_US', 'wizard_button',
|
|
self._name + ',' + state_name + ',' +
|
|
button.state))
|
|
res = cursor.dictfetchall()
|
|
src_md5 = translation_obj.get_src_md5(button.string)
|
|
if not res:
|
|
cursor.execute('INSERT INTO ir_translation '
|
|
'(name, lang, type, src, src_md5, value, module, '
|
|
'fuzzy) '
|
|
'VALUES (%s, %s, %s, %s, %s, %s, %s, %s)',
|
|
(self._name + ',' + state_name + ',' +
|
|
button.state,
|
|
'en_US', 'wizard_button', button.string,
|
|
src_md5, '', module_name, False))
|
|
elif res[0]['src'] != button.string:
|
|
cursor.execute('UPDATE ir_translation '
|
|
'SET src = %s, src_md5 = %s '
|
|
'WHERE id = %s',
|
|
(button.string, src_md5, res[0]['id']))
|
|
|
|
cursor.execute('SELECT id, src FROM ir_translation '
|
|
'WHERE lang = %s '
|
|
'AND type = %s '
|
|
'AND name = %s',
|
|
('en_US', 'error', self._name))
|
|
trans_error = {}
|
|
for trans in cursor.dictfetchall():
|
|
trans_error[trans['src']] = trans
|
|
|
|
for error in self._error_messages.values():
|
|
if error not in trans_error:
|
|
error_md5 = translation_obj.get_src_md5(error)
|
|
cursor.execute('INSERT INTO ir_translation '
|
|
'(name, lang, type, src, src_md5, value, module, fuzzy) '
|
|
'VALUES (%s, %s, %s, %s, %s, %s, %s, %s)',
|
|
(self._name, 'en_US', 'error', error, error_md5, '',
|
|
module_name, False))
|
|
|
|
def create(self):
|
|
"Create a session"
|
|
session_obj = Pool().get('ir.session.wizard')
|
|
return (session_obj.create({}), self.start_state, self.end_state)
|
|
|
|
def delete(self, session_id):
|
|
"Delete the session"
|
|
session_obj = Pool().get('ir.session.wizard')
|
|
session_obj.delete(session_id)
|
|
|
|
def execute(self, session, data, state_name):
|
|
'''
|
|
Execute the wizard state.
|
|
|
|
session is a Session or a Session id
|
|
data is a dictionary with the session data to update
|
|
state_name is the name of state to execute
|
|
|
|
Returns a dictionary with:
|
|
- ``actions``: a list of Action to execute
|
|
- ``view``: a dictionary with:
|
|
- ``fields_view``: a fields/view definition
|
|
- ``defaults``: a dictionary with default values
|
|
- ``buttons``: a list of buttons
|
|
'''
|
|
if state_name == self.end_state:
|
|
return {}
|
|
if isinstance(session, (int, long)):
|
|
session = Session(self, session)
|
|
for key, value in data.iteritems():
|
|
prev_data = session.data[key].copy()
|
|
session.data[key].update(value)
|
|
if prev_data != session.data[key]:
|
|
getattr(session, key).dirty = True
|
|
|
|
state = self.states[state_name]
|
|
result = {}
|
|
|
|
if isinstance(state, StateView):
|
|
view = state.get_view()
|
|
defaults = state.get_defaults(self, session, state_name,
|
|
view['fields'].keys())
|
|
buttons = state.get_buttons(self, state_name)
|
|
result['view'] = {
|
|
'fields_view': view,
|
|
'defaults': defaults,
|
|
'buttons': buttons,
|
|
'state': state_name,
|
|
}
|
|
elif isinstance(state, StateTransition):
|
|
do_result = None
|
|
if isinstance(state, StateAction):
|
|
action = state.get_action()
|
|
do = getattr(self, 'do_%s' % state_name, None)
|
|
if do:
|
|
do_result = do(session, action)
|
|
else:
|
|
do_result = action, {}
|
|
transition = getattr(self, 'transition_%s' % state_name, None)
|
|
if transition:
|
|
result = self.execute(session, {}, transition(session))
|
|
if do_result:
|
|
result.setdefault('actions', []).append(do_result)
|
|
session.save()
|
|
return result
|