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_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 |
- |