Compare commits

..

23 Commits

Author SHA1 Message Date
Emmanuel Cazenave 851a1d751d backoffice: display drafts stats (#72542)
gitea/wcs/pipeline/head There was a failure building this commit Details
2024-02-09 15:56:24 +01:00
Emmanuel Cazenave 4ef907d7f8 misc: store page id on drafts (#85091)
gitea/wcs/pipeline/head This commit looks good Details
2024-02-09 15:55:40 +01:00
Emmanuel Cazenave 71ba4dbce0 formdata: add a page_id attribute (#85091) 2024-02-09 15:55:40 +01:00
Frédéric Péters b91a619512 translation update
gitea/wcs/pipeline/head This commit looks good Details
2024-02-09 15:23:33 +01:00
Frédéric Péters bb7ca2500b misc: force get_id_by_option_text argument as string (#86805)
gitea/wcs/pipeline/head Build queued... Details
2024-02-09 15:22:35 +01:00
Frédéric Péters 8cb0225292 backoffice: add popup to cleanup logged errors (#40821)
gitea/wcs/pipeline/head This commit looks good Details
2024-02-09 15:22:10 +01:00
Frédéric Péters 193a37a902 misc: always refresh formdata before launching initial workflow (#85978)
gitea/wcs/pipeline/head This commit looks good Details
2024-02-09 15:11:53 +01:00
Frédéric Péters 510f68c505 fields: force item prefill to be string if requested (#86763)
gitea/wcs/pipeline/head This commit looks good Details
2024-02-09 14:36:57 +01:00
Lauréline Guérin c28b82745d
backoffice: inspect, link to action for button wf trace (#86575)
gitea/wcs/pipeline/head This commit looks good Details
2024-02-09 10:46:52 +01:00
Pierre Ducroquet 43538c6920 workflows: optimize global_timeout for creation anchor (#86153)
gitea/wcs/pipeline/head This commit looks good Details
2024-02-09 08:42:07 +01:00
Frédéric Péters 44fbe88ea9 translation update
gitea/wcs/pipeline/head This commit looks good Details
2024-02-09 07:40:45 +01:00
Frédéric Péters d9c5a34cf0 misc: remove "back home" link at the bottom of formdatas (#86487)
gitea/wcs/pipeline/head This commit looks good Details
2024-02-09 07:28:17 +01:00
Frédéric Péters e2d8aecc1c workflows: prevent action replay (#86577) 2024-02-09 07:28:07 +01:00
Frédéric Péters c39839a6a6 workflows: record single event for button clicks (#86597) 2024-02-09 07:27:58 +01:00
Frédéric Péters 108b249965 api: change /api/forms/ to not include anonymised forms by default (#86603) 2024-02-09 07:27:47 +01:00
Frédéric Péters fc0baf9389 api: add support for include-anonymised=on/off in form/card lists (#86603) 2024-02-09 07:27:47 +01:00
Frédéric Péters 4abadd3558 carddata: exclude draft/anonymised cards from get_by_id (#86695) 2024-02-09 07:27:36 +01:00
Valentin Deniaud f3d2056be5 tests: ensure no old sessions in test_session_expire (#86739)
gitea/wcs/pipeline/head Build queued... Details
2024-02-09 07:26:42 +01:00
Valentin Deniaud 9101bd1e69 tests: make snapshot count stable in test_studio_home_recent_changes (#86737)
gitea/wcs/pipeline/head This commit looks good Details
2024-02-09 07:26:26 +01:00
Valentin Deniaud 54e78443a6 tests: make i18n tests independant (#86736)
gitea/wcs/pipeline/head This commit looks good Details
2024-02-08 16:56:51 +01:00
Valentin Deniaud b63bc74a4d tests: avoid ambiguous click on translatable message (#86736) 2024-02-08 16:55:13 +01:00
Frédéric Péters 501697e26a backoffice: allow submitting restricted email settings form (#86616)
gitea/wcs/pipeline/head This commit looks good Details
2024-02-06 17:05:58 +01:00
Frédéric Péters 9877859d25 misc: check value set in numeric widget (#86625)
gitea/wcs/pipeline/head Build queued... Details
2024-02-06 15:45:46 +01:00
43 changed files with 617 additions and 91 deletions

View File

@ -32,6 +32,14 @@ def pub():
TranslatableMessage.do_table() # update table with selected languages
TranslatableMessage.wipe()
Workflow.wipe()
FormDef.wipe()
BlockDef.wipe()
Category.wipe()
CardDef.wipe()
MailTemplate.wipe()
return pub
@ -157,17 +165,18 @@ def test_i18n_page(pub):
assert {x.text for x in resp.pyquery('tr td:first-child')} == {'Email body', 'Email Subject'}
# translate a message
resp = resp.click('edit', index=0)
msg = TranslatableMessage.select([Equal('string', 'Email body')])[0]
resp = resp.click('edit', href='/%s/' % msg.id)
resp = resp.form.submit('cancel').follow()
resp = resp.click('edit', index=0)
resp = resp.click('edit', href='/%s/' % msg.id)
assert resp.pyquery('.i18n-orig-string').text() == 'Email body'
resp.form['translation'] = 'Texte du courriel'
resp = resp.form.submit('submit').follow()
msg = TranslatableMessage.select([Equal('string', 'Email body')])[0]
msg = TranslatableMessage.get(msg.id)
assert msg.string_fr == 'Texte du courriel'
# go back
resp = resp.click('edit', index=0)
resp = resp.click('edit', href='/%s/' % msg.id)
assert resp.form['translation'].value == 'Texte du courriel'
resp = resp.form.submit('submit').follow()
@ -179,9 +188,6 @@ def test_i18n_page(pub):
def test_i18n_export(pub):
create_superuser(pub)
Workflow.wipe()
BlockDef.wipe()
TranslatableMessage.wipe()
formdef = FormDef()
formdef.name = 'test title'
@ -233,9 +239,6 @@ def test_i18n_export(pub):
def test_i18n_import(pub):
create_superuser(pub)
Workflow.wipe()
BlockDef.wipe()
TranslatableMessage.wipe()
formdef = FormDef()
formdef.name = 'test title'
@ -333,9 +336,6 @@ def test_i18n_import(pub):
def test_i18n_pagination(pub):
create_superuser(pub)
Workflow.wipe()
BlockDef.wipe()
TranslatableMessage.wipe()
formdef = FormDef()
formdef.name = 'test title'
@ -350,22 +350,15 @@ def test_i18n_pagination(pub):
resp = resp.click('Go to multilinguism page')
# check page limit
assert resp.pyquery('#page-links a').text() == '1 2 3 4 5 6 10 50 100'
assert resp.pyquery('#page-links a').text() == '1 2 3 4 5 10 50 100'
resp = resp.click('50')
assert resp.pyquery('#page-links a').text() == '1 2 3 10 20 100'
assert resp.pyquery('#page-links a').text() == '1 2 10 20 100'
resp = resp.click('20')
resp = resp.click('3')
assert 'offset=40' in resp.request.url
def test_i18n_mark_as_non_translatabe(pub):
TranslatableMessage.wipe()
Workflow.wipe()
FormDef.wipe()
BlockDef.wipe()
Category.wipe()
CardDef.wipe()
MailTemplate.wipe()
create_superuser(pub)
workflow = Workflow(name='workflow')
workflow.add_status('First Status')

View File

@ -284,3 +284,109 @@ def test_logged_error_trace(pub):
resp = app.get(f'/backoffice/studio/logged-errors/{logged_error.id}/')
assert 'pub.record_error(\'Exception' in resp.pyquery('.stack-trace--code')[0].text
assert '\n locals:' in resp.text
def test_logged_error_cleanup(pub):
create_superuser(pub)
FormDef.wipe()
CardDef.wipe()
Workflow.wipe()
pub.loggederror_class.wipe()
formdef = FormDef()
formdef.name = 'foo'
formdef.store()
carddef = CardDef()
carddef.name = 'bar'
carddef.store()
workflow = Workflow()
workflow.name = 'blah'
workflow.store()
# FormDef error
error1 = pub.loggederror_class()
error1.summary = 'LoggedError'
error1.formdef_class = 'FormDef'
error1.formdef_id = formdef.id
error1.workflow_id = workflow.id
error1.first_occurence_timestamp = error1.latest_occurence_timestamp = datetime.datetime.now()
error1.store()
# CardDef error
error2 = pub.loggederror_class()
error2.summary = 'LoggedError'
error2.formdef_class = 'CardDef'
error2.formdef_id = carddef.id
error2.workflow_id = workflow.id
error2.first_occurence_timestamp = error2.latest_occurence_timestamp = datetime.datetime.now()
error2.store()
# workflow-only error
error3 = pub.loggederror_class()
error3.summary = 'LoggedError'
error3.workflow_id = workflow.id
error3.first_occurence_timestamp = error3.latest_occurence_timestamp = datetime.datetime.now()
error3.store()
app = login(get_app(pub))
resp = app.get('/backoffice/studio/logged-errors/')
resp = resp.click('Cleanup')
resp = resp.form.submit('submit')
assert pub.loggederror_class().count() == 3 # nothing removed
# check there's a form error if nothing is checked
resp = app.get('/backoffice/studio/logged-errors/')
resp = resp.click('Cleanup')
resp.form['types$elementformdef'].checked = False
resp.form['types$elementcarddef'].checked = False
resp.form['types$elementothers'].checked = False
resp = resp.form.submit('submit')
assert resp.pyquery('[data-widget-name="types"].widget-with-error')
# check cleanup of only formdef errors
error1.first_occurence_timestamp = (
error1.latest_occurence_timestamp
) = datetime.datetime.now() - datetime.timedelta(days=280)
error1.store()
error2.first_occurence_timestamp = datetime.datetime.now() - datetime.timedelta(days=120)
error2.latest_occurence_timestamp = datetime.datetime.now() - datetime.timedelta(days=80)
error2.store()
error3.first_occurence_timestamp = (
error3.latest_occurence_timestamp
) = datetime.datetime.now() - datetime.timedelta(days=280)
error3.store()
resp = app.get('/backoffice/studio/logged-errors/')
resp = resp.click('Cleanup')
resp.form['types$elementcarddef'].checked = False
resp.form['types$elementothers'].checked = False
resp = resp.form.submit('submit')
assert {x.id for x in pub.loggederror_class().select()} == {error2.id, error3.id}
# check cleanup latest occurence value (error2 should not be cleaned)
resp = app.get('/backoffice/studio/logged-errors/')
resp = resp.click('Cleanup')
resp.form['latest_occurence'] = (datetime.datetime.now() - datetime.timedelta(days=100)).strftime(
'%Y-%m-%d'
)
resp = resp.form.submit('submit')
assert {x.id for x in pub.loggederror_class().select()} == {error2.id}
# check with a more recent date (error2 should be cleaned this time)
resp = app.get('/backoffice/studio/logged-errors/')
resp = resp.click('Cleanup')
resp.form['latest_occurence'] = (datetime.datetime.now() - datetime.timedelta(days=10)).strftime(
'%Y-%m-%d'
)
resp = resp.form.submit('submit')
assert {x.id for x in pub.loggederror_class().select()} == set()
# make formdefs not accessible to current user
pub.cfg['admin-permissions'] = {'forms': ['X']}
pub.write_cfg()
resp = app.get('/backoffice/studio/logged-errors/')
resp = resp.click('Cleanup')
assert [x.attrib['name'] for x in resp.pyquery('[type="checkbox"]')] == [
'types$elementcarddef',
'types$elementothers',
]

View File

@ -98,6 +98,8 @@ def test_settings_disabled_screens(pub):
resp = app.get('/backoffice/settings/emails/options')
assert not resp.pyquery('#form_smtp_server')
resp.form['from'] = 'test@localhost'
resp = resp.form.submit('submit')
pub.site_options.set('options', 'settings-disabled-screens', '')
pub.site_options.set('options', 'settings-hidden-screens', 'import-export')

View File

@ -133,6 +133,7 @@ def test_studio_home_recent_changes(pub):
other_user = pub.user_class(name='other')
other_user.store()
pub.snapshot_class.wipe()
BlockDef.wipe()
CardDef.wipe()
NamedDataSource.wipe()

View File

@ -193,6 +193,17 @@ def test_carddata_include_params(pub, local_user, auth):
resp = get_url('/api/cards/test/%s/?include-workflow=off&include-workflow-data=off' % carddata.id)
assert 'workflow' not in resp.json
resp = get_url('/api/cards/test/list')
assert len(resp.json['data']) == 1
carddata.anonymise()
resp = get_url('/api/cards/test/list')
assert len(resp.json['data']) == 0
carddata.anonymise()
resp = get_url('/api/cards/test/list?include-anonymised=on')
assert len(resp.json['data']) == 1
def test_carddata_user_fields(pub, local_user):
pub.role_class.wipe()

View File

@ -3360,7 +3360,7 @@ def test_api_include_anonymised(pub, local_user):
formdata.anonymise()
resp = get_app(pub).get(sign_uri('/api/forms/', user=local_user))
assert len(resp.json['data']) == 10
assert len(resp.json['data']) == 9
resp = get_app(pub).get(sign_uri('/api/forms/?include-anonymised=on', user=local_user))
assert len(resp.json['data']) == 10

View File

@ -1689,6 +1689,33 @@ def test_backoffice_handling(pub):
assert 'HELLO WORLD' in resp.text
def test_backoffice_parallel_handling(pub, freezer):
create_user(pub)
create_environment(pub)
form_class = FormDef.get_by_urlname('form-title').data_class()
number31 = [x for x in form_class.select() if x.data['1'] == 'FOO BAR 30'][0]
app = login(get_app(pub))
resp = app.get('/backoffice/management/form-title/')
assert re.findall(r'<tbody.*\/tbody>', resp.text, re.DOTALL)[0].count('<tr') == 17
# open formdata twice
resp2 = resp.click(href='%s/' % number31.id)
resp3 = resp.click(href='%s/' % number31.id)
freezer.move_to(datetime.timedelta(seconds=10))
resp2.forms[0]['comment'] = 'HELLO WORLD'
resp2 = resp2.forms[0].submit('button_accept')
resp2 = resp2.follow()
resp3.forms[0]['comment'] = 'HELLO WORLD'
resp3 = resp3.forms[0].submit('button_accept')
assert resp3.pyquery('.global-errors summary').text() == 'Error: parallel execution.'
assert (
resp3.pyquery('.global-errors summary + p').text()
== 'Another action has been performed on this form in the meantime and data may have been changed.'
)
def test_backoffice_handling_global_action(pub):
create_user(pub)

View File

@ -734,7 +734,7 @@ def test_inspect_page_with_related_objects(pub):
def test_inspect_page_actions_traces(pub):
create_user(pub, is_admin=True)
user = create_user(pub, is_admin=True)
FormDef.wipe()
target_formdef = FormDef()
@ -793,6 +793,10 @@ def test_inspect_page_actions_traces(pub):
edit_carddata2.varname = 'edited_card_wrong_target_id'
edit_carddata2.mappings = [Mapping(field_id='0', expression='foo bar blah')]
choice = st1.items[2]
assert choice.key == 'choice'
choice.by = ['logged-users']
workflow.store()
formdef = FormDef()
@ -802,12 +806,19 @@ def test_inspect_page_actions_traces(pub):
formdef.data_class().wipe()
formdata = formdef.data_class()()
formdata.user_id = user.id
formdata.data = {}
formdata.just_created()
formdata.store()
formdata.record_workflow_event('frontoffice-created')
formdata.perform_workflow()
formdata.store()
app = login(get_app(pub))
resp = app.get(formdata.get_url())
resp = resp.form.submit('button_accept')
resp = resp.follow()
formdata.refresh_from_storage()
assert formdata.status == 'wf-accepted'
# change receipt time to get global timeout to run
formdata.receipt_time = time.localtime(time.time() - 3 * 86400)
@ -816,7 +827,6 @@ def test_inspect_page_actions_traces(pub):
formdata.refresh_from_storage()
assert formdata.get_criticality_level_object().name == 'yellow'
app = login(get_app(pub))
resp = app.get(formdata.get_url(backoffice=True), status=200)
resp = resp.click('Data Inspector')
assert '>Actions Tracing</' in resp
@ -826,9 +836,10 @@ def test_inspect_page_actions_traces(pub):
'Created form - target form #1-1',
'Created card - target card #1-1',
'Edited card - target card #1-1',
'Action button - Manual Jump Accept',
'Global action timeout',
]
assert [x.text for x in resp.pyquery('#inspect-timeline strong')] == ['Just Submitted', 'New']
assert [x.text for x in resp.pyquery('#inspect-timeline strong')] == ['Just Submitted', 'New', 'Accepted']
assert [x.text for x in resp.pyquery('#inspect-timeline a.tracing-link') if x.text] == [
'Email',
'Email',
@ -837,6 +848,8 @@ def test_inspect_page_actions_traces(pub):
'Create Card Data',
'Edit Card Data',
'Edit Card Data',
'Email',
'Email',
'Criticality Levels',
]
event_links = [x.attrib['href'] for x in resp.pyquery('#inspect-timeline .event a')]
@ -844,6 +857,7 @@ def test_inspect_page_actions_traces(pub):
'http://example.net/backoffice/management/target-form/1/', # Created form
'http://example.net/backoffice/data/target-card/1/', # Created card
'http://example.net/backoffice/data/target-card/1/', # Edited card
'http://example.net/backoffice/workflows/2/status/new/items/_accept/', # Accept manual jump
'http://example.net/backoffice/workflows/2/global-actions/1/#trigger-%s' % trigger.id,
]
# check all links are valid
@ -853,19 +867,21 @@ def test_inspect_page_actions_traces(pub):
'Created form - target form #1-1',
'Created card - target card #1-1',
'Edited card - target card #1-1',
'Action button - Manual Jump Accept',
'Global action timeout',
]
assert [x.text for x in resp.pyquery('#inspect-timeline .event-error')] == ['Nothing edited']
action_links = [x.attrib['href'] for x in resp.pyquery('#inspect-timeline a.tracing-link')]
assert len(action_links) == 13
assert action_links[0] == 'http://example.net/backoffice/workflows/2/status/just_submitted/'
assert (
action_links[1]
== 'http://example.net/backoffice/workflows/2/status/just_submitted/items/_notify_new_receiver_email/'
)
assert action_links[-5] == 'http://example.net/backoffice/workflows/2/status/new/items/_create/'
assert action_links[-4] == 'http://example.net/backoffice/workflows/2/status/new/items/_create_card/'
assert action_links[-3] == 'http://example.net/backoffice/workflows/2/status/new/items/_edit_card/'
assert action_links[-2] == 'http://example.net/backoffice/workflows/2/status/new/items/_edit_card2/'
assert action_links[-8] == 'http://example.net/backoffice/workflows/2/status/new/items/_create/'
assert action_links[-7] == 'http://example.net/backoffice/workflows/2/status/new/items/_create_card/'
assert action_links[-6] == 'http://example.net/backoffice/workflows/2/status/new/items/_edit_card/'
assert action_links[-5] == 'http://example.net/backoffice/workflows/2/status/new/items/_edit_card2/'
assert action_links[-1] == 'http://example.net/backoffice/workflows/2/global-actions/1/items/1/'
# check details are available
@ -890,6 +906,7 @@ def test_inspect_page_actions_traces(pub):
'Created form - target form #1-1',
'Created card - deleted',
'Edited card - deleted',
'Action button - Manual Jump Accept',
'Global action timeout',
]

View File

@ -1394,6 +1394,51 @@ def test_field_live_item_datasource_prefill_with_request_with_q(pub):
)
@responses.activate
def test_field_live_item_datasource_prefill_with_invalid_data(pub):
NamedDataSource.wipe()
data_source = NamedDataSource(name='foobar')
data_source.data_source = {
'type': 'json',
'value': 'http://remote.example.net/json',
'qs_data': {'plop': '{{ form_var_foo }}'},
}
data_source.query_parameter = 'q'
data_source.id_parameter = 'id'
data_source.store()
data = {'data': [{'id': '1', 'text': 'un'}, {'id': '2', 'text': 'deux', 'x': 'bye'}]}
responses.get('http://remote.example.net/json', json=data)
ds = {'type': 'foobar'}
FormDef.wipe()
formdef = FormDef()
formdef.name = 'Foo'
formdef.fields = [
fields.ItemField(
id='1',
label='item',
data_source=ds,
varname='foo',
prefill={'type': 'string', 'value': '2'},
),
fields.ItemField(
id='2',
label='item',
varname='item',
prefill={'type': 'string', 'value': '{{ form_var_foo_structured_raw }}'}, # nope
data_source=ds,
),
]
formdef.store()
app = get_app(pub)
resp = app.get('/foo/')
app.post(
'/foo/live?modified_field_id[]=init&prefilled_1=on&prefilled_2=on', params=resp.form.submit_fields()
)
def test_field_live_block_string_prefill(pub, http_requests):
FormDef.wipe()
BlockDef.wipe()

View File

@ -457,6 +457,59 @@ def test_form_page_template_list_prefill_by_text(pub):
assert resp.form['f1'].value == str(carddata2.id)
assert 'invalid value selected' not in resp.text
formdef.fields[0].prefill = {'type': 'string', 'value': '{{ %s }}' % carddata2.id}
formdef.enable_tracking_codes = True
formdef.store()
app = get_app(pub)
resp = app.get('/test/')
assert resp.form['f1'].value == str(carddata2.id)
assert 'invalid value selected' not in resp.text
assert app.post('/test/autosave', params=resp.form.submit_fields()).json == {'result': 'success'}
@pytest.mark.parametrize('id_type', [int, str])
def test_form_page_template_list_prefill_by_number(pub, id_type):
NamedDataSource.wipe()
data_source = NamedDataSource(name='foobar')
data_source.data_source = {
'type': 'jsonvalue',
'value': json.dumps([{'id': id_type(1), 'text': 'foo'}, {'id': id_type(2), 'text': 'bar'}]),
}
data_source.store()
formdef = create_formdef()
formdef.data_class().wipe()
formdef.enable_tracking_codes = True
formdef.fields = [
fields.PageField(id='1', label='1st page'),
fields.ItemField(
id='2',
label='item',
varname='item',
required=True,
data_source={'type': data_source.slug},
prefill={'type': 'string', 'value': '{{ 2 }}'},
),
fields.PageField(id='3', label='2nd page'),
fields.ItemField(
id='4',
label='item',
varname='item',
required=True,
data_source={'type': data_source.slug},
prefill={'type': 'string', 'value': '{{ 3 }}'}, # invalid value
),
]
formdef.store()
app = get_app(pub)
resp = app.get('/test/')
assert resp.form['f2'].value == '2'
assert 'invalid value selected' not in resp.text
assert app.post('/test/autosave', params=resp.form.submit_fields()).json == {'result': 'success'}
resp = resp.form.submit('submit')
assert 'invalid value selected' in resp.text
def test_form_page_query_string_list_prefill(pub):
create_user(pub)

View File

@ -1281,3 +1281,31 @@ def test_card_custom_id(pub, ids):
'{{ cards|objects:"foo"|filter_by_identifier:"%s"|first|get:"form_identifier" }}' % ids[-1]
)
assert tmpl.render(context) == str(ids[-1])
def test_card_custom_id_draft(pub):
CardDef.wipe()
carddef = CardDef()
carddef.name = 'foo'
carddef.fields = [
StringField(id='1', label='Test', varname='foo'),
]
carddef.digest_templates = {'default': 'card {{ form_var_foo }}'}
carddef.id_template = '{{ form_var_foo }}'
carddef.store()
card = carddef.data_class()()
card.data = {'1': 'id1'}
card.status = 'draft'
card.store()
card = carddef.data_class()()
card.data = {'1': 'id1'}
card.just_created()
card.store()
assert carddef.data_class().get_by_id('id1').id == card.id
card.anonymise()
with pytest.raises(KeyError):
carddef.data_class().get_by_id('id1')

View File

@ -150,7 +150,7 @@ def test_formdata_create_and_edit_and_bo_field(pub, user):
# creation, bo field first action
assert formdata.evolution[0].parts[1].formdef_type == 'formdef'
assert formdata.evolution[0].parts[1].formdef_id == str(formdef.id)
assert formdata.evolution[0].parts[1].old_data == {'1': 'bar'}
assert formdata.evolution[0].parts[1].old_data == {'1': 'bar', 'bo1': None}
assert formdata.evolution[0].parts[1].new_data == {'1': 'bar', 'bo1': 'bar'}
dt2 = formdata.evolution[0].parts[1].datetime
assert dt2 > dt1

View File

@ -68,6 +68,7 @@ session_max_age: 1
def test_session_expire(pub, user, app):
pub.session_manager.session_class.wipe()
login(app, username='foo', password='foo')
assert 'Logout' in app.get('/')
session = pub.session_manager.session_class.select()[0]

View File

@ -1658,6 +1658,13 @@ def test_numeric_widget():
assert widget.has_error()
assert widget.get_error() == 'You should enter digits only, for example: 123.'
# existing invalid value
widget = NumericWidget('test', max_value=10)
widget.set_value('01.02.03')
mock_form_submission(req, widget, {'test': '01.02.03'})
assert widget.has_error()
assert widget.get_error() == 'You should enter a number, for example: 123.'
def test_css_classes_widget():
for value, result, has_error in (

View File

@ -5,6 +5,7 @@ from quixote import cleanup
from wcs import sessions
from wcs.formdef import FormDef
from wcs.qommon.http_request import HTTPRequest
from wcs.workflow_traces import WorkflowTrace
from wcs.workflows import Workflow
from ..form_pages.test_all import create_user
@ -215,3 +216,36 @@ def test_choice_button_confirmation(pub):
form = formdata.get_workflow_form(user)
html_form = PyQuery(str(form.render()))
assert html_form.find('button').attr('data-ask-for-confirmation') == 'Are you sure?'
def test_choice_workflow_event(pub):
user = create_user(pub)
workflow = Workflow(name='choice')
st1 = workflow.add_status('Status1')
st2 = workflow.add_status('Status2')
choice1 = st1.add_action('choice')
choice1.label = 'foobar1'
choice1.by = ['logged-users']
choice1.status = str(st2.id)
workflow.store()
formdef = FormDef()
formdef.name = 'baz'
formdef.fields = []
formdef.workflow = workflow
formdef.store()
formdata = formdef.data_class()()
formdata.user_id = user.id
formdata.just_created()
formdata.store()
app = login(get_app(pub), username='foo', password='foo')
resp = app.get(formdata.get_url())
resp = resp.form.submit(f'button{choice1.id}')
formdata.refresh_from_storage()
assert formdata.status == f'wf-{st2.id}'
traces = WorkflowTrace.select_for_formdata(formdata)
assert [(x.event, x.event_args) for x in traces] == [('button', {'action_item_id': '1'})]

View File

@ -461,6 +461,7 @@ def test_frontoffice_workflow_form_with_impossible_condition(pub):
formdata = formdef.data_class()()
formdata.user_id = user.id
formdata.just_created()
formdata.status = 'wf-new'
formdata.store()

View File

@ -578,7 +578,7 @@ def test_timeout_tracing(pub, admin_user):
assert [PyQuery(x).text() for x in resp.pyquery('#inspect-timeline li > *:nth-child(2)')] == [
'Created (backoffice submission)',
'Status1',
'Timeout jump',
'Timeout jump - Change Status on Timeout',
'Status2',
'History Message',
]

View File

@ -14,16 +14,18 @@
# You should have received a copy of the GNU General Public License
# along with this program; if not, see <http://www.gnu.org/licenses/>.
import datetime
import re
from django.utils.text import Truncator
from quixote import get_publisher, get_request, get_response, redirect
from quixote.directory import Directory
from quixote.directory import AccessControlled, Directory
from quixote.html import TemplateIO, htmltext
from wcs.backoffice.pagination import pagination_links
from wcs.qommon import N_, _, errors, misc, ngettext, template
from wcs.sql_criterias import Equal, NotEqual, NotNull
from wcs.qommon.form import CheckboxesWidget, DateWidget, Form
from wcs.sql_criterias import Equal, Less, NotEqual, NotNull, Null, Or
class LoggedErrorDirectory(Directory):
@ -132,8 +134,8 @@ class LoggedErrorDirectory(Directory):
return redirect('..')
class LoggedErrorsDirectory(Directory):
_q_exports = ['']
class LoggedErrorsDirectory(AccessControlled, Directory):
_q_exports = ['', 'cleanup']
@classmethod
def get_errors(cls, offset, limit, formdef_class=None, formdef_id=None, workflow_id=None):
@ -208,7 +210,7 @@ class LoggedErrorsDirectory(Directory):
self.formdef_id = formdef_id
self.workflow_id = workflow_id
def _q_index(self):
def _q_access(self):
backoffice_root = get_publisher().get_backoffice_root()
if not (
backoffice_root.is_accessible('forms')
@ -217,6 +219,7 @@ class LoggedErrorsDirectory(Directory):
):
raise errors.AccessForbiddenError()
def _q_index(self):
get_response().breadcrumb.append(('logged-errors/', _('Logged Errors')))
get_response().set_title(_('Logged Errors'))
limit = misc.get_int_or_400(
@ -240,6 +243,58 @@ class LoggedErrorsDirectory(Directory):
},
)
def cleanup(self):
backoffice_root = get_publisher().get_backoffice_root()
form = Form(enctype='multipart/form-data')
options = []
if backoffice_root.is_accessible('forms'):
options.append(('formdef', _('Forms'), 'formdef'))
if backoffice_root.is_accessible('cards'):
options.append(('carddef', _('Card Models'), 'carddef'))
if backoffice_root.is_accessible('workflows'):
options.append(('others', _('Others'), 'others'))
form.add(
CheckboxesWidget,
'types',
title=_('Error types'),
value=[x[0] for x in options], # check all by default
options=options,
required=True,
)
form.add(
DateWidget,
'latest_occurence',
title=_('Latest occurence'),
value=datetime.date.today() - datetime.timedelta(days=180),
required=True,
)
form.add_submit('submit', _('Submit'))
form.add_submit('cancel', _('Cancel'))
if form.get_widget('cancel').parse():
return redirect('.')
if form.get_submit() == 'submit' and not form.has_errors():
type_criterias = []
if 'formdef' in form.get_widget('types').parse():
type_criterias.append(Equal('formdef_class', 'FormDef'))
if 'carddef' in form.get_widget('types').parse():
type_criterias.append(Equal('formdef_class', 'CardDef'))
if 'others' in form.get_widget('types').parse():
type_criterias.append(Null('formdef_class'))
criterias = [
Less('latest_occurence_timestamp', form.get_widget('latest_occurence').parse()),
Or(type_criterias),
]
get_publisher().loggederror_class.wipe(clause=criterias)
return redirect('.')
get_response().set_title(_('Cleanup'))
r = TemplateIO(html=True)
r += htmltext('<h2>%s</h2>') % _('Cleanup')
r += form.render()
return r.getvalue()
def _q_lookup(self, component):
try:
error = get_publisher().loggederror_class.get(component)

View File

@ -375,6 +375,7 @@ class ApiCardPage(ApiFormPageMixin, BackofficeCardPage):
formdata.store()
formdata.just_created()
formdata.store()
formdata.refresh_from_storage()
formdata.record_workflow_event('api-created')
formdata.perform_workflow()
formdata.store()
@ -505,7 +506,7 @@ class ApiFormsDirectory(Directory):
roles_criterias = criterias
criterias = management_directory.get_global_listing_criterias(ignore_user_roles=True)
if not get_query_flag('include-anonymised', default=True):
if not get_query_flag('include-anonymised', default=False):
criterias.append(Null('anonymised'))
related_filter = get_request().form.get('related')
@ -709,6 +710,7 @@ class ApiFormdefDirectory(Directory):
else:
formdata.just_created()
formdata.store()
formdata.refresh_from_storage()
formdata.record_workflow_event('api-created')
formdata.perform_workflow()
formdata.store()

View File

@ -420,6 +420,9 @@ class CardBackOfficeStatusPage(FormBackOfficeStatusPage):
sidebar_recorded_by_agent_message = _(
'The card has been recorded on %(date)s with the identifier %(identifier)s by %(agent)s.'
)
replay_detailed_message = _(
'Another action has been performed on this card in the meantime and data may have been changed.'
)
def should_fold_summary(self, mine, request_user):
return False
@ -495,6 +498,7 @@ class ImportFromCsvAfterJob(AfterJob):
get_publisher().reset_formdata_state()
get_publisher().substitutions.feed(data_instance)
data_instance.refresh_from_storage()
data_instance.record_workflow_event('csv-import-created')
data_instance.perform_workflow()
self.increment_count()
@ -611,6 +615,7 @@ class ImportFromJsonAfterJob(AfterJob):
get_publisher().reset_formdata_state()
get_publisher().substitutions.feed(carddata)
carddata.refresh_from_storage()
carddata.record_workflow_event('json-import-created')
carddata.perform_workflow()
else:

View File

@ -61,9 +61,6 @@ class I18nDirectory(Directory):
if not get_publisher().has_i18n_enabled():
raise errors.TraversalError()
if TranslatableMessage.count() == 0:
return self.scan()
get_response().set_title(_('Multilinguism'))
get_response().breadcrumb.append(('i18n/', _('Multilinguism')))
@ -77,6 +74,9 @@ class I18nDirectory(Directory):
r += htmltext('</div>')
return r.getvalue()
if TranslatableMessage.count() == 0:
return self.scan()
criterias = []
criterias.append(Equal('translatable', not (bool(get_request().form.get('non_translatable')))))
if get_request().form.get('q'):

View File

@ -2706,6 +2706,9 @@ class FormPage(Directory, TempfileDirectoryMixin):
if 'limit' in get_request().form:
limit = misc.get_int_or_400(get_request().form['limit'])
if not get_query_flag('include-anonymised', default=False):
criterias.append(Null('anonymised'))
common_statuses = [id for id, name, _ in get_common_statuses()]
if selected_filter not in common_statuses:

View File

@ -382,6 +382,7 @@ class FormFillPage(PublicFormFillPage):
self.set_tracking_code(filled)
get_session().remove_magictoken(get_request().form.get('magictoken'))
self.clean_submission_context()
filled.refresh_from_storage()
filled.record_workflow_event('backoffice-created')
url = filled.perform_workflow()
return self.redirect_after_submitted(url, filled)

View File

@ -19,7 +19,7 @@ from quixote import get_publisher, get_request, get_session
from wcs.formdata import FormData
from .qommon import _
from .qommon.storage import Equal
from .sql_criterias import Equal, Null, StrictNotEqual
class CardData(FormData):
@ -42,7 +42,14 @@ class CardData(FormData):
@classmethod
def get_by_id(cls, value):
try:
return cls.select([cls._formdef.get_by_id_criteria(value)], limit=1)[0]
return cls.select(
[
StrictNotEqual('status', 'draft'),
Null('anonymised'),
cls._formdef.get_by_id_criteria(value),
],
limit=1,
)[0]
except IndexError:
raise KeyError(value)

View File

@ -292,6 +292,7 @@ def get_carddef_items(data_source):
def get_id_by_option_text(data_source, text_value):
data_source = get_object(data_source)
if data_source:
text_value = str(text_value)
if data_source.data_source.get('type') == 'json' and data_source.query_parameter:
url = data_source.get_json_query_url()
url += urllib.parse.quote(text_value)

View File

@ -260,7 +260,7 @@ class BlockField(WidgetField):
# skip if there are no values
return (None, {})
value_info, value_details = super().get_value_info(data)
if value_info is None and value_details not in (None, {}, {'value_id': None}):
if value_info is None and value_details not in (None, {'value_id': None}):
# buggy digest template created an empty value, switch it to an empty string
# so it's not considered empty in summary page.
value_info = ''

View File

@ -325,7 +325,9 @@ class ItemField(WidgetField, MapOptionsMixin, ItemFieldMixin, ItemWithImageField
data_source = data_sources.get_object(self.data_source)
struct_value = data_source.get_structured_value(value)
if struct_value:
value = str(struct_value.get('id'))
value = struct_value.get('id')
if force_string and value is not None and not isinstance(value, str):
value = str(value)
return (value, explicit_lock)
def get_display_mode(self, data_source=None):

View File

@ -36,7 +36,7 @@ from wcs.qommon.admin.texts import TextsDirectory
from wcs.qommon.upload_storage import get_storage_object
from wcs.utils import record_timings
from wcs.wf.editable import EditableWorkflowStatusItem
from wcs.workflows import RedisplayFormException
from wcs.workflows import RedisplayFormException, ReplayException
from ..qommon import _, audit, errors, misc, template
@ -166,6 +166,10 @@ class FormStatusPage(Directory, FormTemplateMixin):
history_templates = ['wcs/formdata_history.html']
status_templates = ['wcs/formdata_status.html']
replay_detailed_message = _(
'Another action has been performed on this form in the meantime and data may have been changed.'
)
def __init__(self, formdef, filled, register_workflow_subdirs=True, custom_view=None, parent_view=None):
get_publisher().substitutions.feed(filled)
self.formdef = formdef
@ -726,7 +730,18 @@ class FormStatusPage(Directory, FormTemplateMixin):
def submit(self, form):
user = get_request().user
next_url = self.filled.handle_workflow_form(user, form)
next_url = None
try:
next_url = self.filled.handle_workflow_form(user, form)
except ReplayException:
raise RedisplayFormException(
form=form,
error={
'summary': _('Error: parallel execution.'),
'details': self.replay_detailed_message,
},
)
if next_url:
return next_url
if form.has_errors():

View File

@ -1881,6 +1881,7 @@ class FormPage(Directory, TempfileDirectoryMixin, FormTemplateMixin):
url = None
if existing_formdata is None:
self.clean_submission_context()
filled.refresh_from_storage()
filled.record_workflow_event('frontoffice-created')
url = filled.perform_workflow()

View File

@ -4,8 +4,8 @@ msgid ""
msgstr ""
"Project-Id-Version: wcs 0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-02-01 09:47+0100\n"
"PO-Revision-Date: 2024-02-01 09:48+0100\n"
"POT-Creation-Date: 2024-02-09 15:23+0100\n"
"PO-Revision-Date: 2024-02-09 15:23+0100\n"
"Last-Translator: Thomas Noël <tnoel@entrouvert.com>\n"
"Language-Team: french\n"
"Language: fr\n"
@ -58,22 +58,22 @@ msgstr "Rôles donnés par cet accès"
#: admin/api_access.py admin/blocks.py admin/categories.py
#: admin/comment_templates.py admin/data_sources.py admin/fields.py
#: admin/forms.py admin/mail_templates.py admin/roles.py admin/settings.py
#: admin/tests.py admin/users.py admin/workflows.py admin/wscalls.py
#: backoffice/data_management.py backoffice/i18n.py backoffice/management.py
#: backoffice/snapshots.py forms/root.py qommon/admin/emails.py
#: qommon/admin/texts.py qommon/ident/franceconnect.py qommon/ident/idp.py
#: qommon/ident/password.py wf/form.py
#: admin/forms.py admin/logged_errors.py admin/mail_templates.py admin/roles.py
#: admin/settings.py admin/tests.py admin/users.py admin/workflows.py
#: admin/wscalls.py backoffice/data_management.py backoffice/i18n.py
#: backoffice/management.py backoffice/snapshots.py forms/root.py
#: qommon/admin/emails.py qommon/admin/texts.py qommon/ident/franceconnect.py
#: qommon/ident/idp.py qommon/ident/password.py wf/form.py
msgid "Submit"
msgstr "Valider"
#: admin/api_access.py admin/blocks.py admin/categories.py
#: admin/comment_templates.py admin/data_sources.py admin/fields.py
#: admin/forms.py admin/mail_templates.py admin/roles.py admin/settings.py
#: admin/tests.py admin/users.py admin/workflows.py admin/wscalls.py
#: backoffice/data_management.py backoffice/i18n.py backoffice/management.py
#: backoffice/snapshots.py backoffice/submission.py forms/actions.py
#: forms/root.py qommon/admin/emails.py qommon/admin/texts.py
#: admin/forms.py admin/logged_errors.py admin/mail_templates.py admin/roles.py
#: admin/settings.py admin/tests.py admin/users.py admin/workflows.py
#: admin/wscalls.py backoffice/data_management.py backoffice/i18n.py
#: backoffice/management.py backoffice/snapshots.py backoffice/submission.py
#: forms/actions.py forms/root.py qommon/admin/emails.py qommon/admin/texts.py
#: qommon/ident/franceconnect.py qommon/ident/idp.py qommon/ident/password.py
#: qommon/myspace.py
msgid "Cancel"
@ -1012,15 +1012,16 @@ msgstr "Libellé"
msgid "Type"
msgstr "Type"
#: admin/fields.py admin/forms.py admin/settings.py admin/workflows.py
#: api_export_import.py backoffice/journal.py backoffice/management.py
#: formdef.py forms/root.py root.py templates/wcs/backoffice/carddef.html
#: templates/wcs/backoffice/forms.html templates/wcs/backoffice/i18n.html
#: admin/fields.py admin/forms.py admin/logged_errors.py admin/settings.py
#: admin/workflows.py api_export_import.py backoffice/journal.py
#: backoffice/management.py formdef.py forms/root.py root.py
#: templates/wcs/backoffice/carddef.html templates/wcs/backoffice/forms.html
#: templates/wcs/backoffice/i18n.html
msgid "Forms"
msgstr "Formulaires"
#: admin/fields.py admin/settings.py backoffice/cards.py backoffice/journal.py
#: backoffice/root.py templates/wcs/backoffice/cards.html
#: admin/fields.py admin/logged_errors.py admin/settings.py backoffice/cards.py
#: backoffice/journal.py backoffice/root.py templates/wcs/backoffice/cards.html
msgid "Card Models"
msgstr "Modèles de fiche"
@ -1781,6 +1782,22 @@ msgstr "erreur %(class)s (%(message)s)"
msgid "Logged Errors"
msgstr "Erreurs enregistrées"
#: admin/logged_errors.py
msgid "Others"
msgstr "Autres"
#: admin/logged_errors.py
msgid "Error types"
msgstr "Types derreur"
#: admin/logged_errors.py
msgid "Latest occurence"
msgstr "Dernière occurrence"
#: admin/logged_errors.py templates/wcs/backoffice/logged-errors.html
msgid "Cleanup"
msgstr "Nettoyage"
#: admin/mail_templates.py api_export_import.py
#: templates/wcs/backoffice/mail-templates.html
#: templates/wcs/backoffice/workflows.html
@ -3220,7 +3237,7 @@ msgstr "Changement dans lordre des déclencheurs"
msgid "New global action trigger"
msgstr "Nouveau déclencheur daction globale"
#: admin/workflows.py
#: admin/workflows.py workflows.py
msgid "Automatic"
msgstr "Automatique"
@ -3228,7 +3245,7 @@ msgstr "Automatique"
msgid "Manual"
msgstr "Manuel"
#: admin/workflows.py
#: admin/workflows.py workflows.py
msgid "External call"
msgstr "Appel externe"
@ -3802,6 +3819,14 @@ msgstr ""
"La fiche a été enregistrée le %(date)s avec lidentifiant %(identifier)s par "
"%(agent)s."
#: backoffice/data_management.py
msgid ""
"Another action has been performed on this card in the meantime and data may "
"have been changed."
msgstr ""
"Une autre action a eu lieu sur cette fiche entretemps et les données peuvent "
"avoir été modifiées."
#: backoffice/data_management.py
msgid "Importing data into cards"
msgstr "Importation des données dans des fiches"
@ -6244,6 +6269,14 @@ msgstr ""
msgid "Run selected action on all pages"
msgstr "Exécuter laction choisie sur toutes les pages"
#: forms/common.py
msgid ""
"Another action has been performed on this form in the meantime and data may "
"have been changed."
msgstr ""
"Une autre action a eu lieu sur cette demande entretemps et les données "
"peuvent avoir été modifiées."
#: forms/common.py
msgid "ID not available in filtered view"
msgstr "ID non disponible dans la vue filtrée"
@ -6280,6 +6313,10 @@ msgstr ""
msgid "(unlock actions)"
msgstr "(débloquer les actions)"
#: forms/common.py
msgid "Error: parallel execution."
msgstr "Erreur : exécution parallèle."
#: forms/preview.py
msgid "This was only a preview: form was not actually submitted."
msgstr ""
@ -10137,10 +10174,6 @@ msgstr "Supprimer le brouillon"
msgid "You can get back to this page using the following tracking code:"
msgstr "Vous pouvez revenir sur cette page en utilisant ce code de suivi :"
#: templates/wcs/formdata_status.html
msgid "Back Home"
msgstr "Retour à laccueil"
#: templates/wcs/formdata_steps.html
msgid "Steps"
msgstr "Étapes"

View File

@ -32,7 +32,9 @@ def cfg_submit(form, cfg_key, fields):
cfg_key = str(cfg_key)
cfg_dict = get_cfg(cfg_key, {})
for k in fields:
cfg_dict[str(k)] = form.get_widget(k).parse()
widget = form.get_widget(k)
if widget:
cfg_dict[str(k)] = widget.parse()
get_publisher().cfg[cfg_key] = cfg_dict
audit('settings', cfg_key=cfg_key)
get_publisher().write_cfg()

View File

@ -1581,6 +1581,12 @@ class NumericWidget(WcsExtraStringWidget):
elif self.restrict_to_integers and int(self.value) != self.value:
self.set_error_code('type_mismatch')
def set_value(self, value):
try:
super().set_value(parse_decimal(value, do_raise=True, keep_none=True))
except (ArithmeticError, TypeError, ValueError):
super().set_value(None)
def get_error_message_codes(self):
yield from super().get_error_message_codes()
yield 'range_underflow'

View File

@ -3,6 +3,10 @@
{% block appbar-title %}{% trans "Logged Errors" %}{% endblock %}
{% block appbar-actions %}
<a rel="popup" href="cleanup">{% trans "Cleanup" %}</a>
{% endblock %}
{% block content %}
<ul class="objects-list single-links">
{% for error in errors %}

View File

@ -36,8 +36,4 @@
{{ view.actions_workflow_messages|safe }}
{{ workflow_form.render|safe }}
{% endif %}
<div class="back-home-button">
<a href="{{ publisher.get_root_url }}">{% trans "Back Home" %}</a>
</div>
{% endblock %}

View File

@ -148,6 +148,7 @@ class AddAttachmentWorkflowStatusItem(WorkflowStatusItem):
if self.display_button:
form.add_submit('button%s' % self.id, self.button_label or _('Upload File'))
form.get_widget('button%s' % self.id).backoffice_info_text = self.backoffice_info_text
form.get_widget('button%s' % self.id).action_id = self.id
def submit_form(self, form, formdata, user, evo):
if form.get_widget('attachment%s' % self.id):

View File

@ -88,6 +88,9 @@ class ChoiceWorkflowStatusItem(WorkflowStatusJumpItem):
else:
return _('not completed')
def get_line_short_details(self):
return self.label
def get_computed_strings(self):
yield from super().get_computed_strings()
if self.get_expression(self.label, allow_python=False, allow_ezt=False)['type'] != 'text':
@ -107,6 +110,7 @@ class ChoiceWorkflowStatusItem(WorkflowStatusJumpItem):
if not label:
return
widget = form.add_submit('button%s' % self.id, label)
widget.action_id = self.id
if self.identifier:
widget.extra_css_class = 'button-%s' % self.identifier
if self.require_confirmation:
@ -121,7 +125,6 @@ class ChoiceWorkflowStatusItem(WorkflowStatusJumpItem):
if form.get_submit() == 'button%s' % self.id:
wf_status = self.get_target_status(formdata)
if wf_status:
formdata.record_workflow_event('button', action_item_id=self.id)
evo.status = 'wf-%s' % wf_status[0].id
self.handle_markers_stack(formdata)
form.clear_errors()

View File

@ -104,6 +104,7 @@ class CommentableWorkflowStatusItem(WorkflowStatusItem):
form.add_submit('button%s' % self.id, self.button_label)
if form.get_widget('button%s' % self.id):
form.get_widget('button%s' % self.id).backoffice_info_text = self.backoffice_info_text
form.get_widget('button%s' % self.id).action_id = self.id
def submit_form(self, form, formdata, user, evo):
widget = form.get_widget('comment')

View File

@ -312,6 +312,7 @@ class ExportToModel(WorkflowStatusItem):
widget = form.get_widget('button%s' % self.id)
widget.backoffice_info_text = self.backoffice_info_text
widget.prevent_jump_on_submit = True
widget.action_id = self.id
def submit_form(self, form, formdata, user, evo):
if self.method != 'interactive':

View File

@ -301,6 +301,7 @@ class FormWorkflowStatusItem(WorkflowStatusItem):
self.formdef.add_fields_to_form(form, displayed_fields=displayed_fields)
if 'submit' not in form._names and not self.hide_submit_button:
form.add_submit('submit', _('Submit'))
form.get_widget('submit').action_id = self.id
# put varname in a form attribute so it can be used in templates to
# identify the form.

View File

@ -197,6 +197,9 @@ class JumpWorkflowStatusItem(WorkflowStatusJumpItem):
# override parent method to avoid mentioning the condition twice.
return '%s (%s)' % (self.description, self.get_line_details())
def render_as_short_line(self):
return self.description
def get_line_details(self):
if not self.status:
return _('not completed')

View File

@ -68,6 +68,7 @@ class ResubmitWorkflowStatusItem(WorkflowStatusItem):
form.add(SingleSelectWidget, 'resubmit', title=_('Form'), required=True, options=list_forms)
form.add_submit('button%s' % self.id, label, attrs={'class': 'resubmit'})
form.get_widget('button%s' % self.id).backoffice_info_text = self.backoffice_info_text
form.get_widget('button%s' % self.id).action_id = self.id
def submit_form(self, form, formdata, user, evo):
if form.get_submit() != 'button%s' % self.id:

View File

@ -72,7 +72,7 @@ class WorkflowTrace(sql.WorkflowTrace):
'workflow-created-formdata': _('Created form'),
'workflow-created-carddata': _('Created card'),
'workflow-edited-carddata': _('Edited card'),
'workflow-form-submit': _('Action in workflow form'),
'workflow-form-submit': _('Action in workflow form'), # legacy
}.get(self.event, self.event)
def is_global_event(self):
@ -148,7 +148,7 @@ class WorkflowTrace(sql.WorkflowTrace):
status = workflow.get_status(status_id)
return status.get_admin_url()
def get_real_action(self, workflow, status_id, action_id, global_event):
def get_real_action(self, workflow, status_id, action_id, global_event=None):
if global_event:
if not global_event.event_args:
return None
@ -205,6 +205,28 @@ class WorkflowTrace(sql.WorkflowTrace):
self.event_args.get('trigger_id'),
self.get_event_label(),
)
elif self.event_args and self.event_args.get('action_item_id'):
try:
url = '%sitems/%s/' % (
self.get_base_url(formdata.formdef.workflow, self.status_id),
self.event_args.get('action_item_id'),
)
except KeyError:
url = '#missing-%s' % self.event_args['action_item_id']
label = self.get_event_label()
real_action = self.get_real_action(
formdata.formdef.workflow,
self.status_id,
self.event_args['action_item_id'],
)
if real_action and hasattr(real_action, 'render_as_short_line'):
label += ' - %s' % real_action.render_as_short_line()
elif real_action and hasattr(real_action, 'render_as_line'):
label += ' - %s' % real_action.render_as_line()
event_item += htmltext('<span class="event"><a href="%s">%s</a></span>') % (
url,
label,
)
elif self.event == 'workflow-edited-carddata':
# it would usually have external_formdef_id/external_formdata_id and be handled
# earlier; this matches the case when no targetted card could be found.

View File

@ -36,7 +36,7 @@ from quixote.html import TemplateIO, htmlescape, htmltext
import wcs.qommon.storage as st
from wcs.qommon.storage import StorableObject, atomic_write
from wcs.sql_criterias import Contains, Null, StatusReachedTimeoutCriteria, StrictNotEqual
from wcs.sql_criterias import Contains, LessOrEqual, Null, StatusReachedTimeoutCriteria, StrictNotEqual
from .carddef import CardDef
from .categories import WorkflowCategory
@ -201,6 +201,10 @@ class RedisplayFormException(Exception):
form.add_global_errors([error])
class ReplayException(Exception):
pass
def get_role_dependencies(roles):
for role_id in roles or []:
if not role_id:
@ -1730,6 +1734,9 @@ class WorkflowGlobalActionManualTrigger(WorkflowGlobalActionTrigger):
parts.append(_('not assigned'))
return ', '.join([str(x) for x in parts])
def render_as_short_line(self):
return _('Manual')
def get_inspect_view(self):
r = TemplateIO(html=True)
r += htmlescape(self.render_as_line())
@ -1883,6 +1890,9 @@ class WorkflowGlobalActionTimeoutTrigger(WorkflowGlobalActionTrigger):
else:
return _('Automatic (not configured)')
def render_as_short_line(self):
return _('Automatic')
def get_inspect_view(self):
r = TemplateIO(html=True)
r += htmlescape(self.render_as_line())
@ -2126,7 +2136,7 @@ class WorkflowGlobalActionTimeoutTrigger(WorkflowGlobalActionTrigger):
for action, trigger in triggers
if 'form_var' not in str(trigger.timeout)
and (
(trigger.anchor == 'finalized')
(trigger.anchor in ('finalized', 'creation'))
or (trigger.anchor in '1st-arrival' and trigger.anchor_status_first)
or (trigger.anchor in 'latest-arrival' and trigger.anchor_status_latest)
)
@ -2154,14 +2164,19 @@ class WorkflowGlobalActionTimeoutTrigger(WorkflowGlobalActionTrigger):
criterias = [StrictNotEqual('status', 'draft'), Null('anonymised')]
# now we need the criteria for our timeout
trigger_timeout = formdef_timeouts[f'{formdef.xml_root_node}/{formdef.id}']
# limit to forms/cards with appropriate status in their history
if trigger.anchor == 'finalized':
status_ids = endpoint_status_ids
elif trigger.anchor == '1st-arrival':
status_ids = [trigger.anchor_status_first]
elif trigger.anchor == 'latest-arrival':
status_ids = [trigger.anchor_status_latest]
criterias.append(StatusReachedTimeoutCriteria(data_class, status_ids, trigger_timeout))
if trigger.anchor == 'creation':
# limit to forms/cards that are old enough
min_date = localtime() - datetime.timedelta(days=int(trigger_timeout))
criterias.append(LessOrEqual('receipt_time', min_date))
else:
# limit to forms/cards with appropriate status in their history
if trigger.anchor == 'finalized':
status_ids = endpoint_status_ids
elif trigger.anchor == '1st-arrival':
status_ids = [trigger.anchor_status_first]
elif trigger.anchor == 'latest-arrival':
status_ids = [trigger.anchor_status_latest]
criterias.append(StatusReachedTimeoutCriteria(data_class, status_ids, trigger_timeout))
cls.run_trigger_check(
formdef,
triggers=[(action, trigger)],
@ -2258,6 +2273,9 @@ class WorkflowGlobalActionWebserviceTrigger(WorkflowGlobalActionManualTrigger):
else:
return _('External call (not configured)')
def render_as_short_line(self):
return _('External call')
def get_inspect_view(self):
r = TemplateIO(html=True)
r += htmlescape(self.render_as_line())
@ -2349,6 +2367,7 @@ class SerieOfActionsMixin:
def get_action_form(self, filled, user, displayed_fields=None):
form = Form(enctype='multipart/form-data', use_tokens=False)
form.attrs['id'] = 'wf-actions'
form.add_hidden('_ts', str(time.mktime(filled.last_update_time)))
for item in self.items:
if not item.check_auth(filled, user):
continue
@ -2394,6 +2413,8 @@ class SerieOfActionsMixin:
return messages
def handle_form(self, form, filled, user, evo):
if form.get('_ts') != str(time.mktime(filled.last_update_time)):
raise ReplayException()
evo.time = time.localtime()
evo.set_user(formdata=filled, user=user, check_submitter=get_request().is_in_frontoffice())
if not filled.evolution:
@ -2671,6 +2692,12 @@ class WorkflowStatus(SerieOfActionsMixin):
filled.record_workflow_event('global-action-button', global_action_id=action.id)
return filled.perform_global_action(action.id, user)
button = form.get_widget(form.get_submit()) # get clicked button
if hasattr(button, 'action_id'):
# some actions won't have a button name (e.g. a click on a "add block row" button),
# and some actual buttons won't have an action_id ("editable" action).
filled.record_workflow_event('button', action_item_id=button.action_id)
evo = Evolution(filled)
url = super().handle_form(form, filled, user, evo)
if isinstance(url, str):
@ -2683,7 +2710,6 @@ class WorkflowStatus(SerieOfActionsMixin):
if evo.status:
filled.status = evo.status
filled.store()
filled.record_workflow_event('workflow-form-submit')
return filled.perform_workflow()
def get_subdirectories(self, formdata):
@ -3000,6 +3026,14 @@ class WorkflowStatusItem(XmlSerialisable):
label += ' (%s)' % _('conditional')
return label
def render_as_short_line(self):
label = self.description
if hasattr(self, 'get_line_short_details'):
details = self.get_line_short_details()
if details:
label += ' %s' % details
return label
def get_line_details(self):
return ''