From 274ea7bee289a69b9e91cf36efcf6c0d059f94af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Mon, 2 Jan 2012 17:48:17 +0100 Subject: [PATCH] New wizard design review167001 --- CHANGELOG | 1 + doc/ref/index.rst | 1 + doc/ref/wizard.rst | 200 +++++++++++++++++ doc/topics/wizard.rst | 57 +++++ trytond/ir/__init__.py | 1 + trytond/ir/action.py | 42 ++-- trytond/ir/model.py | 56 +++-- trytond/ir/model.xml | 4 +- trytond/ir/module/module.py | 225 +++++++++---------- trytond/ir/module/module.xml | 33 ++- trytond/ir/session.py | 22 ++ trytond/ir/translation.py | 333 ++++++++++++---------------- trytond/ir/translation.xml | 38 ++-- trytond/ir/ui/view.py | 117 +++++----- trytond/res/ir.xml | 13 ++ trytond/res/user.py | 57 ++--- trytond/res/user.xml | 12 +- trytond/test/__tryton__.py | 1 + trytond/test/wizard.py | 47 ++-- trytond/test/wizard.xml | 19 ++ trytond/tests/test_tryton.py | 6 +- trytond/tests/test_wizard.py | 87 ++++++++ trytond/wizard/wizard.py | 406 +++++++++++++++++++++++++---------- 23 files changed, 1145 insertions(+), 633 deletions(-) create mode 100644 doc/ref/wizard.rst create mode 100644 doc/topics/wizard.rst create mode 100644 trytond/ir/session.py create mode 100644 trytond/test/wizard.xml create mode 100644 trytond/tests/test_wizard.py diff --git a/CHANGELOG b/CHANGELOG index 75de93bf13..8aaaa89d58 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,4 @@ +* New Wizard design * Add pyson.Id * Use XML-RPC struct for Decimal and Date * Remove change_default on fields diff --git a/doc/ref/index.rst b/doc/ref/index.rst index aa1316dd40..5cb5a950fa 100644 --- a/doc/ref/index.rst +++ b/doc/ref/index.rst @@ -8,6 +8,7 @@ API Reference :maxdepth: 1 models/index + wizard pyson transaction tools/singleton diff --git a/doc/ref/wizard.rst b/doc/ref/wizard.rst new file mode 100644 index 0000000000..5e385a73ed --- /dev/null +++ b/doc/ref/wizard.rst @@ -0,0 +1,200 @@ +.. _ref-wizard: +.. module:: trytond.wizard + +====== +Wizard +====== + +A wizard is a `finite state machine`_. + +There is also a more :ref:`practical introduction into wizards +`. + +.. _`finite state machine`: http://en.wikipedia.org/wiki/Finite-state_machine + +.. class:: Wizard() + + This is the base for any wizard. It contains the engine for the finite + state machine. A wizard must have some :class:`State` instance attributes + that the engine will use. + +Class attributes are: + +.. attribute:: Wizard._name + + It contains the unique name to reference the wizard throughout the + platform. + +.. attribute:: Wizard.start_state + + It contains the name of the starting state. + +.. attribute:: Wizard.end_state + + It contains the name of the ending state. + +.. attribute:: Wizard._rpc + + Same as :attribute:`trytond.model.Model._rpc`. + +.. attribute:: Wizard.states + + It contains a dictionary with state name as key and :class:`State` as + value. + +Instance methods are: + +.. method:: Wizard.init(module_name) + + Register the wizard. + +.. method:: Wizard.create() + + Create a session for the wizard and returns a tuple containing the session + id, the starting and ending state. + +.. method:: Wizard.delete(session_id) + + Delete the session. + +.. method:: Wizard.execute(session, data, state_name) + + Execute the wizard for the state. + `session` can be an instance of :class:`Session` or a session id. + `data` is a dictionary with the session data to update. + `active_id`, `active_ids` and `active_model` must be set in the context + according to the records on which the wizard is run. + +======= +Session +======= + +.. class:: Session(wizard, session_id) + + A wizard session contains values of each :class:`StateView` associated to + the wizard. + +Instance attributes are: + +.. attribute:: Session.data + + Raw storage of session data. + +Instance methods are: + +.. method:: Session.save() + + Save the session in database. + +===== +State +===== + +.. class:: State() + + This is the base for any wizard state. + +========= +StateView +========= + +.. class:: StateView(model_name, view, buttons) + + A :class:`StateView` is a state that will display a form in the client. + The form is defined by the :class:`~trytond.model.ModelView` with the name + `model_name`, the `XML` id in `view` and the `buttons`. + +Instance attributes are: + +.. attribute:: StateView.model_name + + The name of the :class:`~trytond.model.ModelView`. + +.. attribute:: StateView.view + + The `XML` id of the form view. + +.. attribute:: StateView.buttons + + The list of :class:`Button` instances to display on the form. + +Instance methods are: + +.. method:: StateView.get_view + + Returns the view definition like + :method:`~trytond.model.ModelView.fields_view_get`. + +.. method:: StateView.get_defaults(wizard, session, state_name, fields) + + Return default values for the fields. + + * wizard is a :class:`Wizard` instance + * session is a :class:`Session` instance + * state_name is the name of the :class:`State` + * fields is the list of field names + +.. method:: StateView.get_buttons(wizard, state_name) + + Returns button definitions of the wizard. + + * wizard is a :class:`Wizard` instance + * state_name is the name of the :class:`StateView` instance + +=============== +StateTransition +=============== + +.. class:: StateTransition() + + A :class:`StateTransition` brings the wizard to the `state` returned by the + method having the same name as the state but starting with `transition_`. + +=========== +StateAction +=========== + +.. class:: StateAction(action_id) + + A :class:`StateAction` is a :class:`StateTransition` which let the client + launch an `ir.action`. This action definition can be customized with a + method on wizard having the same name as the state but starting with `do_`. + +Instance attributes are: + +.. attribute:: StateAction.action_id + + The `XML` id of the `ir.action`. + +Instance methods are: + +.. method:: StateAction.get_action() + + Returns the `ir.action` definition. + +====== +Button +====== + +.. class:: Button(string, state[, icon[, default]]) + + A :class:`Button` is a single object containing the definition of a wizard + button. + +Instance attributes are: + +.. attribute:: Button.string + + The label display on the button. + +.. attribute:: Button.state + + The next state to reach if button is clicked. + +.. attribute:: Button.icon + + The name of the icon to display on the button. + +.. attribute:: Button.default + + A boolean to set it as default on the form. diff --git a/doc/topics/wizard.rst b/doc/topics/wizard.rst new file mode 100644 index 0000000000..8bca9c0373 --- /dev/null +++ b/doc/topics/wizard.rst @@ -0,0 +1,57 @@ +.. _topics-wizard: + +====== +Wizard +====== + +A wizard describes a series of steps defined as :class:`trytond.wizard.State`. +The wizard uses a :class:`trytond.wizard.Session` to store data between states. + +The basics: + + * Each wizard is a Python class that subclasses + :class:`trytond.wizard.Wizard`. + + * The states of the wizard are attributes that are instances of + :class:`trytond.wizard.State`. + +Example +======= + +This example defines a wizard which export translations:: + + from trytond.wizard import Wizard, StateView, StateTransition, Button + + class TranslationExport(Wizard): + "Export translation" + _name = "ir.translation.export" + + start = StateView('ir.translation.export.start', + 'ir.translation_export_start_view_form', [ + Button('Cancel', 'end', 'tryton-cancel'), + Button('Export', 'export', 'tryton-ok', default=True), + ]) + export = StateTransition() + result = StateView('ir.translation.export.result', + 'ir.translation_export_result_view_form', [ + Button('Close', 'end', 'tryton-close'), + ]) + + def transition_export(self, session): + pool = Pool() + translation_obj = pool.get('ir.translation') + file_data = translation_obj.translation_export( + session.start.language.code, session.start.module.name) + session.result.file = buffer(file_data) + return 'result' + + def default_result(self, session, fields): + return { + 'file': session.result.file, + } + + TranslationExport() + +Instantiating the class registers the wizard class in the framework. Later the +class will be instantiated once per database and stored in the +:ref:`Pool `. diff --git a/trytond/ir/__init__.py b/trytond/ir/__init__.py index 0021cd6b4e..7a9fad55a2 100644 --- a/trytond/ir/__init__.py +++ b/trytond/ir/__init__.py @@ -15,3 +15,4 @@ from . import module from .cache import * from .date import * from .trigger import * +from .session import * diff --git a/trytond/ir/action.py b/trytond/ir/action.py index 9bdfaf4685..60e4d83423 100644 --- a/trytond/ir/action.py +++ b/trytond/ir/action.py @@ -60,6 +60,25 @@ class Action(ModelSQL, ModelView): return action.action.id return False + def get_action_values(self, type_, action_id): + action_obj = Pool().get(type_) + action_id = action_obj.search([ + ('action', '=', action_id), + ]) + if not action_id: + return + columns = set(action_obj._columns.keys() + + action_obj._inherit_fields.keys()) + columns.add('icon.rec_name') + to_remove = () + if type_ == 'ir.action.report': + to_remove = ('report_content_custom', 'report_content', + 'style_content') + elif type_ == 'ir.action.act_window': + to_remove = ('domain', 'context', 'search_value') + columns.difference_update(to_remove) + return action_obj.read(action_id[0], list(columns)) + Action() @@ -170,6 +189,7 @@ class ActionKeyword(ModelSQL, ModelView): def get_keyword(self, keyword, value): pool = Pool() + action_obj = pool.get('ir.action') res = [] model, model_id = value @@ -186,25 +206,13 @@ class ActionKeyword(ModelSQL, ModelView): for action_keyword_id in action_keyword_ids: action_keyword = self.browse(action_keyword_id) try: - action_obj = pool.get(action_keyword.action.type) + type_ = action_keyword.action.type except UserError: continue - action_id = action_obj.search([ - ('action', '=', action_keyword.action.id), - ]) - if action_id: - columns = set(action_obj._columns.keys() - + action_obj._inherit_fields.keys()) - columns.add('icon.rec_name') - if action_keyword.action.type == 'ir.action.report': - to_remove = ('report_content_custom', 'report_content', - 'style_content') - elif action_keyword.action.type == 'ir.action.act_window': - to_remove = ('domain', 'context', 'search_value') - else: - to_remove = set() - columns.difference_update(to_remove) - res.append(action_obj.read(action_id[0], list(columns))) + values = action_obj.get_action_values(type_, + action_keyword.action.id) + if values: + res.append(values) return res ActionKeyword() diff --git a/trytond/ir/model.py b/trytond/ir/model.py index d8c58974a5..5a30f5eda2 100644 --- a/trytond/ir/model.py +++ b/trytond/ir/model.py @@ -4,7 +4,7 @@ import datetime import re from ..model import ModelView, ModelSQL, fields from ..report import Report -from ..wizard import Wizard +from ..wizard import Wizard, StateView, StateAction, Button from ..transaction import Transaction from ..cache import Cache from ..pool import Pool @@ -506,9 +506,9 @@ class ModelData(ModelSQL, ModelView): ModelData() -class PrintModelGraphInit(ModelView): - 'Print Model Graph Init' - _name = 'ir.model.print_model_graph.init' +class PrintModelGraphStart(ModelView): + 'Print Model Graph' + _name = 'ir.model.print_model_graph.start' _description = __doc__ level = fields.Integer('Level') filter = fields.Text('Filter', help="Entering a Python " @@ -517,30 +517,29 @@ class PrintModelGraphInit(ModelView): def default_level(self): return 1 -PrintModelGraphInit() +PrintModelGraphStart() class PrintModelGraph(Wizard): _name = 'ir.model.print_model_graph' - states = { - 'init': { - 'result': { - 'type': 'form', - 'object': 'ir.model.print_model_graph.init', - 'state': [ - ('end', 'Cancel', 'tryton-cancel'), - ('print', 'Print', 'tryton-ok', True), - ], - }, - }, - 'print': { - 'result': { - 'type': 'print', - 'report': 'ir.model.graph', - 'state': 'end', - }, - }, - } + + start = StateView('ir.model.print_model_graph.start', + 'ir.print_model_graph_start_view_form', [ + Button('Cancel', 'end', 'tryton-cancel'), + Button('Print', 'print_', 'tryton-ok', default=True), + ]) + print_ = StateAction('ir.report_model_graph') + + def transition_print_(self, session): + return 'end' + + def do_print_(self, session, action): + return action, { + 'id': Transaction().context.get('active_id'), + 'ids': Transaction().context.get('active_ids'), + 'level': session.start.level, + 'filter': session.start.filter, + } PrintModelGraph() @@ -548,16 +547,16 @@ PrintModelGraph() class ModelGraph(Report): _name = 'ir.model.graph' - def execute(self, ids, datas): + def execute(self, ids, data): import pydot pool = Pool() model_obj = pool.get('ir.model') action_report_obj = pool.get('ir.action.report') - if not datas['form']['filter']: + if not data['filter']: filter = None else: - filter = re.compile(datas['form']['filter'], re.VERBOSE) + filter = re.compile(data['filter'], re.VERBOSE) action_report_ids = action_report_obj.search([ ('report_name', '=', self._name) ]) @@ -570,8 +569,7 @@ class ModelGraph(Report): graph = pydot.Dot(fontsize="8") graph.set('center', '1') graph.set('ratio', 'auto') - self.fill_graph(models, graph, level=datas['form']['level'], - filter=filter) + self.fill_graph(models, graph, level=data['level'], filter=filter) data = graph.create(prog='dot', format='png') return ('png', buffer(data), False, action_report.name) diff --git a/trytond/ir/model.xml b/trytond/ir/model.xml index cb2f1c0fef..e1addd8aa5 100644 --- a/trytond/ir/model.xml +++ b/trytond/ir/model.xml @@ -249,8 +249,8 @@ this repository contains the full copyright notices and license terms. --> ir.model.graph - - ir.model.print_model_graph.init + + ir.model.print_model_graph.start form - + @@ -147,6 +147,20 @@ this repository contains the full copyright notices and license terms. --> + + ir.module.module.config_wizard.other + form + + + + + + ]]> + + + Perform Pending Installation/Upgrade ir.module.module.install_upgrade @@ -157,8 +171,8 @@ this repository contains the full copyright notices and license terms. --> ir.module.module,-1 - - ir.module.module.install_upgrade.init + + ir.module.module.install_upgrade.start form - - ir.module.module.install_upgrade.start + + ir.module.module.install_upgrade.done form +