Projet

Général

Profil

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

Frédéric Péters, 05 avril 2016 17:09

Télécharger (27,4 ko)

Voir les différences:

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

 tests/test_admin_pages.py           |  47 +++++++-
 tests/test_workflow_import.py       |  11 ++
 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                    | 206 ++++++++++++++++++++++++++++++++++--
 7 files changed, 461 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_workflow_import.py
439 439
    wf2 = assert_import_export_works(wf)
440 440
    assert wf2.criticality_levels[0].name == 'green'
441 441
    assert wf2.criticality_levels[1].name == 'yellow'
442

  
443
def test_global_timeout_trigger():
444
    wf = Workflow(name='global actions')
445
    ac1 = wf.add_global_action('Action', 'ac1')
446
    trigger = ac1.append_trigger('timeout')
447
    trigger.timeout = '2'
448
    trigger.anchor = 'creation'
449

  
450
    wf2 = assert_import_export_works(wf, include_id=True)
451
    assert wf2.global_actions[0].triggers[-1].id == trigger.id
452
    assert wf2.global_actions[0].triggers[-1].anchor == trigger.anchor
tests/test_workflows.py
1028 1028
    outstream = transform_to_pdf(instream)
1029 1029
    assert outstream is not False
1030 1030
    assert outstream.read(10).startswith('%PDF-')
1031

  
1032
def test_global_timeouts(pub):
1033
    FormDef.wipe()
1034
    Workflow.wipe()
1035

  
1036
    workflow = Workflow(name='global-timeouts')
1037
    workflow.possible_status = Workflow.get_default_workflow().possible_status[:]
1038
    workflow.criticality_levels = [
1039
        WorkflowCriticalityLevel(name='green'),
1040
        WorkflowCriticalityLevel(name='yellow'),
1041
        WorkflowCriticalityLevel(name='red'),
1042
    ]
1043
    action = workflow.add_global_action('Timeout Test')
1044
    item = action.append_item('modify_criticality')
1045
    trigger = action.append_trigger('timeout')
1046
    trigger.anchor = 'creation'
1047
    trigger.timeout = '2'
1048
    workflow.store()
1049

  
1050
    formdef = FormDef()
1051
    formdef.name = 'baz'
1052
    formdef.fields = []
1053
    formdef.workflow_id = workflow.id
1054
    formdef.store()
1055

  
1056
    formdata1 = formdef.data_class()()
1057
    formdata1.just_created()
1058
    formdata1.store()
1059

  
1060
    # delay didn't expire yet, no change
1061
    Workflow.apply_global_action_timeouts()
1062
    assert formdef.data_class().get(formdata1.id).get_criticality_level_object().name == 'green'
1063

  
1064
    formdata1.receipt_time = time.localtime(time.time()-3*86400)
1065
    formdata1.store()
1066
    Workflow.apply_global_action_timeouts()
1067
    assert formdef.data_class().get(formdata1.id).get_criticality_level_object().name == 'yellow'
1068

  
1069
    # make sure it's not triggered a second time
1070
    Workflow.apply_global_action_timeouts()
1071
    assert formdef.data_class().get(formdata1.id).get_criticality_level_object().name == 'yellow'
1072

  
1073
    # change id so it's triggered again
1074
    trigger.id = 'XXX1'
1075
    workflow.store()
1076
    Workflow.apply_global_action_timeouts()
1077
    assert formdef.data_class().get(formdata1.id).get_criticality_level_object().name == 'red'
1078
    Workflow.apply_global_action_timeouts()
1079
    assert formdef.data_class().get(formdata1.id).get_criticality_level_object().name == 'red'
1080

  
1081
    # reset formdata to initial state
1082
    formdata1.store()
1083

  
1084
    trigger.anchor = '1st-arrival'
1085
    trigger.anchor_status_first = None
1086
    workflow.store()
1087

  
1088
    Workflow.apply_global_action_timeouts()
1089
    assert formdef.data_class().get(formdata1.id).get_criticality_level_object().name == 'green'
1090

  
1091
    formdata1.evolution[-1].time = time.localtime(time.time()-3*86400)
1092
    formdata1.store()
1093
    Workflow.apply_global_action_timeouts()
1094
    assert formdef.data_class().get(formdata1.id).get_criticality_level_object().name == 'yellow'
1095

  
1096
    formdata1.store() # reset
1097
    trigger.anchor = 'latest-arrival'
1098
    trigger.anchor_status_latest = None
1099
    workflow.store()
1100

  
1101
    formdata1.evolution[-1].time = time.localtime()
1102
    formdata1.store()
1103
    formdata1.jump_status('new')
1104
    formdata1.evolution[-1].time = time.localtime(time.time()-7*86400)
1105
    formdata1.jump_status('accepted')
1106
    formdata1.jump_status('new')
1107
    formdata1.evolution[-1].time = time.localtime(time.time()-1*86400)
1108

  
1109
    Workflow.apply_global_action_timeouts()
1110
    assert formdef.data_class().get(formdata1.id).get_criticality_level_object().name == 'green'
1111

  
1112
    formdata1.evolution[-1].time = time.localtime(time.time()-4*86400)
1113
    formdata1.store()
1114
    Workflow.apply_global_action_timeouts()
1115
    assert formdef.data_class().get(formdata1.id).get_criticality_level_object().name == 'yellow'
1116
    formdata1.store()
1117

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

  
1125
    # limit trigger to formdata with "new" status
1126
    trigger.anchor_status_latest = 'wf-new'
1127
    workflow.store()
1128
    Workflow.apply_global_action_timeouts()
1129
    assert formdef.data_class().get(formdata1.id).get_criticality_level_object().name == 'yellow'
1130
    formdata1.store()
1131

  
1132
    # use python expression as anchor
1133
    #  timestamp
1134
    trigger.anchor = 'python'
1135
    trigger.anchor_expression = repr(time.time())
1136
    workflow.store()
1137
    Workflow.apply_global_action_timeouts()
1138
    assert formdef.data_class().get(formdata1.id).get_criticality_level_object().name == 'green'
1139

  
1140
    trigger.anchor = 'python'
1141
    trigger.anchor_expression = repr(time.time() - 10*86400)
1142
    workflow.store()
1143
    Workflow.apply_global_action_timeouts()
1144
    assert formdef.data_class().get(formdata1.id).get_criticality_level_object().name == 'yellow'
1145
    formdata1.store()
1146

  
1147
    #  datetime object
1148
    trigger.anchor = 'python'
1149
    trigger.anchor_expression = 'datetime.datetime(%s, %s, %s, %s, %s)' % (
1150
            datetime.datetime.now() - datetime.timedelta(days=10)).timetuple()[:5]
1151
    workflow.store()
1152
    Workflow.apply_global_action_timeouts()
1153
    assert formdef.data_class().get(formdata1.id).get_criticality_level_object().name == 'yellow'
1154
    formdata1.store()
1155

  
1156
    # string object
1157
    trigger.anchor = 'python'
1158
    trigger.anchor_expression = '"%04d-%02d-%02d"' % (
1159
            datetime.datetime.now() - datetime.timedelta(days=10)).timetuple()[:3]
1160
    workflow.store()
1161
    Workflow.apply_global_action_timeouts()
1162
    assert formdef.data_class().get(formdata1.id).get_criticality_level_object().name == 'yellow'
1163
    formdata1.store()
1164

  
1165
    # invalid variable
1166
    trigger.anchor = 'python'
1167
    trigger.anchor_expression = 'Ellipsis'
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()
1172

  
1173
    # invalid expression
1174
    trigger.anchor = 'python'
1175
    trigger.anchor_expression = 'XXX'
1176
    workflow.store()
1177
    Workflow.apply_global_action_timeouts()
1178
    assert formdef.data_class().get(formdata1.id).get_criticality_level_object().name == 'green'
1179
    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.cron import CronJob
34
from qommon.misc import C_, get_as_datetime
35
from qommon.publisher import get_publisher_class
29 36
from qommon.storage import StorableObject
30 37
from qommon.form import *
31 38
from qommon import emails, get_cfg, get_logger
......
690 697

  
691 698
        return workflow
692 699

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

  
693 705

  
694 706
class XmlSerialisable(object):
695 707
    node_name = None
......
699 711
        node = ET.Element(self.node_name)
700 712
        if self.key:
701 713
            node.attrib['type'] = self.key
714
        if include_id and self.id:
715
            node.attrib['id'] = self.id
702 716
        for attribute in self.get_parameters():
703 717
            if hasattr(self, '%s_export_to_xml' % attribute):
704 718
                getattr(self, '%s_export_to_xml' % attribute)(node, charset,
......
727 741
        return node
728 742

  
729 743
    def init_with_xml(self, elem, charset, include_id=False):
744
        if include_id and elem.attrib.get('id'):
745
            self.id = elem.attrib.get('id')
730 746
        for attribute in self.get_parameters():
731 747
            el = elem.find(attribute)
732 748
            if hasattr(self, '%s_init_with_xml' % attribute):
......
840 856
class WorkflowGlobalActionTrigger(XmlSerialisable):
841 857
    node_name = 'trigger'
842 858

  
859
    def submit_admin_form(self, form):
860
        for f in self.get_parameters():
861
            widget = form.get_widget(f)
862
            if widget:
863
                value = widget.parse()
864
                if hasattr(self, '%s_parse' % f):
865
                    value = getattr(self, '%s_parse' % f)(value)
866
                setattr(self, f, value)
867

  
843 868

  
844 869
class WorkflowGlobalActionManualTrigger(WorkflowGlobalActionTrigger):
845 870
    key = 'manual'
......
867 892
                                'options': options})
868 893
        return form
869 894

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

  
873 895
    def roles_export_to_xml(self, item, charset, include_id=False):
874 896
        self._roles_export_to_xml('roles', item, charset, include_id=include_id)
875 897

  
......
877 899
        self._roles_init_with_xml('roles', elem, charset, include_id=include_id)
878 900

  
879 901

  
902
class WorkflowGlobalActionTimeoutTriggerMarker(object):
903
    def __init__(self, timeout_id):
904
        self.timeout_id = timeout_id
905

  
906
class WorkflowGlobalActionTimeoutTrigger(WorkflowGlobalActionTrigger):
907
    key = 'timeout'
908
    anchor = None
909
    anchor_expression = None
910
    anchor_status_first = None
911
    anchor_status_latest = None
912
    timeout = None
913

  
914
    def get_parameters(self):
915
        return ('anchor', 'anchor_expression', 'anchor_status_first',
916
                'anchor_status_latest', 'timeout')
917

  
918
    def get_anchor_labels(self):
919
        return collections.OrderedDict([
920
                ('creation', _('Creation')),
921
                ('1st-arrival', _('First arrival in status')),
922
                ('latest-arrival', _('Latest arrival in status')),
923
                ('python', _('Python expression')),
924
        ])
925

  
926
    def render_as_line(self):
927
        if self.anchor and self.timeout:
928
            return _('Timeout, %(timeout)s, relative to: %(anchor)s') % {
929
                    'anchor': self.get_anchor_labels().get(self.anchor).lower(),
930
                    'timeout': _('%s days') % self.timeout}
931
        else:
932
            return _('Timeout (not configured)')
933

  
934
    def form(self, workflow):
935
        form = Form(enctype='multipart/form-data')
936
        options = self.get_anchor_labels().items()
937
        form.add(SingleSelectWidget, 'anchor', title=_('Anchor'),
938
                 options=options, value=self.anchor, required=True,
939
                 attrs={'data-dynamic-display-parent': 'true'})
940

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

  
960
        form.add(StringWidget, 'timeout', title=_('Timeout'),
961
                 value=self.timeout,
962
                 hint=_('Number of days, relative to anchor point.'))
963

  
964
        return form
965

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

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

  
1015
        if anchor_date is None:
1016
            return False
1017

  
1018
        anchor_date = anchor_date + datetime.timedelta(days=int(self.timeout))
1019

  
1020
        return bool(datetime.datetime.now() > anchor_date)
1021

  
1022
    @classmethod
1023
    def apply(cls, workflow):
1024
        triggers = []
1025
        for action in workflow.global_actions or []:
1026
            triggers.extend([(action, x) for x in action.triggers or [] if
1027
                             isinstance(x, WorkflowGlobalActionTimeoutTrigger)])
1028
        if not triggers:
1029
            return
1030

  
1031
        formdefs = [x for x in FormDef.select() if x.workflow_id == workflow.id]
1032
        not_endpoint_status = workflow.get_not_endpoint_status()
1033
        not_endpoint_status_ids = ['wf-%s' % x.id for x in not_endpoint_status]
1034

  
1035
        for formdef in formdefs:
1036
            open_formdata_ids = []
1037
            data_class = formdef.data_class()
1038
            for status in not_endpoint_status_ids:
1039
                open_formdata_ids.extend(data_class.get_ids_with_indexed_value('status', status))
1040
            for formdata in data_class.get_ids(open_formdata_ids, ignore_errors=True):
1041
                get_publisher().substitutions.reset()
1042
                get_publisher().substitutions.feed(get_publisher())
1043
                get_publisher().substitutions.feed(formdef)
1044
                get_publisher().substitutions.feed(formdata)
1045

  
1046
                seen_triggers = []
1047
                for evolution in formdata.evolution:
1048
                    for part in evolution.parts or []:
1049
                        if isinstance(part, WorkflowGlobalActionTimeoutTriggerMarker):
1050
                            seen_triggers.append(part.timeout_id)
1051

  
1052
                for action, trigger in triggers:
1053
                    if trigger.id in seen_triggers:
1054
                        continue # already triggered
1055
                    if trigger.must_trigger(formdata):
1056
                        if not formdata.evolution:
1057
                            continue
1058
                        formdata.evolution[-1].add_part(
1059
                                WorkflowGlobalActionTimeoutTriggerMarker(trigger.id))
1060
                        formdata.store()
1061
                        perform_items(action.items, formdata)
1062
                        break
1063

  
1064

  
880 1065
class WorkflowGlobalAction(object):
881 1066
    id = None
882 1067
    name = None
......
903 1088

  
904 1089
    def append_trigger(self, type):
905 1090
        trigger_types = {
906
            'manual': WorkflowGlobalActionManualTrigger
1091
            'manual': WorkflowGlobalActionManualTrigger,
1092
            'timeout': WorkflowGlobalActionTimeoutTrigger
907 1093
        }
908 1094
        o = trigger_types.get(type)()
909 1095
        if not self.triggers:
910 1096
            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'
1097
        o.id = str(uuid.uuid4())
915 1098
        self.triggers.append(o)
916 1099
        return o
917 1100

  
......
2007 2190
    import wf.criticality
2008 2191

  
2009 2192
from wf.export_to_model import ExportToModel
2193

  
2194
if get_publisher_class():
2195
    # every hour check for global action timeouts.
2196
    get_publisher_class().register_cronjob(
2197
            CronJob(Workflow.apply_global_action_timeouts, hours=range(24)))
2010
-