0001-workflows-add-support-for-global-timeouts-10133.patch
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 |
- |