workflows: allow queryset as id for external workflow action (#56847)
This commit is contained in:
parent
b6623678cb
commit
5f3ada3bd8
|
@ -60,6 +60,7 @@ from wcs.wf.create_formdata import CreateFormdataWorkflowStatusItem, Mapping
|
|||
from wcs.wf.criticality import MODE_DEC, MODE_INC, MODE_SET, ModifyCriticalityWorkflowStatusItem
|
||||
from wcs.wf.dispatch import DispatchWorkflowStatusItem
|
||||
from wcs.wf.export_to_model import ExportToModel, transform_to_pdf
|
||||
from wcs.wf.external_workflow import ManyExternalCallsPart
|
||||
from wcs.wf.form import FormWorkflowStatusItem, WorkflowFormFieldsFormDef
|
||||
from wcs.wf.geolocate import GeolocateWorkflowStatusItem
|
||||
from wcs.wf.jump import JumpWorkflowStatusItem, _apply_timeouts
|
||||
|
@ -6462,3 +6463,143 @@ def test_form_update_after_backoffice_fields(sql_pub):
|
|||
|
||||
conn.commit()
|
||||
cur.close()
|
||||
|
||||
|
||||
def test_call_external_workflow_manual_queryset_targeting(two_pubs):
|
||||
if not two_pubs.is_using_postgresql():
|
||||
pytest.skip('this requires SQL')
|
||||
return
|
||||
|
||||
FormDef.wipe()
|
||||
CardDef.wipe()
|
||||
two_pubs.loggederror_class.wipe()
|
||||
|
||||
# carddef workflow, with global action to increment a counter in its
|
||||
# backoffice fields.
|
||||
carddef_wf = Workflow(name='Carddef Workflow')
|
||||
carddef_wf.add_status(name='New')
|
||||
carddef_wf.backoffice_fields_formdef = WorkflowBackofficeFieldsFormDef(carddef_wf)
|
||||
carddef_wf.backoffice_fields_formdef.fields = [
|
||||
StringField(id='bo0', varname='bo', type='string', label='bo variable'),
|
||||
]
|
||||
global_action = carddef_wf.add_global_action('Update')
|
||||
global_action.append_item('set-backoffice-fields')
|
||||
setbo = global_action.items[0]
|
||||
setbo.fields = [{'field_id': 'bo0', 'value': '{{ form_var_bo|default:"0"|add:1 }}'}]
|
||||
trigger = global_action.append_trigger('webservice') # external call
|
||||
trigger.identifier = 'update'
|
||||
carddef_wf.store()
|
||||
|
||||
# associated carddef
|
||||
carddef = CardDef()
|
||||
carddef.name = 'Data'
|
||||
carddef.fields = [
|
||||
StringField(id='0', label='string', varname='card_string'),
|
||||
]
|
||||
carddef.workflow = carddef_wf
|
||||
carddef.store()
|
||||
carddef.data_class().wipe()
|
||||
|
||||
# and sample carddatas
|
||||
for i in range(1, 5):
|
||||
carddata = carddef.data_class()()
|
||||
carddata.data = {'0': 'Text %s' % i}
|
||||
carddata.store()
|
||||
carddata.just_created()
|
||||
carddata.store()
|
||||
|
||||
# formdef workflow that will trigger the global action
|
||||
wf = Workflow(name='External actions')
|
||||
wf.add_status('Blah')
|
||||
update_global_action = wf.add_global_action('Update linked object data')
|
||||
update_action = update_global_action.append_item('external_workflow_global_action')
|
||||
update_action.slug = 'carddef:%s' % carddef.url_name
|
||||
update_action.target_mode = 'manual'
|
||||
update_action.target_id = None # not configured
|
||||
update_action.trigger_id = 'action:update'
|
||||
wf.store()
|
||||
|
||||
# associated formdef
|
||||
formdef = FormDef()
|
||||
formdef.name = 'External action form'
|
||||
formdef.fields = []
|
||||
formdef.workflow = wf
|
||||
formdef.store()
|
||||
|
||||
# and formdata
|
||||
formdata = formdef.data_class()()
|
||||
formdata.data = {}
|
||||
formdata.store()
|
||||
formdata.just_created()
|
||||
|
||||
# target not configured
|
||||
perform_items([update_action], formdata)
|
||||
assert carddef.data_class().count() == 4
|
||||
assert carddef.data_class().get(1).data['bo0'] is None
|
||||
assert carddef.data_class().get(2).data['bo0'] is None
|
||||
assert carddef.data_class().get(3).data['bo0'] is None
|
||||
assert carddef.data_class().get(4).data['bo0'] is None
|
||||
|
||||
# target all cards
|
||||
update_action.target_id = '{{cards|objects:"%s"}}' % carddef.url_name
|
||||
wf.store()
|
||||
perform_items([update_action], formdata)
|
||||
assert carddef.data_class().get(1).data['bo0'] == '1'
|
||||
assert carddef.data_class().get(2).data['bo0'] == '1'
|
||||
assert carddef.data_class().get(3).data['bo0'] == '1'
|
||||
assert carddef.data_class().get(4).data['bo0'] == '1'
|
||||
status_part = [x for x in formdata.evolution[-1].parts if isinstance(x, ManyExternalCallsPart)][0]
|
||||
assert status_part.running is False
|
||||
assert status_part.is_hidden() is True
|
||||
assert '4 processed' in str(status_part.view())
|
||||
assert set(status_part.processed_ids) == {x.get_display_id() for x in carddef.data_class().select()}
|
||||
|
||||
# target some cards
|
||||
update_action.target_id = (
|
||||
'{{cards|objects:"%s"|filter_by:"card_string"|filter_value:"Text 2"}}' % carddef.url_name
|
||||
)
|
||||
wf.store()
|
||||
perform_items([update_action], formdata)
|
||||
assert carddef.data_class().get(1).data['bo0'] == '1'
|
||||
assert carddef.data_class().get(2).data['bo0'] == '2'
|
||||
assert carddef.data_class().get(3).data['bo0'] == '1'
|
||||
assert carddef.data_class().get(4).data['bo0'] == '1'
|
||||
|
||||
# target a single formdata
|
||||
update_action.target_id = (
|
||||
'{{cards|objects:"%s"|filter_by:"card_string"|filter_value:"Text 2"|first}}' % carddef.url_name
|
||||
)
|
||||
wf.store()
|
||||
perform_items([update_action], formdata)
|
||||
assert carddef.data_class().get(1).data['bo0'] == '1'
|
||||
assert carddef.data_class().get(2).data['bo0'] == '3'
|
||||
assert carddef.data_class().get(3).data['bo0'] == '1'
|
||||
assert carddef.data_class().get(4).data['bo0'] == '1'
|
||||
|
||||
# mismatch in target
|
||||
carddef2 = CardDef()
|
||||
carddef2.name = 'Other data'
|
||||
carddef2.fields = []
|
||||
carddef2.workflow = carddef_wf
|
||||
carddef2.store()
|
||||
|
||||
update_action.slug = 'carddef:%s' % carddef2.url_name
|
||||
update_action.target_id = (
|
||||
'{{cards|objects:"%s"|filter_by:"card_string"|filter_value:"Text 2"}}' % carddef.url_name
|
||||
)
|
||||
wf.store()
|
||||
perform_items([update_action], formdata)
|
||||
assert two_pubs.loggederror_class.count() == 1
|
||||
logged_error = two_pubs.loggederror_class.select()[0]
|
||||
assert logged_error.summary == 'Mismatch in target objects: expected "Other data", got "Data"'
|
||||
|
||||
# mismatch in target, with formdata
|
||||
two_pubs.loggederror_class.wipe()
|
||||
update_action.target_id = (
|
||||
'{{cards|objects:"%s"|filter_by:"card_string"|filter_value:"Text 2"|first}}' % carddef.url_name
|
||||
)
|
||||
wf.store()
|
||||
perform_items([update_action], formdata)
|
||||
assert two_pubs.loggederror_class.count() == 1
|
||||
logged_error = two_pubs.loggederror_class.select()[0]
|
||||
assert logged_error.summary == 'Mismatch in target object: expected "Other data", got "Data"'
|
||||
|
|
|
@ -169,6 +169,11 @@ class FormDef(StorableObject):
|
|||
super().__init__(*args, **kwargs)
|
||||
self.fields = []
|
||||
|
||||
def __eq__(self, other):
|
||||
return bool(
|
||||
isinstance(other, FormDef) and self.xml_root_node == other.xml_root_node and self.id == other.id
|
||||
)
|
||||
|
||||
def migrate(self):
|
||||
changed = False
|
||||
|
||||
|
|
|
@ -14,13 +14,18 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from quixote import get_publisher
|
||||
import uuid
|
||||
|
||||
from quixote import get_publisher, get_request
|
||||
from quixote.html import TemplateIO, htmltext
|
||||
|
||||
from wcs.carddef import CardDef
|
||||
from wcs.formdef import FormDef
|
||||
from wcs.qommon import _
|
||||
from wcs.qommon.form import ComputedExpressionWidget, Form, RadiobuttonsWidget, SingleSelectWidget
|
||||
from wcs.variables import LazyFormData, LazyFormDefObjectsManager
|
||||
from wcs.workflows import (
|
||||
EvolutionPart,
|
||||
Workflow,
|
||||
WorkflowGlobalActionWebserviceTrigger,
|
||||
WorkflowStatusItem,
|
||||
|
@ -29,6 +34,34 @@ from wcs.workflows import (
|
|||
)
|
||||
|
||||
|
||||
class ManyExternalCallsPart(EvolutionPart):
|
||||
processed_ids = None
|
||||
label = None
|
||||
running = True
|
||||
uuid = None
|
||||
|
||||
def __init__(self, label):
|
||||
self.label = label
|
||||
self.uuid = str(uuid.uuid4())
|
||||
self.processed_ids = []
|
||||
|
||||
def is_hidden(self):
|
||||
return bool(not self.running) or not (
|
||||
get_request() and get_request().get_path().startswith('/backoffice/')
|
||||
)
|
||||
|
||||
def view(self):
|
||||
r = TemplateIO(html=True)
|
||||
r += htmltext('<div>')
|
||||
r += (
|
||||
htmltext('<p>%s</p>')
|
||||
% _('Running external actions on "%(label)s" (%(count)s processed)')
|
||||
% {'label': self.label, 'count': len(self.processed_ids)}
|
||||
)
|
||||
r += htmltext('</div>')
|
||||
return r.getvalue()
|
||||
|
||||
|
||||
class ExternalWorkflowGlobalAction(WorkflowStatusItem):
|
||||
|
||||
description = _('External workflow')
|
||||
|
@ -165,12 +198,44 @@ class ExternalWorkflowGlobalAction(WorkflowStatusItem):
|
|||
return
|
||||
|
||||
objectdef = self.get_object_def()
|
||||
target_id = self.compute(self.target_id, formdata=formdata, status_item=self)
|
||||
with get_publisher().complex_data():
|
||||
target_id = self.compute(self.target_id, formdata=formdata, status_item=self, allow_complex=True)
|
||||
if target_id:
|
||||
target_id = get_publisher().get_cached_complex_data(target_id)
|
||||
|
||||
if isinstance(target_id, LazyFormData):
|
||||
if target_id._formdef != objectdef:
|
||||
# abort if it's not the correct formdef/carddef
|
||||
get_publisher().record_error(
|
||||
_('Mismatch in target object: expected "%(object_name)s", got "%(object_name2)s"')
|
||||
% {'object_name': objectdef.name, 'object_name2': target_id._formdef.name},
|
||||
formdata=formdata,
|
||||
status_item=self,
|
||||
)
|
||||
return
|
||||
|
||||
yield target_id._formdata
|
||||
return
|
||||
|
||||
if isinstance(target_id, LazyFormDefObjectsManager):
|
||||
if target_id._formdef != objectdef:
|
||||
# abort if it's not the correct formdef/carddef
|
||||
get_publisher().record_error(
|
||||
_('Mismatch in target objects: expected "%(object_name)s", got "%(object_name2)s"')
|
||||
% {'object_name': objectdef.name, 'object_name2': target_id._formdef.name},
|
||||
formdata=formdata,
|
||||
status_item=self,
|
||||
)
|
||||
return
|
||||
for lazy_formdata in target_id:
|
||||
yield lazy_formdata._formdata
|
||||
return
|
||||
|
||||
if not target_id:
|
||||
return
|
||||
|
||||
try:
|
||||
return objectdef.data_class().get(target_id)
|
||||
yield objectdef.data_class().get(target_id)
|
||||
except KeyError as e:
|
||||
# use custom error message depending on target type
|
||||
get_publisher().record_error(
|
||||
|
@ -183,13 +248,12 @@ class ExternalWorkflowGlobalAction(WorkflowStatusItem):
|
|||
|
||||
def iter_target_datas(self, formdata, objectdef):
|
||||
if self.target_mode == 'manual':
|
||||
# return only target
|
||||
target = self.get_manual_target(formdata)
|
||||
if target:
|
||||
yield target
|
||||
return
|
||||
|
||||
yield from formdata.iter_target_datas(objectdef=objectdef, object_type=self.slug, status_item=self)
|
||||
# return targets
|
||||
yield from self.get_manual_target(formdata)
|
||||
else:
|
||||
yield from formdata.iter_target_datas(
|
||||
objectdef=objectdef, object_type=self.slug, status_item=self
|
||||
)
|
||||
|
||||
def get_parameters(self):
|
||||
return ('slug', 'trigger_id', 'target_mode', 'target_id', 'condition')
|
||||
|
@ -218,7 +282,8 @@ class ExternalWorkflowGlobalAction(WorkflowStatusItem):
|
|||
caller_source = CallerSource(formdata)
|
||||
|
||||
formdata.store()
|
||||
for target_data in self.iter_target_datas(formdata, objectdef):
|
||||
status_part = ManyExternalCallsPart(label=objectdef.name)
|
||||
for i, target_data in enumerate(self.iter_target_datas(formdata, objectdef)):
|
||||
with get_publisher().substitutions.temporary_feed(target_data):
|
||||
get_publisher().substitutions.reset()
|
||||
get_publisher().substitutions.feed(get_publisher())
|
||||
|
@ -227,9 +292,35 @@ class ExternalWorkflowGlobalAction(WorkflowStatusItem):
|
|||
get_publisher().substitutions.feed(caller_source)
|
||||
perform_items(trigger.parent.items, target_data)
|
||||
|
||||
# update local object as it may have been modified by target_data
|
||||
# workflow executions.
|
||||
formdata.refresh_from_storage()
|
||||
# update local object as it may have been modified by target_data
|
||||
# workflow executions.
|
||||
formdata.refresh_from_storage()
|
||||
|
||||
if i == 0:
|
||||
# if there are iterations, add tracking status to object
|
||||
formdata.evolution[-1].add_part(status_part)
|
||||
elif i:
|
||||
# get status object back
|
||||
for evolution in reversed(formdata.evolution):
|
||||
try:
|
||||
status_part = [
|
||||
x
|
||||
for x in evolution.parts
|
||||
if isinstance(x, ManyExternalCallsPart) and x.uuid == status_part.uuid
|
||||
][0]
|
||||
except IndexError:
|
||||
# probably the status changed and the tracking object is no longer available,
|
||||
# do without
|
||||
continue
|
||||
break
|
||||
|
||||
status_part.processed_ids.append(target_data.get_display_id())
|
||||
# after iterating, store
|
||||
formdata.store()
|
||||
|
||||
# note it's now done.
|
||||
status_part.running = False
|
||||
formdata.store()
|
||||
|
||||
|
||||
register_item_class(ExternalWorkflowGlobalAction)
|
||||
|
|
Loading…
Reference in New Issue