Projet

Général

Profil

0002-backoffice-add-checkboxes-to-run-global-actions-on-m.patch

Frédéric Péters, 10 septembre 2019 21:28

Télécharger (23,7 ko)

Voir les différences:

Subject: [PATCH 2/2] backoffice: add checkboxes to run global actions on many
 items at once (#7865)

 tests/test_backoffice_pages.py      | 109 ++++++++++++++++++++++++++++
 wcs/backoffice/management.py        | 100 ++++++++++++++++++++++++-
 wcs/compat.py                       |  10 ++-
 wcs/formdata.py                     |   8 ++
 wcs/forms/backoffice.py             |  44 +++++++++--
 wcs/qommon/static/css/dc2/admin.css |  30 +++++++-
 wcs/qommon/static/js/afterjob.js    |   3 +
 wcs/qommon/static/js/wcs.listing.js |  66 +++++++++++++++++
 wcs/workflows.py                    |  15 ++++
 9 files changed, 372 insertions(+), 13 deletions(-)
tests/test_backoffice_pages.py
985 985
    assert '<h2>Filters</h2>' in resp.body
986 986
    assert 'End: 2013-01-01' in resp.body
987 987

  
988
def test_backoffice_multi_actions(pub):
989
    create_superuser(pub)
990
    create_environment(pub)
991
    formdef = FormDef.get_by_urlname('form-title')
992

  
993
    app = login(get_app(pub))
994
    resp = app.get('/backoffice/management/form-title/')
995
    assert not 'id="multi-actions"' in resp.body
996

  
997
    workflow = Workflow.get_default_workflow()
998
    workflow.id = '2'
999
    action = workflow.add_global_action('FOOBAR')
1000
    jump = action.append_item('jump')
1001
    jump.status = 'finished'
1002
    trigger = action.triggers[0]
1003
    trigger.roles = ['whatever']
1004

  
1005
    workflow.store()
1006
    formdef.workflow_id = workflow.id
1007
    formdef.store()
1008

  
1009
    resp = app.get('/backoffice/management/form-title/')
1010
    assert not 'id="multi-actions"' in resp.body
1011

  
1012
    trigger.roles = [x.id for x in Role.select() if x.name == 'foobar']
1013
    workflow.store()
1014

  
1015
    resp = app.get('/backoffice/management/form-title/')
1016
    assert 'id="multi-actions"' in resp.body
1017
    ids = []
1018
    for checkbox in resp.forms[0].fields['select[]'][1:6]:
1019
        ids.append(checkbox._value)
1020
        checkbox.checked = True
1021
    resp = resp.forms[0].submit('button-action-1')
1022
    assert '?job=' in resp.location
1023
    resp = resp.follow()
1024
    assert 'Executing task &quot;FOOBAR&quot; on forms' in resp.body
1025
    assert '>completed<' in resp.body
1026
    for id in ids:
1027
        assert formdef.data_class().get(id).status == 'wf-finished'
1028

  
1029
    draft_ids = [x.id for x in formdef.data_class().select() if x.status == 'draft']
1030
    resp = app.get('/backoffice/management/form-title/')
1031
    assert resp.forms[0].fields['select[]'][0]._value == '_all'
1032
    resp.forms[0].fields['select[]'][0].checked = True
1033
    resp = resp.forms[0].submit('button-action-1')
1034
    for formdata in formdef.data_class().select():
1035
        if formdata.id in draft_ids:
1036
            assert formdata.status == 'draft'
1037
        else:
1038
            assert formdata.status == 'wf-finished'
1039

  
1040
    for formdata in formdef.data_class().select():
1041
        if formdata.status != 'draft':
1042
            formdata.jump_status('new')
1043
            formdata.store()
1044

  
1045
    # action for other role
1046
    action2 = workflow.add_global_action('OTHER ACTION')
1047
    jump = action2.append_item('jump')
1048
    jump.status = 'accepted'
1049
    trigger = action2.triggers[0]
1050
    trigger.roles = ['whatever']
1051
    workflow.store()
1052
    resp = app.get('/backoffice/management/form-title/')
1053
    assert 'id="multi-actions"' in resp.body
1054
    assert not 'OTHER ACTION' in resp.body
1055

  
1056
    # action for function
1057
    trigger.roles = ['_foobar']
1058
    workflow.store()
1059

  
1060
    resp = app.get('/backoffice/management/form-title/')
1061
    assert 'id="multi-actions"' in resp.body
1062
    assert 'OTHER ACTION' not in resp.body
1063

  
1064
    workflow.roles['_foobar'] = 'Foobar'
1065
    workflow.store()
1066

  
1067
    resp = app.get('/backoffice/management/form-title/')
1068
    assert 'id="multi-actions"' in resp.body
1069
    assert 'OTHER ACTION' in resp.body
1070

  
1071
    # alter some formdata to simulate dispatch action
1072
    stable_ids = []
1073
    for checkbox in resp.forms[0].fields['select[]'][1:6]:
1074
        formdata = formdef.data_class().get(checkbox._value)
1075
        formdata.workflow_roles = {'_foobar': formdef.workflow_roles['_receiver']}
1076
        formdata.store()
1077
        stable_ids.append(formdata.id)
1078

  
1079
    resp = app.get('/backoffice/management/form-title/')
1080
    assert 'OTHER ACTION' in resp.body
1081

  
1082
    resp.forms[0].fields['select[]'][0].checked = True  # _all
1083
    resp = resp.forms[0].submit('button-action-2')
1084
    assert '?job=' in resp.location
1085
    resp = resp.follow()
1086
    assert 'Executing task &quot;OTHER ACTION&quot; on forms' in resp.body
1087
    # check only dispatched formdata have been moved by global action executed
1088
    # on all formdatas
1089
    for formdata in formdef.data_class().select():
1090
        if formdata.id in draft_ids:
1091
            assert formdata.status == 'draft'
1092
        elif formdata.id in stable_ids:
1093
            assert formdata.status == 'wf-accepted'
1094
        else:
1095
            assert formdata.status != 'wf-accepted'
1096

  
988 1097
def test_backoffice_statistics_status_filter(pub):
989 1098
    create_superuser(pub)
990 1099
    create_environment(pub)
wcs/backoffice/management.py
1330 1330
    def listing_top_actions(self):
1331 1331
        return ''
1332 1332

  
1333
    def get_multi_actions(self, user):
1334
        global_actions = self.formdef.workflow.get_global_manual_actions()
1335
        workflow_roles = self.formdef.workflow.roles or {}
1336
        mass_actions = []
1337
        for action_dict in global_actions:
1338
            action_dict['roles'] = [x for x in user.get_roles() if x in action_dict.get('roles') or []]
1339
            if action_dict['functions'] or action_dict['roles']:
1340
                mass_actions.append(action_dict)
1341
        return mass_actions
1342

  
1333 1343
    def _q_index(self):
1334 1344
        self.check_access()
1335 1345
        get_logger().info('backoffice - form %s - listing' % self.formdef.name)
1336 1346

  
1347
        if 'job' in get_request().form:
1348
            return self.job_multi()
1349

  
1337 1350
        fields = self.get_fields_from_query()
1338 1351
        selected_filter = self.get_filter_from_query()
1339 1352
        criterias = self.get_criterias_from_query()
......
1354 1367
        if get_request().get_query():
1355 1368
            qs = '?' + get_request().get_query()
1356 1369

  
1370
        multi_actions = self.get_multi_actions(get_request().user)
1371
        if not get_request().form.get('ajax') == 'true':
1372
            multi_form = Form(id='multi-actions')
1373
            for action in multi_actions:
1374
                attrs = {}
1375
                if action.get('functions'):
1376
                    for function in action.get('functions'):
1377
                        attrs['data-visible_for_%s' % function] = 'true'
1378
                else:
1379
                    attrs['data-visible_for_all'] = 'true'
1380
                multi_form.add_submit('button-action-%s' % action['action'].id, action['action'].name, attrs=attrs)
1381
            if multi_form.is_submitted() and get_request().form.get('select[]'):
1382
                for action in multi_actions:
1383
                    if multi_form.get_submit() == 'button-action-%s' % action['action'].id:
1384
                        return self.submit_multi(
1385
                                action,
1386
                                selected_filter=selected_filter,
1387
                                query=query,
1388
                                criterias=criterias)
1389

  
1357 1390
        table = FormDefUI(self.formdef).listing(fields=fields,
1358 1391
                        selected_filter=selected_filter,
1359 1392
                        limit=int(limit), offset=int(offset), query=query,
1360
                        order_by=order_by, criterias=criterias)
1393
                        order_by=order_by, criterias=criterias,
1394
                        include_checkboxes=bool(multi_actions))
1361 1395
        if get_response().status_code == 302:
1362 1396
            # catch early redirect
1363 1397
            return table
......
1373 1407
        r += get_session().display_message()
1374 1408
        r += self.listing_top_actions()
1375 1409
        r += htmltext('</div>')
1376
        r += table
1410
        if multi_actions:
1411
            multi_form.widgets.append(HtmlWidget(table))
1412
            r += multi_form.render()
1413
        else:
1414
            r += table
1377 1415

  
1378 1416
        get_response().filter['sidebar'] = self.get_formdata_sidebar(qs) + \
1379 1417
                self.get_fields_sidebar(selected_filter, fields, limit=limit,
......
1382 1420

  
1383 1421
        return r.getvalue()
1384 1422

  
1423
    def submit_multi(self, action, selected_filter, query, criterias):
1424
        class ActionJob(object):
1425
            def __init__(self, formdef, query_string, action, item_ids):
1426
                self.formdef = formdef
1427
                self.query_string = query_string
1428
                self.action = action
1429
                self.item_ids = item_ids
1430
                self.user = get_request().user
1431

  
1432
            def execute(self, job=None):
1433
                formdatas = self.formdef.data_class().get_ids(self.item_ids)
1434
                publisher = get_publisher()
1435
                for formdata in formdatas:
1436
                    publisher.substitutions.reset()
1437
                    publisher.substitutions.feed(publisher)
1438
                    publisher.substitutions.feed(self.formdef)
1439
                    publisher.substitutions.feed(formdata)
1440
                    formdata.perform_global_action(self.action['action'].id, self.user)
1441

  
1442
        item_ids = get_request().form['select[]']
1443
        if '_all' in item_ids:
1444
            item_ids = FormDefUI(self.formdef).get_listing_item_ids(
1445
                        selected_filter, user=get_request().user, query=query,
1446
                        criterias=criterias)
1447
        action_job = ActionJob(self.formdef, get_request().get_query(), action, item_ids)
1448
        job = get_response().add_after_job(
1449
                N_('Executing task "%s" on forms') % action['action'].name,
1450
                action_job.execute)
1451
        job.query_string = get_request().get_query()
1452
        job.store()
1453
        return redirect('./?job=%s' % job.id)
1454

  
1455
    def job_multi(self):
1456
        try:
1457
            job = AfterJob.get(get_request().form.get('job'))
1458
        except KeyError:
1459
            return redirect('.')
1460

  
1461
        html_top('management', title=_('Executing Task'))
1462
        r = TemplateIO(html=True)
1463
        r += get_session().display_message()
1464
        get_response().add_javascript(['jquery.js', 'afterjob.js'])
1465
        r += htmltext('<dl class="job-status">')
1466
        r += htmltext('<dt>')
1467
        r += _(job.label)
1468
        r += htmltext('</dt>')
1469
        r += htmltext('<dd>')
1470
        r += htmltext('<span class="afterjob" id="%s">') % job.id
1471
        r += _(job.status)
1472
        r += htmltext('</span>')
1473
        r += htmltext('</dd>')
1474
        r += htmltext('</dl>')
1475

  
1476
        r += htmltext('<div class="done">')
1477
        r += htmltext('<a data-redirect-auto="true" href="./?%s">%s</a>') % (job.query_string, _('Back to Listing'))
1478
        r += htmltext('</div>')
1479
        return r.getvalue()
1480

  
1385 1481
    def csv_tuple_heading(self, fields):
1386 1482
        heading_fields = [] # '#id', _('time'), _('userlabel'), _('status')]
1387 1483
        for field in fields:
wcs/compat.py
116 116
            params['charset'] = site_charset
117 117
        if not self.form:
118 118
            self.form = {}
119
        for k, v in self.django_request.POST.items():
119
        for k in self.django_request.POST:
120 120
            if isinstance(k, unicode):
121 121
                k = k.encode(site_charset)
122
            if isinstance(v, unicode):
123
                v = v.encode(site_charset)
122
            if k.endswith('[]'):
123
                v = [x.encode(site_charset) for x in self.django_request.POST.getlist(k)]
124
            else:
125
                v = self.django_request.POST[k]
126
                if isinstance(v, unicode):
127
                    v = v.encode(site_charset)
124 128
            self.form[k] = v
125 129

  
126 130
        for k, upload_file in self.django_request.FILES.items():
wcs/formdata.py
480 480
        url = perform_items(wf_status.items, self)
481 481
        return url
482 482

  
483
    def perform_global_action(self, action_id, user):
484
        from wcs.workflows import perform_items
485
        for action in self.formdef.workflow.get_global_actions_for_user(self, user):
486
            if action.id != action_id:
487
                continue
488
            perform_items(action.items, self)
489
            break
490

  
483 491
    def get_workflow_messages(self, position='top'):
484 492
        wf_status = self.get_status()
485 493
        if not wf_status:
wcs/forms/backoffice.py
31 31

  
32 32
    def listing(self, fields, selected_filter='all', url_action=None,
33 33
                    items=None, offset=0, limit=0,
34
                    query=None, order_by=None, criterias=None):
34
                    query=None, order_by=None, criterias=None,
35
                    include_checkboxes=False):
35 36

  
36 37
        partial_display = False
37 38

  
......
68 69
        r += htmltext('<table id="listing" class="main compact">')
69 70

  
70 71
        r += htmltext('<colgroup>')
72
        if include_checkboxes:
73
            r += htmltext('<col/>')  # checkbox
71 74
        r += htmltext('<col/>') # lock
72 75
        r += htmltext('<col/>')
73 76
        r += htmltext('<col/>')
......
78 81
        using_postgresql = get_publisher().is_using_postgresql()
79 82

  
80 83
        r += htmltext('<thead><tr>')
84
        if include_checkboxes:
85
            r += htmltext('<th class="select"><input type="checkbox" name="select[]" value="_all"/></th>')
81 86
        if self.formdef.workflow.criticality_levels and using_postgresql:
82 87
            r += htmltext('<th style="width: 4ex;" data-field-sort-key="criticality_level"><span></span></th>')
83 88
        else:
......
105 110
            r += htmltext('</th>')
106 111
        r += htmltext('</tr></thead>')
107 112
        r += htmltext('<tbody>')
108
        r += htmltext(self.tbody(fields, items, url_action))
113
        r += htmltext(self.tbody(fields, items, url_action,
114
            include_checkboxes=include_checkboxes))
109 115
        r += htmltext('</tbody>')
110 116
        r += htmltext('</table>')
111 117

  
......
115 121

  
116 122
        return r.getvalue()
117 123

  
118
    def get_listing_items(self, selected_filter='all', offset=None,
119
            limit=None, query=None, order_by=None, user=None, criterias=None, anonymise=False):
120
        user = user or get_request().user
124
    def get_listing_item_ids(self, selected_filter='all', query=None, order_by=None, user=None, criterias=None, anonymise=False):
121 125
        formdata_class = self.formdef.data_class()
122 126
        if selected_filter == 'all':
123 127
            item_ids = formdata_class.keys()
......
161 165
                        'concerned_roles', str(role)))
162 166
                item_ids = list(set(item_ids).intersection(concerned_ids))
163 167

  
168
        return item_ids
169

  
170
    def get_listing_items(self, selected_filter='all', offset=None,
171
            limit=None, query=None, order_by=None, user=None, criterias=None, anonymise=False):
172
        user = user or get_request().user
173
        formdata_class = self.formdef.data_class()
174

  
175
        item_ids = self.get_listing_item_ids(
176
                selected_filter=selected_filter,
177
                query=query,
178
                user=user,
179
                criterias=criterias,
180
                anonymise=anonymise)
181

  
164 182
        if order_by and not hasattr(formdata_class, 'get_sorted_ids'):
165 183
            # get_sorted_ids is only implemented in the SQL backend
166 184
            order_by = None
......
186 204
        return (items, total_count)
187 205

  
188 206

  
189
    def tbody(self, fields=None, items=None, url_action=None):
207
    def tbody(self, fields=None, items=None, url_action=None, include_checkboxes=False):
190 208
        r = TemplateIO(html=True)
191 209
        if url_action:
192 210
            pass
......
194 212
        else:
195 213
            url_action = ''
196 214
        root_url = get_publisher().get_root_url()
197
        visited_objects = get_publisher().get_visited_objects(exclude_user=get_session().user)
215
        user = get_request().user
216
        visited_objects = get_publisher().get_visited_objects(exclude_user=user.id)
198 217
        include_criticality_level = bool(self.formdef.workflow.criticality_levels)
199 218
        for i, filled in enumerate(items):
200 219
            classes = ['status-%s-%s' % (filled.formdef.workflow.id, filled.status)]
......
224 243
            if filled.anonymised:
225 244
                data += ' data-anonymised="true"'
226 245
            r += htmltext('<tr class="%s"%s>' % (' '.join(classes), data))
246
            if include_checkboxes:
247
                r += htmltext('<td class="select"><input type="checkbox" name="select[]" ')
248
                r += htmltext('value="%s"') % filled.id
249
                workflow_roles = {}
250
                workflow_roles.update(self.formdef.workflow_roles)
251
                if filled.workflow_roles:
252
                    workflow_roles.update(filled.workflow_roles)
253
                for function_key, function_value in workflow_roles.items():
254
                    if function_value in user.get_roles():
255
                        r += htmltext(' data-is-%s="true" ' % function_key)
256
                r += htmltext('/></td>')
227 257
            if include_criticality_level:
228 258
                r += htmltext('<td %s></td>' % style) # criticality_level
229 259
            else:
wcs/qommon/static/css/dc2/admin.css
395 395
	opacity: 0.5;
396 396
}
397 397

  
398
#listing tbody tr td {
399
	cursor: pointer;
400
}
401

  
402
#listing tbody tr td.select {
403
	cursor: default;
404
}
405

  
398 406
#listing tbody tr:hover td {
399 407
	background: #dde;
400
	cursor: pointer;
401 408
}
402 409

  
403 410
#listing.main th,
......
413 420
	cursor: inherit;
414 421
}
415 422

  
423
table#listing .select {
424
	width: 1rem;
425
	text-align: center;
426
}
427

  
428
table#listing .select input {
429
	margin: 2px 0;
430
}
431

  
416 432
table.main th[data-field-sort-key] span:after {
417 433
	padding-left: 1ex;
418 434
	content: "\f0dc"; /* sort */
......
1807 1823
a.button.button-paragraph:hover p {
1808 1824
	color: white;
1809 1825
}
1826

  
1827
#main-content form#multi-actions {
1828
	padding: 0;
1829
}
1830

  
1831
#main-content form#multi-actions div.buttons {
1832
	padding: 0 0.5rem;
1833
}
1834

  
1835
table#listing tr.checked td {
1836
	background: #ddf;
1837
}
wcs/qommon/static/js/afterjob.js
8 8
                function () {
9 9
                    var current_text = $(this).text();
10 10
                    if (current_text.indexOf('completed') == 0) {
11
                        if ($('div.done a[data-redirect-auto]').length) {
12
                            window.location = $('div.done a[data-redirect-auto]').attr('href');
13
                        }
11 14
                        $(this).text(current_text.substr(10, current_text.length));
12 15
                        $(this).addClass('activity-done');
13 16
                        $('.afterjob-running').hide();
wcs/qommon/static/js/wcs.listing.js
12 12
    event.stopPropagation();
13 13
  });
14 14
  $('#listing tbody tr').on('mouseup', function(event) {
15
    var $target = $(event.target);
16
    if ($target.is('input[type=checkbox]')) {
17
      return false;
18
    }
19
    if ($target.is('td.select')) {
20
      $target.find('input').click();
21
      return false;
22
    }
15 23
    var data_link = $(this).data('link');
16 24
    if (data_link) {
17 25
      if (data_link.indexOf('http://') == -1 && data_link.indexOf('https://') == -1) {
......
25 33
      return false;
26 34
    }
27 35
  });
36
  if ($('#listing tbody input[type=checkbox]:checked').length == 0) {
37
    $('form#multi-actions div.buttons').hide();
38
  } else {
39
    $('form#multi-actions div.buttons').show();
40
  }
41
  $('#listing tbody input[type=checkbox]').each(function() {
42
    if ($(this).is(':checked')) {
43
      $(this).parents('tr').addClass('checked');
44
    } else {
45
      $(this).parents('tr').removeClass('checked');
46
    }
47
  });
48
  $('#listing input[type=checkbox]').on('change', function() {
49
    if ($(this).is(':checked')) {
50
      if ($(this).is('[value=_all]')) {
51
        $(this).parents('table').find('tbody td.select input').prop('checked', true);
52
        $(this).parents('table').find('tbody tr').addClass('checked');
53
      } else {
54
        $(this).parents('tr').addClass('checked');
55
      }
56
    } else {
57
      if ($(this).is('[value=_all]')) {
58
        $(this).parents('table').find('tbody td.select input').prop('checked', false);
59
        $(this).parents('table').find('tbody tr').removeClass('checked');
60
      } else {
61
        $(this).parents('tr').removeClass('checked');
62
        $('#listing input[type=checkbox][value=_all]').prop('checked', false);
63
      }
64
    }
65
    if ($('#listing tbody input[type=checkbox]:checked').length == 0) {
66
      $('form#multi-actions div.buttons').hide();
67
      return;
68
    } else {
69
      $('form#multi-actions div.buttons button').each(function(idx, elem) {
70
        var visible = false;
71
        for (var key in $(elem).first().data()) {
72
          if (key == 'visible_for_all') {
73
            visible = true;
74
            break;
75
          }
76
          if ($('input[type=checkbox][data-is-' + key.substr(12) + ']:checked').length) {
77
            visible = true;
78
            break;
79
          }
80
        }
81
        if (visible) {
82
          $(elem).parents('div.widget').show();
83
        } else {
84
          $(elem).parents('div.widget').hide();
85
        }
86
      });
87
      $('form#multi-actions div.buttons').show();
88
    }
89
  });
28 90
}
29 91

  
30 92
function prepare_column_headers() {
......
61 123
}
62 124

  
63 125
function autorefresh_table() {
126
  if ($('#multi-actions input:checked').length) {
127
    // disable autorefresh when multiselection is enabled
128
    return;
129
  }
64 130
  $(document).trigger('backoffice-filter-change',
65 131
      {qs: $('form#listing-settings').serialize(), auto: true});
66 132
}
wcs/workflows.py
413 413
        self.global_actions.append(action)
414 414
        return action
415 415

  
416
    def get_global_manual_actions(self):
417
        actions = []
418
        for action in self.global_actions or []:
419
            roles = []
420
            for trigger in action.triggers or []:
421
                if not isinstance(trigger, WorkflowGlobalActionManualTrigger):
422
                    continue
423
                roles.extend(trigger.roles or [])
424
            functions = [x for x in roles if x in self.roles]
425
            roles = [x for x in roles if x not in self.roles]
426
            if functions or roles:
427
                actions.append({'action': action, 'roles': roles, 'functions': functions})
428
        return actions
429

  
430

  
416 431
    def get_global_actions_for_user(self, formdata, user):
417 432
        if not user:
418 433
            return []
419
-