Projet

Général

Profil

0001-workflows-allow-interactive-actions-in-global-action.patch

Frédéric Péters, 02 septembre 2022 18:04

Télécharger (50,9 ko)

Voir les différences:

Subject: [PATCH] workflows: allow interactive actions in global actions
 (#16782)

 tests/backoffice_pages/test_all.py            | 133 +++++++++++++
 tests/backoffice_pages/test_carddata.py       |  64 +++++++
 tests/form_pages/test_all.py                  |  62 ++++++
 tests/workflow/test_all.py                    |  44 ++---
 wcs/admin/workflows.py                        |   4 +-
 wcs/backoffice/data_management.py             |   1 +
 wcs/backoffice/management.py                  |   9 +
 wcs/carddef.py                                |   4 +-
 wcs/formdata.py                               |  10 +-
 wcs/formdef.py                                |   4 +-
 wcs/forms/actions.py                          | 162 +++++++++++++++-
 wcs/forms/common.py                           |   5 +-
 .../backoffice/global-interactive-action.html |  14 ++
 .../wcs/global-interactive-action.html        |   5 +
 .../includes/global-interactive-action.html   |   6 +
 wcs/wf/attachment.py                          |   5 +-
 wcs/wf/choice.py                              |  15 +-
 wcs/wf/comment.py                             |   5 +-
 wcs/wf/display_message.py                     |  41 ++--
 wcs/wf/export_to_model.py                     |   3 +
 wcs/wf/form.py                                |   5 +-
 wcs/workflows.py                              | 181 ++++++++++++------
 22 files changed, 664 insertions(+), 118 deletions(-)
 create mode 100644 wcs/templates/wcs/backoffice/global-interactive-action.html
 create mode 100644 wcs/templates/wcs/global-interactive-action.html
 create mode 100644 wcs/templates/wcs/includes/global-interactive-action.html
tests/backoffice_pages/test_all.py
846 846
        assert 'session_user=admin' in content
847 847

  
848 848

  
849
def test_backoffice_multi_actions_interactive(pub):
850
    create_superuser(pub)
851
    create_environment(pub)
852
    formdef = FormDef.get_by_urlname('form-title')
853

  
854
    app = login(get_app(pub))
855
    resp = app.get('/backoffice/management/form-title/')
856
    assert 'id="multi-actions"' in resp.text  # always there
857

  
858
    workflow = Workflow.get_default_workflow()
859
    workflow.id = '2'
860
    action = workflow.add_global_action('FOOBAR')
861

  
862
    form_action = action.add_action('form')
863
    form_action.varname = 'blah'
864
    form_action.formdef = WorkflowFormFieldsFormDef(item=form_action)
865
    form_action.formdef.fields.append(
866
        fields.StringField(id='1', label='Test', varname='test', type='string', required=True)
867
    )
868
    register_comment = action.add_action('register-comment')
869
    register_comment.comment = 'HELLO {{ form_workflow_form_blah_var_test }}'
870

  
871
    trigger = action.triggers[0]
872
    trigger.roles = [x.id for x in pub.role_class.select() if x.name == 'foobar']
873
    workflow.store()
874

  
875
    formdef.workflow_id = workflow.id
876
    formdef.store()
877

  
878
    resp = app.get('/backoffice/management/form-title/?limit=20')
879
    assert 'id="multi-actions"' in resp.text
880
    for checkbox in resp.forms[0].fields['select[]'][1:6]:
881
        checkbox.checked = True
882
    resp = resp.forms[0].submit('button-action-1')
883
    assert '/actions/' in resp.location
884
    resp = resp.follow()
885
    resp = resp.follow()  # back to form listing
886
    assert 'Error: empty action' in resp.text
887

  
888
    form_action.by = trigger.roles
889
    workflow.store()
890

  
891
    resp = app.get('/backoffice/management/form-title/?limit=20')
892
    ids = []
893
    for checkbox in resp.forms[0].fields['select[]'][1:6]:
894
        ids.append(checkbox._value)
895
        checkbox.checked = True
896

  
897
    resp = resp.forms[0].submit('button-action-1')
898
    assert '/actions/' in resp.location
899
    resp = resp.follow()
900
    assert '5 selected forms' in resp.text
901
    resp = resp.form.submit('submit')
902
    assert resp.pyquery('#form_error_fblah_1').text() == 'required field'
903
    resp.form['fblah_1'] = 'GLOBAL INTERACTIVE ACTION'
904
    resp = resp.form.submit('submit')
905

  
906
    assert '?job=' in resp.location
907
    resp = resp.follow()
908
    assert 'Executing task "FOOBAR" on forms' in resp.text
909
    assert '>completed<' in resp.text
910
    assert (
911
        resp.pyquery.find('[data-redirect-auto]').attr['href']
912
        == '/backoffice/management/form-title/?limit=20'
913
    )
914
    for id in ids:
915
        pub.substitutions.reset()
916
        pub.substitutions.feed(formdef.data_class().get(id))
917
        context = pub.substitutions.get_context_variables(mode='lazy')
918
        assert context['form_number_raw'] == id
919
        assert context['form_workflow_form_blah_var_test'].get_value() == 'GLOBAL INTERACTIVE ACTION'
920

  
921

  
849 922
def test_backoffice_map(pub):
850 923
    create_user(pub)
851 924
    create_environment(pub)
......
1072 1145
    assert resp.text.count('WORKFLOW COMMENT') == 2
1073 1146

  
1074 1147

  
1148
def test_backoffice_global_interactive_action(pub):
1149
    create_user(pub)
1150

  
1151
    formdef = FormDef()
1152
    formdef.name = 'test global action'
1153
    formdef.fields = []
1154

  
1155
    workflow = Workflow.get_default_workflow()
1156
    workflow.id = '2'
1157
    action = workflow.add_global_action('FOOBAR')
1158

  
1159
    display = action.add_action('displaymsg')
1160
    display.message = 'This is a message'
1161
    display.to = []
1162

  
1163
    form_action = action.add_action('form')
1164
    form_action.varname = 'blah'
1165
    form_action.formdef = WorkflowFormFieldsFormDef(item=form_action)
1166
    form_action.formdef.fields.append(
1167
        fields.StringField(id='1', label='Test', varname='test', type='string', required=True)
1168
    )
1169
    register_comment = action.add_action('register-comment')
1170
    register_comment.comment = 'HELLO {{ form_workflow_form_blah_var_test }}'
1171
    trigger = action.triggers[0]
1172
    trigger.roles = [x.id for x in pub.role_class.select() if x.name == 'foobar']
1173

  
1174
    workflow.store()
1175
    formdef.workflow_id = workflow.id
1176
    formdef.workflow_roles = {'_receiver': 1}
1177
    formdef.store()
1178

  
1179
    formdata = formdef.data_class()()
1180
    formdata.just_created()
1181
    formdata.store()
1182

  
1183
    app = login(get_app(pub))
1184
    resp = app.get(formdata.get_url(backoffice=True))
1185
    assert 'button-action-1' in resp.form.fields
1186
    resp = resp.form.submit('button-action-1')
1187
    resp = resp.follow()  # -> error, empty action
1188
    resp = resp.follow()  # -> back to form
1189
    assert 'Error: empty action' in resp.text
1190

  
1191
    form_action.by = trigger.roles
1192
    workflow.store()
1193

  
1194
    resp = app.get(formdata.get_url(backoffice=True))
1195
    resp = resp.form.submit('button-action-1')
1196
    resp = resp.follow()
1197
    assert 'This is a message' in resp.text
1198
    resp = resp.form.submit('submit')
1199
    assert resp.pyquery('#form_error_fblah_1').text() == 'required field'
1200
    resp.form['fblah_1'] = 'GLOBAL INTERACTIVE ACTION'
1201
    resp = resp.form.submit('submit')
1202
    assert resp.location == formdata.get_url(backoffice=True)
1203
    resp = resp.follow()
1204

  
1205
    assert 'HELLO GLOBAL INTERACTIVE ACTION' in resp.text
1206

  
1207

  
1075 1208
def test_backoffice_submission_context(pub):
1076 1209
    user = create_user(pub)
1077 1210
    create_environment(pub)
tests/backoffice_pages/test_carddata.py
13 13
from wcs.categories import CardDefCategory
14 14
from wcs.formdef import FormDef
15 15
from wcs.qommon.http_request import HTTPRequest
16
from wcs.wf.form import WorkflowFormFieldsFormDef
16 17
from wcs.workflows import Workflow, WorkflowBackofficeFieldsFormDef
17 18

  
18 19
from ..utilities import clean_temporary_pub, create_temporary_pub, get_app, login
......
1142 1143
    assert 'Add another Child' not in resp
1143 1144
    assert resp.text.count('/backoffice/data/adult/add/?_popup=1') == 1
1144 1145
    assert '/backoffice/data/child/add/?_popup=1' not in resp
1146

  
1147

  
1148
def test_backoffice_card_global_interactive_action(pub):
1149
    user = create_user(pub)
1150

  
1151
    workflow = CardDef.get_default_workflow()
1152
    workflow.id = None
1153
    action = workflow.add_global_action('FOOBAR')
1154

  
1155
    display = action.add_action('displaymsg')
1156
    display.message = 'This is a message'
1157
    display.to = []
1158

  
1159
    form_action = action.add_action('form')
1160
    form_action.varname = 'blah'
1161
    form_action.formdef = WorkflowFormFieldsFormDef(item=form_action)
1162
    form_action.formdef.fields.append(
1163
        fields.StringField(id='1', label='Test', varname='test', type='string', required=True)
1164
    )
1165
    register_comment = action.add_action('register-comment')
1166
    register_comment.comment = 'HELLO {{ form_workflow_form_blah_var_test }}'
1167
    trigger = action.triggers[0]
1168
    trigger.roles = [user.roles[0]]
1169

  
1170
    workflow.store()
1171

  
1172
    CardDef.wipe()
1173
    carddef = CardDef()
1174
    carddef.name = 'foo'
1175
    carddef.fields = []
1176
    carddef.workflow_id = workflow.id
1177
    carddef.workflow_roles = {'_editor': user.roles[0]}
1178
    carddef.store()
1179

  
1180
    carddef.data_class().wipe()
1181
    carddata = carddef.data_class()()
1182
    carddata.data = {}
1183
    carddata.just_created()
1184
    carddata.store()
1185

  
1186
    app = login(get_app(pub))
1187
    resp = app.get(carddata.get_url(backoffice=True))
1188
    assert 'button-action-1' in resp.form.fields
1189
    resp = resp.form.submit('button-action-1')
1190
    resp = resp.follow()  # -> error, empty action
1191
    resp = resp.follow()  # -> back to form
1192
    assert 'Error: empty action' in resp.text
1193

  
1194
    form_action.by = trigger.roles
1195
    workflow.store()
1196

  
1197
    resp = app.get(carddata.get_url(backoffice=True))
1198
    resp = resp.form.submit('button-action-1')
1199
    resp = resp.follow()
1200
    assert 'This is a message' in resp.text
1201
    resp = resp.form.submit('submit')
1202
    assert resp.pyquery('#form_error_fblah_1').text() == 'required field'
1203
    resp.form['fblah_1'] = 'GLOBAL INTERACTIVE ACTION'
1204
    resp = resp.form.submit('submit')
1205
    assert resp.location == carddata.get_url(backoffice=True)
1206
    resp = resp.follow()
1207

  
1208
    assert 'HELLO GLOBAL INTERACTIVE ACTION' in resp.text
tests/form_pages/test_all.py
9752 9752
    app = get_app(pub)
9753 9753
    resp = app.get('/test/go-to-backoffice')
9754 9754
    assert resp.location.endswith('/backoffice/forms/%s/' % formdef.id)
9755

  
9756

  
9757
def test_global_interactive_action(pub):
9758
    user = create_user(pub)
9759

  
9760
    formdef = FormDef()
9761
    formdef.name = 'test global action'
9762
    formdef.fields = []
9763

  
9764
    workflow = Workflow.get_default_workflow()
9765
    workflow.id = '2'
9766
    action = workflow.add_global_action('FOOBAR')
9767

  
9768
    display = action.add_action('displaymsg')
9769
    display.message = 'This is a message'
9770
    display.to = []
9771

  
9772
    form_action = action.add_action('form')
9773
    form_action.varname = 'blah'
9774
    form_action.formdef = WorkflowFormFieldsFormDef(item=form_action)
9775
    form_action.formdef.fields.append(
9776
        fields.StringField(id='1', label='Test', varname='test', type='string', required=True)
9777
    )
9778
    register_comment = action.add_action('register-comment')
9779
    register_comment.comment = 'HELLO {{ form_workflow_form_blah_var_test }}'
9780
    trigger = action.triggers[0]
9781
    trigger.roles = ['_submitter']
9782

  
9783
    workflow.store()
9784
    formdef.workflow_id = workflow.id
9785
    formdef.workflow_roles = {'_receiver': 1}
9786
    formdef.store()
9787

  
9788
    formdata = formdef.data_class()()
9789
    formdata.user_id = user.id
9790
    formdata.just_created()
9791
    formdata.perform_workflow()
9792
    formdata.store()
9793

  
9794
    app = login(get_app(pub), username='foo', password='foo')
9795
    resp = app.get(formdata.get_url(backoffice=False))
9796
    assert 'button-action-1' in resp.form.fields
9797
    resp = resp.form.submit('button-action-1')
9798
    resp = resp.follow()  # -> error, empty action
9799
    resp = resp.follow()  # -> back to form
9800
    assert 'Error: empty action' in resp.text
9801

  
9802
    form_action.by = trigger.roles
9803
    workflow.store()
9804

  
9805
    resp = app.get(formdata.get_url(backoffice=False))
9806
    resp = resp.form.submit('button-action-1')
9807
    resp = resp.follow()
9808
    assert 'This is a message' in resp.text
9809
    resp = resp.form.submit('submit')
9810
    assert resp.pyquery('#form_error_fblah_1').text() == 'required field'
9811
    resp.form['fblah_1'] = 'GLOBAL INTERACTIVE ACTION'
9812
    resp = resp.form.submit('submit')
9813
    assert resp.location == formdata.get_url(backoffice=False)
9814
    resp = resp.follow()
9815

  
9816
    assert 'HELLO GLOBAL INTERACTIVE ACTION' in resp.text
tests/workflow/test_all.py
3025 3025
    display_message.parent = st1
3026 3026

  
3027 3027
    display_message.message = 'test'
3028
    assert display_message.get_message(formdata) == 'test'
3028
    assert display_message.get_message(formdata) == '<p>test</p>'
3029 3029

  
3030 3030
    display_message.message = '{{ number }}'
3031
    assert display_message.get_message(formdata) == str(formdata.id)
3031
    assert display_message.get_message(formdata) == '<p>%s</p>' % formdata.id
3032 3032

  
3033 3033
    display_message.message = '[number]'
3034
    assert display_message.get_message(formdata) == str(formdata.id)
3034
    assert display_message.get_message(formdata) == '<p>%s</p>' % formdata.id
3035 3035

  
3036 3036
    display_message.message = '{{ bar }}'
3037
    assert display_message.get_message(formdata) == 'Foobar'
3037
    assert display_message.get_message(formdata) == '<p>Foobar</p>'
3038 3038

  
3039 3039
    display_message.message = '[bar]'
3040
    assert display_message.get_message(formdata) == 'Foobar'
3040
    assert display_message.get_message(formdata) == '<p>Foobar</p>'
3041 3041

  
3042 3042
    # makes sure the string is correctly escaped for HTML
3043 3043
    display_message.message = '{{ foo }}'
3044
    assert display_message.get_message(formdata) == '1 &lt; 3'
3044
    assert display_message.get_message(formdata) == '<p>1 &lt; 3</p>'
3045 3045
    display_message.message = '[foo]'
3046
    assert display_message.get_message(formdata) == '1 &lt; 3'
3046
    assert display_message.get_message(formdata) == '<p>1 &lt; 3</p>'
3047 3047

  
3048 3048

  
3049 3049
def test_workflow_display_message_to(pub):
......
3072 3072

  
3073 3073
    display_message.message = 'all'
3074 3074
    display_message.to = None
3075
    assert display_message.get_message(formdata) == 'all'
3076
    assert formdata.get_workflow_messages(user=pub._request._user) == ['all']
3075
    assert display_message.get_message(formdata) == '<p>all</p>'
3076
    assert formdata.get_workflow_messages(user=pub._request._user) == ['<p>all</p>']
3077 3077

  
3078 3078
    display_message.message = 'to-role'
3079 3079
    display_message.to = [role.id]
......
3086 3086
    assert display_message.get_message(formdata) == ''
3087 3087
    assert formdata.get_workflow_messages(user=pub._request._user) == []
3088 3088
    user.roles = [role.id]
3089
    assert display_message.get_message(formdata) == 'to-role'
3090
    assert formdata.get_workflow_messages(user=pub._request._user) == ['to-role']
3089
    assert display_message.get_message(formdata) == '<p>to-role</p>'
3090
    assert formdata.get_workflow_messages(user=pub._request._user) == ['<p>to-role</p>']
3091 3091

  
3092 3092
    user.roles = []
3093 3093
    display_message.message = 'to-submitter'
......
3095 3095
    assert display_message.get_message(formdata) == ''
3096 3096
    assert formdata.get_workflow_messages(user=pub._request._user) == []
3097 3097
    formdata.user_id = user.id
3098
    assert display_message.get_message(formdata) == 'to-submitter'
3099
    assert formdata.get_workflow_messages(user=pub._request._user) == ['to-submitter']
3098
    assert display_message.get_message(formdata) == '<p>to-submitter</p>'
3099
    assert formdata.get_workflow_messages(user=pub._request._user) == ['<p>to-submitter</p>']
3100 3100

  
3101 3101
    display_message.message = 'to-role-or-submitter'
3102 3102
    display_message.to = [role.id, '_submitter']
3103
    assert display_message.get_message(formdata) == 'to-role-or-submitter'
3104
    assert formdata.get_workflow_messages(user=pub._request._user) == ['to-role-or-submitter']
3103
    assert display_message.get_message(formdata) == '<p>to-role-or-submitter</p>'
3104
    assert formdata.get_workflow_messages(user=pub._request._user) == ['<p>to-role-or-submitter</p>']
3105 3105
    formdata.user_id = None
3106 3106
    assert display_message.get_message(formdata) == ''
3107 3107
    assert formdata.get_workflow_messages(user=pub._request._user) == []
3108 3108
    user.roles = [role.id]
3109
    assert display_message.get_message(formdata) == 'to-role-or-submitter'
3110
    assert formdata.get_workflow_messages(user=pub._request._user) == ['to-role-or-submitter']
3109
    assert display_message.get_message(formdata) == '<p>to-role-or-submitter</p>'
3110
    assert formdata.get_workflow_messages(user=pub._request._user) == ['<p>to-role-or-submitter</p>']
3111 3111
    formdata.user_id = user.id
3112
    assert display_message.get_message(formdata) == 'to-role-or-submitter'
3113
    assert formdata.get_workflow_messages(user=pub._request._user) == ['to-role-or-submitter']
3112
    assert display_message.get_message(formdata) == '<p>to-role-or-submitter</p>'
3113
    assert formdata.get_workflow_messages(user=pub._request._user) == ['<p>to-role-or-submitter</p>']
3114 3114

  
3115 3115
    display_message.to = [role2.id]
3116 3116
    assert display_message.get_message(formdata) == ''
......
3120 3120
    display_message2 = st1.add_action('displaymsg')
3121 3121
    display_message2.message = 'd2'
3122 3122
    display_message2.to = [role.id, '_submitter']
3123
    assert formdata.get_workflow_messages(user=pub._request._user) == ['d2']
3123
    assert formdata.get_workflow_messages(user=pub._request._user) == ['<p>d2</p>']
3124 3124
    user.roles = [role.id, role2.id]
3125
    assert 'd1' in formdata.get_workflow_messages(user=pub._request._user)
3126
    assert 'd2' in formdata.get_workflow_messages(user=pub._request._user)
3125
    assert '<p>d1</p>' in formdata.get_workflow_messages(user=pub._request._user)
3126
    assert '<p>d2</p>' in formdata.get_workflow_messages(user=pub._request._user)
3127 3127

  
3128 3128

  
3129 3129
def test_workflow_display_message_line_details(pub):
wcs/admin/workflows.py
702 702
        available_items.sort(key=lambda x: misc.simplify(x.description))
703 703

  
704 704
        for category, category_label in categories:
705
            options = [(x.key, x.description) for x in available_items if x.category == category]
705
            options = [
706
                (x.key, x(parent=self.status).description) for x in available_items if x.category == category
707
            ]
706 708
            form.add(
707 709
                SingleSelectWidget,
708 710
                'action-%s' % category,
wcs/backoffice/data_management.py
114 114
        'map',
115 115
        'geojson',
116 116
        'add',
117
        'actions',
117 118
        ('export-spreadsheet', 'export_spreadsheet'),
118 119
        ('save-view', 'save_view'),
119 120
        ('delete-view', 'delete_view'),
wcs/backoffice/management.py
764 764
        'export',
765 765
        'map',
766 766
        'geojson',
767
        'actions',
767 768
        ('export-spreadsheet', 'export_spreadsheet'),
768 769
        ('filter-options', 'filter_options'),
769 770
        ('save-view', 'save_view'),
......
795 796
            if update_breadcrumbs:
796 797
                get_response().breadcrumb.append((view.get_url_slug() + '/', view.title))
797 798
        self.set_default_view()
799
        from wcs.forms.actions import ActionsDirectory
800

  
801
        self.actions = ActionsDirectory()
798 802

  
799 803
    def set_default_view(self):
800 804
        if not get_request():
......
2215 2219
                'receipt_time', [Contains('id', [int(x) for x in item_ids])]
2216 2220
            )
2217 2221

  
2222
        if action['action'].is_interactive():
2223
            return redirect(
2224
                action['action'].get_global_interactive_form_url(formdef=self.formdef, ids=item_ids)
2225
            )
2226

  
2218 2227
        job = get_response().add_after_job(
2219 2228
            MassActionAfterJob(
2220 2229
                label=_('Executing task "%s" on forms') % action['action'].name,
wcs/carddef.py
26 26
from wcs.categories import CardDefCategory
27 27
from wcs.formdef import FormDef, FormDefDoesNotExist, get_formdefs_of_all_kinds
28 28

  
29
from .qommon import _, force_text, misc
29
from .qommon import _, force_text, misc, pgettext_lazy
30 30
from .qommon.storage import ElementEqual, ElementILike, Equal, Null, StrictNotEqual
31 31

  
32 32
if not hasattr(types, 'ClassType'):
......
46 46
    xml_root_node = 'carddef'
47 47
    verbose_name = _('Card model')
48 48
    verbose_name_plural = _('Card models')
49
    item_name = pgettext_lazy('item', 'card')
50
    item_name_plural = pgettext_lazy('item', 'cards')
49 51

  
50 52
    confirmation = False
51 53

  
wcs/formdata.py
672 672
        wf_status = self.get_visible_status(user=user)
673 673
        if not wf_status:
674 674
            return []
675
        messages = []
676
        for item in wf_status.items:
677
            if not item.check_condition(self):
678
                continue
679
            if hasattr(item, 'get_message'):
680
                message = item.get_message(self, position=position)
681
                if message:
682
                    messages.append(message)
683
        return messages
675
        return wf_status.get_messages(formdata=self, position=position)
684 676

  
685 677
    def get_status(self, status=None):
686 678
        if not status:
wcs/formdef.py
38 38
from . import data_sources, fields
39 39
from .categories import Category
40 40
from .formdata import FormData
41
from .qommon import PICKLE_KWARGS, _, force_str, get_cfg
41
from .qommon import PICKLE_KWARGS, _, force_str, get_cfg, pgettext_lazy
42 42
from .qommon.admin.emails import EmailsDirectory
43 43
from .qommon.cron import CronJob
44 44
from .qommon.form import Form, HtmlWidget, UploadedFile
......
126 126
    backoffice_section = 'forms'
127 127
    verbose_name = _('Form')
128 128
    verbose_name_plural = _('Forms')
129
    item_name = pgettext_lazy('item', 'form')
130
    item_name_plural = pgettext_lazy('item', 'forms')
129 131

  
130 132
    name = None
131 133
    description = None
wcs/forms/actions.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, redirect
17
from quixote import get_publisher, get_request, get_response, get_session, redirect
18 18
from quixote.directory import Directory
19 19
from quixote.errors import PublishError
20 20

  
21 21
from wcs.carddef import CardDef
22 22
from wcs.formdef import FormDef
23 23
from wcs.forms.common import FormTemplateMixin
24
from wcs.qommon import _, errors, misc, template
25
from wcs.qommon.afterjobs import AfterJob
26
from wcs.qommon.backoffice.menu import html_top as backoffice_html_top
27
from wcs.qommon.form import Form
28
from wcs.qommon.http_request import HTTPRequest
24 29
from wcs.wf.jump import jump_and_perform
25

  
26
from ..qommon import _, errors, misc, template
27
from ..qommon.form import Form
30
from wcs.workflows import perform_items
28 31

  
29 32

  
30 33
class MissingOrExpiredToken(PublishError):
......
45 48
            token = get_publisher().token_class.get(component)
46 49
        except KeyError:
47 50
            raise MissingOrExpiredToken()
48
        if token.type != 'action':
49
            raise errors.TraversalError()
50
        return ActionDirectory(token)
51
        if token.type == 'action':
52
            return ActionDirectory(token)
53
        if token.type == 'global-interactive-action':
54
            return GlobalInteractiveActionDirectory(token)
55
        raise errors.TraversalError()
51 56

  
52 57

  
53 58
class ActionDirectory(Directory, FormTemplateMixin):
......
108 113
        return template.QommonTemplateResponse(
109 114
            templates=list(self.get_formdef_template_variants(self.templates)), context=context
110 115
        )
116

  
117

  
118
class GlobalInteractiveActionDirectory(Directory, FormTemplateMixin):
119
    _q_exports = ['']
120

  
121
    def __init__(self, token):
122
        self.token = token
123
        formdef_type = self.token.context.get('form_type', 'formdef')
124
        if formdef_type == 'carddef':
125
            formdef_class = CardDef
126
        elif formdef_type == 'formdef':
127
            formdef_class = FormDef
128
        else:
129
            raise errors.TraversalError()
130

  
131
        self.formdef = formdef_class.get_by_urlname(self.token.context['form_slug'])
132

  
133
        try:
134
            self.formdata = self.formdef.data_class().get(self.token.context['form_ids'][0])
135
        except KeyError:
136
            raise MissingFormdata()
137

  
138
        self.action = None
139
        for action in self.formdef.workflow.global_actions or []:
140
            if action.id == self.token.context['action_id']:
141
                self.action = action
142
                break
143
        else:
144
            raise MissingOrExpiredToken()
145

  
146
    def _q_index(self):
147
        if get_request().is_in_backoffice():
148
            if isinstance(self.formdef, CardDef):
149
                section = 'data_management'
150
            else:
151
                section = 'management'
152
            get_response().breadcrumb.append(('', self.action.name))
153
            backoffice_html_top(section, title=self.formdef.name)
154
            template_name = 'wcs/backoffice/global-interactive-action.html'
155
        else:
156
            template.html_top(title=self.formdef.name)
157
            template_name = 'wcs/global-interactive-action.html'
158
        form = self.action.get_action_form(self.formdata, user=get_request().user)
159
        if not form:
160
            # empty form, nothing to do
161
            get_session().message = ('error', _('Error: empty action'))
162
            return redirect(self.token.context['return_url'])
163
        if not form.is_submitted() or form.has_errors():
164
            messages = self.action.get_messages()
165
            context = {
166
                'html_form': form,
167
                'action': self.action,
168
                'ids': self.token.context['form_ids'],
169
                'formdata': self.formdata,
170
                'workflow_messages': messages,
171
            }
172
            return template.QommonTemplateResponse(templates=[template_name], context=context)
173

  
174
        if len(self.token.context['form_ids']) > 1:
175
            # mass action
176
            job = get_response().add_after_job(
177
                GlobalInteractiveMassActionAfterJob(
178
                    label=_('Executing task "%s" on forms') % self.action.name,
179
                    formdef=self.formdef,
180
                    request_form=get_request().form,
181
                    user_id=get_request().user.id,
182
                    action_id=self.action.id,
183
                    item_ids=self.token.context['form_ids'],
184
                    return_url=self.token.context['return_url'],
185
                )
186
            )
187
            job.store()
188
            self.token.remove_self()
189
            return redirect(job.get_processing_url())
190

  
191
        get_publisher().substitutions.reset()
192
        get_publisher().substitutions.feed(get_publisher())
193
        get_publisher().substitutions.feed(self.formdata)
194
        status = self.formdata.status
195
        url = self.action.handle_form(form, self.formdata, user=get_request().user)
196
        if self.formdata.status == status:
197
            # if there's no status change run non-interactive items from global ction
198
            url = (
199
                perform_items(
200
                    self.action.items, self.formdata, event=('global-interactive-action', self.action.id)
201
                )
202
                or url
203
            )
204
        if not url:
205
            url = self.formdata.get_url(backoffice=bool(get_request().is_in_backoffice()))
206
        self.token.remove_self()
207
        return redirect(url)
208

  
209

  
210
class GlobalInteractiveMassActionAfterJob(AfterJob):
211
    def __init__(self, formdef, **kwargs):
212
        super().__init__(formdef_class=formdef.__class__, formdef_id=formdef.id, **kwargs)
213

  
214
    def execute(self):
215
        self.total_count = len(self.kwargs['item_ids'])
216

  
217
        # restore request form
218
        publisher = get_publisher()
219
        req = HTTPRequest(None, {'SERVER_NAME': publisher.tenant.hostname, 'SCRIPT_NAME': ''})
220
        req.form = self.kwargs['request_form']
221

  
222
        formdef = self.kwargs['formdef_class'].get(self.kwargs['formdef_id'])
223
        data_class = formdef.data_class()
224

  
225
        for action in formdef.workflow.global_actions or []:
226
            if action.id == self.kwargs['action_id']:
227
                break
228
        else:
229
            # maybe action got removed from workflow?
230
            return
231

  
232
        user = publisher.user_class.get(self.kwargs['user_id'])
233
        for item_id in self.kwargs['item_ids']:
234
            publisher._set_request(req)
235
            publisher.substitutions.reset()
236
            publisher.substitutions.feed(get_publisher())
237
            formdata = data_class.get(item_id)
238
            publisher.substitutions.feed(formdata)
239

  
240
            status = formdata.status
241
            form = action.get_action_form(formdata, user=user)
242
            form.method = 'get'
243
            action.handle_form(form, formdata, user=user)
244
            # reset request to avoid emails being created as afterjobs
245
            publisher._set_request(None)
246
            if formdata.status == status:
247
                # if there's no status change run non-interactive items from global ction
248
                perform_items(action.items, formdata, event=('global-interactive-action', action.id))
249
            self.increment_count()
250

  
251
    def done_action_url(self):
252
        return self.kwargs['return_url']
253

  
254
    def done_action_label(self):
255
        return _('Back to Listing')
256

  
257
    def done_button_attributes(self):
258
        return {'data-redirect-auto': 'true'}
wcs/forms/common.py
226 226
                else:
227 227
                    r += htmltext('<div class="workflow-messages %s">' % position)
228 228
                for workflow_message in workflow_messages:
229
                    if workflow_message.startswith('<'):
230
                        r += htmltext(workflow_message)
231
                    else:
232
                        r += htmltext('<p>%s</p>' % workflow_message)
229
                    r += htmltext(workflow_message)
233 230
                r += htmltext('</div>')
234 231
                return r.getvalue()
235 232
        return ''
wcs/templates/wcs/backoffice/global-interactive-action.html
1
{% extends "wcs/backoffice/base.html" %}
2
{% load i18n %}
3

  
4
{% block appbar-title %}{{ action.name }} -
5
{% if ids|length == 1 %}
6
{{ formdata.formdef.item_name }} - {{ formdata.id_display }}
7
{% else %}
8
{% blocktrans with len=ids|length item_name=formdata.formdef.item_name_plural %}{{ len }} selected {{ item_name }}{% endblocktrans %}
9
{% endif %}
10
{% endblock %}
11

  
12
{% block content %}
13
{% include "wcs/includes/global-interactive-action.html" %}
14
{% endblock %}
wcs/templates/wcs/global-interactive-action.html
1
{% extends template_base %}
2

  
3
{% block body %}
4
{% include "wcs/includes/global-interactive-action.html" %}
5
{% endblock %}
wcs/templates/wcs/includes/global-interactive-action.html
1
{% if workflow_messages %}
2
  <div class="workflow-messages">
3
  {% for message in workflow_messages %}{{ message|safe }}{% endfor %}
4
  </div>
5
{% endif %}
6
{{ html_form.render|safe }}
wcs/wf/attachment.py
92 92
    category = 'interaction'
93 93
    endpoint = False
94 94
    waitpoint = True
95
    ok_in_global_action = False
95
    ok_in_global_action = True
96 96

  
97 97
    title = None
98 98
    display_title = True
......
128 128
        else:
129 129
            return _('not completed')
130 130

  
131
    def is_interactive(self):
132
        return True
133

  
131 134
    def fill_form(self, form, formdata, user, **kwargs):
132 135
        if self.display_title:
133 136
            title = self.title or _('Upload File')
wcs/wf/choice.py
25 25
    WidgetList,
26 26
    WysiwygTextWidget,
27 27
)
28
from wcs.workflows import WorkflowStatus, WorkflowStatusJumpItem, register_item_class
28
from wcs.workflows import WorkflowGlobalAction, WorkflowStatus, WorkflowStatusJumpItem, register_item_class
29 29

  
30 30

  
31 31
class ChoiceWorkflowStatusItem(WorkflowStatusJumpItem):
32
    description = _('Manual Jump')
33 32
    key = 'choice'
34 33
    endpoint = False
35 34
    waitpoint = True
36
    ok_in_global_action = False
35
    ok_in_global_action = True
37 36

  
38 37
    label = None
39 38
    identifier = None
......
42 41
    require_confirmation = False
43 42
    ignore_form_errors = False
44 43

  
44
    @property
45
    def description(self):
46
        if isinstance(self.parent, WorkflowGlobalAction):
47
            return _('Manual Jump (interactive)')
48
        else:
49
            return _('Manual Jump')
50

  
45 51
    def get_label(self):
46 52
        expression = self.get_expression(self.label, allow_python=False, allow_ezt=False)
47 53
        if expression['type'] == 'text':
......
83 89
        if self.get_expression(self.label, allow_python=False, allow_ezt=False)['type'] != 'text':
84 90
            yield self.label
85 91

  
92
    def is_interactive(self):
93
        return True
94

  
86 95
    def fill_form(self, form, formdata, user, **kwargs):
87 96
        label = self.compute(self.label, allow_python=False, allow_ezt=False)
88 97
        if not label:
wcs/wf/comment.py
52 52
    category = 'interaction'
53 53
    endpoint = False
54 54
    waitpoint = True
55
    ok_in_global_action = False
55
    ok_in_global_action = True
56 56

  
57 57
    required = False
58 58
    varname = None
......
68 68
        else:
69 69
            return _('not completed')
70 70

  
71
    def is_interactive(self):
72
        return True
73

  
71 74
    def fill_form(self, form, formdata, user, **kwargs):
72 75
        if 'comment' not in [x.name for x in form.widgets]:
73 76
            if self.label is None:
wcs/wf/display_message.py
22 22
from wcs.qommon import _, ezt, misc
23 23
from wcs.qommon.form import ComputedExpressionWidget, SingleSelectWidget, TextWidget, WidgetListOfRoles
24 24
from wcs.qommon.template import Template
25
from wcs.workflows import WorkflowStatusItem, register_item_class
25
from wcs.workflows import WorkflowGlobalAction, WorkflowStatusItem, register_item_class
26 26

  
27 27

  
28 28
class DisplayMessageWorkflowStatusItem(WorkflowStatusItem):
......
30 30
    key = 'displaymsg'
31 31
    category = 'interaction'
32 32
    support_substitution_variables = True
33
    ok_in_global_action = False
33
    ok_in_global_action = True
34 34

  
35 35
    to = None
36 36
    position = 'top'
......
38 38
    message = None
39 39

  
40 40
    def get_line_details(self):
41
        in_global_action = isinstance(self.parent, WorkflowGlobalAction)
41 42
        parts = []
42
        if self.position == 'top':
43
        if in_global_action:
44
            pass
45
        elif self.position == 'top':
43 46
            parts.append(_('top of page'))
44 47
        elif self.position == 'bottom':
45 48
            parts.append(_('bottom of page'))
......
53 56
        yield from super().get_computed_strings()
54 57
        yield self.message
55 58

  
56
    def get_message(self, filled, position='top'):
57
        if not (self.message and self.position == position and filled.is_for_current_user(self.to)):
59
    def get_message(self, formdata, position='top'):
60
        if not self.message:
58 61
            return ''
62
        if formdata and not formdata.is_for_current_user(self.to):
63
            return ''
64

  
65
        in_global_action = isinstance(self.parent, WorkflowGlobalAction)
66
        if self.position != position and not in_global_action:
67
            return
59 68

  
60
        dict = copy.copy(get_publisher().substitutions.get_context_variables('lazy'))
61
        dict['date'] = misc.localstrftime(filled.receipt_time)
62
        dict['number'] = filled.id
63
        handling_role = filled.get_handling_role()
64
        if handling_role and handling_role.details:
65
            dict['receiver'] = handling_role.details.replace('\n', '<br />')
69
        if formdata:
70
            ctx = copy.copy(get_publisher().substitutions.get_context_variables('lazy'))
71
            ctx['date'] = misc.localstrftime(formdata.receipt_time)
72
            ctx['number'] = formdata.id
73
            handling_role = formdata.get_handling_role()
74
            if handling_role and handling_role.details:
75
                ctx['receiver'] = handling_role.details.replace('\n', '<br />')
76
        else:
77
            ctx = get_publisher().substitutions.get_context_variables('lazy')
66 78

  
67 79
        message = self.message
68 80
        if self.level:
69 81
            message = '<div class="%snotice">%s</div>' % (self.level, message)
82
        elif not message.startswith('<'):
83
            message = '<p>%s</p>' % message
70 84

  
71 85
        try:
72
            return Template(message, ezt_format=ezt.FORMAT_HTML).render(dict)
86
            return Template(message, ezt_format=ezt.FORMAT_HTML).render(ctx)
73 87
        except Exception as e:
74 88
            get_publisher().record_error(
75 89
                error_summary=_('Error in template of workflow message (%s)') % e, exception=e, notify=True
......
78 92

  
79 93
    def add_parameters_widgets(self, form, parameters, prefix='', formdef=None, **kwargs):
80 94
        super().add_parameters_widgets(form, parameters, prefix=prefix, formdef=formdef, **kwargs)
95
        in_global_action = isinstance(self.parent, WorkflowGlobalAction)
81 96
        if 'message' in parameters:
82 97
            form.add(
83 98
                TextWidget,
......
102 117
                    ('error', _('Error')),
103 118
                ],
104 119
            )
105
        if 'position' in parameters:
120
        if 'position' in parameters and not in_global_action:
106 121
            form.add(
107 122
                SingleSelectWidget,
108 123
                '%sposition' % prefix,
wcs/wf/export_to_model.py
263 263
        else:
264 264
            return _('no model set')
265 265

  
266
    def is_interactive(self):
267
        return bool(self.method == 'interactive')
268

  
266 269
    def fill_form(self, form, formdata, user, **kwargs):
267 270
        if self.method != 'interactive':
268 271
            return
wcs/wf/form.py
112 112
    description = _('Form')
113 113
    key = 'form'
114 114
    category = 'interaction'
115
    ok_in_global_action = False
115
    ok_in_global_action = True
116 116
    endpoint = False
117 117
    waitpoint = True
118 118

  
......
251 251
                # already prefixed
252 252
                pass
253 253

  
254
    def is_interactive(self):
255
        return True
256

  
254 257
    def fill_form(self, form, formdata, user, displayed_fields=None, **kwargs):
255 258
        if not self.formdef:
256 259
            return
wcs/workflows.py
414 414
            'global-action': _('Global action'),
415 415
            'global-action-timeout': _('Global action timeout'),
416 416
            'global-api-trigger': _('API Trigger'),
417
            'global-interactive-action': _('Global action (interactive)'),
417 418
            'global-external-workflow': _('Trigger by external workflow'),
418 419
            'json-import-created': _('Created (by JSON import)'),
419 420
            'timeout-jump': _('Timeout jump'),
......
1730 1731
        for action in self.items or []:
1731 1732
            yield from action.get_dependencies()
1732 1733

  
1734
    def get_action_form(self, filled, user, displayed_fields=None):
1735
        form = Form(enctype='multipart/form-data', use_tokens=False)
1736
        form.attrs['id'] = 'wf-actions'
1737
        for item in self.items:
1738
            if not item.check_auth(filled, user):
1739
                continue
1740
            if not item.check_condition(filled):
1741
                continue
1742
            item.fill_form(form, filled, user, displayed_fields=displayed_fields)
1743

  
1744
        if form.widgets or form.submit_widgets:
1745
            return form
1746
        else:
1747
            return None
1748

  
1749
    def get_active_items(self, form, filled, user):
1750
        for item in self.items:
1751
            if hasattr(item, 'by'):
1752
                for role in item.by or []:
1753
                    if role == logged_users_role().id:
1754
                        break
1755
                    if role == '_submitter':
1756
                        if filled.is_submitter(user):
1757
                            break
1758
                        continue
1759
                    if user is None:
1760
                        continue
1761
                    if filled.get_function_roles(role).intersection(user.get_roles()):
1762
                        break
1763
                else:
1764
                    continue
1765
            if not item.check_condition(filled):
1766
                continue
1767
            yield item
1768

  
1769
    def get_messages(self, formdata=None, position='top'):
1770
        messages = []
1771
        for item in self.items or []:
1772
            if not hasattr(item, 'get_message'):
1773
                continue
1774
            if not item.check_condition(formdata):
1775
                continue
1776
            message = item.get_message(formdata, position=position)
1777
            if message:
1778
                messages.append(message)
1779
        return messages
1780

  
1781
    def handle_form(self, form, filled, user, evo):
1782
        evo.time = time.localtime()
1783
        if user:
1784
            if filled.is_submitter(user):
1785
                evo.who = '_submitter'
1786
            else:
1787
                evo.who = user.id
1788
        if not filled.evolution:
1789
            filled.evolution = []
1790

  
1791
        for item in self.get_active_items(form, filled, user):
1792
            next_url = item.submit_form(form, filled, user, evo)
1793
            if next_url is True:
1794
                break
1795
            if next_url:
1796
                if not form.has_errors():
1797
                    if evo.parts or evo.status or evo.comment or evo.status:
1798
                        # add evolution entry only if there's some content
1799
                        # within, i.e. do not register anything in the case of
1800
                        # a single edit action (where the evolution should be
1801
                        # appended only after successful edit).
1802
                        filled.evolution.append(evo)
1803
                        if evo.status:
1804
                            filled.status = evo.status
1805
                        filled.store()
1806
                return next_url
1807

  
1808
        return next_url
1809

  
1733 1810

  
1734 1811
class WorkflowGlobalAction(SerieOfActionsMixin):
1735 1812
    id = None
......
1747 1824
            del odict['parent']
1748 1825
        return odict
1749 1826

  
1827
    def is_interactive(self):
1828
        for item in self.items or []:
1829
            if item.is_interactive():
1830
                return True
1831
        return False
1832

  
1833
    def get_global_interactive_form_url(self, formdef=None, ids=None):
1834
        token = get_publisher().token_class(size=32)
1835
        token.type = 'global-interactive-action'
1836
        token.context = {
1837
            'action_id': self.id,
1838
            'form_slug': formdef.slug,
1839
            'form_type': formdef.xml_root_node,
1840
            'form_ids': ids,
1841
            'return_url': get_request().get_path_query(),
1842
        }
1843
        token.store()
1844
        if get_request().is_in_backoffice():
1845
            return formdef.get_url(backoffice=True) + 'actions/%s/#' % token.id
1846
        else:
1847
            return '/actions/%s/#' % token.id
1848

  
1849
    def handle_form(self, form, filled, user):
1850
        evo = Evolution()
1851
        url = super().handle_form(form, filled, user, evo)
1852
        if isinstance(url, str):
1853
            return url
1854
        filled.evolution.append(evo)
1855
        if evo.status:
1856
            filled.status = evo.status
1857
        filled.store()
1858

  
1750 1859
    def get_admin_url(self):
1751 1860
        return '%sglobal-actions/%s/' % (self.parent.get_admin_url(), self.id)
1752 1861

  
......
1866 1975
        return changed
1867 1976

  
1868 1977
    def get_action_form(self, filled, user, displayed_fields=None):
1869
        form = Form(enctype='multipart/form-data', use_tokens=False)
1870
        form.attrs['id'] = 'wf-actions'
1871
        for item in self.items:
1872
            if not item.check_auth(filled, user):
1873
                continue
1874
            if not item.check_condition(filled):
1875
                continue
1876
            item.fill_form(form, filled, user, displayed_fields=displayed_fields)
1978
        form = super().get_action_form(filled, user, displayed_fields=displayed_fields)
1979
        if form is None:
1980
            form = Form(enctype='multipart/form-data', use_tokens=False)
1981
            form.attrs['id'] = 'wf-actions'
1877 1982

  
1878 1983
        for action in filled.formdef.workflow.get_global_actions_for_user(filled, user):
1879 1984
            form.add_submit('button-action-%s' % action.id, action.name)
......
1888 1993
        else:
1889 1994
            return None
1890 1995

  
1891
    def get_active_items(self, form, filled, user):
1892
        for item in self.items:
1893
            if hasattr(item, 'by'):
1894
                for role in item.by or []:
1895
                    if role == logged_users_role().id:
1896
                        break
1897
                    if role == '_submitter':
1898
                        if filled.is_submitter(user):
1899
                            break
1900
                        continue
1901
                    if user is None:
1902
                        continue
1903
                    if filled.get_function_roles(role).intersection(user.get_roles()):
1904
                        break
1905
                else:
1906
                    continue
1907
            if not item.check_condition(filled):
1908
                continue
1909
            yield item
1910

  
1911 1996
    def get_admin_url(self):
1912 1997
        return self.parent.get_admin_url() + 'status/%s/' % self.id
1913 1998

  
......
1919 2004
        # check for global actions
1920 2005
        for action in filled.formdef.workflow.get_global_actions_for_user(filled, user):
1921 2006
            if 'button-action-%s' % action.id in get_request().form:
2007
                if action.is_interactive():
2008
                    return action.get_global_interactive_form_url(formdef=filled.formdef, ids=[filled.id])
1922 2009
                url = filled.perform_global_action(action.id, user, event_name='global-action-button')
1923 2010
                if url:
1924 2011
                    return url
1925 2012
                return
1926 2013

  
1927 2014
        evo = Evolution()
1928
        evo.time = time.localtime()
1929
        if user:
1930
            if filled.is_submitter(user):
1931
                evo.who = '_submitter'
1932
            else:
1933
                evo.who = user.id
1934
        if not filled.evolution:
1935
            filled.evolution = []
1936

  
1937
        for item in self.get_active_items(form, filled, user):
1938
            next_url = item.submit_form(form, filled, user, evo)
1939
            if next_url is True:
1940
                break
1941
            if next_url:
1942
                if not form.has_errors():
1943
                    if evo.parts or evo.status or evo.comment or evo.status:
1944
                        # add evolution entry only if there's some content
1945
                        # within, i.e. do not register anything in the case of
1946
                        # a single edit action (where the evolution should be
1947
                        # appended only after successful edit).
1948
                        filled.evolution.append(evo)
1949
                        if evo.status:
1950
                            filled.status = evo.status
1951
                        filled.store()
1952
                return next_url
1953

  
2015
        url = super().handle_form(form, filled, user, evo)
2016
        if isinstance(url, str):
2017
            return url
1954 2018
        if form.has_errors():
1955 2019
            return
1956 2020

  
......
2046 2110
                self.require_confirmation = action.require_confirmation
2047 2111
                self.action = action
2048 2112

  
2113
            def is_interactive(self):
2114
                return False
2115

  
2049 2116
        for action in self.items or []:
2050 2117
            if not isinstance(action, ChoiceWorkflowStatusItem):
2051 2118
                continue
......
2159 2226
    directory_class = None
2160 2227
    support_substitution_variables = False
2161 2228

  
2229
    def __init__(self, parent=None):
2230
        self.parent = parent
2231

  
2162 2232
    @classmethod
2163 2233
    def init(cls):
2164 2234
        pass
......
2232 2302
    def fill_form(self, form, formdata, user, **kwargs):
2233 2303
        pass
2234 2304

  
2305
    def is_interactive(self):
2306
        return False
2307

  
2235 2308
    def evaluate_live_form(self, form, formdata, user):
2236 2309
        pass
2237 2310

  
2238
-