Projet

Général

Profil

0001-workflows-allow-queryset-as-id-for-external-workflow.patch

Frédéric Péters, 29 octobre 2021 15:26

Télécharger (13,7 ko)

Voir les différences:

Subject: [PATCH] workflows: allow queryset as id for external workflow action
 (#56847)

 tests/workflow/test_all.py  | 140 ++++++++++++++++++++++++++++++++++++
 wcs/formdef.py              |   5 ++
 wcs/wf/external_workflow.py | 115 +++++++++++++++++++++++++----
 3 files changed, 247 insertions(+), 13 deletions(-)
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
-