Projet

Général

Profil

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

Frédéric Péters, 02 novembre 2021 13:02

Télécharger (14 ko)

Voir les différences:

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

 tests/workflow/test_all.py  | 141 ++++++++++++++++++++++++++++++++++++
 wcs/formdef.py              |   5 ++
 wcs/wf/external_workflow.py | 119 ++++++++++++++++++++++++++----
 3 files changed, 251 insertions(+), 14 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
    assert set(status_part.processed_ids) == {x.get_display_id() for x in carddef.data_class().select()}
6556

  
6557
    # target some cards
6558
    update_action.target_id = (
6559
        '{{cards|objects:"%s"|filter_by:"card_string"|filter_value:"Text 2"}}' % carddef.url_name
6560
    )
6561
    wf.store()
6562
    perform_items([update_action], formdata)
6563
    assert carddef.data_class().get(1).data['bo0'] == '1'
6564
    assert carddef.data_class().get(2).data['bo0'] == '2'
6565
    assert carddef.data_class().get(3).data['bo0'] == '1'
6566
    assert carddef.data_class().get(4).data['bo0'] == '1'
6567

  
6568
    # target a single formdata
6569
    update_action.target_id = (
6570
        '{{cards|objects:"%s"|filter_by:"card_string"|filter_value:"Text 2"|first}}' % carddef.url_name
6571
    )
6572
    wf.store()
6573
    perform_items([update_action], formdata)
6574
    assert carddef.data_class().get(1).data['bo0'] == '1'
6575
    assert carddef.data_class().get(2).data['bo0'] == '3'
6576
    assert carddef.data_class().get(3).data['bo0'] == '1'
6577
    assert carddef.data_class().get(4).data['bo0'] == '1'
6578

  
6579
    # mismatch in target
6580
    carddef2 = CardDef()
6581
    carddef2.name = 'Other data'
6582
    carddef2.fields = []
6583
    carddef2.workflow = carddef_wf
6584
    carddef2.store()
6585

  
6586
    update_action.slug = 'carddef:%s' % carddef2.url_name
6587
    update_action.target_id = (
6588
        '{{cards|objects:"%s"|filter_by:"card_string"|filter_value:"Text 2"}}' % carddef.url_name
6589
    )
6590
    wf.store()
6591
    perform_items([update_action], formdata)
6592
    assert two_pubs.loggederror_class.count() == 1
6593
    logged_error = two_pubs.loggederror_class.select()[0]
6594
    assert logged_error.summary == 'Mismatch in target objects: expected "Other data", got "Data"'
6595

  
6596
    # mismatch in target, with formdata
6597
    two_pubs.loggederror_class.wipe()
6598
    update_action.target_id = (
6599
        '{{cards|objects:"%s"|filter_by:"card_string"|filter_value:"Text 2"|first}}' % carddef.url_name
6600
    )
6601
    wf.store()
6602
    perform_items([update_action], formdata)
6603
    assert two_pubs.loggederror_class.count() == 1
6604
    logged_error = two_pubs.loggederror_class.select()[0]
6605
    assert logged_error.summary == 'Mismatch in target object: expected "Other data", got "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
from quixote import get_publisher
17
import uuid
18

  
19
from quixote import get_publisher, get_request
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
    processed_ids = None
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
        self.processed_ids = []
47

  
48
    def is_hidden(self):
49
        return bool(not self.running) or not (
50
            get_request() and get_request().get_path().startswith('/backoffice/')
51
        )
52

  
53
    def view(self):
54
        r = TemplateIO(html=True)
55
        r += htmltext('<div>')
56
        r += (
57
            htmltext('<p>%s</p>')
58
            % _('Running external actions on "%(label)s" (%(count)s processed)')
59
            % {'label': self.label, 'count': len(self.processed_ids)}
60
        )
61
        r += htmltext('</div>')
62
        return r.getvalue()
63

  
64

  
32 65
class ExternalWorkflowGlobalAction(WorkflowStatusItem):
33 66

  
34 67
    description = _('External workflow')
......
165 198
            return
166 199

  
167 200
        objectdef = self.get_object_def()
168
        target_id = self.compute(self.target_id, formdata=formdata, status_item=self)
201
        with get_publisher().complex_data():
202
            target_id = self.compute(self.target_id, formdata=formdata, status_item=self, allow_complex=True)
203
            if target_id:
204
                target_id = get_publisher().get_cached_complex_data(target_id)
205

  
206
        if isinstance(target_id, LazyFormData):
207
            if target_id._formdef != objectdef:
208
                # abort if it's not the correct formdef/carddef
209
                get_publisher().record_error(
210
                    _('Mismatch in target object: expected "%(object_name)s", got "%(object_name2)s"')
211
                    % {'object_name': objectdef.name, 'object_name2': target_id._formdef.name},
212
                    formdata=formdata,
213
                    status_item=self,
214
                )
215
                return
216

  
217
            yield target_id._formdata
218
            return
219

  
220
        if isinstance(target_id, LazyFormDefObjectsManager):
221
            if target_id._formdef != objectdef:
222
                # abort if it's not the correct formdef/carddef
223
                get_publisher().record_error(
224
                    _('Mismatch in target objects: expected "%(object_name)s", got "%(object_name2)s"')
225
                    % {'object_name': objectdef.name, 'object_name2': target_id._formdef.name},
226
                    formdata=formdata,
227
                    status_item=self,
228
                )
229
                return
230
            for lazy_formdata in target_id:
231
                yield lazy_formdata._formdata
232
            return
233

  
169 234
        if not target_id:
170 235
            return
171 236

  
172 237
        try:
173
            return objectdef.data_class().get(target_id)
238
            yield objectdef.data_class().get(target_id)
174 239
        except KeyError as e:
175 240
            # use custom error message depending on target type
176 241
            get_publisher().record_error(
......
183 248

  
184 249
    def iter_target_datas(self, formdata, objectdef):
185 250
        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)
251
            # return targets
252
            yield from self.get_manual_target(formdata)
253
        else:
254
            yield from formdata.iter_target_datas(
255
                objectdef=objectdef, object_type=self.slug, status_item=self
256
            )
193 257

  
194 258
    def get_parameters(self):
195 259
        return ('slug', 'trigger_id', 'target_mode', 'target_id', 'condition')
......
218 282
        caller_source = CallerSource(formdata)
219 283

  
220 284
        formdata.store()
221
        for target_data in self.iter_target_datas(formdata, objectdef):
285
        status_part = ManyExternalCallsPart(label=objectdef.name)
286
        for i, target_data in enumerate(self.iter_target_datas(formdata, objectdef)):
222 287
            with get_publisher().substitutions.temporary_feed(target_data):
223 288
                get_publisher().substitutions.reset()
224 289
                get_publisher().substitutions.feed(get_publisher())
......
227 292
                get_publisher().substitutions.feed(caller_source)
228 293
                perform_items(trigger.parent.items, target_data)
229 294

  
230
        # update local object as it may have been modified by target_data
231
        # workflow executions.
232
        formdata.refresh_from_storage()
295
            # update local object as it may have been modified by target_data
296
            # workflow executions.
297
            formdata.refresh_from_storage()
298

  
299
            if i == 0:
300
                # if there are iterations, add tracking status to object
301
                formdata.evolution[-1].add_part(status_part)
302
            elif i:
303
                # get status object back
304
                for evolution in reversed(formdata.evolution):
305
                    try:
306
                        status_part = [
307
                            x
308
                            for x in evolution.parts
309
                            if isinstance(x, ManyExternalCallsPart) and x.uuid == status_part.uuid
310
                        ][0]
311
                    except IndexError:
312
                        # probably the status changed and the tracking object is no longer available,
313
                        # do without
314
                        continue
315
                    break
316

  
317
            status_part.processed_ids.append(target_data.get_display_id())
318
            # after iterating, store
319
            formdata.store()
320

  
321
        # note it's now done.
322
        status_part.running = False
323
        formdata.store()
233 324

  
234 325

  
235 326
register_item_class(ExternalWorkflowGlobalAction)
236
-