Projet

Général

Profil

0001-workflows-add-support-for-global-timeouts-10133.patch

Frédéric Péters, 04 avril 2016 22:06

Télécharger (25,8 ko)

Voir les différences:

Subject: [PATCH] workflows: add support for global timeouts (#10133)

 tests/test_admin_pages.py           |  47 ++++++++-
 tests/test_workflows.py             | 149 ++++++++++++++++++++++++++
 wcs/admin/workflows.py              |  48 ++++++++-
 wcs/qommon/static/css/dc2/admin.css |   1 +
 wcs/qommon/static/js/qommon.js      |  24 +++--
 wcs/workflows.py                    | 201 ++++++++++++++++++++++++++++++++++--
 6 files changed, 445 insertions(+), 25 deletions(-)
tests/test_admin_pages.py
1685 1685
    resp = app.get('/backoffice/workflows/%s/' % workflow.id)
1686 1686
    resp = resp.click('Global Action')
1687 1687
    assert len(Workflow.get(workflow.id).global_actions[0].triggers) == 1
1688
    resp = resp.click(href='triggers/1/', index=0)
1688
    resp = resp.click(href='triggers/%s/' % Workflow.get(workflow.id).global_actions[0].triggers[0].id,
1689
            index=0)
1689 1690
    assert resp.form['roles$element0'].value == 'None'
1690 1691
    resp.form['roles$element0'].value = '_receiver'
1691 1692
    resp = resp.form.submit('submit')
......
1693 1694

  
1694 1695
    resp = app.get('/backoffice/workflows/%s/' % workflow.id)
1695 1696
    resp = resp.click('Global Action')
1696
    resp = resp.click(href='triggers/1/', index=0)
1697
    resp = resp.click(href='triggers/%s/' % Workflow.get(workflow.id).global_actions[0].triggers[0].id,
1698
            index=0)
1697 1699
    assert resp.form['roles$element0'].value == '_receiver'
1698 1700
    resp.form['roles$element1'].value = '_submitter'
1699 1701
    resp = resp.form.submit('submit')
......
1702 1704

  
1703 1705
    resp = app.get('/backoffice/workflows/%s/' % workflow.id)
1704 1706
    resp = resp.click('Global Action')
1705
    resp = resp.click(href='triggers/1/', index=0)
1707
    resp = resp.click(href='triggers/%s/' % Workflow.get(workflow.id).global_actions[0].triggers[0].id,
1708
            index=0)
1706 1709
    resp.form['roles$element1'].value = 'None'
1707 1710
    resp = resp.form.submit('submit')
1708 1711
    assert Workflow.get(workflow.id).global_actions[0].triggers[0].roles == ['_receiver']
1709 1712

  
1713
def test_workflows_global_actions_timeout_triggers(pub):
1714
    create_superuser(pub)
1715
    create_role()
1716

  
1717
    Workflow.wipe()
1718
    workflow = Workflow(name='foo')
1719
    workflow.store()
1720

  
1721
    app = login(get_app(pub))
1722
    resp = app.get('/backoffice/workflows/%s/' % workflow.id)
1723
    resp = resp.click('add global action')
1724
    resp.forms[0]['name'] = 'Global Action'
1725
    resp = resp.forms[0].submit('submit')
1726
    resp = resp.follow()
1727

  
1728
    resp = resp.click('Global Action')
1729

  
1730
    # test removing the existing manual trigger
1731
    resp = resp.click(href='triggers/%s/delete' % Workflow.get(workflow.id).global_actions[0].triggers[0].id)
1732
    resp = resp.forms[0].submit()
1733
    resp = resp.follow()
1734

  
1735
    assert len(Workflow.get(workflow.id).global_actions[0].triggers) == 0
1736

  
1737
    # test adding a timeout trigger
1738
    resp.forms[1]['type'] = 'Timeout'
1739
    resp = resp.forms[1].submit()
1740
    resp = resp.follow()
1741

  
1742
    assert 'Timeout (not configured)' in resp.body
1743

  
1744
    resp = resp.click(href='triggers/%s/' % Workflow.get(workflow.id).global_actions[0].triggers[0].id, index=0)
1745
    resp.form['timeout'] = '3'
1746
    resp = resp.form.submit('submit')
1747

  
1748
    assert Workflow.get(workflow.id).global_actions[0].triggers[0].timeout == '3'
1749

  
1750

  
1710 1751
def test_workflows_criticality_levels(pub):
1711 1752
    create_superuser(pub)
1712 1753
    create_role()
tests/test_workflows.py
1020 1020
    outstream = transform_to_pdf(instream)
1021 1021
    assert outstream is not False
1022 1022
    assert outstream.read(10).startswith('%PDF-')
1023

  
1024
def test_global_timeouts(pub):
1025
    FormDef.wipe()
1026
    Workflow.wipe()
1027

  
1028
    workflow = Workflow(name='global-timeouts')
1029
    workflow.possible_status = Workflow.get_default_workflow().possible_status[:]
1030
    workflow.criticality_levels = [
1031
        WorkflowCriticalityLevel(name='green'),
1032
        WorkflowCriticalityLevel(name='yellow'),
1033
        WorkflowCriticalityLevel(name='red'),
1034
    ]
1035
    action = workflow.add_global_action('Timeout Test')
1036
    item = action.append_item('modify_criticality')
1037
    trigger = action.append_trigger('timeout')
1038
    trigger.anchor = 'creation'
1039
    trigger.timeout = '2'
1040
    workflow.store()
1041

  
1042
    formdef = FormDef()
1043
    formdef.name = 'baz'
1044
    formdef.fields = []
1045
    formdef.workflow_id = workflow.id
1046
    formdef.store()
1047

  
1048
    formdata1 = formdef.data_class()()
1049
    formdata1.just_created()
1050
    formdata1.store()
1051

  
1052
    # delay didn't expire yet, no change
1053
    Workflow.apply_global_action_timeouts()
1054
    assert formdef.data_class().get(formdata1.id).get_criticality_level_object().name == 'green'
1055

  
1056
    formdata1.receipt_time = time.localtime(time.time()-3*86400)
1057
    formdata1.store()
1058
    Workflow.apply_global_action_timeouts()
1059
    assert formdef.data_class().get(formdata1.id).get_criticality_level_object().name == 'yellow'
1060

  
1061
    # make sure it's not triggered a second time
1062
    Workflow.apply_global_action_timeouts()
1063
    assert formdef.data_class().get(formdata1.id).get_criticality_level_object().name == 'yellow'
1064

  
1065
    # change id so it's triggered again
1066
    trigger.id = 'XXX1'
1067
    workflow.store()
1068
    Workflow.apply_global_action_timeouts()
1069
    assert formdef.data_class().get(formdata1.id).get_criticality_level_object().name == 'red'
1070
    Workflow.apply_global_action_timeouts()
1071
    assert formdef.data_class().get(formdata1.id).get_criticality_level_object().name == 'red'
1072

  
1073
    # reset formdata to initial state
1074
    formdata1.store()
1075

  
1076
    trigger.anchor = '1st-arrival'
1077
    trigger.anchor_status_first = None
1078
    workflow.store()
1079

  
1080
    Workflow.apply_global_action_timeouts()
1081
    assert formdef.data_class().get(formdata1.id).get_criticality_level_object().name == 'green'
1082

  
1083
    formdata1.evolution[-1].time = time.localtime(time.time()-3*86400)
1084
    formdata1.store()
1085
    Workflow.apply_global_action_timeouts()
1086
    assert formdef.data_class().get(formdata1.id).get_criticality_level_object().name == 'yellow'
1087

  
1088
    formdata1.store() # reset
1089
    trigger.anchor = 'latest-arrival'
1090
    trigger.anchor_status_latest = None
1091
    workflow.store()
1092

  
1093
    formdata1.evolution[-1].time = time.localtime()
1094
    formdata1.store()
1095
    formdata1.jump_status('new')
1096
    formdata1.evolution[-1].time = time.localtime(time.time()-7*86400)
1097
    formdata1.jump_status('accepted')
1098
    formdata1.jump_status('new')
1099
    formdata1.evolution[-1].time = time.localtime(time.time()-1*86400)
1100

  
1101
    Workflow.apply_global_action_timeouts()
1102
    assert formdef.data_class().get(formdata1.id).get_criticality_level_object().name == 'green'
1103

  
1104
    formdata1.evolution[-1].time = time.localtime(time.time()-4*86400)
1105
    formdata1.store()
1106
    Workflow.apply_global_action_timeouts()
1107
    assert formdef.data_class().get(formdata1.id).get_criticality_level_object().name == 'yellow'
1108
    formdata1.store()
1109

  
1110
    # limit trigger to formdata with "accepted" status
1111
    trigger.anchor_status_latest = 'wf-accepted'
1112
    workflow.store()
1113
    Workflow.apply_global_action_timeouts()
1114
    assert formdef.data_class().get(formdata1.id).get_criticality_level_object().name == 'green'
1115
    formdata1.store()
1116

  
1117
    # limit trigger to formdata with "new" status
1118
    trigger.anchor_status_latest = 'wf-new'
1119
    workflow.store()
1120
    Workflow.apply_global_action_timeouts()
1121
    assert formdef.data_class().get(formdata1.id).get_criticality_level_object().name == 'yellow'
1122
    formdata1.store()
1123

  
1124
    # use python expression as anchor
1125
    #  timestamp
1126
    trigger.anchor = 'python'
1127
    trigger.anchor_expression = repr(time.time())
1128
    workflow.store()
1129
    Workflow.apply_global_action_timeouts()
1130
    assert formdef.data_class().get(formdata1.id).get_criticality_level_object().name == 'green'
1131

  
1132
    trigger.anchor = 'python'
1133
    trigger.anchor_expression = repr(time.time() - 10*86400)
1134
    workflow.store()
1135
    Workflow.apply_global_action_timeouts()
1136
    assert formdef.data_class().get(formdata1.id).get_criticality_level_object().name == 'yellow'
1137
    formdata1.store()
1138

  
1139
    #  datetime object
1140
    trigger.anchor = 'python'
1141
    trigger.anchor_expression = 'datetime.datetime(%s, %s, %s, %s, %s)' % (
1142
            datetime.datetime.now() - datetime.timedelta(days=10)).timetuple()[:5]
1143
    workflow.store()
1144
    Workflow.apply_global_action_timeouts()
1145
    assert formdef.data_class().get(formdata1.id).get_criticality_level_object().name == 'yellow'
1146
    formdata1.store()
1147

  
1148
    # string object
1149
    trigger.anchor = 'python'
1150
    trigger.anchor_expression = '"%04d-%02d-%02d"' % (
1151
            datetime.datetime.now() - datetime.timedelta(days=10)).timetuple()[:3]
1152
    workflow.store()
1153
    Workflow.apply_global_action_timeouts()
1154
    assert formdef.data_class().get(formdata1.id).get_criticality_level_object().name == 'yellow'
1155
    formdata1.store()
1156

  
1157
    # invalid variable
1158
    trigger.anchor = 'python'
1159
    trigger.anchor_expression = 'Ellipsis'
1160
    workflow.store()
1161
    Workflow.apply_global_action_timeouts()
1162
    assert formdef.data_class().get(formdata1.id).get_criticality_level_object().name == 'green'
1163
    formdata1.store()
1164

  
1165
    # invalid expression
1166
    trigger.anchor = 'python'
1167
    trigger.anchor_expression = 'XXX'
1168
    workflow.store()
1169
    Workflow.apply_global_action_timeouts()
1170
    assert formdef.data_class().get(formdata1.id).get_criticality_level_object().name == 'green'
1171
    formdata1.store()
wcs/admin/workflows.py
321 321
            return redirect('..')
322 322

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

  
1037 1037
class GlobalActionPage(WorkflowStatusPage):
1038 1038
    _q_exports = ['', 'new', 'delete', 'newitem', ('items', 'items_dir'),
1039
                  'edit', ('triggers', 'triggers_dir'),
1039
                  'update_order', 'edit', 'newtrigger', ('triggers', 'triggers_dir'),
1040
                  'update_triggers_order',
1040 1041
                  ('backoffice-info-text', 'backoffice_info_text'),]
1041 1042

  
1042 1043
    def __init__(self, workflow, action_id, html_top):
......
1100 1101

  
1101 1102
        r += htmltext('<div class="bo-block">')
1102 1103
        r += htmltext('<h2>%s</h2>') % _('Triggers')
1103
        r += htmltext('<ul id="items-list" class="biglist sortable">')
1104
        r += htmltext('<ul id="items-list" class="biglist sortable" data-order-function="update_triggers_order">')
1104 1105
        for trigger in self.action.triggers:
1105
            r += htmltext('<li>')
1106
            r += htmltext('<li class="biglistitem" id="trigId_%s">') % trigger.id
1106 1107
            r += htmltext('<a rel="popup" href="triggers/%s/">%s</a>') % (
1107 1108
                    trigger.id, trigger.render_as_line())
1108 1109
            r += htmltext('<p class="commands">')
1109 1110
            r += command_icon('triggers/%s/' % trigger.id, 'edit', popup=True)
1111
            r += command_icon('triggers/%s/delete' % trigger.id, 'remove', popup=True)
1110 1112
            r += htmltext('</li>')
1111 1113
        r+= htmltext('</ul>')
1112 1114
        r += htmltext('</div>') # bo-block
......
1183 1185
            r += htmltext('<h3>%s</h3>') % _('New Item')
1184 1186
            r += self.get_new_item_form().render()
1185 1187
            r += htmltext('</div>')
1188
            r += htmltext('<div id="new-trigger">')
1189
            r += htmltext('<h3>%s</h3>') % _('New Trigger')
1190
            r += self.get_new_trigger_form().render()
1191
            r += htmltext('</div>')
1186 1192
        return r.getvalue()
1187 1193

  
1194
    def update_triggers_order(self):
1195
        request = get_request()
1196
        new_order = request.form['order'].strip(';').split(';')
1197
        self.action.triggers = [ [x for x in self.action.triggers if x.id == y][0] for y in new_order]
1198
        self.workflow.store()
1199
        return 'ok'
1200

  
1201
    def newtrigger(self):
1202
        form = self.get_new_trigger_form()
1203

  
1204
        if not form.is_submitted() or form.has_errors():
1205
            get_session().message = ('error', _('Submitted form was not filled properly.'))
1206
            return redirect('.')
1207

  
1208
        if form.get_widget('type').parse():
1209
            self.action.append_trigger(form.get_widget('type').parse())
1210
        else:
1211
            get_session().message = ('error', _('Submitted form was not filled properly.'))
1212
            return redirect('.')
1213

  
1214
        self.workflow.store()
1215
        return redirect('.')
1216

  
1217
    def get_new_trigger_form(self):
1218
        form = Form(enctype='multipart/form-data', action='newtrigger')
1219
        available_triggers = [
1220
            ('timeout', _('Timeout')),
1221
            ('manual', _('Manual')),
1222
        ]
1223
        form.add(SingleSelectWidget, 'type', title=_('Type'),
1224
                required=True, options=available_triggers)
1225
        form.add_submit('submit', _('Add'))
1226
        return form
1227

  
1188 1228

  
1189 1229
class GlobalActionsDirectory(Directory):
1190 1230
    _q_exports = ['', 'new']
wcs/qommon/static/css/dc2/admin.css
156 156
}
157 157

  
158 158
div#sidebar div.news h3,
159
div#new-trigger h3,
159 160
div#new-field h3 {
160 161
	margin: 0;
161 162
	font-size: 100%;
wcs/qommon/static/js/qommon.js
1
function prepare_dynamic_widgets()
2
{
3
  $('[data-dynamic-display-parent]').on('change keyup', function() {
4
    var sel1 = '[data-dynamic-display-child-of="' + $(this).attr('name') + '"]';
5
    var sel2 = '[data-dynamic-display-value="' + $(this).val() + '"]';
6
    $(sel1).hide();
7
    $(sel1 + sel2).show();
8
  });
9
  $('[data-dynamic-display-child-of]').hide();
10
  $('select[data-dynamic-display-parent]').trigger('change');
11
  $('[data-dynamic-display-parent]:checked').trigger('change');
12
}
13

  
1 14
$(function() {
2 15
  $('[data-content-url]').each(function(idx, elem) {
3 16
    $.ajax({url: $(elem).data('content-url'),
......
9 22
            error: function(error) { windows.console && console.log('bouh', error); }
10 23
           });
11 24
  });
12
  $('[data-dynamic-display-parent]').on('change keyup', function() {
13
    var sel1 = '[data-dynamic-display-child-of="' + $(this).attr('name') + '"]';
14
    var sel2 = '[data-dynamic-display-value="' + $(this).val() + '"]';
15
    $(sel1).hide();
16
    $(sel1 + sel2).show();
17
  });
18
  $('[data-dynamic-display-child-of]').hide();
19
  $('select[data-dynamic-display-parent]').trigger('change');
20
  $('[data-dynamic-display-parent]:checked').trigger('change');
25
  prepare_dynamic_widgets();
26
  $(document).on('wcs:dialog-loaded', prepare_dynamic_widgets);
21 27
});
wcs/workflows.py
15 15
# along with this program; if not, see <http://www.gnu.org/licenses/>.
16 16

  
17 17
from qommon import ezt
18
import collections
18 19
from cStringIO import StringIO
19 20
import copy
21
import datetime
20 22
import xml.etree.ElementTree as ET
21 23
import random
22 24
import os
23 25
import string
26
import sys
27
import time
28
import uuid
24 29

  
25 30
from quixote import get_request, redirect
26 31

  
27 32
import qommon.misc
28
from qommon.misc import C_
33
from qommon.misc import C_, get_as_datetime
34
from qommon.publisher import get_publisher_class
29 35
from qommon.storage import StorableObject
30 36
from qommon.form import *
31 37
from qommon import emails, get_cfg, get_logger
......
690 696

  
691 697
        return workflow
692 698

  
699
    @classmethod
700
    def apply_global_action_timeouts(cls):
701
        for workflow in cls.select():
702
            WorkflowGlobalActionTimeoutTrigger.apply(workflow)
703

  
693 704

  
694 705
class XmlSerialisable(object):
695 706
    node_name = None
......
840 851
class WorkflowGlobalActionTrigger(XmlSerialisable):
841 852
    node_name = 'trigger'
842 853

  
854
    def submit_admin_form(self, form):
855
        for f in self.get_parameters():
856
            widget = form.get_widget(f)
857
            if widget:
858
                value = widget.parse()
859
                if hasattr(self, '%s_parse' % f):
860
                    value = getattr(self, '%s_parse' % f)(value)
861
                setattr(self, f, value)
862

  
843 863

  
844 864
class WorkflowGlobalActionManualTrigger(WorkflowGlobalActionTrigger):
845 865
    key = 'manual'
......
867 887
                                'options': options})
868 888
        return form
869 889

  
870
    def submit(self, form):
871
        self.roles = form.get_widget('roles').parse()
872

  
873 890
    def roles_export_to_xml(self, item, charset, include_id=False):
874 891
        self._roles_export_to_xml('roles', item, charset, include_id=include_id)
875 892

  
......
877 894
        self._roles_init_with_xml('roles', elem, charset, include_id=include_id)
878 895

  
879 896

  
897
class WorkflowGlobalActionTimeoutTriggerMarker(object):
898
    def __init__(self, timeout_id):
899
        self.timeout_id = timeout_id
900

  
901
class WorkflowGlobalActionTimeoutTrigger(WorkflowGlobalActionTrigger):
902
    key = 'timeout'
903
    anchor = None
904
    anchor_expression = None
905
    anchor_status_first = None
906
    anchor_status_latest = None
907
    timeout = None
908

  
909
    def get_parameters(self):
910
        return ('anchor', 'anchor_expression', 'anchor_status_first',
911
                'anchor_status_latest', 'timeout')
912

  
913
    def get_anchor_labels(self):
914
        return collections.OrderedDict([
915
                ('creation', _('Creation')),
916
                ('1st-arrival', _('First arrival in status')),
917
                ('latest-arrival', _('Latest arrival in status')),
918
                ('python', _('Python expression')),
919
        ])
920

  
921
    def render_as_line(self):
922
        if self.anchor and self.timeout:
923
            return _('Timeout, %(timeout)s, relative to: %(anchor)s') % {
924
                    'anchor': self.get_anchor_labels().get(self.anchor).lower(),
925
                    'timeout': _('%s days') % self.timeout}
926
        else:
927
            return _('Timeout (not configured)')
928

  
929
    def form(self, workflow):
930
        form = Form(enctype='multipart/form-data')
931
        options = self.get_anchor_labels().items()
932
        form.add(SingleSelectWidget, 'anchor', title=_('Anchor'),
933
                 options=options, value=self.anchor, required=True,
934
                 attrs={'data-dynamic-display-parent': 'true'})
935

  
936
        form.add(StringWidget, 'anchor_expression', title=_('Expression'), size=80,
937
                 value=self.anchor_expression,
938
                 attrs={'data-dynamic-display-child-of': 'anchor',
939
                        'data-dynamic-display-value': _('Python expression')})
940
        possible_status = [(None, _('Current Status'), None)]
941
        possible_status.extend([('wf-%s' % x.id, x.name, x.id) for x in workflow.possible_status])
942
        form.add(SingleSelectWidget, 'anchor_status_first', title=_('Status'),
943
                 options=possible_status,
944
                 value=self.anchor_status_first,
945
                 attrs={'data-dynamic-display-child-of': 'anchor',
946
                        'data-dynamic-display-value': _('First arrival in status')}
947
                 )
948
        form.add(SingleSelectWidget, 'anchor_status_latest', title=_('Status'),
949
                 options=possible_status,
950
                 value=self.anchor_status_latest,
951
                 attrs={'data-dynamic-display-child-of': 'anchor',
952
                        'data-dynamic-display-value': _('Latest arrival in status')}
953
                 )
954

  
955
        form.add(StringWidget, 'timeout', title=_('Timeout'),
956
                 value=self.timeout,
957
                 hint=_('Number of days, relative to anchor point.'))
958

  
959
        return form
960

  
961
    def must_trigger(self, formdata):
962
        anchor_date = None
963
        if self.anchor == 'creation':
964
            anchor_date = formdata.receipt_time
965
        elif self.anchor == '1st-arrival':
966
            anchor_status = self.anchor_status_first or formdata.status
967
            for evolution in formdata.evolution:
968
                if evolution.status == anchor_status:
969
                    anchor_date = evolution.time
970
                    break
971
        elif self.anchor == 'latest-arrival':
972
            anchor_status = self.anchor_status_latest or formdata.status
973
            for evolution in reversed(formdata.evolution):
974
                if evolution.status == anchor_status:
975
                    anchor_date = evolution.time
976
                    break
977
        elif self.anchor == 'python':
978
            variables = get_publisher().substitutions.get_context_variables()
979
            try:
980
                anchor_date = eval(self.anchor_expression,
981
                        get_publisher().get_global_eval_dict(), variables)
982
            except:
983
                # get the variables in the locals() namespace so they are
984
                # displayed within the trace.
985
                expression = self.anchor_expression
986
                global_variables = get_publisher().get_global_eval_dict()
987
                get_publisher().notify_of_exception(sys.exc_info(), context='[TIMEOUTS]')
988

  
989
        # convert anchor_date to datetime.datetime()
990
        if isinstance(anchor_date, datetime.datetime):
991
            pass
992
        elif isinstance(anchor_date, datetime.date):
993
            pass
994
        elif isinstance(anchor_date, time.struct_time):
995
            anchor_date = datetime.datetime.fromtimestamp(time.mktime(anchor_date))
996
        elif isinstance(anchor_date, basestring):
997
            try:
998
                anchor_date = get_as_datetime(anchor_date)
999
            except ValueError:
1000
                get_publisher().notify_of_exception(sys.exc_info(), context='[TIMEOUTS]')
1001
                anchor_date = None
1002
        elif anchor_date:
1003
            # timestamp
1004
            try:
1005
                anchor_date = datetime.datetime.fromtimestamp(anchor_date)
1006
            except TypeError:
1007
                get_publisher().notify_of_exception(sys.exc_info(), context='[TIMEOUTS]')
1008
                anchor_date = None
1009

  
1010
        if anchor_date is None:
1011
            return False
1012

  
1013
        anchor_date = anchor_date + datetime.timedelta(days=int(self.timeout))
1014

  
1015
        return bool(datetime.datetime.now() > anchor_date)
1016

  
1017
    @classmethod
1018
    def apply(cls, workflow):
1019
        triggers = []
1020
        for action in workflow.global_actions or []:
1021
            triggers.extend([(action, x) for x in action.triggers or [] if
1022
                             isinstance(x, WorkflowGlobalActionTimeoutTrigger)])
1023
        if not triggers:
1024
            return
1025

  
1026
        formdefs = [x for x in FormDef.select() if x.workflow_id == workflow.id]
1027
        not_endpoint_status = workflow.get_not_endpoint_status()
1028
        not_endpoint_status_ids = ['wf-%s' % x.id for x in not_endpoint_status]
1029

  
1030
        for formdef in formdefs:
1031
            open_formdata_ids = []
1032
            data_class = formdef.data_class()
1033
            for status in not_endpoint_status_ids:
1034
                open_formdata_ids.extend(data_class.get_ids_with_indexed_value('status', status))
1035
            for formdata in data_class.get_ids(open_formdata_ids, ignore_errors=True):
1036
                get_publisher().substitutions.reset()
1037
                get_publisher().substitutions.feed(get_publisher())
1038
                get_publisher().substitutions.feed(formdef)
1039
                get_publisher().substitutions.feed(formdata)
1040

  
1041
                seen_triggers = []
1042
                for evolution in formdata.evolution:
1043
                    for part in evolution.parts or []:
1044
                        if isinstance(part, WorkflowGlobalActionTimeoutTriggerMarker):
1045
                            seen_triggers.append(part.timeout_id)
1046

  
1047
                for action, trigger in triggers:
1048
                    if trigger.id in seen_triggers:
1049
                        continue # already triggered
1050
                    if trigger.must_trigger(formdata):
1051
                        if not formdata.evolution:
1052
                            continue
1053
                        formdata.evolution[-1].add_part(
1054
                                WorkflowGlobalActionTimeoutTriggerMarker(trigger.id))
1055
                        formdata.store()
1056
                        perform_items(action.items, formdata)
1057
                        break
1058

  
1059

  
880 1060
class WorkflowGlobalAction(object):
881 1061
    id = None
882 1062
    name = None
......
903 1083

  
904 1084
    def append_trigger(self, type):
905 1085
        trigger_types = {
906
            'manual': WorkflowGlobalActionManualTrigger
1086
            'manual': WorkflowGlobalActionManualTrigger,
1087
            'timeout': WorkflowGlobalActionTimeoutTrigger
907 1088
        }
908 1089
        o = trigger_types.get(type)()
909 1090
        if not self.triggers:
910 1091
            self.triggers = []
911
        if self.triggers:
912
            o.id = str(max([lax_int(x.id) for x in self.triggers]) + 1)
913
        else:
914
            o.id = '1'
1092
        o.id = str(uuid.uuid4())
915 1093
        self.triggers.append(o)
916 1094
        return o
917 1095

  
......
2007 2185
    import wf.criticality
2008 2186

  
2009 2187
from wf.export_to_model import ExportToModel
2188

  
2189
if get_publisher_class():
2190
    # every hour check for global action timeouts.
2191
    get_publisher_class().register_cronjob(
2192
            CronJob(Workflow.apply_global_action_timeouts, hours=range(24)))
2010
-