From e09070dda0c9d2d76c75e012cc3ea44f60877c77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20=C3=89vrard?= Date: Thu, 11 Aug 2011 15:54:36 +0200 Subject: [PATCH] Use function name in workflow instead of python code --- CHANGELOG | 1 + trytond/backend/mysql/init.sql | 4 +- trytond/backend/postgresql/init.sql | 4 +- trytond/backend/sqlite/init.sql | 4 +- trytond/test/__init__.py | 1 + trytond/test/__tryton__.py | 3 +- trytond/test/workflow.py | 35 +++++++ trytond/test/workflow.xml | 41 ++++++++ trytond/tests/test_workflow.py | 66 +++++++++++++ trytond/workflow/expr.py | 80 --------------- trytond/workflow/workflow.py | 148 +++++++++++++++------------- trytond/workflow/workflow.xml | 26 +++-- 12 files changed, 245 insertions(+), 168 deletions(-) create mode 100644 trytond/test/workflow.py create mode 100644 trytond/test/workflow.xml create mode 100644 trytond/tests/test_workflow.py delete mode 100644 trytond/workflow/expr.py diff --git a/CHANGELOG b/CHANGELOG index db94a58be5..e7cb59ece6 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,4 @@ +* Remove use of python code in workflow XML * Remove ir.action.wizard_size * Rename expand and fill attributes into yexpand and yfill * Add xalign and yalign as field attributes diff --git a/trytond/backend/mysql/init.sql b/trytond/backend/mysql/init.sql index 3e37beb233..d2b3c86eef 100644 --- a/trytond/backend/mysql/init.sql +++ b/trytond/backend/mysql/init.sql @@ -105,12 +105,12 @@ CREATE TABLE wkf_activity ( subflow BIGINT, split_mode VARCHAR(255), join_mode VARCHAR(255), - kind VARCHAR(255) NOT NULL, name VARCHAR(255), signal_send VARCHAR(255), flow_start BOOLEAN NOT NULL, flow_stop BOOLEAN NOT NULL, - action TEXT, + stop_other BOOLEAN NOT NULL, + action VARCHAR(255), PRIMARY KEY(id), CONSTRAINT wkf_activity_workflow_fkey FOREIGN KEY (workflow) REFERENCES wkf (id) ON DELETE CASCADE, CONSTRAINT wkf_activity_subflow_fkey FOREIGN KEY (subflow) REFERENCES wkf (id) ON DELETE SET NULL diff --git a/trytond/backend/postgresql/init.sql b/trytond/backend/postgresql/init.sql index 3e3c4229c5..bb83dbd803 100644 --- a/trytond/backend/postgresql/init.sql +++ b/trytond/backend/postgresql/init.sql @@ -127,12 +127,12 @@ CREATE TABLE wkf_activity ( subflow INTEGER, split_mode VARCHAR NOT NULL, join_mode VARCHAR NOT NULL, - kind VARCHAR NOT NULL, name VARCHAR NOT NULL, signal_send VARCHAR, flow_start BOOLEAN NOT NULL, flow_stop BOOLEAN NOT NULL, - action TEXT, + stop_other BOOLEAN NOT NULL, + action VARCHAR, PRIMARY KEY(id), FOREIGN KEY (workflow) REFERENCES wkf (id) ON DELETE CASCADE, FOREIGN KEY (subflow) REFERENCES wkf (id) ON DELETE SET NULL diff --git a/trytond/backend/sqlite/init.sql b/trytond/backend/sqlite/init.sql index 9d1879c227..e20292c445 100644 --- a/trytond/backend/sqlite/init.sql +++ b/trytond/backend/sqlite/init.sql @@ -90,12 +90,12 @@ CREATE TABLE wkf_activity ( subflow INTEGER, split_mode VARCHAR, join_mode VARCHAR, - kind VARCHAR, name VARCHAR, signal_send VARCHAR, flow_start BOOLEAN, flow_stop BOOLEAN, - action TEXT + stop_other BOOLEAN, + action VARCHAR ); CREATE TABLE wkf_transition ( diff --git a/trytond/test/__init__.py b/trytond/test/__init__.py index e457e73eb7..3b5643eca3 100644 --- a/trytond/test/__init__.py +++ b/trytond/test/__init__.py @@ -9,3 +9,4 @@ from export_data import * from trigger import * from access import * from wizard import * +from workflow import * diff --git a/trytond/test/__tryton__.py b/trytond/test/__tryton__.py index d854433951..90de2fc51e 100644 --- a/trytond/test/__tryton__.py +++ b/trytond/test/__tryton__.py @@ -5,10 +5,11 @@ 'description': '''A module for unittest. ''', 'active': True, - 'depends': ['ir', 'res'], + 'depends': ['ir', 'res', 'workflow'], 'xml': [ 'import_data.xml', 'sequence.xml', + 'workflow.xml', ], 'translation': [ ], diff --git a/trytond/test/workflow.py b/trytond/test/workflow.py new file mode 100644 index 0000000000..4df177e727 --- /dev/null +++ b/trytond/test/workflow.py @@ -0,0 +1,35 @@ +# 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 trytond.model import ModelSQL, ModelWorkflow, fields + + +class WorkflowedModel(ModelWorkflow, ModelSQL): + _name = 'test.workflowed' + + name = fields.Char('Name') + state = fields.Char('State') + value = fields.Integer('Value') + + def default_transitions(self): + return 0 + + def default_value(self): + return 0 + + def workflow_start(self, workflowed): + self.write([workflowed.id], {'state': 'Start'}) + + def workflow_middle(self, workflowed): + self.write([workflowed.id], {'state': 'Middle'}) + + def workflow_end(self, workflowed): + self.write([workflowed.id], {'state': 'End'}) + + def start_middle_ok(self, workflowed): + return workflowed.value > 4 + + def middle_end_ok(self, workflowed): + return workflowed.value < 8 + +WorkflowedModel() diff --git a/trytond/test/workflow.xml b/trytond/test/workflow.xml new file mode 100644 index 0000000000..a19f47af94 --- /dev/null +++ b/trytond/test/workflow.xml @@ -0,0 +1,41 @@ + + + + + + Test workflow + test.workflowed + + + + Start + workflow_start + + + + + Middle + workflow_middle + + + + End + workflow_end + + + + + + + start_middle_ok + + + + + middle_end_ok + + + diff --git a/trytond/tests/test_workflow.py b/trytond/tests/test_workflow.py new file mode 100644 index 0000000000..c7bcb91058 --- /dev/null +++ b/trytond/tests/test_workflow.py @@ -0,0 +1,66 @@ +# This file is part of Tryton. The COPYRIGHT file at the top level of this +# repository contains the full copyright notices and license terms. + +import unittest + +from trytond.transaction import Transaction +from trytond.tests.test_tryton import (POOL, DB_NAME, USER, CONTEXT, + install_module) + + +class WorkflowTestCase(unittest.TestCase): + + def setUp(self): + install_module('test') + self.workflow_obj = POOL.get('test.workflowed') + + def test0010object_creation(self): + 'Test workflow object creation' + with Transaction().start(DB_NAME, USER, CONTEXT) as transaction: + wkf_id = self.workflow_obj.create({ + 'name': 'value = 3', + 'value': 3, + }) + self.assertEqual(self.workflow_obj.read(wkf_id)['state'], 'Start') + + wkf_id = self.workflow_obj.create({ + 'name': 'value = 10', + 'value': 10, + }) + self.assertEqual(self.workflow_obj.read(wkf_id)['state'], 'Middle') + + wkf_id = self.workflow_obj.create({ + 'name': 'value = 6', + 'value': 6, + }) + self.assertEqual(self.workflow_obj.read(wkf_id)['state'], 'End') + + transaction.cursor.rollback() + + def test0020object_modification(self): + 'Test workflow object modification' + with Transaction().start(DB_NAME, USER, CONTEXT): + wkf_id = self.workflow_obj.create({ + 'name': 'value = 3', + 'value': 3, + }) + wkf_id = self.workflow_obj.create({ + 'name': 'value = 10', + 'value': 10, + }) + + wkf_id, = self.workflow_obj.search([('name', '=', 'value = 3')]) + self.workflow_obj.write(wkf_id, {'value': 11}) + self.assertEqual(self.workflow_obj.read(wkf_id)['state'], 'Middle') + + wkf_id, = self.workflow_obj.search([('name', '=', 'value = 10')]) + self.workflow_obj.write(wkf_id, {'value': 6}) + self.assertEqual(self.workflow_obj.read(wkf_id)['state'], 'End') + + +def suite(): + return unittest.TestLoader().loadTestsFromTestCase(WorkflowTestCase) + + +if __name__ == '__main__': + unittest.TextTestRunner(verbosity=2).run(suite()) diff --git a/trytond/workflow/expr.py b/trytond/workflow/expr.py deleted file mode 100644 index 268d1b0986..0000000000 --- a/trytond/workflow/expr.py +++ /dev/null @@ -1,80 +0,0 @@ -#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 trytond.pool import Pool -from trytond.tools import safe_eval -from trytond.transaction import Transaction - - -class EnvCall(object): - - def __init__(self, obj, method, obj_id): - self.obj = obj - self.method = method - self.obj_id = obj_id - - def __call__(self, *args, **kargs): - return getattr(self.obj, self.method)(self.obj_id, *args, **kargs) - - -class Env(dict): - - def __init__(self, model, obj_id): - super(Env, self).__init__() - self.model = model - self.obj_id = obj_id - self.obj = Pool(Transaction().cursor.dbname).get(model) - self.browse = self.obj.browse(obj_id) - self.columns = self.obj._columns.keys() + \ - self.obj._inherit_fields.keys() - - def __getitem__(self, key): - if key == 'context': - return Transaction().context - elif key in self.columns: - res = self.browse[key] - return res - elif key in dir(self.obj): - return EnvCall(self.obj, key, self.obj_id) - else: - return super(Env, self).__getitem__(key) - -def eval_expr(model, obj_id, action): - res = False - env = Env(model, obj_id) - for line in action.split('\n'): - if line == 'True': - res = True - elif line =='False': - res = False - else: - res = safe_eval(line, env) - return res - -def execute(model, obj_id, activity): - ''' - Execute - - :param model: the model name - :param obj_id: the record id - :param activity: a BrowseRecord of workflow.activity - ''' - return eval_expr(model, obj_id, activity.action) - -def check(model, obj_id, transition, signal): - ''' - Check - - :param model: the model name - :param obj_id: the record id - :param transition: a BrowseRecord of workflow.transition - ''' - if transition.signal: - if signal != transition.signal: - return False - - if transition.group and Transaction().user != 0: - user_obj = Pool(Transaction().cursor.dbname).get('res.user') - user_groups = user_obj.get_groups() - if transition.group.id not in user_groups: - return False - return eval_expr(model, obj_id, transition.condition) diff --git a/trytond/workflow/workflow.py b/trytond/workflow/workflow.py index c8969d93f9..ad4592a896 100644 --- a/trytond/workflow/workflow.py +++ b/trytond/workflow/workflow.py @@ -1,13 +1,12 @@ #This file is part of Tryton. The COPYRIGHT file at the top level of #this repository contains the full copyright notices and license terms. import os -import expr import base64 from trytond.model import ModelView, ModelSQL, fields from trytond.report import Report from trytond.tools import exec_command_pipe from trytond.backend import TableHandler -from trytond.pyson import Eval, Equal, Not +from trytond.pyson import Eval, Equal, Not, Bool from trytond.transaction import Transaction from trytond.pool import Pool @@ -63,30 +62,27 @@ class WorkflowActivity(ModelSQL, ModelView): ('XOR', 'Xor'), ('AND', 'And'), ], 'Join Mode', required=True) - kind = fields.Selection([ - ('dummy', 'Dummy'), - ('function', 'Function'), - ('subflow', 'Subflow'), - ('stopall', 'Stop All'), - ], 'Kind', required=True) - action = fields.Text('Action', states={ - 'readonly': Equal(Eval('kind'), 'dummy'), - 'required': Equal(Eval('kind'), 'function'), - }) + method = fields.Char('Method') flow_start = fields.Boolean('Flow Start') flow_stop = fields.Boolean('Flow Stop') - subflow = fields.Many2One('workflow', 'Subflow', states={ - 'readonly': Not(Equal(Eval('kind'), 'subflow')), - 'required': Equal(Eval('kind'), 'subflow'), - }) + stop_other = fields.Boolean('Stop Other') + subflow = fields.Many2One('workflow', 'Subflow') signal_send = fields.Char('Signal (subflow.*)') out_transitions = fields.One2Many('workflow.transition', 'act_from', 'Outgoing transitions') in_transitions = fields.One2Many('workflow.transition', 'act_to', 'Incoming transitions') - #TODO add a _constraint on subflow without action - #to have the same model than the workflow + def init(self, module_name): + super(WorkflowActivity, self).init(module_name) + cursor = Transaction().cursor + table = TableHandler(cursor, self, module_name) + + # Migration from 2.0 + if table.column_exist('kind'): + cursor.execute('UPDATE "%s" SET stop_other = %%s ' + "WHERE kind='stopall'" % self._table, (True,)) + table.drop_column('kind', exception=True) def default_kind(self): return 'dummy' @@ -103,6 +99,9 @@ class WorkflowActivity(ModelSQL, ModelView): def default_flow_stop(self): return False + def default_stop_other(self): + return False + WorkflowActivity() @@ -113,7 +112,9 @@ class WorkflowTransition(ModelSQL, ModelView): _rec_name = 'signal' _description = __doc__ trigger_model = fields.Char('Trigger Type') - trigger_expr_id = fields.Char('Trigger Expr ID') + trigger_ids = fields.Char('Trigger Expr ID', states={ + 'required': Bool(Eval('trigger_model')), + }) signal = fields.Char('Signal (button Name)') group = fields.Many2One('res.group', 'Group Required') condition = fields.Char('Condition', required=True) @@ -125,7 +126,7 @@ class WorkflowTransition(ModelSQL, ModelView): 'trans_id', 'inst_id', 'Instances') def default_condition(self): - return 'True' + return '' WorkflowTransition() @@ -327,19 +328,20 @@ class WorkflowWorkitem(ModelSQL, ModelView): triggers = triggers and not res if triggers: - for transition in activity.out_transitions: - if transition.trigger_model: - ids = expr.eval_expr( workitem.instance.res_type, - workitem.instance.res_id, - transition.trigger_expr_id) - with Transaction().set_user(0): - for res_id in ids: - trigger_obj.create({ - 'model': transition.trigger_model, - 'res_id': res_id, - 'instance': workitem.instance.id, - 'workitem': workitem.id, - }) + trigger_transitions = (t for t in activity.out_transitions + if t.trigger_model) + for transition in trigger_transitions: + model_obj = pool.get(workitem.instance.res_type) + model_ids_fct = getattr(model_obj, transition.trigger_ids) + ids = model_ids_fct(model_obj.browse(workitem.instance.res_id)) + with Transaction().set_user(0): + for res_id in ids: + trigger_obj.create({ + 'model': transition.trigger_model, + 'res_id': res_id, + 'instance': workitem.instance.id, + 'workitem': workitem.id, + }) return True def _state_set(self, workitem, state): @@ -358,18 +360,9 @@ class WorkflowWorkitem(ModelSQL, ModelView): instance_obj.validate(overflow.instance, activity.signal_send, force_running=True) - if activity.kind == 'dummy': - if workitem.state == 'active': - self._state_set(workitem, 'complete') - elif activity.kind == 'function': - if workitem.state == 'active': - self._state_set(workitem, 'running') - expr.execute(workitem.instance.res_type, - workitem.instance.res_id, activity) - self._state_set(workitem, 'complete') - elif activity.kind == 'stopall': - if workitem.state == 'active': - self._state_set(workitem, 'running') + if workitem.state == 'active': + self._state_set(workitem, 'running') + if activity.stop_other: ids = self.search([ ('instance', '=', workitem.instance.id), ('id', '!=', workitem.id), @@ -377,41 +370,64 @@ class WorkflowWorkitem(ModelSQL, ModelView): #XXX check if delete must not be replace by _state_set 'complete' with Transaction().set_user(0): self.delete(ids) - if activity.action: - expr.execute(workitem.instance.res_type, - workitem.instance.res_id, activity) - self._state_set(workitem, 'complete') - elif activity.kind == 'subflow': - if workitem.state == 'active': - self._state_set(workitem, 'running') - if activity.action: - id_new = expr.execute(workitem.instance.res_type, - workitem.instance.res_id, activity) - if not id_new: - with Transaction().set_user(0): - self.delete(workitem.id) - return False + if activity.method: + id_new = self._execute_action(workitem, activity) + else: + id_new = None + if activity.subflow: + if activity.method and id_new: with Transaction().set_user(0): instance_id = instance_obj.search([ ('res_id', '=', id_new), ('workflow', '=', activity.subflow.id), ], limit=1)[0] - else: + elif not activity.method: instance_id = instance_obj.create({ 'res_type': workitem.instance.res_type, 'res_id': workitem.instance.res_id, 'workflow': activity.subflow.id, }) + else: + with Transaction().set_user(0): + self.delete(workitem.id) + return False self.write(workitem.id, { 'subflow': instance_id, }) #XXX must be changed with a cache reset on BrowseRecord workitem._data[workitem.id]['subflow'] = instance_id - elif workitem.state == 'running': - if workitem.subflow.state == 'complete': - self._state_set(workitem, 'complete') + else: + self._state_set(workitem, 'complete') + elif workitem.state == 'running' and activity.subflow: + if workitem.subflow.state == 'complete': + self._state_set(workitem, 'complete') + return True + def _execute_action(self, workitem, activity): + model_obj = Pool().get(workitem.instance.res_type) + wkf_action = getattr(model_obj, activity.method) + return wkf_action(model_obj.browse(workitem.instance.res_id)) + + def _activate_transition(self, workitem, transition, signal): + pool = Pool() + user_obj = pool.get('res.user') + if transition.signal: + if signal != transition.signal: + return False + + if transition.group and Transaction().user != 0: + user_groups = user_obj.get_groups() + if transition.group.id not in user_groups: + return False + + model_obj = pool.get(workitem.instance.res_type) + if transition.condition: + test_fct = getattr(model_obj, transition.condition) + return test_fct(model_obj.browse(workitem.instance.res_id)) + else: + return True + def _split_test(self, workitem, split_mode, signal=None): pool = Pool() instance_obj = pool.get('workflow.instance') @@ -419,8 +435,7 @@ class WorkflowWorkitem(ModelSQL, ModelView): transitions = [] if split_mode == 'XOR' or split_mode == 'OR': for transition in workitem.activity.out_transitions: - if expr.check(workitem.instance.res_type, - workitem.instance.res_id, transition, signal): + if self._activate_transition(workitem, transition, signal): test = True transitions.append(transition) if split_mode == 'XOR': @@ -428,8 +443,7 @@ class WorkflowWorkitem(ModelSQL, ModelView): else: test = True for transition in workitem.activity.out_transitions: - if not expr.check(workitem.instance.res_type, - workitem.instance.res_id, transition, signal): + if not self._activate_transition(workitem, transition, signal): test = False break if transition.id not in \ diff --git a/trytond/workflow/workflow.xml b/trytond/workflow/workflow.xml index 31087c0115..2fc046f836 100644 --- a/trytond/workflow/workflow.xml +++ b/trytond/workflow/workflow.xml @@ -88,10 +88,8 @@ this repository contains the full copyright notices and license terms. -->