From f5bb8ca8bff6f23981766650434f2f9b65ef0245 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= Date: Mon, 4 Jan 2016 16:04:22 +0100 Subject: [PATCH 1/2] workflows: add possibility to dispatch function according to a variable (#9091) --- tests/test_admin_pages.py | 33 +++++++++ tests/test_workflow_import.py | 50 +++++++++++++ tests/test_workflows.py | 41 +++++++++++ wcs/wf/dispatch.py | 159 +++++++++++++++++++++++++++++++++++++++--- wcs/workflows.py | 4 +- 5 files changed, 278 insertions(+), 9 deletions(-) diff --git a/tests/test_admin_pages.py b/tests/test_admin_pages.py index 75a2c16..b62be08 100644 --- a/tests/test_admin_pages.py +++ b/tests/test_admin_pages.py @@ -1329,6 +1329,39 @@ def test_workflows_add_all_actions(pub): resp = resp.follow() # redirect to items/ resp = resp.follow() # redirect to ./ +def test_workflows_edit_dispatch_action(pub): + create_superuser(pub) + role = create_role() + Workflow.wipe() + workflow = Workflow(name='foo') + workflow.add_status(name='baz') + workflow.store() + + app = login(get_app(pub)) + resp = app.get('/backoffice/workflows/1/') + resp = resp.click('baz') + + resp.forms[0]['type'] = 'Assign a Function' + resp = resp.forms[0].submit() + resp = resp.follow() + + resp = resp.click('Assign a Function') + resp.form['rules$element0$value'].value = 'FOOBAR' + resp.form['rules$element0$role_id'].value = str(role.id) + resp = resp.form.submit('submit') + resp = resp.follow() + resp = resp.follow() + + resp = resp.click('Assign a Function') + assert resp.form['rules$element0$value'].value == 'FOOBAR' + resp.form['rules$element1$value'].value = 'BARFOO' + resp.form['rules$element1$role_id'].value = str(role.id) + resp = resp.form.submit('submit') + + workflow = Workflow.get(workflow.id) + assert workflow.possible_status[0].items[0].rules == [ + {'value': 'FOOBAR', 'role_id': '1'}, {'value': 'BARFOO', 'role_id': '1'}] + def test_workflows_variables(pub): create_superuser(pub) create_role() diff --git a/tests/test_workflow_import.py b/tests/test_workflow_import.py index 8a99c8a..e4dd835 100644 --- a/tests/test_workflow_import.py +++ b/tests/test_workflow_import.py @@ -8,6 +8,7 @@ from wcs import publisher from wcs.workflows import Workflow, CommentableWorkflowStatusItem from wcs.wf.wscall import WebserviceCallStatusItem +from wcs.wf.dispatch import DispatchWorkflowStatusItem from wcs.wf.register_comment import RegisterCommenterWorkflowStatusItem from wcs.roles import Role from wcs.fields import StringField @@ -356,3 +357,52 @@ def test_global_actions(): assert wf2.global_actions[0].triggers[0].roles == [role.id] wf2 = assert_import_export_works(wf, True) + +def test_complex_dispatch_action(): + wf = Workflow(name='status') + st1 = wf.add_status('Status1', 'st1') + + Role.wipe() + + role1 = Role() + role1.name = 'Test Role 1' + role1.store() + + role2 = Role() + role2.name = 'Test Role 2' + role2.store() + + dispatch = DispatchWorkflowStatusItem() + dispatch.id = '_dispatch' + dispatch.role_key = '_receiver' + dispatch.dispatch_type = 'automatic' + dispatch.variable = 'plop' + dispatch.rules = [{'value': 'a', 'role_id': role1.id}, + {'value': 'b', 'role_id': role2.id}] + st1.items.append(dispatch) + dispatch.parent = st1 + + wf2 = assert_import_export_works(wf) + assert wf2.possible_status[0].items[0].variable == dispatch.variable + assert wf2.possible_status[0].items[0].rules == dispatch.rules + assert wf2.possible_status[0].items[0].dispatch_type == 'automatic' + + Role.wipe() + + role3 = Role() + role3.name = 'Test Role 1' + role3.store() + + role4 = Role() + role4.name = 'Test Role 2' + role4.store() + + role1.remove_self() + role2.remove_self() + + xml_export_orig = export_to_indented_xml(wf, include_id=True) + wf2 = Workflow.import_from_xml_tree(xml_export_orig) + assert wf2.possible_status[0].items[0].variable == dispatch.variable + assert wf2.possible_status[0].items[0].rules == [ + {'value': 'a', 'role_id': role3.id}, {'value': 'b', 'role_id': role4.id}] + assert wf2.possible_status[0].items[0].dispatch_type == 'automatic' diff --git a/tests/test_workflows.py b/tests/test_workflows.py index 1b65900..a4347e8 100644 --- a/tests/test_workflows.py +++ b/tests/test_workflows.py @@ -170,6 +170,47 @@ def test_dispatch(pub): item.perform(formdata) assert formdata.workflow_roles == {'_receiver': '1'} +def test_dispatch_auto(pub): + formdef = FormDef() + formdef.name = 'baz' + formdef.fields = [ + StringField(id='1', label='Test', type='string', varname='foo'), + ] + formdef.store() + + item = DispatchWorkflowStatusItem() + item.role_key = '_receiver' + item.dispatch_type = 'automatic' + + formdata = formdef.data_class()() + pub.substitutions.feed(formdata) + item.perform(formdata) + assert not formdata.workflow_roles + + item.variable = 'form_var_foo' + item.rules = [ + {'role_id': '1', 'value': 'foo'}, + {'role_id': '2', 'value': 'bar'}, + ] + + item.perform(formdata) + assert not formdata.workflow_roles + + # no match + formdata.data = {'1': 'XXX'} + item.perform(formdata) + assert not formdata.workflow_roles + + # match + formdata.data = {'1': 'foo'} + item.perform(formdata) + assert formdata.workflow_roles == {'_receiver': '1'} + + # other match + formdata.data = {'1': 'bar'} + item.perform(formdata) + assert formdata.workflow_roles == {'_receiver': '2'} + def test_roles(pub): user = pub.user_class() user.store() diff --git a/wcs/wf/dispatch.py b/wcs/wf/dispatch.py index 7e6df67..f30b8f3 100644 --- a/wcs/wf/dispatch.py +++ b/wcs/wf/dispatch.py @@ -14,9 +14,67 @@ # You should have received a copy of the GNU General Public License # along with this program; if not, see . +import collections +import xml.etree.ElementTree as ET + from qommon.form import * +from qommon import get_logger from wcs.roles import get_user_roles -from wcs.workflows import WorkflowStatusItem, register_item_class +from wcs.workflows import XmlSerialisable, WorkflowStatusItem, register_item_class + + +class AutomaticDispatchRowWidget(CompositeWidget): + def __init__(self, name, value=None, **kwargs): + CompositeWidget.__init__(self, name, value, **kwargs) + if not value: + value = {} + self.add(StringWidget, name='value', title=_('Value'), + value=value.get('value'), **kwargs) + self.add(SingleSelectWidget, name='role_id', title=_('Role'), + value=value.get('role_id'), + options=[(None, '----', None)] + get_user_roles()) + + def _parse(self, request): + if self.get('value') or self.get('role_id'): + self.value = { + 'value': self.get('value'), + 'role_id': self.get('role_id') + } + else: + self.value = None + + +class AutomaticDispatchTableWidget(WidgetListAsTable): + readonly = False + def __init__(self, name, **kwargs): + super(AutomaticDispatchTableWidget, self).__init__(name, + element_type=AutomaticDispatchRowWidget, **kwargs) + + +class RuleNode(XmlSerialisable): + node_name = 'rule' + + def __init__(self, rule={}): + self.role_id = rule.get('role_id') + self.value = rule.get('value') + + def as_dict(self): + return { + 'role_id': self.role_id, + 'value': self.value + } + + def get_parameters(self): + return ('role_id', 'value') + + def role_id_export_to_xml(self, item, charset, include_id=False): + self._role_export_to_xml('role_id', item, charset, + include_id=include_id) + + def role_id_init_with_xml(self, elem, charset, include_id=False): + self._role_init_with_xml('role_id', elem, charset, + include_id=include_id) + class DispatchWorkflowStatusItem(WorkflowStatusItem): description = N_('Assign a Function') @@ -24,9 +82,12 @@ class DispatchWorkflowStatusItem(WorkflowStatusItem): role_id = None role_key = None + dispatch_type = 'manual' + variable = None + rules = None def get_parameters(self): - return ('role_key', 'role_id') + return ('role_key', 'role_id', 'dispatch_type', 'variable', 'rules') def role_id_export_to_xml(self, item, charset, include_id=False): self._role_export_to_xml('role_id', item, charset, @@ -36,6 +97,29 @@ class DispatchWorkflowStatusItem(WorkflowStatusItem): self._role_init_with_xml('role_id', elem, charset, include_id=include_id) + def rules_export_to_xml(self, item, charset, include_id=False): + if not self.rules: + return + + rules_node = ET.SubElement(item, 'rules') + for rule in self.rules: + rules_node.append(RuleNode(rule).export_to_xml(charset=charset, + include_id=include_id)) + + return rules_node + + def rules_init_with_xml(self, elem, charset, include_id=False): + rules = [] + if elem is None: + return + for rule_xml_node in elem.findall('rule'): + rule_node = RuleNode() + rule_node.init_with_xml(rule_xml_node, charset, + include_id=include_id) + rules.append(rule_node.as_dict()) + if rules: + self.rules = rules + def render_as_line(self): if self.role_key: function_label = self.parent.parent.roles.get(self.role_key, '?') @@ -49,18 +133,77 @@ class DispatchWorkflowStatusItem(WorkflowStatusItem): form.add(SingleSelectWidget, '%srole_key' % prefix, title=_('Function to Set'), value=self.role_key, options=[(None, '----')] + self.parent.parent.roles.items()) + dispatch_types = collections.OrderedDict( + [('manual', _('Manual')), ('automatic', _('Automatic'))]) + if 'dispatch_type' in parameters: + form.add(RadiobuttonsWidget, '%sdispatch_type' % prefix, + title=_('Type'), + options=dispatch_types.items(), + value=self.dispatch_type, + required=True, + attrs={'data-dynamic-display-parent': 'true'}) if 'role_id' in parameters: form.add(SingleSelectWidget, '%srole_id' % prefix, title=_('Role'), value=str(self.role_id), - options=[(None, '----', None)] + get_user_roles()) + options=[(None, '----', None)] + get_user_roles(), + attrs={ + 'data-dynamic-display-child-of': '%sdispatch_type' % prefix, + 'data-dynamic-display-value': dispatch_types.get('manual'), + } + ) + if 'variable' in parameters: + form.add(StringWidget, '%svariable' % prefix, + title=_('Variable'), + value=self.variable, + attrs={ + 'data-dynamic-display-child-of': '%sdispatch_type' % prefix, + 'data-dynamic-display-value': dispatch_types.get('automatic'), + } + ) + if 'rules' in parameters: + form.add(AutomaticDispatchTableWidget, '%srules' % prefix, + title=_('Rules'), + value=self.rules, + attrs={ + 'data-dynamic-display-child-of': '%sdispatch_type' % prefix, + 'data-dynamic-display-value': dispatch_types.get('automatic'), + } + ) def perform(self, formdata): - if not (self.role_id and self.role_key): - return if not formdata.workflow_roles: formdata.workflow_roles = {} - formdata.workflow_roles[self.role_key] = str(self.role_id) - formdata.store() -register_item_class(DispatchWorkflowStatusItem) + new_role_id = None + if self.dispatch_type == 'manual' or not self.dispatch_type: + if not (self.role_id and self.role_key): + return + new_role_id = self.role_id + elif self.dispatch_type == 'automatic': + if not (self.role_key and self.variable and self.rules): + return + variables = get_publisher().substitutions.get_context_variables() + # convert the given value to a few different types, to allow more + # diversity in matching. + variable_values = [variables.get(self.variable)] + if not variable_values[0]: + variable_values.append(None) + try: + variable_values.append(int(variable_values[0])) + except (ValueError, TypeError): + pass + + for rule in self.rules: + if rule.get('value') in variable_values: + new_role_id = rule.get('role_id') + break + + if new_role_id: + if not Role.has_key(new_role_id): + get_logger().error('error in dispatch, missing role %s' % new_role_id) + else: + formdata.workflow_roles[self.role_key] = str(new_role_id) + formdata.store() + +register_item_class(DispatchWorkflowStatusItem) diff --git a/wcs/workflows.py b/wcs/workflows.py index 201fe19..f7cc357 100644 --- a/wcs/workflows.py +++ b/wcs/workflows.py @@ -675,10 +675,12 @@ class Workflow(StorableObject): class XmlSerialisable(object): node_name = None + key = None def export_to_xml(self, charset, include_id=False): node = ET.Element(self.node_name) - node.attrib['type'] = self.key + if self.key: + node.attrib['type'] = self.key for attribute in self.get_parameters(): if hasattr(self, '%s_export_to_xml' % attribute): getattr(self, '%s_export_to_xml' % attribute)(node, charset, -- 2.7.0.rc3