Compare commits
23 Commits
2b26cc3c67
...
851a1d751d
Author | SHA1 | Date |
---|---|---|
Emmanuel Cazenave | 851a1d751d | |
Emmanuel Cazenave | 4ef907d7f8 | |
Emmanuel Cazenave | 71ba4dbce0 | |
Frédéric Péters | b91a619512 | |
Frédéric Péters | bb7ca2500b | |
Frédéric Péters | 8cb0225292 | |
Frédéric Péters | 193a37a902 | |
Frédéric Péters | 510f68c505 | |
Lauréline Guérin | c28b82745d | |
Pierre Ducroquet | 43538c6920 | |
Frédéric Péters | 44fbe88ea9 | |
Frédéric Péters | d9c5a34cf0 | |
Frédéric Péters | e2d8aecc1c | |
Frédéric Péters | c39839a6a6 | |
Frédéric Péters | 108b249965 | |
Frédéric Péters | fc0baf9389 | |
Frédéric Péters | 4abadd3558 | |
Valentin Deniaud | f3d2056be5 | |
Valentin Deniaud | 9101bd1e69 | |
Valentin Deniaud | 54e78443a6 | |
Valentin Deniaud | b63bc74a4d | |
Frédéric Péters | 501697e26a | |
Frédéric Péters | 9877859d25 |
|
@ -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')
|
||||
|
|
|
@ -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',
|
||||
]
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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',
|
||||
]
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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'})]
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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',
|
||||
]
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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'):
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 = ''
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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 d’erreur"
|
||||
|
||||
#: 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 l’ordre des déclencheurs"
|
|||
msgid "New global action trigger"
|
||||
msgstr "Nouveau déclencheur d’action 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 l’identifiant %(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 l’action 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 à l’accueil"
|
||||
|
||||
#: templates/wcs/formdata_steps.html
|
||||
msgid "Steps"
|
||||
msgstr "Étapes"
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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':
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 ''
|
||||
|
||||
|
|
Loading…
Reference in New Issue