0001-workflows-allow-queryset-as-id-for-external-workflow.patch
tests/workflow/test_all.py | ||
---|---|---|
60 | 60 |
from wcs.wf.criticality import MODE_DEC, MODE_INC, MODE_SET, ModifyCriticalityWorkflowStatusItem |
61 | 61 |
from wcs.wf.dispatch import DispatchWorkflowStatusItem |
62 | 62 |
from wcs.wf.export_to_model import ExportToModel, transform_to_pdf |
63 |
from wcs.wf.external_workflow import ManyExternalCallsPart |
|
63 | 64 |
from wcs.wf.form import FormWorkflowStatusItem, WorkflowFormFieldsFormDef |
64 | 65 |
from wcs.wf.geolocate import GeolocateWorkflowStatusItem |
65 | 66 |
from wcs.wf.jump import JumpWorkflowStatusItem, _apply_timeouts |
... | ... | |
6462 | 6463 | |
6463 | 6464 |
conn.commit() |
6464 | 6465 |
cur.close() |
6466 | ||
6467 | ||
6468 |
def test_call_external_workflow_manual_queryset_targeting(two_pubs): |
|
6469 |
if not two_pubs.is_using_postgresql(): |
|
6470 |
pytest.skip('this requires SQL') |
|
6471 |
return |
|
6472 | ||
6473 |
FormDef.wipe() |
|
6474 |
CardDef.wipe() |
|
6475 |
two_pubs.loggederror_class.wipe() |
|
6476 | ||
6477 |
# carddef workflow, with global action to increment a counter in its |
|
6478 |
# backoffice fields. |
|
6479 |
carddef_wf = Workflow(name='Carddef Workflow') |
|
6480 |
carddef_wf.add_status(name='New') |
|
6481 |
carddef_wf.backoffice_fields_formdef = WorkflowBackofficeFieldsFormDef(carddef_wf) |
|
6482 |
carddef_wf.backoffice_fields_formdef.fields = [ |
|
6483 |
StringField(id='bo0', varname='bo', type='string', label='bo variable'), |
|
6484 |
] |
|
6485 |
global_action = carddef_wf.add_global_action('Update') |
|
6486 |
global_action.append_item('set-backoffice-fields') |
|
6487 |
setbo = global_action.items[0] |
|
6488 |
setbo.fields = [{'field_id': 'bo0', 'value': '{{ form_var_bo|default:"0"|add:1 }}'}] |
|
6489 |
trigger = global_action.append_trigger('webservice') # external call |
|
6490 |
trigger.identifier = 'update' |
|
6491 |
carddef_wf.store() |
|
6492 | ||
6493 |
# associated carddef |
|
6494 |
carddef = CardDef() |
|
6495 |
carddef.name = 'Data' |
|
6496 |
carddef.fields = [ |
|
6497 |
StringField(id='0', label='string', varname='card_string'), |
|
6498 |
] |
|
6499 |
carddef.workflow = carddef_wf |
|
6500 |
carddef.store() |
|
6501 |
carddef.data_class().wipe() |
|
6502 | ||
6503 |
# and sample carddatas |
|
6504 |
for i in range(1, 5): |
|
6505 |
carddata = carddef.data_class()() |
|
6506 |
carddata.data = {'0': 'Text %s' % i} |
|
6507 |
carddata.store() |
|
6508 |
carddata.just_created() |
|
6509 |
carddata.store() |
|
6510 | ||
6511 |
# formdef workflow that will trigger the global action |
|
6512 |
wf = Workflow(name='External actions') |
|
6513 |
wf.add_status('Blah') |
|
6514 |
update_global_action = wf.add_global_action('Update linked object data') |
|
6515 |
update_action = update_global_action.append_item('external_workflow_global_action') |
|
6516 |
update_action.slug = 'carddef:%s' % carddef.url_name |
|
6517 |
update_action.target_mode = 'manual' |
|
6518 |
update_action.target_id = None # not configured |
|
6519 |
update_action.trigger_id = 'action:update' |
|
6520 |
wf.store() |
|
6521 | ||
6522 |
# associated formdef |
|
6523 |
formdef = FormDef() |
|
6524 |
formdef.name = 'External action form' |
|
6525 |
formdef.fields = [] |
|
6526 |
formdef.workflow = wf |
|
6527 |
formdef.store() |
|
6528 | ||
6529 |
# and formdata |
|
6530 |
formdata = formdef.data_class()() |
|
6531 |
formdata.data = {} |
|
6532 |
formdata.store() |
|
6533 |
formdata.just_created() |
|
6534 | ||
6535 |
# target not configured |
|
6536 |
perform_items([update_action], formdata) |
|
6537 |
assert carddef.data_class().count() == 4 |
|
6538 |
assert carddef.data_class().get(1).data['bo0'] is None |
|
6539 |
assert carddef.data_class().get(2).data['bo0'] is None |
|
6540 |
assert carddef.data_class().get(3).data['bo0'] is None |
|
6541 |
assert carddef.data_class().get(4).data['bo0'] is None |
|
6542 | ||
6543 |
# target all cards |
|
6544 |
update_action.target_id = '{{cards|objects:"%s"}}' % carddef.url_name |
|
6545 |
wf.store() |
|
6546 |
perform_items([update_action], formdata) |
|
6547 |
assert carddef.data_class().get(1).data['bo0'] == '1' |
|
6548 |
assert carddef.data_class().get(2).data['bo0'] == '1' |
|
6549 |
assert carddef.data_class().get(3).data['bo0'] == '1' |
|
6550 |
assert carddef.data_class().get(4).data['bo0'] == '1' |
|
6551 |
status_part = [x for x in formdata.evolution[-1].parts if isinstance(x, ManyExternalCallsPart)][0] |
|
6552 |
assert status_part.running is False |
|
6553 |
assert status_part.is_hidden() is True |
|
6554 |
assert '4 processed' in str(status_part.view()) |
|
6555 | ||
6556 |
# target some cards |
|
6557 |
update_action.target_id = ( |
|
6558 |
'{{cards|objects:"%s"|filter_by:"card_string"|filter_value:"Text 2"}}' % carddef.url_name |
|
6559 |
) |
|
6560 |
wf.store() |
|
6561 |
perform_items([update_action], formdata) |
|
6562 |
assert carddef.data_class().get(1).data['bo0'] == '1' |
|
6563 |
assert carddef.data_class().get(2).data['bo0'] == '2' |
|
6564 |
assert carddef.data_class().get(3).data['bo0'] == '1' |
|
6565 |
assert carddef.data_class().get(4).data['bo0'] == '1' |
|
6566 | ||
6567 |
# target a single formdata |
|
6568 |
update_action.target_id = ( |
|
6569 |
'{{cards|objects:"%s"|filter_by:"card_string"|filter_value:"Text 2"|first}}' % carddef.url_name |
|
6570 |
) |
|
6571 |
wf.store() |
|
6572 |
perform_items([update_action], formdata) |
|
6573 |
assert carddef.data_class().get(1).data['bo0'] == '1' |
|
6574 |
assert carddef.data_class().get(2).data['bo0'] == '3' |
|
6575 |
assert carddef.data_class().get(3).data['bo0'] == '1' |
|
6576 |
assert carddef.data_class().get(4).data['bo0'] == '1' |
|
6577 | ||
6578 |
# mismatch in target |
|
6579 |
carddef2 = CardDef() |
|
6580 |
carddef2.name = 'Other data' |
|
6581 |
carddef2.fields = [] |
|
6582 |
carddef2.workflow = carddef_wf |
|
6583 |
carddef2.store() |
|
6584 | ||
6585 |
update_action.slug = 'carddef:%s' % carddef2.url_name |
|
6586 |
update_action.target_id = ( |
|
6587 |
'{{cards|objects:"%s"|filter_by:"card_string"|filter_value:"Text 2"}}' % carddef.url_name |
|
6588 |
) |
|
6589 |
wf.store() |
|
6590 |
perform_items([update_action], formdata) |
|
6591 |
assert two_pubs.loggederror_class.count() == 1 |
|
6592 |
logged_error = two_pubs.loggederror_class.select()[0] |
|
6593 |
assert logged_error.summary == 'Mismatch in target objects: "Other data" vs "Data"' |
|
6594 | ||
6595 |
# mismatch in target, with formdata |
|
6596 |
two_pubs.loggederror_class.wipe() |
|
6597 |
update_action.target_id = ( |
|
6598 |
'{{cards|objects:"%s"|filter_by:"card_string"|filter_value:"Text 2"|first}}' % carddef.url_name |
|
6599 |
) |
|
6600 |
wf.store() |
|
6601 |
perform_items([update_action], formdata) |
|
6602 |
assert two_pubs.loggederror_class.count() == 1 |
|
6603 |
logged_error = two_pubs.loggederror_class.select()[0] |
|
6604 |
assert logged_error.summary == 'Mismatch in target object: "Other data" vs "Data"' |
wcs/formdef.py | ||
---|---|---|
169 | 169 |
super().__init__(*args, **kwargs) |
170 | 170 |
self.fields = [] |
171 | 171 | |
172 |
def __eq__(self, other): |
|
173 |
return bool( |
|
174 |
isinstance(other, FormDef) and self.xml_root_node == other.xml_root_node and self.id == other.id |
|
175 |
) |
|
176 | ||
172 | 177 |
def migrate(self): |
173 | 178 |
changed = False |
174 | 179 |
wcs/wf/external_workflow.py | ||
---|---|---|
14 | 14 |
# You should have received a copy of the GNU General Public License |
15 | 15 |
# along with this program; if not, see <http://www.gnu.org/licenses/>. |
16 | 16 | |
17 |
import uuid |
|
18 | ||
17 | 19 |
from quixote import get_publisher |
20 |
from quixote.html import TemplateIO, htmltext |
|
18 | 21 | |
19 | 22 |
from wcs.carddef import CardDef |
20 | 23 |
from wcs.formdef import FormDef |
21 | 24 |
from wcs.qommon import _ |
22 | 25 |
from wcs.qommon.form import ComputedExpressionWidget, Form, RadiobuttonsWidget, SingleSelectWidget |
26 |
from wcs.variables import LazyFormData, LazyFormDefObjectsManager |
|
23 | 27 |
from wcs.workflows import ( |
28 |
EvolutionPart, |
|
24 | 29 |
Workflow, |
25 | 30 |
WorkflowGlobalActionWebserviceTrigger, |
26 | 31 |
WorkflowStatusItem, |
... | ... | |
29 | 34 |
) |
30 | 35 | |
31 | 36 | |
37 |
class ManyExternalCallsPart(EvolutionPart): |
|
38 |
count = 0 |
|
39 |
label = None |
|
40 |
running = True |
|
41 |
uuid = None |
|
42 | ||
43 |
def __init__(self, label): |
|
44 |
self.label = label |
|
45 |
self.uuid = str(uuid.uuid4()) |
|
46 | ||
47 |
def is_hidden(self): |
|
48 |
return bool(not self.running) |
|
49 | ||
50 |
def view(self): |
|
51 |
r = TemplateIO(html=True) |
|
52 |
r += htmltext('<div>') |
|
53 |
r += ( |
|
54 |
htmltext('<p>%s</p>') |
|
55 |
% _('Running external actions on "%(label)s" (%(count)s processed)') |
|
56 |
% {'label': self.label, 'count': self.count} |
|
57 |
) |
|
58 |
r += htmltext('</div>') |
|
59 |
return r.getvalue() |
|
60 | ||
61 | ||
32 | 62 |
class ExternalWorkflowGlobalAction(WorkflowStatusItem): |
33 | 63 | |
34 | 64 |
description = _('External workflow') |
... | ... | |
165 | 195 |
return |
166 | 196 | |
167 | 197 |
objectdef = self.get_object_def() |
168 |
target_id = self.compute(self.target_id, formdata=formdata, status_item=self) |
|
198 |
with get_publisher().complex_data(): |
|
199 |
target_id = self.compute(self.target_id, formdata=formdata, status_item=self, allow_complex=True) |
|
200 |
if target_id: |
|
201 |
target_id = get_publisher().get_cached_complex_data(target_id) |
|
202 | ||
203 |
if isinstance(target_id, LazyFormData): |
|
204 |
if target_id._formdef != objectdef: |
|
205 |
# abort if it's not the correct formdef/carddef |
|
206 |
get_publisher().record_error( |
|
207 |
_('Mismatch in target object: "%(object_name)s" vs "%(object_name2)s"') |
|
208 |
% {'object_name': objectdef.name, 'object_name2': target_id._formdef.name}, |
|
209 |
formdata=formdata, |
|
210 |
status_item=self, |
|
211 |
) |
|
212 |
return |
|
213 | ||
214 |
yield target_id._formdata |
|
215 |
return |
|
216 | ||
217 |
if isinstance(target_id, LazyFormDefObjectsManager): |
|
218 |
if target_id._formdef != objectdef: |
|
219 |
# abort if it's not the correct formdef/carddef |
|
220 |
get_publisher().record_error( |
|
221 |
_('Mismatch in target objects: "%(object_name)s" vs "%(object_name2)s"') |
|
222 |
% {'object_name': objectdef.name, 'object_name2': target_id._formdef.name}, |
|
223 |
formdata=formdata, |
|
224 |
status_item=self, |
|
225 |
) |
|
226 |
return |
|
227 |
for lazy_formdata in target_id: |
|
228 |
yield lazy_formdata._formdata |
|
229 |
return |
|
230 | ||
169 | 231 |
if not target_id: |
170 | 232 |
return |
171 | 233 | |
172 | 234 |
try: |
173 |
return objectdef.data_class().get(target_id)
|
|
235 |
yield objectdef.data_class().get(target_id)
|
|
174 | 236 |
except KeyError as e: |
175 | 237 |
# use custom error message depending on target type |
176 | 238 |
get_publisher().record_error( |
... | ... | |
183 | 245 | |
184 | 246 |
def iter_target_datas(self, formdata, objectdef): |
185 | 247 |
if self.target_mode == 'manual': |
186 |
# return only target |
|
187 |
target = self.get_manual_target(formdata) |
|
188 |
if target: |
|
189 |
yield target |
|
190 |
return |
|
191 | ||
192 |
yield from formdata.iter_target_datas(objectdef=objectdef, object_type=self.slug, status_item=self) |
|
248 |
# return targets |
|
249 |
yield from self.get_manual_target(formdata) |
|
250 |
else: |
|
251 |
yield from formdata.iter_target_datas( |
|
252 |
objectdef=objectdef, object_type=self.slug, status_item=self |
|
253 |
) |
|
193 | 254 | |
194 | 255 |
def get_parameters(self): |
195 | 256 |
return ('slug', 'trigger_id', 'target_mode', 'target_id', 'condition') |
... | ... | |
218 | 279 |
caller_source = CallerSource(formdata) |
219 | 280 | |
220 | 281 |
formdata.store() |
221 |
for target_data in self.iter_target_datas(formdata, objectdef): |
|
282 |
status_part = ManyExternalCallsPart(label=objectdef.name) |
|
283 |
for i, target_data in enumerate(self.iter_target_datas(formdata, objectdef)): |
|
222 | 284 |
with get_publisher().substitutions.temporary_feed(target_data): |
223 | 285 |
get_publisher().substitutions.reset() |
224 | 286 |
get_publisher().substitutions.feed(get_publisher()) |
... | ... | |
227 | 289 |
get_publisher().substitutions.feed(caller_source) |
228 | 290 |
perform_items(trigger.parent.items, target_data) |
229 | 291 | |
230 |
# update local object as it may have been modified by target_data |
|
231 |
# workflow executions. |
|
232 |
formdata.refresh_from_storage() |
|
292 |
# update local object as it may have been modified by target_data |
|
293 |
# workflow executions. |
|
294 |
formdata.refresh_from_storage() |
|
295 | ||
296 |
if i == 1: |
|
297 |
# if there are several iterations, add tracking status to object |
|
298 |
formdata.evolution[-1].add_part(status_part) |
|
299 |
elif i: |
|
300 |
# get status object back |
|
301 |
for evolution in reversed(formdata.evolution): |
|
302 |
try: |
|
303 |
status_part = [ |
|
304 |
x |
|
305 |
for x in evolution.parts |
|
306 |
if isinstance(x, ManyExternalCallsPart) and x.uuid == status_part.uuid |
|
307 |
][0] |
|
308 |
except IndexError: |
|
309 |
# probably the status changed and the tracking object is no longer available, |
|
310 |
# do without |
|
311 |
continue |
|
312 |
break |
|
313 |
if i: |
|
314 |
status_part.count = i + 1 |
|
315 |
# after iterating, store |
|
316 |
formdata.store() |
|
317 | ||
318 |
# if there were many calls, note it's now done. |
|
319 |
if status_part.count > 1: |
|
320 |
status_part.running = False |
|
321 |
formdata.store() |
|
233 | 322 | |
234 | 323 | |
235 | 324 |
register_item_class(ExternalWorkflowGlobalAction) |
236 |
- |