wcs/tests/workflow/test_jump.py

632 lines
18 KiB
Python

import datetime
from unittest import mock
import pytest
from pyquery import PyQuery
from quixote import cleanup
from wcs.fields import DateField, StringField
from wcs.formdef import FormDef
from wcs.qommon.http_request import HTTPRequest
from wcs.wf.jump import JumpWorkflowStatusItem, _apply_timeouts
from wcs.workflows import Workflow, perform_items
from ..utilities import clean_temporary_pub, create_temporary_pub, get_app, login
from .test_all import admin_user # noqa pylint: disable=unused-import
def setup_module(module):
cleanup()
def teardown_module(module):
clean_temporary_pub()
@pytest.fixture
def pub(request):
pub = create_temporary_pub()
pub.cfg['language'] = {'language': 'en'}
pub.cfg['identification'] = {'methods': ['password']}
pub.write_cfg()
req = HTTPRequest(None, {'SERVER_NAME': 'example.net', 'SCRIPT_NAME': ''})
req.response.filter = {}
req._user = None
pub._set_request(req)
pub.set_config(req)
return pub
def rewind(formdata, seconds):
# utility function to move formdata back in time
formdata.receipt_time = formdata.receipt_time - datetime.timedelta(seconds=seconds)
formdata.evolution[-1].time = formdata.evolution[-1].time - datetime.timedelta(seconds=seconds)
def test_jump_nothing(pub):
FormDef.wipe()
formdef = FormDef()
formdef.name = 'foobar'
formdef.store()
formdata = formdef.data_class()()
item = JumpWorkflowStatusItem()
assert item.check_condition(formdata) is True
def test_jump_datetime_condition(pub):
FormDef.wipe()
formdef = FormDef()
formdef.name = 'foobar'
formdef.store()
formdata = formdef.data_class()()
item = JumpWorkflowStatusItem()
yesterday = datetime.datetime.now() - datetime.timedelta(days=1)
item.condition = {
'type': 'python',
'value': 'datetime.datetime.now() > datetime.datetime(%s, %s, %s)' % yesterday.timetuple()[:3],
}
assert item.check_condition(formdata) is True
tomorrow = datetime.datetime.now() + datetime.timedelta(days=1)
item.condition = {
'type': 'python',
'value': 'datetime.datetime.now() > datetime.datetime(%s, %s, %s)' % tomorrow.timetuple()[:3],
}
assert item.check_condition(formdata) is False
def test_jump_date_conditions(pub):
FormDef.wipe()
formdef = FormDef()
formdef.name = 'foobar'
formdef.fields = [DateField(id='2', label='Date', varname='date')]
formdef.store()
# create/store/get, to make sure the date format is acceptable
formdata = formdef.data_class()()
formdata.data = {'2': DateField().convert_value_from_str('2015-01-04')}
formdata.store()
formdata = formdef.data_class().get(formdata.id)
pub.substitutions.feed(formdata)
item = JumpWorkflowStatusItem()
item.condition = {
'type': 'python',
'value': 'utils.make_date(form_var_date) == utils.make_date("2015-01-04")',
}
assert item.check_condition(formdata) is True
item = JumpWorkflowStatusItem()
item.condition = {'type': 'python', 'value': 'utils.time_delta(form_var_date, "2015-01-04").days == 0'}
assert item.check_condition(formdata) is True
item = JumpWorkflowStatusItem()
item.condition = {'type': 'python', 'value': 'utils.time_delta(utils.today(), "2015-01-04").days > 0'}
assert item.check_condition(formdata) is True
item = JumpWorkflowStatusItem()
item.condition = {
'type': 'python',
'value': 'utils.time_delta(datetime.datetime.now(), "2015-01-04").days > 0',
}
assert item.check_condition(formdata) is True
item = JumpWorkflowStatusItem()
item.condition = {
'type': 'python',
'value': 'utils.time_delta(utils.time.localtime(), "2015-01-04").days > 0',
}
assert item.check_condition(formdata) is True
def test_jump_count_condition(pub):
FormDef.wipe()
formdef = FormDef()
formdef.name = 'foobar'
formdef.store()
pub.substitutions.feed(formdef)
formdef.data_class().wipe()
formdata = formdef.data_class()()
item = JumpWorkflowStatusItem()
item.condition = {'type': 'python', 'value': 'form_objects.count < 2'}
assert item.check_condition(formdata) is True
for _ in range(10):
formdata = formdef.data_class()()
formdata.just_created()
formdata.store()
item.condition = {'type': 'python', 'value': 'form_objects.count < 2'}
assert item.check_condition(formdata) is False
def test_jump_bad_python_condition(pub):
FormDef.wipe()
formdef = FormDef()
formdef.name = 'foobar'
formdef.store()
pub.substitutions.feed(formdef)
formdef.data_class().wipe()
formdata = formdef.data_class()()
item = JumpWorkflowStatusItem()
item.condition = {'type': 'python', 'value': 'form_var_foobar == 0'}
assert item.check_condition(formdata) is False
assert pub.loggederror_class.count() == 1
logged_error = pub.loggederror_class.select()[0]
assert logged_error.summary == 'Failed to evaluate condition'
assert logged_error.exception_class == 'NameError'
assert logged_error.exception_message == "name 'form_var_foobar' is not defined"
assert logged_error.context == {
'stack': [
{
'condition': 'form_var_foobar == 0',
'condition_type': 'python',
'source_label': 'Automatic Jump',
'source_url': '',
}
]
}
pub.loggederror_class.wipe()
item.condition = {'type': 'python', 'value': '~ invalid ~'}
assert item.check_condition(formdata) is False
assert pub.loggederror_class.count() == 1
logged_error = pub.loggederror_class.select()[0]
assert logged_error.summary == 'Failed to evaluate condition'
assert logged_error.exception_class == 'SyntaxError'
assert logged_error.exception_message == 'invalid syntax (<string>, line 1)'
assert logged_error.context == {
'stack': [
{
'condition': '~ invalid ~',
'source_url': '',
'source_label': 'Automatic Jump',
'condition_type': 'python',
}
]
}
def test_jump_django_conditions(pub):
FormDef.wipe()
formdef = FormDef()
formdef.name = 'foobar'
formdef.fields = [
StringField(id='1', label='Test', varname='foo'),
]
formdef.store()
formdata = formdef.data_class()()
formdata.data = {'1': 'hello'}
pub.substitutions.feed(formdata)
item = JumpWorkflowStatusItem()
item.condition = {'type': 'django', 'value': '1 < 2'}
assert item.check_condition(formdata) is True
item.condition = {'type': 'django', 'value': 'form_var_foo == "hello"'}
assert item.check_condition(formdata) is True
item.condition = {'type': 'django', 'value': 'form_var_foo|first|upper == "H"'}
assert item.check_condition(formdata) is True
item.condition = {'type': 'django', 'value': 'form_var_foo|first|upper == "X"'}
assert item.check_condition(formdata) is False
assert pub.loggederror_class.count() == 0
item.condition = {'type': 'django', 'value': '~ invalid ~'}
assert item.check_condition(formdata) is False
assert pub.loggederror_class.count() == 1
logged_error = pub.loggederror_class.select()[0]
assert logged_error.summary == 'Failed to evaluate condition'
assert logged_error.exception_class == 'TemplateSyntaxError'
assert logged_error.exception_message == "Could not parse the remainder: '~' from '~'"
assert logged_error.context == {
'stack': [
{
'condition': '~ invalid ~',
'source_url': '',
'source_label': 'Automatic Jump',
'condition_type': 'django',
}
]
}
def test_timeout(pub):
workflow = Workflow(name='timeout')
st1 = workflow.add_status('Status1', 'st1')
workflow.add_status('Status2', 'st2')
jump = st1.add_action('jump', id='_jump')
jump.by = ['_submitter', '_receiver']
jump.timeout = 30 * 60 # 30 minutes
jump.status = 'st2'
workflow.store()
formdef = FormDef()
formdef.name = 'baz'
formdef.fields = []
formdef.workflow_id = workflow.id
assert formdef.get_workflow().id == workflow.id
formdef.store()
formdata = formdef.data_class()()
formdata.just_created()
rewind(formdata, seconds=40 * 60)
formdata.store()
formdata_id = formdata.id
_apply_timeouts(pub)
assert formdef.data_class().get(formdata_id).status == 'wf-st2'
formdata = formdef.data_class()()
formdata.just_created()
formdata.store()
formdata_id = formdata.id
with mock.patch('wcs.wf.jump.JumpWorkflowStatusItem.check_condition') as must_jump:
must_jump.return_value = False
_apply_timeouts(pub)
assert must_jump.call_count == 0 # not enough time has passed
# check a lower than minimal delay is not considered
jump.timeout = 5 * 50 # 5 minutes
workflow.store()
rewind(formdata, seconds=10 * 60)
formdata.store()
_apply_timeouts(pub)
assert must_jump.call_count == 0
# but is executed once delay is reached
rewind(formdata, seconds=10 * 60)
formdata.store()
_apply_timeouts(pub)
assert must_jump.call_count == 1
# check a templated timeout is considered as minimal delay for explicit evaluation
jump.timeout = '{{ "0" }}'
workflow.store()
_apply_timeouts(pub)
assert must_jump.call_count == 2
# check there's no crash on workflow without jumps
formdef = FormDef()
formdef.name = 'xxx'
formdef.store()
_apply_timeouts(pub)
def test_timeout_with_humantime_template(pub):
workflow = Workflow(name='timeout')
st1 = workflow.add_status('Status1', 'st1')
workflow.add_status('Status2', 'st2')
jump = st1.add_action('jump', id='_jump')
jump.by = ['_submitter', '_receiver']
jump.timeout = '{{ 30 }} minutes'
jump.status = 'st2'
workflow.store()
formdef = FormDef()
formdef.name = 'baz'
formdef.fields = []
formdef.workflow_id = workflow.id
assert formdef.get_workflow().id == workflow.id
formdef.store()
formdata = formdef.data_class()()
formdata.just_created()
formdata.store()
formdata_id = formdata.id
_apply_timeouts(pub)
assert formdef.data_class().get(formdata_id).status == 'wf-st1' # no change
rewind(formdata, seconds=40 * 60)
formdata.store()
_apply_timeouts(pub)
assert formdef.data_class().get(formdata_id).status == 'wf-st2'
# invalid timeout value
jump.timeout = '{{ 30 }} plop'
workflow.store()
formdef.refresh_from_storage()
formdata = formdef.data_class()()
formdata.just_created()
formdata.store()
formdata_id = formdata.id
pub.loggederror_class.wipe()
rewind(formdata, seconds=40 * 60)
formdata.store()
_apply_timeouts(pub)
assert formdef.data_class().get(formdata_id).status == 'wf-st1' # no change
assert pub.loggederror_class.count() == 1
logged_error = pub.loggederror_class.select()[0]
assert logged_error.summary == "Error in timeout value '30 plop' (computed from '{{ 30 }} plop')"
# template timeout value returning nothing
jump.timeout = '{% if 1 %}{% endif %}'
workflow.store()
formdef.refresh_from_storage()
formdata = formdef.data_class()()
formdata.just_created()
formdata.store()
formdata_id = formdata.id
pub.loggederror_class.wipe()
rewind(formdata, seconds=40 * 60)
formdata.store()
_apply_timeouts(pub)
assert formdef.data_class().get(formdata_id).status == 'wf-st1' # no change
assert pub.loggederror_class.count() == 1
logged_error = pub.loggederror_class.select()[0]
assert logged_error.summary == "Error in timeout value '' (computed from '{% if 1 %}{% endif %}')"
def test_legacy_timeout(pub):
workflow = Workflow(name='timeout')
st1 = workflow.add_status('Status1', 'st1')
workflow.add_status('Status2', 'st2')
jump = st1.add_action('timeout', id='_jump')
jump.timeout = 30 * 60 # 30 minutes
jump.status = 'st2'
workflow.store()
formdef = FormDef()
formdef.name = 'baz'
formdef.fields = []
formdef.workflow_id = workflow.id
assert formdef.get_workflow().id == workflow.id
formdef.store()
formdata = formdef.data_class()()
formdata.just_created()
rewind(formdata, seconds=40 * 60)
formdata.store()
formdata_id = formdata.id
_apply_timeouts(pub)
assert formdef.data_class().get(formdata_id).status == 'wf-st2'
def test_timeout_then_remove(pub):
workflow = Workflow(name='timeout-then-remove')
st1 = workflow.add_status('Status1', 'st1')
st2 = workflow.add_status('Status2', 'st2')
jump = st1.add_action('jump', id='_jump')
jump.by = ['_submitter', '_receiver']
jump.timeout = 30 * 60 # 30 minutes
jump.status = 'st2'
st2.add_action('remove')
workflow.store()
formdef = FormDef()
formdef.name = 'baz%s' % id(pub)
formdef.fields = []
formdef.workflow_id = workflow.id
assert formdef.get_workflow().id == workflow.id
formdef.store()
formdata = formdef.data_class()()
formdata.just_created()
rewind(formdata, seconds=40 * 60)
formdata.store()
formdata.record_workflow_event('frontoffice-created')
formdata_id = formdata.id
assert str(formdata_id) in [str(x) for x in formdef.data_class().keys()]
assert bool(formdata.get_workflow_traces())
_apply_timeouts(pub)
assert not str(formdata_id) in [str(x) for x in formdef.data_class().keys()]
# check workflow traces are removed
assert not bool(formdata.get_workflow_traces())
def test_timeout_with_mark(pub):
workflow = Workflow(name='timeout')
st1 = workflow.add_status('Status1', 'st1')
workflow.add_status('Status2', 'st2')
jump = st1.add_action('jump', id='_jump')
jump.by = ['_submitter', '_receiver']
jump.timeout = 30 * 60 # 30 minutes
jump.status = 'st2'
jump.set_marker_on_status = True
workflow.store()
formdef = FormDef()
formdef.name = 'baz'
formdef.fields = []
formdef.workflow_id = workflow.id
assert formdef.get_workflow().id == workflow.id
formdef.store()
formdata = formdef.data_class()()
formdata.just_created()
rewind(formdata, seconds=40 * 60)
formdata.store()
formdata_id = formdata.id
_apply_timeouts(pub)
formdata = formdef.data_class().get(formdata_id)
assert formdata.workflow_data.get('_markers_stack') == [{'status_id': 'st1'}]
def test_timeout_on_anonymised(pub):
workflow = Workflow(name='timeout')
st1 = workflow.add_status('Status1', 'st1')
workflow.add_status('Status2', 'st2')
jump = st1.add_action('timeout', id='_jump')
jump.timeout = 30 * 60 # 30 minutes
jump.status = 'st2'
workflow.store()
formdef = FormDef()
formdef.name = 'baz'
formdef.fields = []
formdef.workflow_id = workflow.id
assert formdef.get_workflow().id == workflow.id
formdef.store()
formdata = formdef.data_class()()
formdata.just_created()
rewind(formdata, seconds=40 * 60)
formdata.anonymise()
formdata.store()
formdata_id = formdata.id
_apply_timeouts(pub)
assert formdef.data_class().get(formdata_id).status == 'wf-st1' # no change
def test_jump_missing_previous_mark(pub):
FormDef.wipe()
Workflow.wipe()
workflow = Workflow(name='jump-mark')
st1 = workflow.add_status('Status1', 'st1')
jump = st1.add_action('jump', id='_jump')
jump.by = ['_submitter', '_receiver']
jump.status = '_previous'
jump.timeout = 30 * 60 # 30 minutes
workflow.store()
formdef = FormDef()
formdef.name = 'baz'
formdef.fields = []
formdef.workflow_id = workflow.id
formdef.store()
formdata = formdef.data_class()()
formdata.just_created()
rewind(formdata, seconds=40 * 60)
formdata.store()
pub.loggederror_class.wipe()
_apply_timeouts(pub)
assert pub.loggederror_class.count() == 1
def test_conditional_jump_vs_tracing(pub):
workflow = Workflow(name='wf')
st1 = workflow.add_status('Status1', 'st1')
workflow.add_status('Status2', 'st2')
comment = st1.add_action('register-comment')
comment.comment = 'hello world'
jump1 = st1.add_action('jump')
jump1.parent = st1
jump1.condition = {'type': 'django', 'value': 'False'}
jump1.status = 'wf-st2'
jump2 = st1.add_action('jump')
jump2.parent = st1
jump2.status = 'wf-st2'
workflow.store()
formdef = FormDef()
formdef.name = 'baz'
formdef.workflow = workflow
formdef.store()
formdata = formdef.data_class()()
formdata.just_created()
formdata.store()
perform_items(st1.items, formdata)
formdata.refresh_from_storage()
assert [(x.action_item_key, x.action_item_id) for x in formdata.get_workflow_traces()][-2:] == [
('register-comment', str(comment.id)),
('jump', str(jump2.id)),
]
def test_timeout_tracing(pub, admin_user):
workflow = Workflow(name='timeout')
st1 = workflow.add_status('Status1', 'st1')
st2 = workflow.add_status('Status2', 'st2')
jump = st1.add_action('timeout', id='_jump')
jump.timeout = 30 * 60 # 30 minutes
jump.status = 'st2'
add_message = st2.add_action('register-comment')
add_message.comment = 'hello'
workflow.store()
formdef = FormDef()
formdef.name = 'baz'
formdef.fields = []
formdef.workflow_id = workflow.id
assert formdef.get_workflow().id == workflow.id
formdef.store()
formdata = formdef.data_class()()
formdata.just_created()
rewind(formdata, seconds=40 * 60)
formdata.store()
formdata.record_workflow_event('backoffice-created')
_apply_timeouts(pub)
resp = login(get_app(pub), username='admin', password='admin').get(
formdata.get_backoffice_url() + 'inspect'
)
assert [PyQuery(x).text() for x in resp.pyquery('#inspect-timeline li > *:nth-child(2)')] == [
'Created (backoffice submission)',
'Status1',
'Timeout jump - Change Status on Timeout',
'Status2',
'History Message',
]
def test_jump_self_timeout(pub):
FormDef.wipe()
Workflow.wipe()
workflow = Workflow(name='timeout')
st1 = workflow.add_status('Status1', 'st1')
jump = st1.add_action('jump')
jump.timeout = 30 * 60 # 30 minutes
jump.status = 'st1'
workflow.store()
formdef = FormDef()
formdef.name = 'baz'
formdef.fields = []
formdef.workflow_id = workflow.id
formdef.store()
formdata = formdef.data_class()()
formdata.just_created()
rewind(formdata, seconds=40 * 60)
formdata.store()
formdata.record_workflow_event('backoffice-created')
_apply_timeouts(pub)