Projet

Général

Profil

0001-general-add-support-for-global-actions-3659.patch

Frédéric Péters, 13 décembre 2015 16:46

Télécharger (57,2 ko)

Voir les différences:

Subject: [PATCH] general: add support for global actions (#3659)

 tests/test_admin_pages.py       |  90 +++++++
 tests/test_backoffice_pages.py  |  38 +++
 tests/test_workflow_import.py   |  26 ++
 wcs/admin/workflows.py          | 321 +++++++++++++++++++++--
 wcs/backoffice/management.py    |   2 +-
 wcs/formdata.py                 |   3 +-
 wcs/forms/common.py             |   2 +-
 wcs/qommon/static/js/biglist.js |   5 +-
 wcs/wf/aggregation_email.py     |   1 +
 wcs/wf/attachment.py            |   1 +
 wcs/wf/export_to_model.py       |   1 +
 wcs/wf/form.py                  |   1 +
 wcs/wf/timeout_jump.py          |   1 +
 wcs/workflows.py                | 556 +++++++++++++++++++++++++++-------------
 14 files changed, 849 insertions(+), 199 deletions(-)
tests/test_admin_pages.py
1469 1469
    assert set(Workflow.get(workflow.id).possible_status[2].visibility) == set(
1470 1470
            ['_receiver', '_other-function'])
1471 1471

  
1472
def test_workflows_global_actions(pub):
1473
    create_superuser(pub)
1474
    create_role()
1475

  
1476
    Workflow.wipe()
1477
    workflow = Workflow(name='foo')
1478
    workflow.store()
1479

  
1480
    app = login(get_app(pub))
1481
    resp = app.get('/backoffice/workflows/%s/' % workflow.id)
1482
    resp = resp.click('add global action')
1483
    resp = resp.forms[0].submit('cancel')
1484
    assert not Workflow.get(workflow.id).global_actions
1485

  
1486
    resp = app.get('/backoffice/workflows/%s/' % workflow.id)
1487
    resp = resp.click('add global action')
1488
    resp.forms[0]['name'] = 'Global Action'
1489
    resp = resp.forms[0].submit('submit')
1490
    assert Workflow.get(workflow.id).global_actions[0].name == 'Global Action'
1491

  
1492
    # test rename
1493
    resp = app.get('/backoffice/workflows/%s/' % workflow.id)
1494
    resp = resp.click('Global Action')
1495
    resp = resp.click('Change Action Name')
1496
    resp.forms[0]['name'] = 'Renamed Action'
1497
    resp = resp.forms[0].submit('submit')
1498
    assert Workflow.get(workflow.id).global_actions[0].name == 'Renamed Action'
1499

  
1500
    # test removal
1501
    resp = app.get('/backoffice/workflows/%s/' % workflow.id)
1502
    resp = resp.click('Renamed Action')
1503
    resp = resp.click('Delete')
1504
    resp = resp.forms[0].submit('delete')
1505
    assert not Workflow.get(workflow.id).global_actions
1506

  
1507
def test_workflows_global_actions_edit(pub):
1508
    create_superuser(pub)
1509
    create_role()
1510

  
1511
    Workflow.wipe()
1512
    workflow = Workflow(name='foo')
1513
    workflow.store()
1514

  
1515
    app = login(get_app(pub))
1516
    resp = app.get('/backoffice/workflows/%s/' % workflow.id)
1517
    resp = resp.click('add global action')
1518
    resp.forms[0]['name'] = 'Global Action'
1519
    resp = resp.forms[0].submit('submit')
1520
    resp = resp.follow()
1521

  
1522
    resp = resp.click('Global Action')
1523

  
1524
    # test adding all actions
1525
    for action in [x[0] for x in resp.forms[0]['type'].options]:
1526
        resp.forms[0]['type'] = action
1527
        resp = resp.forms[0].submit()
1528
        resp = resp.follow()
1529

  
1530
    # test visiting
1531
    action_id = Workflow.get(workflow.id).global_actions[0].id
1532
    for item in Workflow.get(workflow.id).global_actions[0].items:
1533
        resp = app.get('/backoffice/workflows/%s/global-actions/%s/items/%s/' % (
1534
            workflow.id, action_id, item.id))
1535

  
1536
    # test modifying a trigger
1537
    resp = app.get('/backoffice/workflows/%s/' % workflow.id)
1538
    resp = resp.click('Global Action')
1539
    assert len(Workflow.get(workflow.id).global_actions[0].triggers) == 1
1540
    resp = resp.click(href='triggers/1/', index=0)
1541
    assert resp.form['roles$element0'].value == 'None'
1542
    resp.form['roles$element0'].value = '_receiver'
1543
    resp = resp.form.submit('submit')
1544
    assert Workflow.get(workflow.id).global_actions[0].triggers[0].roles == ['_receiver']
1545

  
1546
    resp = app.get('/backoffice/workflows/%s/' % workflow.id)
1547
    resp = resp.click('Global Action')
1548
    resp = resp.click(href='triggers/1/', index=0)
1549
    assert resp.form['roles$element0'].value == '_receiver'
1550
    resp.form['roles$element1'].value = '_submitter'
1551
    resp = resp.form.submit('submit')
1552
    assert Workflow.get(workflow.id).global_actions[0].triggers[0].roles == [
1553
            '_receiver', '_submitter']
1554

  
1555
    resp = app.get('/backoffice/workflows/%s/' % workflow.id)
1556
    resp = resp.click('Global Action')
1557
    resp = resp.click(href='triggers/1/', index=0)
1558
    resp.form['roles$element1'].value = 'None'
1559
    resp = resp.form.submit('submit')
1560
    assert Workflow.get(workflow.id).global_actions[0].triggers[0].roles == ['_receiver']
1561

  
1472 1562
def test_users(pub):
1473 1563
    create_superuser(pub)
1474 1564
    app = login(get_app(pub))
tests/test_backoffice_pages.py
18 18
from wcs.roles import Role
19 19
from wcs.workflows import (Workflow, CommentableWorkflowStatusItem,
20 20
        ChoiceWorkflowStatusItem, EditableWorkflowStatusItem)
21
from wcs.wf.jump import JumpWorkflowStatusItem
22
from wcs.wf.register_comment import RegisterCommenterWorkflowStatusItem
21 23
from wcs.wf.wscall import WebserviceCallStatusItem
22 24
from wcs.categories import Category
23 25
from wcs.formdef import FormDef
......
477 479
    assert FormDef.get_by_urlname('form-title').data_class().get(number31).status == 'wf-accepted'
478 480
    assert 'HELLO WORLD' in resp.body
479 481

  
482
def test_backoffice_handling_global_action(pub):
483
    create_user(pub)
484
    create_environment(pub)
485

  
486
    formdef = FormDef()
487
    formdef.name = 'test global action'
488
    formdef.fields = []
489

  
490
    workflow = Workflow.get_default_workflow()
491
    workflow.id = '2'
492
    action = workflow.add_global_action('FOOBAR')
493
    register_comment = action.append_item('register-comment')
494
    register_comment.comment = 'HELLO WORLD GLOBAL ACTION'
495
    jump = action.append_item('jump')
496
    jump.status = 'finished'
497
    trigger = action.triggers[0]
498
    trigger.roles = [x.id for x in Role.select() if x.name == 'foobar']
499

  
500
    workflow.store()
501
    formdef.workflow_id = workflow.id
502
    formdef.workflow_roles = {'_receiver': 1}
503
    formdef.store()
504

  
505
    formdata = formdef.data_class()()
506
    formdata.just_created()
507
    formdata.store()
508

  
509
    app = login(get_app(pub))
510
    resp = app.get('/backoffice/management/%s/%s/' % (formdef.url_name, formdata.id))
511
    assert 'button-action-1' in resp.form.fields
512
    resp = resp.form.submit('button-action-1')
513

  
514
    resp = app.get('/backoffice/management/%s/%s/' % (formdef.url_name, formdata.id))
515
    assert 'HELLO WORLD GLOBAL ACTION' in resp.body
516
    assert formdef.data_class().get(formdata.id).status == 'wf-finished'
517

  
480 518
def test_backoffice_submission_context(pub):
481 519
    user = create_user(pub)
482 520
    create_environment(pub)
tests/test_workflow_import.py
8 8

  
9 9
from wcs.workflows import Workflow, CommentableWorkflowStatusItem
10 10
from wcs.wf.wscall import WebserviceCallStatusItem
11
from wcs.wf.register_comment import RegisterCommenterWorkflowStatusItem
11 12
from wcs.roles import Role
12 13
from wcs.fields import StringField
13 14

  
......
316 317
    wf2 = assert_import_export_works(wf)
317 318
    assert wf2.possible_status[0].backoffice_info_text == '<p>Foo</p>'
318 319
    assert wf2.possible_status[0].items[0].backoffice_info_text == '<p>Bar</p>'
320

  
321
def test_global_actions():
322
    role = Role()
323
    role.id = '5'
324
    role.name = 'Test Role'
325
    role.store()
326

  
327
    wf = Workflow(name='global actions')
328
    ac1 = wf.add_global_action('Action', 'ac1')
329
    ac1.backoffice_info_text = '<p>Foo</p>'
330

  
331
    add_to_journal = RegisterCommenterWorkflowStatusItem()
332
    add_to_journal.id = '_add_to_journal'
333
    add_to_journal.comment = 'HELLO WORLD'
334
    ac1.items.append(add_to_journal)
335
    add_to_journal.parent = ac1
336

  
337
    trigger = ac1.triggers[0]
338
    assert trigger.key == 'manual'
339
    trigger.roles = [role.id]
340

  
341
    wf2 = assert_import_export_works(wf)
342
    assert wf2.global_actions[0].triggers[0].roles == [role.id]
343

  
344
    wf2 = assert_import_export_works(wf, True)
wcs/admin/workflows.py
229 229
class WorkflowItemPage(Directory):
230 230
    _q_exports = ['', 'delete']
231 231

  
232
    def __init__(self, workflow, status, component, html_top):
232
    def __init__(self, workflow, parent, component, html_top):
233 233
        try:
234
            self.item = [x for x in status.items if x.id == component][0]
234
            self.item = [x for x in parent.items if x.id == component][0]
235 235
        except (IndexError, ValueError):
236 236
            raise errors.TraversalError()
237 237
        self.workflow = workflow
238
        self.status = status
238
        self.parent = parent
239 239
        self.html_top = html_top
240 240
        get_response().breadcrumb.append(('items/%s/' % component, _(self.item.description)))
241 241

  
......
259 259
        if not form.get_submit() == 'submit' or form.has_errors():
260 260
            self.html_top('%s - %s' % (_('Workflow'), self.workflow.name))
261 261
            r = TemplateIO(html=True)
262
            r += htmltext('<h2>%s - %s</h2>') % (self.workflow.name, self.status.name)
262
            r += htmltext('<h2>%s - %s</h2>') % (self.workflow.name, self.parent.name)
263 263
            r += htmltext('<h3>%s</h3>') % _(self.item.description)
264 264
            r += form.render()
265 265
            if self.item.support_substitution_variables:
......
286 286
            r += form.render()
287 287
            return r.getvalue()
288 288
        else:
289
            del self.status.items[self.status.items.index(self.item)]
289
            del self.parent.items[self.parent.items.index(self.item)]
290 290
            self.workflow.store()
291 291
            return redirect('../../')
292 292

  
293 293
    def _q_lookup(self, component):
294
        t = self.item.q_admin_lookup(self.workflow, self.status, component,
294
        t = self.item.q_admin_lookup(self.workflow, self.parent, component,
295 295
                self.html_top)
296 296
        if t:
297 297
            return t
298 298
        return Directory._q_lookup(self, component)
299 299

  
300 300

  
301
class WorkflowItemsDir(Directory):
301
class GlobalActionTriggerPage(Directory):
302
    _q_exports = ['', 'delete']
303

  
304
    def __init__(self, workflow, action, component, html_top):
305
        try:
306
            self.trigger = [x for x in action.triggers if x.id == component][0]
307
        except (IndexError, ValueError):
308
            raise errors.TraversalError()
309
        self.workflow = workflow
310
        self.action = action
311
        self.status = action
312
        self.html_top = html_top
313
        get_response().breadcrumb.append(('triggers/%s/' % component, _('Trigger')))
314

  
315
    def _q_index(self):
316
        form = self.trigger.form(self.workflow)
317
        form.add_submit('submit', _('Save'))
318
        form.add_submit('cancel', _('Cancel'))
319

  
320
        if form.get_widget('cancel').parse():
321
            return redirect('..')
322

  
323
        if form.get_submit() == 'submit' and not form.has_errors():
324
            self.trigger.submit(form)
325
            if not form.has_errors():
326
                self.workflow.store()
327
                return redirect('../../')
328

  
329
        self.html_top('%s - %s' % (_('Workflow'), self.workflow.name))
330
        r = TemplateIO(html=True)
331
        r += htmltext('<h2>%s - %s</h2>') % (self.workflow.name, self.action.name)
332
        r += form.render()
333
        return r.getvalue()
334

  
335
    def delete(self):
336
        form = Form(enctype='multipart/form-data')
337
        form.widgets.append(HtmlWidget('<p>%s</p>' % _('You are about to remove a trigger.')))
338
        form.add_submit('delete', _('Submit'))
339
        form.add_submit('cancel', _('Cancel'))
340
        if form.get_widget('cancel').parse():
341
            return redirect('../../')
342
        if not form.is_submitted() or form.has_errors():
343
            get_response().breadcrumb.append(('delete', _('Delete')))
344
            self.html_top(title=_('Delete Trigger'))
345
            r = TemplateIO(html=True)
346
            r += htmltext('<h2>%s</h2>') % _('Deleting Trigger')
347
            r += form.render()
348
            return r.getvalue()
349
        else:
350
            del self.action.triggers[self.action.triggers.index(self.trigger)]
351
            self.workflow.store()
352
            return redirect('../../')
353

  
354
class ToChildDirectory(Directory):
302 355
    _q_exports = ['']
356
    klass = None
303 357

  
304 358
    def __init__(self, workflow, status, html_top):
305 359
        self.workflow = workflow
......
307 361
        self.html_top = html_top
308 362

  
309 363
    def _q_lookup(self, component):
310
        return WorkflowItemPage(self.workflow, self.status, component,
311
                self.html_top)
364
        return self.klass(self.workflow, self.status, component, self.html_top)
312 365

  
313 366
    def _q_index(self):
314 367
        return redirect('..')
315 368

  
369

  
370
class WorkflowItemsDir(ToChildDirectory):
371
    klass = WorkflowItemPage
372

  
373

  
374
class GlobalActionTriggersDir(ToChildDirectory):
375
    klass = GlobalActionTriggerPage
376

  
377

  
378
class GlobalActionItemsDir(ToChildDirectory):
379
    klass = WorkflowItemPage
380

  
381

  
316 382
class WorkflowStatusPage(Directory):
317 383
    _q_exports = ['', 'delete', 'newitem', ('items', 'items_dir'),
318 384
                  'update_order', 'edit', 'reassign', 'visibility',
......
341 407
        r += htmltext('%s - %s</h2>') % (self.workflow.name, self.status.name)
342 408
        r += get_session().display_message()
343 409

  
344
        r += htmltext('<div class="bo-block">')
345
        r += htmltext('<h3>%s ') % _('Possible Status:')
346
        r += htmltext('%s</h3>') % self.status.name
347
        r += htmltext('</div>')
348

  
349 410
        if self.status.get_visibility_restricted_roles():
350 411
            r += htmltext('<div class="bo-block">')
351 412
            r += _('This status is hidden from the user.')
......
411 472

  
412 473
        return r.getvalue()
413 474

  
414

  
415 475
    def get_sidebar(self):
416 476
        get_response().add_javascript(['jquery.js', 'jquery-ui.js', 'popup.js',
417 477
            'jquery.colourpicker.js'])
......
437 497
            r += htmltext('</div>')
438 498
        return r.getvalue()
439 499

  
500
    def is_item_available(self, item):
501
        return item.is_available()
502

  
440 503
    def get_new_item_form(self):
441 504
        form = Form(enctype='multipart/form-data', action = 'newitem')
442
        available_items = [x for x in item_classes if x.is_available()]
505
        available_items = [x for x in item_classes if self.is_item_available(x)]
443 506
        def cmp_items(x, y):
444 507
            t = cmp(x.category and x.category[0], y.category and y.category[0])
445 508
            if t:
......
460 523
        self.workflow.store()
461 524
        return 'ok'
462 525

  
463

  
464 526
    def newitem(self):
465 527
        form = self.get_new_item_form()
466 528

  
......
478 540

  
479 541
        return redirect('.')
480 542

  
481

  
482 543
    def delete(self):
483 544
        form = Form(enctype="multipart/form-data")
484 545
        form.widgets.append(HtmlWidget('<p>%s</p>' % _(
......
888 949
        return redirect('..')
889 950

  
890 951

  
952
class GlobalActionPage(WorkflowStatusPage):
953
    _q_exports = ['', 'new', 'delete', 'newitem', ('items', 'items_dir'),
954
                  'edit', ('triggers', 'triggers_dir'),
955
                  ('backoffice-info-text', 'backoffice_info_text'),]
956

  
957
    def __init__(self, workflow, action_id, html_top):
958
        self.html_top = html_top
959
        self.workflow = workflow
960
        try:
961
            self.action = [x for x in self.workflow.global_actions if x.id == action_id][0]
962
        except IndexError:
963
            raise errors.TraversalError()
964
        self.status = self.action
965
        self.items_dir = GlobalActionItemsDir(workflow, self.action, html_top)
966
        self.triggers_dir = GlobalActionTriggersDir(workflow, self.action, html_top)
967

  
968
    def _q_traverse(self, path):
969
        get_response().breadcrumb.append(
970
                ('global-actions/%s/' % self.action.id, _('Global Action: %s') % self.action.name))
971
        return Directory._q_traverse(self, path)
972

  
973
    def is_item_available(self, item):
974
        return item.is_available() and item.ok_in_global_action
975

  
976
    def _q_index(self):
977
        self.html_top('%s - %s' % (_('Workflow'), self.workflow.name))
978
        r = TemplateIO(html=True)
979
        get_response().add_javascript(['jquery.js', 'jquery-ui.js', 'biglist.js',
980
            'ckeditor/ckeditor.js', 'qommon.wysiwyg.js', 'ckeditor/adapters/jquery.js'])
981

  
982
        r += htmltext('<h2>%s - ') % _('Workflow')
983
        r += htmltext('%s - %s</h2>') % (self.workflow.name, self.action.name)
984
        r += get_session().display_message()
985

  
986
        r += htmltext('<div class="bo-block">')
987
        r += htmltext('<h2>%s</h2>') % _('Actions')
988
        if not self.action.items:
989
            r += htmltext('<p>%s</p>') % _('There are not yet any items in this action.')
990
        else:
991
            if str(self.workflow.id).startswith('_'):
992
                r += htmltext('<ul id="items-list" class="biglist">')
993
            else:
994
                r += htmltext('<p>')
995
                r += _('Use drag and drop to reorder items.')
996
                r += htmltext('</p>')
997
                r += htmltext('<ul id="items-list" class="biglist sortable">')
998
            for i, item in enumerate(self.action.items):
999
                r += htmltext('<li class="biglistitem" id="itemId_%s">') % item.id
1000
                if str(self.workflow.id).startswith('_'):
1001
                    r += item.render_as_line()
1002
                else:
1003
                    if hasattr(item, 'fill_admin_form'):
1004
                        r += htmltext('<a href="items/%s/">%s</a>') % (item.id, item.render_as_line())
1005
                    else:
1006
                        r += item.render_as_line()
1007
                    r += htmltext('<p class="commands">')
1008
                    if hasattr(item, 'fill_admin_form'):
1009
                        r += command_icon('items/%s/' % item.id, 'edit')
1010
                    r += command_icon('items/%s/delete' % item.id, 'remove', popup = True)
1011
                    r += htmltext('</p>')
1012
                r += htmltext('</li>')
1013
            r += htmltext('</ul>')
1014
        r += htmltext('</div>') # bo-block
1015

  
1016
        r += htmltext('<div class="bo-block">')
1017
        r += htmltext('<h2>%s</h2>') % _('Triggers')
1018
        r += htmltext('<ul id="items-list" class="biglist sortable">')
1019
        for trigger in self.action.triggers:
1020
            r += htmltext('<li>')
1021
            r += htmltext('<a rel="popup" href="triggers/%s/">%s</a>') % (
1022
                    trigger.id, trigger.render_as_line())
1023
            r += htmltext('<p class="commands">')
1024
            r += command_icon('triggers/%s/' % trigger.id, 'edit', popup=True)
1025
            r += htmltext('</li>')
1026
        r+= htmltext('</ul>')
1027
        r += htmltext('</div>') # bo-block
1028

  
1029
        r += htmltext('<p><a href="../../">%s</a></p>') % _('Back to workflow main page')
1030

  
1031
        get_response().filter['sidebar'] = self.get_sidebar()
1032

  
1033
        return r.getvalue()
1034

  
1035
    def delete(self):
1036
        form = Form(enctype='multipart/form-data')
1037
        form.widgets.append(HtmlWidget('<p>%s</p>' % _(
1038
                        'You are about to remove an action.')))
1039
        form.add_submit('delete', _('Submit'))
1040
        form.add_submit('cancel', _('Cancel'))
1041
        if form.get_widget('cancel').parse():
1042
            return redirect('../../')
1043
        if not form.is_submitted() or form.has_errors():
1044
            get_response().breadcrumb.append(('delete', _('Delete')))
1045
            self.html_top(title = _('Delete Action'))
1046
            r = TemplateIO(html=True)
1047
            r += htmltext('<h2>%s %s</h2>') % (_('Deleting Action:'), self.action.name)
1048
            r += form.render()
1049
            return r.getvalue()
1050

  
1051
        del self.workflow.global_actions[self.workflow.global_actions.index(self.action)]
1052
        self.workflow.store()
1053
        return redirect('../../')
1054

  
1055
    def edit(self):
1056
        form = Form(enctype = 'multipart/form-data')
1057
        form.add(StringWidget, 'name', title=_('Action Name'), required=True,
1058
                size=30, value=self.action.name)
1059
        form.add_submit('submit', _('Submit'))
1060
        form.add_submit('cancel', _('Cancel'))
1061
        if form.get_widget('cancel').parse():
1062
            return redirect('..')
1063

  
1064
        if form.is_submitted() and not form.has_errors():
1065
            new_name = str(form.get_widget('name').parse())
1066
            if [x for x in self.workflow.global_actions if x.name == new_name]:
1067
                form.get_widget('name').set_error(
1068
                        _('There is already an action with that name.'))
1069
            else:
1070
                self.action.name = new_name
1071
                self.workflow.store()
1072
                return redirect('.')
1073

  
1074
        self.html_top(title=_('Edit Action Name'))
1075
        get_response().breadcrumb.append( ('edit', _('Edit')) )
1076
        r = TemplateIO(html=True)
1077
        r += htmltext('<h2>%s</h2>') % _('Edit Action Name')
1078
        r += form.render()
1079
        return r.getvalue()
1080

  
1081
    def get_sidebar(self):
1082
        get_response().add_javascript(['jquery.js', 'jquery-ui.js', 'popup.js',
1083
            'jquery.colourpicker.js'])
1084
        r = TemplateIO(html=True)
1085
        if str(self.workflow.id).startswith('_'):
1086
            r += htmltext('<p>')
1087
            r += _('''This is the default workflow, you cannot edit it but you can
1088
                 duplicate it to base your own workflow on it.''')
1089
            r += htmltext('</p>')
1090
        else:
1091
            r += htmltext('<ul id="sidebar-actions">')
1092
            r += htmltext('<li><a href="edit" rel="popup">%s</a></li>') % _('Change Action Name')
1093
            r += htmltext('<li><a href="backoffice-info-text" rel="popup">%s</a></li>'
1094
                    ) % _('Change Backoffice Information Text')
1095
            r += htmltext('<li><a href="delete" rel="popup">%s</a></li>') % _('Delete')
1096
            r += htmltext('</ul>')
1097
            r += htmltext('<div id="new-field">')
1098
            r += htmltext('<h3>%s</h3>') % _('New Item')
1099
            r += self.get_new_item_form().render()
1100
            r += htmltext('</div>')
1101
        return r.getvalue()
1102

  
1103

  
1104
class GlobalActionsDirectory(Directory):
1105
    _q_exports = ['', 'new']
1106

  
1107
    def __init__(self, workflow, html_top):
1108
        self.workflow = workflow
1109
        self.html_top = html_top
1110

  
1111
    def _q_lookup(self, component):
1112
        return GlobalActionPage(self.workflow, component, self.html_top)
1113

  
1114
    def _q_index(self):
1115
        return redirect('..')
1116

  
1117
    def new(self):
1118
        form = Form(enctype='multipart/form-data')
1119
        form.add(StringWidget, 'name', title=_('Name'), required=True, size=50)
1120
        form.add_submit('submit', _('Add'))
1121
        form.add_submit('cancel', _('Cancel'))
1122
        if form.get_widget('cancel').parse():
1123
            return redirect('..')
1124

  
1125
        if form.is_submitted() and not form.has_errors():
1126
            name = form.get_widget('name').parse()
1127
            action = self.workflow.add_global_action(name)
1128
            self.workflow.store()
1129
            return redirect('%s/' % action.id)
1130

  
1131
        get_response().breadcrumb.append(('new', _('New Global Action')))
1132
        html_top('workflows', title=_('New Global Action'))
1133
        r = TemplateIO(html=True)
1134
        r += htmltext('<h2>%s</h2>') % _('New Global Action')
1135
        r += form.render()
1136
        return r.getvalue()
1137

  
1138

  
891 1139
class WorkflowPage(Directory):
892 1140
    _q_exports = ['', 'edit', 'delete', 'newstatus', ('status', 'status_dir'), 'update_order',
893 1141
            'duplicate', 'export', 'svg', ('variables', 'variables_dir'),
894
            ('functions', 'functions_dir')]
1142
            'update_actions_order',
1143
            ('functions', 'functions_dir'), ('global-actions', 'global_actions_dir')]
895 1144

  
896 1145
    def __init__(self, component, html_top):
897 1146
        try:
......
903 1152
        self.status_dir = WorkflowStatusDirectory(self.workflow, html_top)
904 1153
        self.variables_dir = VariablesDirectory(self.workflow)
905 1154
        self.functions_dir = FunctionsDirectory(self.workflow)
1155
        self.global_actions_dir = GlobalActionsDirectory(self.workflow, html_top)
906 1156
        get_response().breadcrumb.append((component + '/', self.workflow.name))
907 1157

  
908 1158
    def _q_index(self):
......
992 1242
                                field.id, field.label)
993 1243
                        if not '*' in field.varname:
994 1244
                            r += htmltext(' (<code>%s</code>)') % field.varname
995
                        r += htmltext('</li>')
1245
                        r += htmltext('</a></li>')
996 1246
                r += htmltext('</ul>')
997 1247
            r += htmltext('</div>')
998 1248

  
1249
        if not str(self.workflow.id).startswith('_') or self.workflow.global_actions:
1250
            r += htmltext('<div class="bo-block">')
1251
            r += htmltext('<h3>%s') % _('Global Actions')
1252
            if not str(self.workflow.id).startswith('_'):
1253
                r += htmltext(' <span class="change">(<a rel="popup" href="global-actions/new">%s</a>)</span>') % _('add global action')
1254
            r += htmltext('</h3>')
1255

  
1256
            if not str(self.workflow.id).startswith('_'):
1257
                r += htmltext('<ul id="status-list" class="biglist sortable" '
1258
                              'data-order-function="update_actions_order">')
1259
            else:
1260
                r += htmltext('<ul id="status-list" class="biglist">')
1261

  
1262
            for action in (self.workflow.global_actions or []):
1263
                r += htmltext('<li class="biglistitem" id="itemId_%s">' % action.id)
1264
                if not str(self.workflow.id).startswith('_'):
1265
                    r += htmltext('<a href="global-actions/%s/">%s</a>') % (
1266
                            action.id, action.name)
1267
                else:
1268
                    r += htmltext('<a>%s</a>') % action.name
1269
                r += htmltext('</li>')
1270
            r += htmltext('</ul>')
1271
            r += htmltext('</div>')
1272

  
999 1273
        r += htmltext('</div>') # .splitcontent-right
1000 1274

  
1001 1275
        r += htmltext('<br style="clear:both;"/>')
......
1073 1347
        self.workflow.store()
1074 1348
        return 'ok'
1075 1349

  
1350
    def update_actions_order(self):
1351
        request = get_request()
1352
        new_order = request.form['order'].strip(';').split(';')
1353
        self.workflow.global_actions = [ [x for x in self.workflow.global_actions if
1354
                                        x.id == y][0] for y in new_order]
1355
        self.workflow.store()
1356
        return 'ok'
1076 1357

  
1077 1358
    def newstatus(self):
1078 1359
        form = Form(enctype='multipart/form-data', action = 'newstatus')
wcs/backoffice/management.py
1580 1580

  
1581 1581

  
1582 1582
class FormBackOfficeStatusPage(FormStatusPage):
1583
    _q_exports = ['', 'download', 'json', 'wfedit']
1583
    _q_exports = ['', 'download', 'json', 'wfedit', 'action']
1584 1584

  
1585 1585
    def html_top(self, title = None):
1586 1586
        return html_top('management', title)
wcs/formdata.py
260 260
        url = None
261 261
        get_publisher().substitutions.feed(self)
262 262
        wf_status = self.get_status()
263
        url = wf_status.perform_items(self)
263
        from wcs.workflows import perform_items
264
        url = perform_items(wf_status.items, self)
264 265
        return url
265 266

  
266 267
    def display_workflow_message(self):
wcs/forms/common.py
89 89

  
90 90

  
91 91
class FormStatusPage(Directory):
92
    _q_exports = ['', 'download', 'json']
92
    _q_exports = ['', 'download', 'json', 'action']
93 93
    _q_extra_exports = []
94 94

  
95 95
    def html_top(self, title = None):
wcs/qommon/static/js/biglist.js
16 16
                update : function(event, ui)
17 17
                {
18 18
                    result = '';
19
                    items = $('ul.biglist li');
19
                    items = $(ui.item).parent().find('li');
20 20
                    for (i=0; i < items.length; i++) {
21 21
                        var item = items[i];
22 22
                        var item_id = item.id.substr(7, 50);
......
24 24
                          result += item_id + ';';
25 25
                        }
26 26
                    }
27
                    $.post('update_order', {'order': result});
27
                    var order_function = $(this).data('order-function') || 'update_order';
28
                    $.post(order_function, {'order': result});
28 29
                },
29 30
            }
30 31
        );
wcs/wf/aggregation_email.py
28 28
class AggregationEmailWorkflowStatusItem(WorkflowStatusItem):
29 29
    description = N_('Aggregate to summary email')
30 30
    key = 'aggregationemail'
31
    ok_in_global_action = False
31 32

  
32 33
    to = []
33 34

  
wcs/wf/attachment.py
68 68
    key = 'addattachment'
69 69
    endpoint = False
70 70
    waitpoint = True
71
    ok_in_global_action = False
71 72

  
72 73
    title = None
73 74
    display_title = True
wcs/wf/export_to_model.py
131 131
    description = N_('Create Document')
132 132
    key = 'export_to_model'
133 133
    support_substitution_variables = True
134
    ok_in_global_action = False
134 135

  
135 136
    endpoint = False
136 137
    waitpoint = True
wcs/wf/form.py
63 63
class FormWorkflowStatusItem(WorkflowStatusItem):
64 64
    description = N_('Display a form')
65 65
    key = 'form'
66
    ok_in_global_action = False
66 67

  
67 68
    by = []
68 69
    formdef = None
wcs/wf/timeout_jump.py
24 24
    description = N_('Change Status on Timeout')
25 25
    key = 'timeout'
26 26
    waitpoint = True
27
    ok_in_global_action = False
27 28

  
28 29
    timeout = None
29 30

  
wcs/workflows.py
48 48
        return -1
49 49

  
50 50

  
51
def perform_items(items, formdata, depth=20):
52
    if depth == 0: # prevents infinite loops
53
        return
54
    url = None
55
    old_status = formdata.status
56
    for item in items:
57
        try:
58
            url = item.perform(formdata) or url
59
        except AbortActionException:
60
            break
61
        if formdata.status != old_status:
62
            break
63
    if formdata.status != old_status:
64
        if not formdata.evolution:
65
            formdata.evolution = []
66
        evo = Evolution()
67
        evo.time = time.localtime()
68
        evo.status = formdata.status
69
        formdata.evolution.append(evo)
70
        formdata.store()
71
        # performs the items of the new status
72
        wf_status = formdata.get_status()
73
        url = perform_items(wf_status.items, formdata, depth=depth-1) or url
74
    return url
75

  
76

  
51 77
class WorkflowImportError(Exception):
52 78
    pass
53 79

  
......
187 213
        return {'attachments': AttachmentsSubstitutionProxy(formdata) }
188 214

  
189 215

  
216
class DuplicateGlobalActionNameError(Exception):
217
    pass
218

  
219

  
190 220
class DuplicateStatusNameError(Exception):
191 221
    pass
192 222

  
......
220 250
    possible_status = None
221 251
    roles = None
222 252
    variables_formdef = None
253
    global_actions = None
223 254

  
224 255
    last_modification_time = None
225 256
    last_modification_user_id = None
......
229 260
        self.name = name
230 261
        self.possible_status = []
231 262
        self.roles = {'_receiver': _('Recipient')}
263
        self.global_actions = []
232 264

  
233 265
    def migrate(self):
234 266
        changed = False
......
240 272
        for status in self.possible_status:
241 273
            changed |= status.migrate()
242 274

  
275
        if not self.global_actions:
276
            self.global_actions = []
277

  
243 278
        if changed:
244 279
            self.store()
245 280

  
......
295 330
                return status
296 331
        raise KeyError()
297 332

  
333
    def add_global_action(self, name, id=None):
334
        if [x for x in self.global_actions if x.name == name]:
335
            raise DuplicateGlobalActionNameError()
336
        action = WorkflowGlobalAction(name)
337
        action.parent = self
338
        action.append_trigger('manual')
339

  
340
        if id is None:
341
            if self.global_actions:
342
                action.id = str(max([lax_int(x.id) for x in self.global_actions]) + 1)
343
            else:
344
                action.id = '1'
345
        else:
346
            action.id = id
347
        self.global_actions.append(action)
348
        return action
349

  
350
    def get_global_actions_for_user(self, formdata, user):
351
        if not user:
352
            return []
353
        actions = []
354
        for action in self.global_actions or []:
355
            for trigger in action.triggers or []:
356
                if isinstance(trigger, WorkflowGlobalActionManualTrigger):
357
                    roles = [get_role_translation(formdata, x)
358
                             for x in trigger.roles]
359
                    if set(roles).intersection(user.roles or []):
360
                        actions.append(action)
361
                        break
362
        return actions
363

  
298 364
    def __setstate__(self, dict):
299 365
        self.__dict__.update(dict)
300
        for s in self.possible_status:
366
        for s in self.possible_status + (self.global_actions or []):
301 367
            s.parent = self
302
            for i, item in enumerate(s.items):
368
            triggers = getattr(s, 'triggers', None) or []
369
            for i, item in enumerate(s.items + triggers):
303 370
                item.parent = s
304 371
                if not item.id:
305 372
                    item.id =  '%d' % (i+1)
......
379 446
            possible_status.append(status.export_to_xml(charset=charset,
380 447
                include_id=include_id))
381 448

  
449
        if self.global_actions:
450
            global_actions = ET.SubElement(root, 'global_actions')
451
            for action in self.global_actions:
452
                global_actions.append(action.export_to_xml(charset=charset,
453
                    include_id=include_id))
454

  
382 455
        if self.variables_formdef:
383 456
            variables = ET.SubElement(root, 'variables')
384 457
            formdef = ET.SubElement(variables, 'formdef')
......
434 507
            status_o.init_with_xml(status, charset, include_id=include_id)
435 508
            workflow.possible_status.append(status_o)
436 509

  
510
        workflow.global_actions = []
511
        global_actions = tree.find('global_actions')
512
        if global_actions is not None:
513
            for action in global_actions:
514
                action_o = WorkflowGlobalAction()
515
                action_o.parent = workflow
516
                action_o.init_with_xml(action, charset, include_id=include_id)
517
                workflow.global_actions.append(action_o)
518

  
437 519
        variables = tree.find('variables')
438 520
        if variables is not None:
439 521
            formdef = variables.find('formdef')
......
456 538
        return t
457 539

  
458 540
    def render_list_of_roles(self, roles):
459
        t = []
460
        for r in roles:
461
            role_label = get_role_translation_label(self, r)
462
            if role_label:
463
                t.append(role_label)
464
        return ', '.join(t)
541
        return render_list_of_roles(self, roles)
465 542

  
466 543
    def get_unknown_workflow(cls):
467 544
        workflow = Workflow(name=_('Unknown'))
......
596 673
    get_default_workflow = classmethod(get_default_workflow)
597 674

  
598 675

  
676
class XmlSerialisable(object):
677
    node_name = None
678

  
679
    def export_to_xml(self, charset, include_id=False):
680
        node = ET.Element(self.node_name)
681
        node.attrib['type'] = self.key
682
        for attribute in self.get_parameters():
683
            if hasattr(self, '%s_export_to_xml' % attribute):
684
                getattr(self, '%s_export_to_xml' % attribute)(node, charset,
685
                        include_id=include_id)
686
                continue
687
            if hasattr(self, attribute) and getattr(self, attribute) is not None:
688
                el = ET.SubElement(node, attribute)
689
                val = getattr(self, attribute)
690
                if type(val) is dict:
691
                    for k, v in val.items():
692
                        ET.SubElement(el, k).text = unicode(v, charset, 'replace')
693
                elif type(val) is list:
694
                    if attribute[-1] == 's':
695
                        atname = attribute[:-1]
696
                    else:
697
                        atname = 'item'
698
                    for v in val:
699
                        ET.SubElement(el, atname).text = unicode(str(v), charset, 'replace')
700
                elif type(val) in (str, unicode):
701
                    if type(val) is unicode:
702
                        el.text = val
703
                    else:
704
                        el.text = unicode(val, charset, 'replace')
705
                else:
706
                    el.text = str(val)
707
        return node
708

  
709
    def init_with_xml(self, elem, charset, include_id=False):
710
        for attribute in self.get_parameters():
711
            el = elem.find(attribute)
712
            if hasattr(self, '%s_init_with_xml' % attribute):
713
                getattr(self, '%s_init_with_xml' % attribute)(el, charset,
714
                        include_id=include_id)
715
                continue
716
            if el is None:
717
                continue
718
            if el.getchildren():
719
                if type(getattr(self, attribute)) is list:
720
                    v = [x.text.encode(charset) for x in el.getchildren()]
721
                elif type(getattr(self, attribute)) is dict:
722
                    v = {}
723
                    for e in el.getchildren():
724
                        v[e.tag] = e.text.encode(charset)
725
                else:
726
                    # ???
727
                    raise AssertionError
728
                setattr(self, attribute, v)
729
            else:
730
                if el.text is None:
731
                    setattr(self, attribute, None)
732
                elif el.text in ('False', 'True'): # bools
733
                    setattr(self, attribute, eval(el.text))
734
                elif type(getattr(self, attribute)) is int:
735
                    setattr(self, attribute, int(el.text.encode(charset)))
736
                else:
737
                    setattr(self, attribute, el.text.encode(charset))
738

  
739
    def _roles_export_to_xml(self, attribute, item, charset, include_id=False):
740
        if not hasattr(self, attribute) or not getattr(self, attribute):
741
            return
742
        el = ET.SubElement(item, attribute)
743
        for role_id in getattr(self, attribute):
744
            if role_id is None:
745
                continue
746
            role_id = str(role_id)
747
            if role_id.startswith('_') or role_id == 'logged-users':
748
                role = unicode(role_id, charset)
749
            else:
750
                try:
751
                    role = unicode(Role.get(role_id).name, charset)
752
                except KeyError:
753
                    role = unicode(role_id, charset)
754
            sub = ET.SubElement(el, 'item')
755
            sub.attrib['role_id'] = role_id
756
            sub.text = role
757

  
758
    def _roles_init_with_xml(self, attribute, elem, charset, include_id=False):
759
        if elem is None:
760
            setattr(self, attribute, [])
761
        else:
762
            imported_roles = []
763
            for child in elem.getchildren():
764
                imported_roles.append(self._get_role_id_from_xml(child,
765
                    charset, include_id=include_id))
766
            setattr(self, attribute, imported_roles)
767

  
768
    def _role_export_to_xml(self, attribute, item, charset, include_id=False):
769
        if not hasattr(self, attribute) or not getattr(self, attribute):
770
            return
771
        role_id = str(getattr(self, attribute))
772
        if role_id.startswith('_') or role_id == 'logged-users':
773
            role = unicode(role_id, charset)
774
        else:
775
            try:
776
                role = unicode(Role.get(role_id).name, charset)
777
            except KeyError:
778
                role = unicode(role_id, charset)
779
        sub = ET.SubElement(item, attribute)
780
        if include_id:
781
            sub.attrib['role_id'] = role_id
782
        sub.text = role
783

  
784
    def _get_role_id_from_xml(self, elem, charset, include_id=False):
785
        if elem is None:
786
            return None
787

  
788
        value = elem.text.encode(charset)
789

  
790
        # look for known static values
791
        if value.startswith('_') or value == 'logged-users':
792
            return value
793

  
794
        # if we import using id, only look at the role_id attribute
795
        if include_id:
796
            if not 'role_id' in elem.attrib:
797
                return None
798
            role_id = str(elem.attrib['role_id'])
799
            if Role.has_key(role_id):
800
                return role_id
801
            else:
802
                return None
803

  
804
        # if not using id, look up on the name
805
        for role in Role.select(ignore_errors=True):
806
            if role.name == value:
807
                return role.id
808

  
809
        # and if there's no match, create a new role
810
        role = Role()
811
        role.name = value
812
        role.store()
813
        return role.id
814

  
815
    def _role_init_with_xml(self, attribute, elem, charset, include_id=False):
816
        setattr(self, attribute, self._get_role_id_from_xml(elem, charset,
817
            include_id=include_id))
818

  
819

  
820
class WorkflowGlobalActionTrigger(XmlSerialisable):
821
    node_name = 'trigger'
822

  
823

  
824
class WorkflowGlobalActionManualTrigger(WorkflowGlobalActionTrigger):
825
    key = 'manual'
826
    roles = None
827

  
828
    def get_parameters(self):
829
        return ('roles',)
830

  
831
    def render_as_line(self):
832
        if self.roles:
833
            return _('Manual by %s') % render_list_of_roles(
834
                    self.parent.parent, self.roles)
835
        else:
836
            return _('Manual (not assigned)')
837

  
838
    def form(self, workflow):
839
        form = Form(enctype='multipart/form-data')
840
        options = [(None, '---', None)]
841
        options += workflow.get_list_of_roles(include_logged_in_users=False)
842
        form.add(WidgetList, 'roles', title=_('Roles'),
843
                element_type=SingleSelectWidget,
844
                value=self.roles,
845
                add_element_label=_('Add Role'),
846
                element_kwargs={'render_br': False,
847
                                'options': options})
848
        return form
849

  
850
    def submit(self, form):
851
        self.roles = form.get_widget('roles').parse()
852

  
853
    def roles_export_to_xml(self, item, charset, include_id=False):
854
        self._roles_export_to_xml('roles', item, charset, include_id=include_id)
855

  
856
    def roles_init_with_xml(self, elem, charset, include_id=False):
857
        self._roles_init_with_xml('roles', elem, charset, include_id=include_id)
858

  
859

  
860
class WorkflowGlobalAction(object):
861
    id = None
862
    name = None
863
    items = None
864
    triggers = None
865
    backoffice_info_text = None
866

  
867
    def __init__(self, name=None):
868
        self.name = name
869
        self.items = []
870

  
871
    def append_item(self, type):
872
        for klass in item_classes:
873
            if klass.key == type:
874
                o = klass()
875
                if self.items:
876
                    o.id = str(max([lax_int(x.id) for x in self.items]) + 1)
877
                else:
878
                    o.id = '1'
879
                self.items.append(o)
880
                return o
881
        else:
882
            raise KeyError()
883

  
884
    def append_trigger(self, type):
885
        trigger_types = {
886
            'manual': WorkflowGlobalActionManualTrigger
887
        }
888
        o = trigger_types.get(type)()
889
        if not self.triggers:
890
            self.triggers = []
891
        if self.triggers:
892
            o.id = str(max([lax_int(x.id) for x in self.triggers]) + 1)
893
        else:
894
            o.id = '1'
895
        self.triggers.append(o)
896
        return o
897

  
898
    def export_to_xml(self, charset, include_id=False):
899
        status = ET.Element('action')
900
        ET.SubElement(status, 'id').text = unicode(self.id, charset)
901
        ET.SubElement(status, 'name').text = unicode(self.name, charset)
902

  
903
        if self.backoffice_info_text:
904
            ET.SubElement(status, 'backoffice_info_text').text = unicode(
905
                    self.backoffice_info_text, charset)
906

  
907
        items = ET.SubElement(status, 'items')
908
        for item in self.items:
909
            items.append(item.export_to_xml(charset=charset,
910
                include_id=include_id))
911

  
912
        triggers = ET.SubElement(status, 'triggers')
913
        for trigger in self.triggers:
914
            triggers.append(trigger.export_to_xml(charset=charset,
915
                include_id=include_id))
916

  
917
        return status
918

  
919
    def init_with_xml(self, elem, charset, include_id=False):
920
        self.id = elem.find('id').text.encode(charset)
921
        self.name = elem.find('name').text.encode(charset)
922
        if elem.find('backoffice_info_text') is not None:
923
            self.backoffice_info_text = elem.find('backoffice_info_text').text.encode(charset)
924

  
925
        self.items = []
926
        for item in elem.find('items'):
927
            item_type = item.attrib['type']
928
            self.append_item(item_type)
929
            item_o = self.items[-1]
930
            item_o.parent = self
931
            item_o.init_with_xml(item, charset, include_id=include_id)
932

  
933
        self.triggers = []
934
        for trigger in elem.find('triggers'):
935
            trigger_type = trigger.attrib['type']
936
            self.append_trigger(trigger_type)
937
            trigger_o = self.triggers[-1]
938
            trigger_o.parent = self
939
            trigger_o.init_with_xml(trigger, charset, include_id=include_id)
940

  
599 941

  
600 942
class WorkflowStatus(object):
601 943
    id = None
......
645 987
                return item
646 988
        raise KeyError()
647 989

  
648
    def perform_items(self, formdata, depth=20):
649
        if depth == 0: # prevents infinite loops
650
            return
651
        url = None
652
        old_status = formdata.status
653
        for item in self.items:
654
            try:
655
                url = item.perform(formdata) or url
656
            except AbortActionException:
657
                break
658
            if formdata.status != old_status:
659
                break
660
        if formdata.status != old_status:
661
            if not formdata.evolution:
662
                formdata.evolution = []
663
            evo = Evolution()
664
            evo.time = time.localtime()
665
            evo.status = formdata.status
666
            formdata.evolution.append(evo)
667
            formdata.store()
668
            # performs the items of the new status
669
            wf_status = formdata.get_status()
670
            url = wf_status.perform_items(formdata, depth=depth-1) or url
671
        return url
672

  
673 990
    def get_action_form(self, filled, user):
674 991
        form = Form(enctype='multipart/form-data', use_tokens = False)
675 992
        for item in self.items:
......
677 994
                continue
678 995
            item.fill_form(form, filled, user)
679 996

  
997
        for action in filled.formdef.workflow.get_global_actions_for_user(filled, user):
998
            form.add_submit('button-action-%s' % action.id, action.name)
999
            if form.get_widget('button-action-%s' % action.id):
1000
                form.get_widget('button-action-%s' % action.id).backoffice_info_text = action.backoffice_info_text
1001

  
680 1002
        if form.widgets or form.submit_widgets:
681 1003
            return form
682 1004
        else:
683 1005
            return None
684 1006

  
685 1007
    def handle_form(self, form, filled, user):
1008
        # check for global actions
1009
        for action in filled.formdef.workflow.get_global_actions_for_user(filled, user):
1010
            if 'button-action-%s' % action.id in get_request().form:
1011
                url = perform_items(action.items, filled)
1012
                if url:
1013
                    return redirect(url)
1014
                return
1015

  
686 1016
        evo = Evolution()
687 1017
        evo.time = time.localtime()
688 1018
        if user:
......
822 1152
            item_o.parent = self
823 1153
            item_o.init_with_xml(item, charset, include_id=include_id)
824 1154

  
825
class WorkflowStatusItem(object):
1155

  
1156
class WorkflowStatusItem(XmlSerialisable):
1157
    node_name = 'item'
826 1158
    description = 'XX'
827 1159
    category = None # (key, label)
828 1160
    id = None
829 1161

  
830 1162
    endpoint = True # means it's not possible to interact, and/or cause a status change
831 1163
    waitpoint = False # means it's possible to wait (user interaction, or other event)
1164
    ok_in_global_action = True # means it can be used in a global action
832 1165
    directory_name = None
833 1166
    directory_class = None
834 1167
    support_substitution_variables = False
......
937 1270
            label = self.render_as_line()
938 1271
        return label
939 1272

  
940
    def export_to_xml(self, charset, include_id=False):
941
        item = ET.Element('item')
942
        item.attrib['type'] = self.key
943
        for attribute in self.get_parameters():
944
            if hasattr(self, '%s_export_to_xml' % attribute):
945
                getattr(self, '%s_export_to_xml' % attribute)(item, charset,
946
                        include_id=include_id)
947
                continue
948
            if hasattr(self, attribute) and getattr(self, attribute) is not None:
949
                el = ET.SubElement(item, attribute)
950
                val = getattr(self, attribute)
951
                if type(val) is dict:
952
                    for k, v in val.items():
953
                        ET.SubElement(el, k).text = unicode(v, charset, 'replace')
954
                elif type(val) is list:
955
                    if attribute[-1] == 's':
956
                        atname = attribute[:-1]
957
                    else:
958
                        atname = 'item'
959
                    for v in val:
960
                        ET.SubElement(el, atname).text = unicode(str(v), charset, 'replace')
961
                elif type(val) in (str, unicode):
962
                    if type(val) is unicode:
963
                        el.text = val
964
                    else:
965
                        el.text = unicode(val, charset, 'replace')
966
                else:
967
                    el.text = str(val)
968
        return item
969

  
970
    def init_with_xml(self, elem, charset, include_id=False):
971
        for attribute in self.get_parameters():
972
            el = elem.find(attribute)
973
            if hasattr(self, '%s_init_with_xml' % attribute):
974
                getattr(self, '%s_init_with_xml' % attribute)(el, charset,
975
                        include_id=include_id)
976
                continue
977
            if el is None:
978
                continue
979
            if el.getchildren():
980
                if type(getattr(self, attribute)) is list:
981
                    v = [x.text.encode(charset) for x in el.getchildren()]
982
                elif type(getattr(self, attribute)) is dict:
983
                    v = {}
984
                    for e in el.getchildren():
985
                        v[e.tag] = e.text.encode(charset)
986
                else:
987
                    # ???
988
                    raise AssertionError
989
                setattr(self, attribute, v)
990
            else:
991
                if el.text is None:
992
                    setattr(self, attribute, None)
993
                elif el.text in ('False', 'True'): # bools
994
                    setattr(self, attribute, eval(el.text))
995
                elif type(getattr(self, attribute)) is int:
996
                    setattr(self, attribute, int(el.text.encode(charset)))
997
                else:
998
                    setattr(self, attribute, el.text.encode(charset))
999

  
1000
    def _roles_export_to_xml(self, attribute, item, charset, include_id=False):
1001
        if not hasattr(self, attribute) or not getattr(self, attribute):
1002
            return
1003
        el = ET.SubElement(item, attribute)
1004
        for role_id in getattr(self, attribute):
1005
            if role_id is None:
1006
                continue
1007
            role_id = str(role_id)
1008
            if role_id.startswith('_') or role_id == 'logged-users':
1009
                role = unicode(role_id, charset)
1010
            else:
1011
                try:
1012
                    role = unicode(Role.get(role_id).name, charset)
1013
                except KeyError:
1014
                    role = unicode(role_id, charset)
1015
            sub = ET.SubElement(el, 'item')
1016
            sub.attrib['role_id'] = role_id
1017
            sub.text = role
1018

  
1019
    def _roles_init_with_xml(self, attribute, elem, charset, include_id=False):
1020
        if elem is None:
1021
            setattr(self, attribute, [])
1022
        else:
1023
            imported_roles = []
1024
            for child in elem.getchildren():
1025
                imported_roles.append(self._get_role_id_from_xml(child,
1026
                    charset, include_id=include_id))
1027
            setattr(self, attribute, imported_roles)
1028

  
1029
    def _role_export_to_xml(self, attribute, item, charset, include_id=False):
1030
        if not hasattr(self, attribute) or not getattr(self, attribute):
1031
            return
1032
        role_id = str(getattr(self, attribute))
1033
        if role_id.startswith('_') or role_id == 'logged-users':
1034
            role = unicode(role_id, charset)
1035
        else:
1036
            try:
1037
                role = unicode(Role.get(role_id).name, charset)
1038
            except KeyError:
1039
                role = unicode(role_id, charset)
1040
        sub = ET.SubElement(item, attribute)
1041
        if include_id:
1042
            sub.attrib['role_id'] = role_id
1043
        sub.text = role
1044

  
1045
    def _get_role_id_from_xml(self, elem, charset, include_id=False):
1046
        if elem is None:
1047
            return None
1048

  
1049
        value = elem.text.encode(charset)
1050

  
1051
        # look for known static values
1052
        if value.startswith('_') or value == 'logged-users':
1053
            return value
1054

  
1055
        # if we import using id, only look at the role_id attribute
1056
        if include_id:
1057
            if not 'role_id' in elem.attrib:
1058
                return None
1059
            role_id = str(elem.attrib['role_id'])
1060
            if Role.has_key(role_id):
1061
                return role_id
1062
            else:
1063
                return None
1064

  
1065
        # if not using id, look up on the name
1066
        for role in Role.select(ignore_errors=True):
1067
            if role.name == value:
1068
                return role.id
1069

  
1070
        # and if there's no match, create a new role
1071
        role = Role()
1072
        role.name = value
1073
        role.store()
1074
        return role.id
1075

  
1076
    def _role_init_with_xml(self, attribute, elem, charset, include_id=False):
1077
        setattr(self, attribute, self._get_role_id_from_xml(elem, charset,
1078
            include_id=include_id))
1079

  
1080 1273
    def by_export_to_xml(self, item, charset, include_id=False):
1081 1274
        self._roles_export_to_xml('by', item, charset, include_id=include_id)
1082 1275

  
......
1153 1346
        except KeyError:
1154 1347
            return
1155 1348

  
1349
def render_list_of_roles(workflow, roles):
1350
    t = []
1351
    for r in roles:
1352
        role_label = get_role_translation_label(workflow, r)
1353
        if role_label:
1354
            t.append(role_label)
1355
    return ', '.join(t)
1356

  
1357

  
1156 1358
item_classes = []
1157 1359

  
1158 1360
def register_item_class(klass):
......
1166 1368
    key = 'commentable'
1167 1369
    endpoint = False
1168 1370
    waitpoint = True
1371
    ok_in_global_action = False
1169 1372

  
1170 1373
    varname = None
1171 1374
    label = None
......
1281 1484
    key = 'choice'
1282 1485
    endpoint = False
1283 1486
    waitpoint = True
1487
    ok_in_global_action = False
1284 1488

  
1285 1489
    label = None
1286 1490
    by = []
......
1333 1537
class JumpOnSubmitWorkflowStatusItem(WorkflowStatusJumpItem):
1334 1538
    description = N_('Change Status on Submit')
1335 1539
    key = 'jumponsubmit'
1540
    ok_in_global_action = False
1336 1541

  
1337 1542
    def render_as_line(self):
1338 1543
        if self.status:
......
1574 1779
    description = N_('Display message')
1575 1780
    key = 'displaymsg'
1576 1781
    support_substitution_variables = True
1782
    ok_in_global_action = False
1577 1783

  
1578 1784
    message = None
1579 1785

  
......
1611 1817
class RedirectToStatusWorkflowStatusItem(WorkflowStatusItem):
1612 1818
    description = N_('Redirect to Status Page')
1613 1819
    key = 'redirectstatus'
1820
    ok_in_global_action = False
1614 1821

  
1615 1822
    backoffice = False
1616 1823

  
......
1635 1842
    key = 'editable'
1636 1843
    endpoint = False
1637 1844
    waitpoint = True
1845
    ok_in_global_action = False
1638 1846

  
1639 1847
    by = []
1640 1848
    status = None
1641
-