Compare commits
193 Commits
dbcbca44e4
...
21ee97c706
Author | SHA1 | Date |
---|---|---|
Frédéric Péters | 21ee97c706 | |
Frédéric Péters | 4be6ff7f9f | |
Valentin Deniaud | 2927168c83 | |
Valentin Deniaud | d8cb5e4737 | |
Valentin Deniaud | a2a4b1536b | |
Frédéric Péters | 0fcf440e0a | |
Valentin Deniaud | 22f8886b9d | |
Valentin Deniaud | d6771b2869 | |
Frédéric Péters | c191656d4a | |
Frédéric Péters | 4e0b3469f1 | |
Frédéric Péters | b6c83cca37 | |
Frédéric Péters | aa070498e4 | |
Frédéric Péters | 0dc5d3267f | |
Frédéric Péters | 61ce06676d | |
Frédéric Péters | 291fea5b8b | |
Frédéric Péters | 62d178f73c | |
Frédéric Péters | b9b6912385 | |
Frédéric Péters | 992bc9720a | |
Lauréline Guérin | d43865b2a5 | |
serghei | 63880dddd4 | |
Frédéric Péters | 30a7476ac1 | |
Frédéric Péters | 5460fedfba | |
Serghei Mihai | 12bdb4a498 | |
Serghei Mihai | ddbe8f65de | |
Frédéric Péters | d1a52fa4a5 | |
Frédéric Péters | ae2cc0cfe9 | |
Frédéric Péters | c9d6bb9f15 | |
Frédéric Péters | 2590ea3b7e | |
Frédéric Péters | cd12d4ea1b | |
Valentin Deniaud | 5c2928af03 | |
Valentin Deniaud | 03879b5e04 | |
Valentin Deniaud | 03e05bd537 | |
Valentin Deniaud | fa60aba429 | |
Valentin Deniaud | 8d1c683d7f | |
Valentin Deniaud | 3f359c3b59 | |
Valentin Deniaud | c883b48e28 | |
Frédéric Péters | f5419a2fa7 | |
Valentin Deniaud | c7c870f11d | |
Valentin Deniaud | 2a5106d4d7 | |
Valentin Deniaud | 60971c2c99 | |
Valentin Deniaud | ea73acbce9 | |
Frédéric Péters | 1a384effa4 | |
Frédéric Péters | 81373a2af9 | |
Frédéric Péters | c82031b4d0 | |
Frédéric Péters | 3cd6f61a3c | |
Frédéric Péters | 8bc1001676 | |
Valentin Deniaud | 3ec516e0d0 | |
Frédéric Péters | 8f39a1a94a | |
Frédéric Péters | ccc87a959c | |
Frédéric Péters | f969f302af | |
Frédéric Péters | 8d40fba739 | |
Frédéric Péters | 8e0bae99f4 | |
Frédéric Péters | c79300ac0c | |
Frédéric Péters | b924ee5744 | |
Frédéric Péters | 9f59c1277d | |
Frédéric Péters | f7ec9ad128 | |
Frédéric Péters | e905fd8f2c | |
Frédéric Péters | 5caf453d0d | |
Frédéric Péters | 3629de518c | |
Frédéric Péters | 1429af460b | |
Frédéric Péters | 3584897812 | |
Frédéric Péters | ed8a60e98f | |
Frédéric Péters | 965ed7a48c | |
Frédéric Péters | 6913b19ebe | |
Frédéric Péters | aa46c007c3 | |
Frédéric Péters | 61b938e08b | |
Frédéric Péters | 4869b1badb | |
Frédéric Péters | 86301756f1 | |
Frédéric Péters | 459d4f5598 | |
Frédéric Péters | 636c464259 | |
Yann Weber | eb862fc7f2 | |
Frédéric Péters | caaa2ccd7b | |
Frédéric Péters | 881a0424ce | |
Frédéric Péters | c8e926afb2 | |
Frédéric Péters | 43a38f860b | |
Frédéric Péters | 0218b41a3f | |
Frédéric Péters | 8b568965bb | |
Frédéric Péters | 091bc6e05a | |
Valentin Deniaud | 53f1a6e3ae | |
Valentin Deniaud | 976017b31d | |
Valentin Deniaud | e7e2ed825b | |
Valentin Deniaud | 310fb84f0f | |
Valentin Deniaud | ec19de0756 | |
Valentin Deniaud | 077c54a09f | |
Valentin Deniaud | dd5e34097d | |
Valentin Deniaud | bd33723448 | |
Frédéric Péters | 7a7dd7bf5f | |
Lauréline Guérin | 94292ccad2 | |
Valentin Deniaud | 5538527b95 | |
Lauréline Guérin | cb1974bdf1 | |
Emmanuel Cazenave | 9ff89e41da | |
Lauréline Guérin | e8cd2aa824 | |
Lauréline Guérin | ff5299b79b | |
Lauréline Guérin | 39fed220a5 | |
Lauréline Guérin | 6bce31c255 | |
Thomas NOËL | 120e643490 | |
Lauréline Guérin | 112727460e | |
Valentin Deniaud | 38373f6862 | |
Lauréline Guérin | bf32ad0b56 | |
Frédéric Péters | ca2fe34b14 | |
Frédéric Péters | b42d0ae6b0 | |
Frédéric Péters | 55897c68b3 | |
Frédéric Péters | 26ca816b59 | |
Frédéric Péters | 17af831882 | |
Frédéric Péters | 575fe5a4fa | |
Lauréline Guérin | c81031052b | |
Frédéric Péters | df99864ada | |
Frédéric Péters | 7a29133b1d | |
Frédéric Péters | ecf811b0c2 | |
Valentin Deniaud | 6a26c0ef91 | |
Valentin Deniaud | 462228fd23 | |
Valentin Deniaud | 27a0a87bf8 | |
Valentin Deniaud | cc62ff430c | |
Frédéric Péters | 63d0dec57f | |
Frédéric Péters | 9c08789abf | |
Frédéric Péters | 4e269e532f | |
Paul Marillonnet | f4cef2dcd7 | |
Paul Marillonnet | 378758e0c5 | |
Valentin Deniaud | d6ff746d8b | |
Valentin Deniaud | 36a1e4de91 | |
Valentin Deniaud | c560de4ed5 | |
Valentin Deniaud | 745be4a1b4 | |
Valentin Deniaud | b5e58a310a | |
Valentin Deniaud | a75c6a458f | |
Valentin Deniaud | bebb1ce78c | |
Valentin Deniaud | be98943e62 | |
Frédéric Péters | a4d4307d6f | |
Frédéric Péters | 1c2314f9a7 | |
Frédéric Péters | 8a888864bd | |
Frédéric Péters | 3224d8b919 | |
Frédéric Péters | e24c7110eb | |
Frédéric Péters | 1466457170 | |
Frédéric Péters | bdd17296b4 | |
Frédéric Péters | 031e72c38a | |
Frédéric Péters | 73aae2d0c6 | |
Frédéric Péters | 3b4617e887 | |
Frédéric Péters | 781e4e4c52 | |
Frédéric Péters | 5ec12c0c0e | |
Frédéric Péters | d6ecc7194e | |
Frédéric Péters | f6f217f2e5 | |
Frédéric Péters | 27e54042ff | |
Frédéric Péters | d0426014db | |
Emmanuel Cazenave | 418787f078 | |
Frédéric Péters | 6b28012fec | |
Frédéric Péters | dba47ed1ba | |
Frédéric Péters | b76f3df2c2 | |
Frédéric Péters | 8b6d9d658e | |
Frédéric Péters | 51ccebebc0 | |
Frédéric Péters | 0973014218 | |
Frédéric Péters | 723945d2d2 | |
Frédéric Péters | 81f2abeab2 | |
Frédéric Péters | 64a8dbdfc5 | |
Frédéric Péters | 6e53e339cd | |
Frédéric Péters | 990dde7060 | |
Frédéric Péters | ee6d557f6e | |
Frédéric Péters | 6d4f720219 | |
Frédéric Péters | 770f2dbae2 | |
Frédéric Péters | 6f6859098a | |
Frédéric Péters | 8985a905ae | |
Frédéric Péters | dc21f05960 | |
Frédéric Péters | c5c8c0fe9d | |
Frédéric Péters | 6ab4be07ac | |
Frédéric Péters | e3fc9c1dd8 | |
Frédéric Péters | 63e5c01c47 | |
Frédéric Péters | d931f93684 | |
Frédéric Péters | 66ca6a5298 | |
Valentin Deniaud | dc473b7378 | |
Frédéric Péters | d0358afa40 | |
Frédéric Péters | 4d5b309986 | |
Lauréline Guérin | e0857ce653 | |
Frédéric Péters | 8c3374e790 | |
Frédéric Péters | 03435d40a6 | |
Frédéric Péters | 70b7087ad9 | |
Valentin Deniaud | 9afbbccb13 | |
Corentin Sechet | 0c225cf254 | |
Valentin Deniaud | a23457fdbf | |
Frédéric Péters | 9c12c01712 | |
Thomas NOËL | 89b4d350ab | |
Valentin Deniaud | 721bdc4e44 | |
Thomas NOËL | 955f012b3d | |
Valentin Deniaud | 0ed9d5d0a0 | |
Valentin Deniaud | 6fd4b87ff5 | |
Valentin Deniaud | d4c3e7dc4e | |
Valentin Deniaud | 7199e84903 | |
Valentin Deniaud | e76e33808b | |
Valentin Deniaud | 0d82f03e59 | |
Valentin Deniaud | 03669bb847 | |
Frédéric Péters | c0d2d36b3c | |
Frédéric Péters | bdb24e21e9 | |
Frédéric Péters | 3a4b8c9cc7 | |
Frédéric Péters | 083f3cf3dd | |
Frédéric Péters | 520e52d1a7 | |
Frédéric Péters | 69249df789 |
|
@ -23,10 +23,12 @@ Depends: graphviz,
|
|||
python3-django-ratelimit,
|
||||
python3-dnspython,
|
||||
python3-emoji,
|
||||
python3-freezegun,
|
||||
python3-hobo,
|
||||
python3-lasso,
|
||||
python3-lxml,
|
||||
python3-pil,
|
||||
python3-psutil,
|
||||
python3-psycopg2,
|
||||
python3-pyproj,
|
||||
python3-quixote,
|
||||
|
|
|
@ -386,6 +386,7 @@ Une API existe pour récupérer le schéma de données d’un modèle de fiches.
|
|||
"disabled_redirection" : null,
|
||||
"discussion" : false,
|
||||
"drafts_lifespan" : null,
|
||||
"drafts_max_per_user" : null,
|
||||
"enable_tracking_codes" : false,
|
||||
"expiration_date" : null,
|
||||
"fields" : [
|
||||
|
|
2
setup.py
2
setup.py
|
@ -204,6 +204,8 @@ setup(
|
|||
'setproctitle',
|
||||
'phonenumbers',
|
||||
'emoji',
|
||||
'psutil',
|
||||
'freezegun',
|
||||
],
|
||||
package_dir={'wcs': 'wcs'},
|
||||
packages=find_packages(),
|
||||
|
|
|
@ -31,6 +31,7 @@ def create_superuser(pub):
|
|||
|
||||
user1 = pub.user_class(name='admin')
|
||||
user1.is_admin = True
|
||||
user1.email = 'admin@example.com'
|
||||
user1.store()
|
||||
|
||||
account1 = PasswordAccount(id='admin')
|
||||
|
|
|
@ -387,6 +387,13 @@ def test_block_use_in_formdef(pub):
|
|||
resp = resp.form.submit('submit')
|
||||
assert resp.pyquery('#form_error_max_items').text() == 'required field'
|
||||
|
||||
# check there's no crash if block is missing
|
||||
block.remove_self()
|
||||
resp = app.get(formdef.get_admin_url() + 'fields/')
|
||||
assert resp.pyquery('#fields-list .type-block .type').text() == 'Block of fields (foobar, missing)'
|
||||
resp = resp.click('Edit', href='%s/' % formdef.fields[0].id)
|
||||
assert resp.pyquery('.field-edit--subtitle').text() == 'Block of fields (foobar, missing)'
|
||||
|
||||
|
||||
def test_block_use_in_workflow_backoffice_fields(pub):
|
||||
create_superuser(pub)
|
||||
|
@ -495,7 +502,7 @@ def test_removed_block_in_form_fields_list(pub):
|
|||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/forms/%s/fields/' % formdef.id)
|
||||
assert 'Field Block (removed, missing)' in resp.text
|
||||
assert 'Block of fields (removed, missing)' in resp.text
|
||||
|
||||
|
||||
def test_block_edit_field_warnings(pub):
|
||||
|
@ -642,6 +649,8 @@ def test_block_field_statistics_data_update(pub):
|
|||
|
||||
def test_block_test_results(pub):
|
||||
create_superuser(pub)
|
||||
TestDef.wipe()
|
||||
TestResult.wipe()
|
||||
BlockDef.wipe()
|
||||
block = BlockDef()
|
||||
block.name = 'foobar'
|
||||
|
@ -676,3 +685,37 @@ def test_block_test_results(pub):
|
|||
resp.form['varname'] = 'test_3'
|
||||
resp = resp.form.submit('submit').follow()
|
||||
assert TestResult.count() == 1
|
||||
|
||||
|
||||
def test_block_documentation(pub):
|
||||
create_superuser(pub)
|
||||
|
||||
BlockDef.wipe()
|
||||
blockdef = FormDef()
|
||||
blockdef.name = 'block title'
|
||||
blockdef.fields = [fields.BoolField(id='1', label='Bool')]
|
||||
blockdef.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
|
||||
resp = app.get(blockdef.get_admin_url())
|
||||
assert resp.pyquery('.documentation[hidden]')
|
||||
resp = app.post_json(blockdef.get_admin_url() + 'update-documentation', {'content': '<p>doc</p>'})
|
||||
assert resp.json == {'err': 0, 'empty': False, 'changed': True}
|
||||
blockdef.refresh_from_storage()
|
||||
assert blockdef.documentation == '<p>doc</p>'
|
||||
resp = app.get(blockdef.get_admin_url())
|
||||
assert resp.pyquery('.documentation:not([hidden])')
|
||||
|
||||
resp = app.get(blockdef.get_admin_url() + 'fields/1/')
|
||||
assert resp.pyquery('.documentation[hidden]')
|
||||
assert resp.pyquery('#sidebar[hidden]')
|
||||
resp = app.post_json(
|
||||
blockdef.get_admin_url() + 'fields/1/update-documentation', {'content': '<p>doc</p>'}
|
||||
)
|
||||
assert resp.json == {'err': 0, 'empty': False, 'changed': True}
|
||||
blockdef.refresh_from_storage()
|
||||
assert blockdef.fields[0].documentation == '<p>doc</p>'
|
||||
resp = app.get(blockdef.get_admin_url() + 'fields/1/')
|
||||
assert resp.pyquery('.documentation:not([hidden])')
|
||||
assert resp.pyquery('#sidebar:not([hidden])')
|
||||
|
|
|
@ -437,6 +437,10 @@ def test_card_id_template(pub):
|
|||
resp = resp.click('Templates')
|
||||
assert 'id_template' not in resp.text
|
||||
|
||||
# check a severe warning is displayed on field removal
|
||||
resp = app.get(carddef.fields[0].get_admin_url() + 'delete')
|
||||
assert 'This field may be used in the card custom identifiers' in resp.pyquery('.errornotice').text()
|
||||
|
||||
|
||||
def test_card_digest_template(pub):
|
||||
create_superuser(pub)
|
||||
|
@ -1118,3 +1122,93 @@ def test_cards_last_test_result(pub):
|
|||
|
||||
resp = resp.click('Last tests run')
|
||||
assert 'Result #%s' % test_result.id in resp.text
|
||||
|
||||
|
||||
def test_cards_management_options(pub):
|
||||
create_superuser(pub)
|
||||
|
||||
CardDef.wipe()
|
||||
carddef = CardDef()
|
||||
carddef.name = 'foo'
|
||||
carddef.fields = [
|
||||
fields.StringField(id='1', label='Test', varname='test'),
|
||||
]
|
||||
carddef.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/cards/1/')
|
||||
|
||||
# Misc management
|
||||
assert_option_display(resp, 'Management', 'Default')
|
||||
resp = resp.click('Management', href='options/management')
|
||||
assert resp.forms[0]['management_sidebar_items$elementgeneral'].checked is True
|
||||
assert resp.forms[0]['management_sidebar_items$elementdownload-files'].checked is False
|
||||
resp.forms[0]['management_sidebar_items$elementdownload-files'].checked = True
|
||||
resp = resp.forms[0].submit().follow()
|
||||
assert_option_display(resp, 'Management', 'Custom')
|
||||
assert 'general' in CardDef.get(1).management_sidebar_items
|
||||
assert 'download-files' in CardDef.get(1).management_sidebar_items
|
||||
|
||||
resp = resp.click('Management', href='options/management')
|
||||
resp.forms[0]['management_sidebar_items$elementgeneral'].checked = False
|
||||
resp = resp.forms[0].submit().follow()
|
||||
assert 'general' not in CardDef.get(1).management_sidebar_items
|
||||
|
||||
resp = resp.click('Management', href='options/management')
|
||||
resp.forms[0]['management_sidebar_items$elementgeneral'].checked = True
|
||||
resp.forms[0]['management_sidebar_items$elementdownload-files'].checked = False
|
||||
assert 'management_sidebar_items$elementuser' not in resp.forms[0].fields
|
||||
resp = resp.forms[0].submit().follow()
|
||||
assert CardDef.get(1).management_sidebar_items == {'__default__'}
|
||||
|
||||
carddef.user_support = 'optional'
|
||||
carddef.store()
|
||||
|
||||
resp = resp.click('Management', href='options/management')
|
||||
assert resp.forms[0]['management_sidebar_items$elementuser'].checked is True
|
||||
resp = resp.forms[0].submit().follow()
|
||||
assert CardDef.get(1).management_sidebar_items == {'__default__'}
|
||||
|
||||
assert_option_display(resp, 'Management', 'Default')
|
||||
resp = resp.click('Management', href='options/management')
|
||||
assert resp.form['history_pane_default_mode'].value == 'collapsed'
|
||||
resp = resp.form.submit().follow()
|
||||
assert_option_display(resp, 'Templates', 'Default')
|
||||
resp = resp.click('Management', href='options/management')
|
||||
resp.form['history_pane_default_mode'].value = 'expanded'
|
||||
resp = resp.form.submit().follow()
|
||||
assert_option_display(resp, 'Templates', 'Custom')
|
||||
resp = resp.click('Management', href='options/management')
|
||||
assert resp.form['history_pane_default_mode'].value == 'expanded'
|
||||
|
||||
|
||||
def test_card_documentation(pub):
|
||||
create_superuser(pub)
|
||||
|
||||
CardDef.wipe()
|
||||
carddef = FormDef()
|
||||
carddef.name = 'card title'
|
||||
carddef.fields = [fields.BoolField(id='1', label='Bool')]
|
||||
carddef.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
|
||||
resp = app.get(carddef.get_admin_url())
|
||||
assert resp.pyquery('.documentation[hidden]')
|
||||
resp = app.post_json(carddef.get_admin_url() + 'update-documentation', {'content': '<p>doc</p>'})
|
||||
assert resp.json == {'err': 0, 'empty': False, 'changed': True}
|
||||
carddef.refresh_from_storage()
|
||||
assert carddef.documentation == '<p>doc</p>'
|
||||
resp = app.get(carddef.get_admin_url())
|
||||
assert resp.pyquery('.documentation:not([hidden])')
|
||||
|
||||
resp = app.get(carddef.get_admin_url() + 'fields/1/')
|
||||
assert resp.pyquery('.documentation[hidden]')
|
||||
assert resp.pyquery('#sidebar[hidden]')
|
||||
resp = app.post_json(carddef.get_admin_url() + 'fields/1/update-documentation', {'content': '<p>doc</p>'})
|
||||
assert resp.json == {'err': 0, 'empty': False, 'changed': True}
|
||||
carddef.refresh_from_storage()
|
||||
assert carddef.fields[0].documentation == '<p>doc</p>'
|
||||
resp = app.get(carddef.get_admin_url() + 'fields/1/')
|
||||
assert resp.pyquery('.documentation:not([hidden])')
|
||||
assert resp.pyquery('#sidebar:not([hidden])')
|
||||
|
|
|
@ -195,7 +195,6 @@ def test_data_sources_new(pub):
|
|||
resp = app.get('/backoffice/settings/data-sources/')
|
||||
resp = resp.click('New Data Source')
|
||||
resp.forms[0]['name'] = 'a new data source'
|
||||
resp.forms[0]['description'] = 'description of the data source'
|
||||
|
||||
resp.forms[0]['data_source$type'] = 'python'
|
||||
resp.forms[0]['data_source$value'] = repr([{'id': '1', 'text': 'un'}, {'id': '2', 'text': 'deux'}])
|
||||
|
@ -209,13 +208,11 @@ def test_data_sources_new(pub):
|
|||
assert 'Edit Data Source' in resp.text
|
||||
|
||||
assert NamedDataSource.get(1).name == 'a new data source'
|
||||
assert NamedDataSource.get(1).description == 'description of the data source'
|
||||
|
||||
# add a second one
|
||||
resp = app.get('/backoffice/settings/data-sources/')
|
||||
resp = resp.click('New Data Source')
|
||||
resp.forms[0]['name'] = 'an other data source'
|
||||
resp.forms[0]['description'] = 'description of the data source'
|
||||
resp.forms[0]['data_source$type'] = 'python'
|
||||
resp = resp.forms[0].submit('data_source$apply')
|
||||
resp.forms[0]['data_source$value'] = repr([{'id': '1', 'text': 'un'}, {'id': '2', 'text': 'deux'}])
|
||||
|
@ -408,7 +405,6 @@ def test_data_sources_category(pub):
|
|||
resp = app.get('/backoffice/settings/data-sources/categories/')
|
||||
resp = resp.click('New Category')
|
||||
resp.form['name'] = 'a new category'
|
||||
resp.form['description'] = 'description of the category'
|
||||
resp = resp.form.submit('submit')
|
||||
assert DataSourceCategory.count() == 1
|
||||
category = DataSourceCategory.select()[0]
|
||||
|
@ -440,7 +436,6 @@ def test_data_sources_category(pub):
|
|||
resp = app.get('/backoffice/settings/data-sources/categories/')
|
||||
resp = resp.click('New Category')
|
||||
resp.form['name'] = 'a second category'
|
||||
resp.form['description'] = 'description of the category'
|
||||
resp = resp.form.submit('submit')
|
||||
assert DataSourceCategory.count() == 2
|
||||
category2 = [x for x in DataSourceCategory.select() if x.id != category.id][0]
|
||||
|
@ -872,13 +867,10 @@ def test_data_sources_edit(pub):
|
|||
|
||||
resp = resp.click(href='edit')
|
||||
assert resp.forms[0]['name'].value == 'foobar'
|
||||
resp.forms[0]['description'] = 'data source description'
|
||||
resp = resp.forms[0].submit('submit')
|
||||
assert resp.location == 'http://example.net/backoffice/settings/data-sources/1/'
|
||||
resp = resp.follow()
|
||||
|
||||
assert NamedDataSource.get(1).description == 'data source description'
|
||||
|
||||
resp = app.get('/backoffice/settings/data-sources/1/edit')
|
||||
assert '>Data Attribute</label>' in resp.text
|
||||
assert '>Id Attribute</label>' in resp.text
|
||||
|
@ -1146,3 +1138,22 @@ def test_data_sources_agenda_refresh(mock_collect, pub, chrono_url):
|
|||
resp = resp.follow()
|
||||
assert 'Agendas will be updated in the background.' in resp.text
|
||||
assert NamedDataSource.count() == 2
|
||||
|
||||
|
||||
def test_datasource_documentation(pub):
|
||||
create_superuser(pub)
|
||||
|
||||
NamedDataSource.wipe()
|
||||
data_source = NamedDataSource(name='foobar')
|
||||
data_source.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
|
||||
resp = app.get(data_source.get_admin_url())
|
||||
assert resp.pyquery('.documentation[hidden]')
|
||||
resp = app.post_json(data_source.get_admin_url() + 'update-documentation', {'content': '<p>doc</p>'})
|
||||
assert resp.json == {'err': 0, 'empty': False, 'changed': True}
|
||||
data_source.refresh_from_storage()
|
||||
assert data_source.documentation == '<p>doc</p>'
|
||||
resp = app.get(data_source.get_admin_url())
|
||||
assert resp.pyquery('.documentation:not([hidden])')
|
||||
|
|
|
@ -2,6 +2,7 @@ import io
|
|||
import json
|
||||
import os
|
||||
import zipfile
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
from quixote.http_request import Upload as QuixoteUpload
|
||||
|
@ -23,7 +24,12 @@ from wcs.wf.geolocate import GeolocateWorkflowStatusItem
|
|||
from wcs.wf.jump import JumpWorkflowStatusItem
|
||||
from wcs.wf.notification import SendNotificationWorkflowStatusItem
|
||||
from wcs.wf.redirect_to_url import RedirectToUrlWorkflowStatusItem
|
||||
from wcs.workflows import Workflow, WorkflowBackofficeFieldsFormDef, WorkflowImportError
|
||||
from wcs.workflows import (
|
||||
Workflow,
|
||||
WorkflowBackofficeFieldsFormDef,
|
||||
WorkflowImportError,
|
||||
WorkflowVariablesFieldsFormDef,
|
||||
)
|
||||
from wcs.wscalls import NamedWsCall, NamedWsCallImportError
|
||||
|
||||
from ..utilities import clean_temporary_pub, create_temporary_pub, get_app, login
|
||||
|
@ -102,6 +108,10 @@ def test_deprecations(pub):
|
|||
workflow.backoffice_fields_formdef.fields = [
|
||||
fields.TableField(id='bo1', label='table field'),
|
||||
]
|
||||
workflow.variables_formdef = WorkflowVariablesFieldsFormDef(workflow)
|
||||
workflow.variables_formdef.fields = [
|
||||
fields.TableField(id='wfvar1', label='other table field'),
|
||||
]
|
||||
st0 = workflow.add_status('Status0', 'st0')
|
||||
|
||||
display = st0.add_action('displaymsg')
|
||||
|
@ -266,6 +276,7 @@ def test_deprecations(pub):
|
|||
assert [x.text for x in resp.pyquery('.section--fields li a')] == [
|
||||
'foobar / Field "table field"',
|
||||
'foobar / Field "ranked field"',
|
||||
'Options of workflow "test" / Field "other table field"',
|
||||
'Backoffice fields of workflow "test" / Field "table field"',
|
||||
]
|
||||
assert [x.text for x in resp.pyquery('.section--actions li a')] == [
|
||||
|
@ -607,7 +618,7 @@ def test_deprecations_on_import(pub):
|
|||
job.check_deprecated_elements_in_object(formdef)
|
||||
assert str(excinfo.value) == 'Python expression detected'
|
||||
with pytest.raises(FormdefImportError) as excinfo:
|
||||
FormDef.import_from_xml_tree(formdef_xml)
|
||||
FormDef.import_from_xml_tree(formdef_xml, check_deprecated=True)
|
||||
assert str(excinfo.value) == 'Python expression detected'
|
||||
|
||||
job = DeprecationsScan()
|
||||
|
@ -615,7 +626,7 @@ def test_deprecations_on_import(pub):
|
|||
job.check_deprecated_elements_in_object(blockdef)
|
||||
assert str(excinfo.value) == 'Python expression detected'
|
||||
with pytest.raises(BlockdefImportError) as excinfo:
|
||||
BlockDef.import_from_xml_tree(blockdef_xml)
|
||||
BlockDef.import_from_xml_tree(blockdef_xml, check_deprecated=True)
|
||||
assert str(excinfo.value) == 'Python expression detected'
|
||||
|
||||
job = DeprecationsScan()
|
||||
|
@ -623,7 +634,7 @@ def test_deprecations_on_import(pub):
|
|||
job.check_deprecated_elements_in_object(workflow)
|
||||
assert str(excinfo.value) == 'Python expression detected'
|
||||
with pytest.raises(WorkflowImportError) as excinfo:
|
||||
Workflow.import_from_xml_tree(workflow_xml)
|
||||
Workflow.import_from_xml_tree(workflow_xml, check_deprecated=True)
|
||||
assert str(excinfo.value) == 'Python expression detected'
|
||||
|
||||
job = DeprecationsScan()
|
||||
|
@ -631,7 +642,7 @@ def test_deprecations_on_import(pub):
|
|||
job.check_deprecated_elements_in_object(data_source)
|
||||
assert str(excinfo.value) == 'Python expression detected'
|
||||
with pytest.raises(NamedDataSourceImportError) as excinfo:
|
||||
NamedDataSource.import_from_xml_tree(data_source_xml)
|
||||
NamedDataSource.import_from_xml_tree(data_source_xml, check_deprecated=True)
|
||||
assert str(excinfo.value) == 'Python expression detected'
|
||||
|
||||
job = DeprecationsScan()
|
||||
|
@ -639,10 +650,17 @@ def test_deprecations_on_import(pub):
|
|||
job.check_deprecated_elements_in_object(wscall)
|
||||
assert str(excinfo.value) == 'Python expression detected'
|
||||
with pytest.raises(NamedWsCallImportError) as excinfo:
|
||||
NamedWsCall.import_from_xml_tree(wscall_xml)
|
||||
NamedWsCall.import_from_xml_tree(wscall_xml, check_deprecated=True)
|
||||
assert str(excinfo.value) == 'Python expression detected'
|
||||
|
||||
# no python expressions
|
||||
job = DeprecationsScan()
|
||||
job.check_deprecated_elements_in_object(mail_template)
|
||||
MailTemplate.import_from_xml_tree(mail_template_xml)
|
||||
|
||||
# check that DeprecationsScan is not run on object load
|
||||
with mock.patch(
|
||||
'wcs.backoffice.deprecations.DeprecationsScan.check_deprecated_elements_in_object'
|
||||
) as check:
|
||||
NamedDataSource.get(data_source.id)
|
||||
assert check.call_args_list == []
|
||||
|
|
|
@ -8,6 +8,7 @@ import xml.etree.ElementTree as ET
|
|||
|
||||
import pytest
|
||||
import responses
|
||||
from django.utils.timezone import localtime
|
||||
from pyquery import PyQuery
|
||||
from webtest import Upload
|
||||
|
||||
|
@ -263,6 +264,14 @@ def test_forms_edit_management(pub, formdef):
|
|||
resp = resp.forms[0].submit().follow()
|
||||
assert FormDef.get(1).management_sidebar_items == {'__default__'}
|
||||
|
||||
# unselect all
|
||||
resp = resp.click('Management', href='options/management')
|
||||
for field in resp.forms[0].fields:
|
||||
if field.startswith('management_sidebar_items$'):
|
||||
resp.forms[0][field].checked = False
|
||||
resp = resp.forms[0].submit().follow()
|
||||
assert FormDef.get(1).management_sidebar_items == set()
|
||||
|
||||
|
||||
def test_forms_edit_tracking_code(pub, formdef):
|
||||
create_superuser(pub)
|
||||
|
@ -284,6 +293,7 @@ def test_forms_edit_tracking_code(pub, formdef):
|
|||
|
||||
resp = resp.click('Form Tracking')
|
||||
assert resp.forms[0]['drafts_lifespan'].value == ''
|
||||
assert resp.forms[0]['drafts_max_per_user'].value == ''
|
||||
resp = resp.forms[0].submit().follow() # check empty value is ok
|
||||
|
||||
resp = resp.click('Form Tracking')
|
||||
|
@ -297,6 +307,20 @@ def test_forms_edit_tracking_code(pub, formdef):
|
|||
resp = resp.forms[0].submit().follow()
|
||||
assert FormDef.get(1).drafts_lifespan == '5'
|
||||
|
||||
resp = resp.click('Form Tracking')
|
||||
resp.forms[0]['drafts_max_per_user'].value = 'xxx'
|
||||
resp = resp.forms[0].submit()
|
||||
assert 'Maximum must be between 2 and 100 drafts.' in resp
|
||||
resp.forms[0]['drafts_max_per_user'].value = '120'
|
||||
resp = resp.forms[0].submit()
|
||||
assert 'Maximum must be between 2 and 100 drafts.' in resp
|
||||
resp.forms[0]['drafts_max_per_user'].value = '1'
|
||||
resp = resp.forms[0].submit()
|
||||
assert 'Maximum must be between 2 and 100 drafts.' in resp
|
||||
resp.forms[0]['drafts_max_per_user'].value = '3'
|
||||
resp = resp.forms[0].submit().follow()
|
||||
assert FormDef.get(1).drafts_max_per_user == '3'
|
||||
|
||||
formdef.fields = [
|
||||
fields.StringField(id='1', label='VerifyString'),
|
||||
fields.DateField(id='2', label='VerifyDate'),
|
||||
|
@ -1118,12 +1142,6 @@ def test_form_workflow_options(pub):
|
|||
resp = app.get('/backoffice/forms/1/')
|
||||
assert '"workflow-options"' not in resp.text
|
||||
|
||||
pub.site_options.set('options', 'enable-workflow-variable-parameter', 'true')
|
||||
with open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w') as fd:
|
||||
pub.site_options.write(fd)
|
||||
resp = app.get('/backoffice/forms/1/')
|
||||
assert '"workflow-options"' in resp.text
|
||||
|
||||
|
||||
def test_form_workflow_variables(pub):
|
||||
create_superuser(pub)
|
||||
|
@ -1964,7 +1982,7 @@ def test_form_preview_map_field(pub):
|
|||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/forms/1/')
|
||||
assert 'qommon.map.js' in resp.text
|
||||
assert resp.pyquery('#map-f1')
|
||||
assert resp.pyquery('#form_f1.qommon-map')
|
||||
|
||||
|
||||
def test_form_preview_do_not_log_error(pub):
|
||||
|
@ -3676,6 +3694,41 @@ def test_form_edit_field_warnings(pub):
|
|||
assert not resp.pyquery('aside .errornotice')
|
||||
assert resp.pyquery('aside form[action=new]')
|
||||
|
||||
BlockDef.wipe()
|
||||
block = BlockDef()
|
||||
block.name = 'foobar'
|
||||
block.fields = [
|
||||
fields.StringField(id='123', required=True, label='Test'),
|
||||
fields.StringField(id='234', required=True, label='Test2'),
|
||||
fields.CommentField(id='345', label='comment'),
|
||||
]
|
||||
block.store()
|
||||
|
||||
FormDef.wipe()
|
||||
formdef = FormDef()
|
||||
formdef.name = 'form title'
|
||||
formdef.fields = [
|
||||
fields.StringField(id='1', label='Test'),
|
||||
fields.BlockField(id='2', label='Block field', block_slug='foobar'),
|
||||
]
|
||||
formdef.store()
|
||||
resp = app.get('/backoffice/forms/%s/fields/' % formdef.id)
|
||||
assert not resp.pyquery('.warningnotice')
|
||||
formdef.fields[1].default_items_count = 1100
|
||||
formdef.store()
|
||||
resp = app.get('/backoffice/forms/%s/fields/' % formdef.id)
|
||||
assert (
|
||||
resp.pyquery('.warningnotice')
|
||||
.text()
|
||||
.startswith('There are at least 2201 data fields, including fields in blocks.')
|
||||
)
|
||||
|
||||
# no crash if default_items_count is none
|
||||
formdef.fields[1].default_items_count = None
|
||||
formdef.store()
|
||||
resp = app.get('/backoffice/forms/%s/fields/' % formdef.id)
|
||||
assert not resp.pyquery('.warningnotice')
|
||||
|
||||
FormDef.wipe()
|
||||
|
||||
|
||||
|
@ -4801,6 +4854,135 @@ def test_admin_form_inspect_validation(pub):
|
|||
assert not resp.pyquery('[data-field-id="4"] .parameter-validation').length
|
||||
|
||||
|
||||
def test_admin_form_inspect_drafts(pub):
|
||||
create_superuser(pub)
|
||||
FormDef.wipe()
|
||||
formdef = FormDef()
|
||||
formdef.name = 'form title'
|
||||
formdef.enable_tracking_codes = True
|
||||
formdef.fields = [
|
||||
fields.PageField(id='0', label='1st page'),
|
||||
fields.StringField(id='1', label='string 1'),
|
||||
fields.PageField(id='2', label='2nd page'),
|
||||
fields.StringField(id='3', label='string 2'),
|
||||
fields.PageField(id='4', label='3rd page'),
|
||||
fields.StringField(id='5', label='string 3'),
|
||||
]
|
||||
formdef.store()
|
||||
formdef.data_class().wipe()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/forms/%s/inspect' % formdef.id)
|
||||
assert resp.pyquery('#inspect-drafts p').text() == 'There are currently no drafts for this form.'
|
||||
|
||||
data_class = formdef.data_class()
|
||||
for page_id in ('0', '2', '4', '_confirmation_page', 'xxxx'):
|
||||
formdata = data_class()
|
||||
formdata.status = 'draft'
|
||||
formdata.page_id = page_id
|
||||
formdata.receipt_time = localtime()
|
||||
formdata.store()
|
||||
|
||||
# create a non-draft
|
||||
formdata = formdef.data_class()()
|
||||
formdata.just_created()
|
||||
formdata.store()
|
||||
|
||||
# create a non-draft but before draft duration
|
||||
formdata = formdef.data_class()()
|
||||
formdata.just_created()
|
||||
formdata.receipt_time = localtime() - datetime.timedelta(days=200)
|
||||
formdata.store()
|
||||
|
||||
resp = app.get('/backoffice/forms/%s/inspect' % formdef.id)
|
||||
assert resp.pyquery('#inspect-drafts h2').text() == 'Key indicators on existing drafts'
|
||||
assert resp.pyquery('#inspect-drafts .infonotice').text() == 'Covered period: last 100 days.'
|
||||
|
||||
assert resp.pyquery('table[data-table-id="rate-among-drafts"] tr[data-page-id="0"]').length == 1
|
||||
assert (
|
||||
resp.pyquery('table[data-table-id="rate-among-drafts"] tr[data-page-id="0"] td.label').text()
|
||||
== '1st page'
|
||||
)
|
||||
assert (
|
||||
resp.pyquery('table[data-table-id="rate-among-drafts"] tr[data-page-id="0"] td.percent').text()
|
||||
== '20%'
|
||||
)
|
||||
assert (
|
||||
resp.pyquery('table[data-table-id="rate-among-drafts"] tr[data-page-id="0"] td.total').text()
|
||||
== '(1/5)'
|
||||
)
|
||||
|
||||
assert resp.pyquery('table[data-table-id="rate-among-drafts"] tr[data-page-id="2"]').length == 1
|
||||
assert (
|
||||
resp.pyquery('table[data-table-id="rate-among-drafts"] tr[data-page-id="2"] td.label').text()
|
||||
== '2nd page'
|
||||
)
|
||||
assert (
|
||||
resp.pyquery('table[data-table-id="rate-among-drafts"] tr[data-page-id="2"] td.percent').text()
|
||||
== '20%'
|
||||
)
|
||||
assert (
|
||||
resp.pyquery('table[data-table-id="rate-among-drafts"] tr[data-page-id="2"] td.total').text()
|
||||
== '(1/5)'
|
||||
)
|
||||
|
||||
assert resp.pyquery('table[data-table-id="rate-among-drafts"] tr[data-page-id="4"]').length == 1
|
||||
assert (
|
||||
resp.pyquery('table[data-table-id="rate-among-drafts"] tr[data-page-id="4"] td.label').text()
|
||||
== '3rd page'
|
||||
)
|
||||
assert (
|
||||
resp.pyquery('table[data-table-id="rate-among-drafts"] tr[data-page-id="4"] td.percent').text()
|
||||
== '20%'
|
||||
)
|
||||
assert (
|
||||
resp.pyquery('table[data-table-id="rate-among-drafts"] tr[data-page-id="4"] td.total').text()
|
||||
== '(1/5)'
|
||||
)
|
||||
|
||||
assert (
|
||||
resp.pyquery('table[data-table-id="rate-among-drafts"] tr[data-page-id="_confirmation_page"]').length
|
||||
== 1
|
||||
)
|
||||
assert (
|
||||
resp.pyquery(
|
||||
'table[data-table-id="rate-among-drafts"] tr[data-page-id="_confirmation_page"] td.label'
|
||||
).text()
|
||||
== 'Confirmation page'
|
||||
)
|
||||
assert (
|
||||
resp.pyquery(
|
||||
'table[data-table-id="rate-among-drafts"] tr[data-page-id="_confirmation_page"] td.percent'
|
||||
).text()
|
||||
== '20%'
|
||||
)
|
||||
assert (
|
||||
resp.pyquery(
|
||||
'table[data-table-id="rate-among-drafts"] tr[data-page-id="_confirmation_page"] td.total'
|
||||
).text()
|
||||
== '(1/5)'
|
||||
)
|
||||
|
||||
assert resp.pyquery('table[data-table-id="rate-among-drafts"] tr[data-page-id="_unknown"]').length == 1
|
||||
assert (
|
||||
resp.pyquery('table[data-table-id="rate-among-drafts"] tr[data-page-id="_unknown"] td.label').text()
|
||||
== 'Unknown'
|
||||
)
|
||||
assert (
|
||||
resp.pyquery('table[data-table-id="rate-among-drafts"] tr[data-page-id="_unknown"] td.percent').text()
|
||||
== '20%'
|
||||
)
|
||||
assert (
|
||||
resp.pyquery('table[data-table-id="rate-among-drafts"] tr[data-page-id="_unknown"] td.total').text()
|
||||
== '(1/5)'
|
||||
)
|
||||
|
||||
# check completion rate
|
||||
assert resp.pyquery('.completion-rate .percent').text() == '16.7%'
|
||||
assert resp.pyquery('.completion-rate .total').text() == '(1/6)'
|
||||
assert 'width: 16.6' in resp.pyquery('.completion-rate .bar span').attr.style
|
||||
|
||||
|
||||
def test_form_import_fields(pub):
|
||||
create_superuser(pub)
|
||||
create_role(pub)
|
||||
|
@ -4976,3 +5158,56 @@ def test_forms_last_test_result(pub, formdef):
|
|||
TestDef.remove_object(testdef.id)
|
||||
resp = app.get('/backoffice/forms/1/')
|
||||
assert 'Last tests run' not in resp.text
|
||||
|
||||
|
||||
def test_admin_form_sql_integrity_error(pub):
|
||||
create_superuser(pub)
|
||||
|
||||
FormDef.wipe()
|
||||
formdef = FormDef()
|
||||
formdef.name = 'form title'
|
||||
formdef.fields = [fields.BoolField(id='1', label='Bool')]
|
||||
formdef.store()
|
||||
|
||||
formdef.fields = [fields.StringField(id='1', label='String')]
|
||||
formdef.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get(formdef.get_admin_url())
|
||||
assert (
|
||||
resp.pyquery('.errornotice summary').text()
|
||||
== 'There are integrity errors in the database column types.'
|
||||
)
|
||||
assert resp.pyquery('.errornotice li').text() == 'String, expected: character varying, got: boolean.'
|
||||
|
||||
|
||||
def test_form_documentation(pub):
|
||||
create_superuser(pub)
|
||||
|
||||
FormDef.wipe()
|
||||
formdef = FormDef()
|
||||
formdef.name = 'form title'
|
||||
formdef.fields = [fields.BoolField(id='1', label='Bool')]
|
||||
formdef.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
|
||||
resp = app.get(formdef.get_admin_url())
|
||||
assert resp.pyquery('.documentation[hidden]')
|
||||
resp = app.post_json(formdef.get_admin_url() + 'update-documentation', {'content': '<p>doc</p>'})
|
||||
assert resp.json == {'err': 0, 'empty': False, 'changed': True}
|
||||
formdef.refresh_from_storage()
|
||||
assert formdef.documentation == '<p>doc</p>'
|
||||
resp = app.get(formdef.get_admin_url())
|
||||
assert resp.pyquery('.documentation:not([hidden])')
|
||||
|
||||
resp = app.get(formdef.get_admin_url() + 'fields/1/')
|
||||
assert resp.pyquery('.documentation[hidden]')
|
||||
assert resp.pyquery('#sidebar[hidden]')
|
||||
resp = app.post_json(formdef.get_admin_url() + 'fields/1/update-documentation', {'content': '<p>doc</p>'})
|
||||
assert resp.json == {'err': 0, 'empty': False, 'changed': True}
|
||||
formdef.refresh_from_storage()
|
||||
assert formdef.fields[0].documentation == '<p>doc</p>'
|
||||
resp = app.get(formdef.get_admin_url() + 'fields/1/')
|
||||
assert resp.pyquery('.documentation:not([hidden])')
|
||||
assert resp.pyquery('#sidebar:not([hidden])')
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import datetime
|
||||
import decimal
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
@ -7,11 +8,13 @@ from django.utils.timezone import make_aware
|
|||
from webtest import Upload
|
||||
|
||||
from wcs import fields, workflow_tests
|
||||
from wcs.admin.settings import UserFieldsFormDef
|
||||
from wcs.blocks import BlockDef
|
||||
from wcs.carddef import CardDef
|
||||
from wcs.formdef import FormDef
|
||||
from wcs.qommon.http_request import HTTPRequest
|
||||
from wcs.qommon.upload_storage import PicklableUpload
|
||||
from wcs.sql_criterias import NotNull
|
||||
from wcs.testdef import TestDef, TestResult, WebserviceResponse
|
||||
from wcs.workflow_tests import WorkflowTests
|
||||
from wcs.workflows import Workflow, WorkflowBackofficeFieldsFormDef
|
||||
|
@ -30,6 +33,8 @@ def pub():
|
|||
pub.cfg['identification'] = {'methods': ['password']}
|
||||
pub.write_cfg()
|
||||
|
||||
pub.user_class.wipe()
|
||||
BlockDef.wipe()
|
||||
FormDef.wipe()
|
||||
TestDef.wipe()
|
||||
TestResult.wipe()
|
||||
|
@ -43,7 +48,7 @@ def teardown_module(module):
|
|||
|
||||
|
||||
def test_tests_page(pub):
|
||||
user = create_superuser(pub)
|
||||
create_superuser(pub)
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test title'
|
||||
|
@ -61,8 +66,12 @@ def test_tests_page(pub):
|
|||
resp.form['name'] = 'First test'
|
||||
resp = resp.form.submit()
|
||||
|
||||
users = pub.user_class.select([NotNull('test_uuid')])
|
||||
assert len(users) == 1
|
||||
test_user = users[0]
|
||||
|
||||
testdef = TestDef.select()[0]
|
||||
assert testdef.agent_id == str(user.id)
|
||||
assert testdef.agent_id == test_user.test_uuid
|
||||
|
||||
resp = resp.follow()
|
||||
assert 'Edit test data' in resp.text
|
||||
|
@ -92,6 +101,9 @@ def test_tests_page(pub):
|
|||
resp = resp.click('Second test')
|
||||
assert 'This test is empty' in resp.text
|
||||
|
||||
resp = resp.click('History')
|
||||
assert 'Creation (empty)' in resp.text
|
||||
|
||||
# test run with empty test is allowed
|
||||
app.get('/backoffice/forms/1/tests/results/run').follow()
|
||||
|
||||
|
@ -145,9 +157,13 @@ def test_tests_page_creation_from_formdata(pub):
|
|||
assert 'First test' in resp.text
|
||||
assert 'abcdefg' in resp.text
|
||||
|
||||
users = pub.user_class.select([NotNull('test_uuid')])
|
||||
assert len(users) == 1
|
||||
test_user = users[0]
|
||||
|
||||
testdef = TestDef.select()[0]
|
||||
assert testdef.data['user']['id'] == 1
|
||||
assert testdef.agent_id == str(user.id)
|
||||
assert testdef.user_uuid == test_user.test_uuid
|
||||
assert testdef.agent_id == test_user.test_uuid
|
||||
assert not testdef.is_in_backoffice
|
||||
|
||||
formdata = formdef.data_class()()
|
||||
|
@ -166,7 +182,7 @@ def test_tests_page_creation_from_formdata(pub):
|
|||
assert 'hijklmn' in resp.text
|
||||
|
||||
testdef = TestDef.select()[1]
|
||||
assert testdef.data['user'] is None
|
||||
assert not testdef.user_uuid
|
||||
assert testdef.is_in_backoffice
|
||||
|
||||
|
||||
|
@ -363,6 +379,129 @@ def test_tests_status_page_image_field(pub):
|
|||
resp.follow(status=404)
|
||||
|
||||
|
||||
def test_tests_history_page(pub):
|
||||
user = create_superuser(pub)
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test title'
|
||||
formdef.fields = [fields.StringField(id='1', varname='test_field', label='Test Field')]
|
||||
formdef.store()
|
||||
|
||||
formdata = formdef.data_class()()
|
||||
formdata.just_created()
|
||||
formdata.receipt_time = make_aware(datetime.datetime(2021, 1, 1, 0, 0))
|
||||
formdata.data['1'] = 'This is a test'
|
||||
formdata.user_id = user.id
|
||||
formdata.store()
|
||||
|
||||
testdef = TestDef.create_from_formdata(formdef, formdata)
|
||||
testdef.name = 'Test 1'
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.ButtonClick(id='1', button_name='xxx'),
|
||||
]
|
||||
# create one snapshot
|
||||
testdef.store()
|
||||
|
||||
response = WebserviceResponse()
|
||||
response.testdef_id = testdef.id
|
||||
response.name = 'Fake response'
|
||||
response.url = 'http://example.com/json'
|
||||
response.payload = '{"foo": "bar"}'
|
||||
response.store()
|
||||
|
||||
# create second snapshot
|
||||
testdef.name = 'Test 2'
|
||||
testdef.store()
|
||||
|
||||
# create third snapshot
|
||||
testdef.name = 'Test 3'
|
||||
testdef.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
|
||||
resp = app.get('/backoffice/forms/1/tests/%s/' % testdef.id)
|
||||
resp = resp.click('History')
|
||||
assert [x.attrib['class'] for x in resp.pyquery.find('.snapshots-list tr')] == [
|
||||
'new-day',
|
||||
'collapsed',
|
||||
'collapsed',
|
||||
]
|
||||
|
||||
# export snapshot
|
||||
resp_export = resp.click('Export', index=1)
|
||||
assert resp_export.content_type == 'application/x-wcs-snapshot'
|
||||
assert '>Test 2<' in resp_export.text
|
||||
|
||||
# view snapshot
|
||||
view_resp = resp.click('View', index=1)
|
||||
assert '<h2>Test 2</h2>' in view_resp.text
|
||||
assert 'Options' not in resp.text
|
||||
assert 'Delete' not in resp.text
|
||||
assert 'Edit' not in resp.text
|
||||
|
||||
resp = view_resp.click('Workflow tests')
|
||||
assert 'Simulate click on action button' in resp.text
|
||||
assert 'Add' not in resp.text
|
||||
assert 'Delete' not in resp.text
|
||||
assert 'Duplicate' not in resp.text
|
||||
|
||||
resp = resp.click('Edit')
|
||||
assert '>Submit<' not in resp.text
|
||||
|
||||
resp = view_resp.click('Webservice responses')
|
||||
assert 'New' not in resp.text
|
||||
assert 'Remove' not in resp.text
|
||||
assert 'Duplicate' not in resp.text
|
||||
|
||||
resp = resp.click('Fake response')
|
||||
assert 'Edit webservice response' in resp.text
|
||||
assert '>Submit<' not in resp.text
|
||||
|
||||
resp = view_resp.click('Inspect')
|
||||
|
||||
assert 'form_var_test_field' in resp.text
|
||||
|
||||
resp.form['django-condition'] = 'form_var_test_field == "This is a test"'
|
||||
resp = resp.form.submit()
|
||||
assert 'Condition result' in resp.text
|
||||
assert 'result-true' in resp.text
|
||||
|
||||
# restore as new
|
||||
assert TestDef.count() == 1
|
||||
assert WorkflowTests.count() == 1
|
||||
assert WebserviceResponse.count() == 1
|
||||
|
||||
resp = view_resp.click('Restore version')
|
||||
resp.form['action'] = 'as-new'
|
||||
resp = resp.form.submit('submit').follow()
|
||||
|
||||
assert TestDef.count() == 2
|
||||
assert WorkflowTests.count() == 2
|
||||
assert WebserviceResponse.count() == 2
|
||||
assert '<h2>Test 2</h2>' in resp.text
|
||||
|
||||
# restore as current
|
||||
resp = view_resp.click('Restore version')
|
||||
resp.form['action'] = 'overwrite'
|
||||
resp = resp.form.submit('submit').follow()
|
||||
|
||||
assert TestDef.count() == 2
|
||||
assert WorkflowTests.count() == 2
|
||||
assert WebserviceResponse.count() == 2
|
||||
assert '<h2>Test 2</h2>' in resp.text
|
||||
|
||||
# restore first version as current, making sure webservice response is deleted
|
||||
resp = resp.click('History')
|
||||
resp = resp.click('Restore', index=2)
|
||||
resp.form['action'] = 'overwrite'
|
||||
resp = resp.form.submit('submit').follow()
|
||||
|
||||
assert TestDef.count() == 2
|
||||
assert WorkflowTests.count() == 2
|
||||
assert WebserviceResponse.count() == 1
|
||||
assert '<h2>Test 1</h2>' in resp.text
|
||||
|
||||
|
||||
def test_tests_edit(pub):
|
||||
create_superuser(pub)
|
||||
user = pub.user_class(name='test user')
|
||||
|
@ -370,6 +509,7 @@ def test_tests_edit(pub):
|
|||
user.store()
|
||||
new_user = pub.user_class(name='new user')
|
||||
new_user.email = 'new@example.com'
|
||||
new_user.test_uuid = '42'
|
||||
new_user.store()
|
||||
|
||||
formdef = FormDef()
|
||||
|
@ -395,7 +535,7 @@ def test_tests_edit(pub):
|
|||
|
||||
resp = resp.click('Options')
|
||||
resp.form['name'] = 'Second test'
|
||||
resp.form['user'] = new_user.id
|
||||
resp.form['user'] = new_user.test_uuid
|
||||
resp = resp.form.submit('submit').follow()
|
||||
assert 'Second test' in resp.text
|
||||
assert 'new user' in resp.text
|
||||
|
@ -407,7 +547,7 @@ def test_tests_edit(pub):
|
|||
assert 'new user' not in resp.text
|
||||
|
||||
resp = resp.click('Options')
|
||||
resp.form['user'] = new_user.id
|
||||
resp.form['user'] = new_user.test_uuid
|
||||
resp = resp.form.submit('submit').follow()
|
||||
assert 'Second test' in resp.text
|
||||
assert 'new user' in resp.text
|
||||
|
@ -702,6 +842,13 @@ def test_tests_edit_data_live_url(formdef_class, pub):
|
|||
required=True,
|
||||
condition={'type': 'django', 'value': 'form_var_foo == "ok"'},
|
||||
),
|
||||
fields.StringField(
|
||||
id='3',
|
||||
label='Condi 2',
|
||||
varname='bar2',
|
||||
required=True,
|
||||
condition={'type': 'django', 'value': 'form_var_foo and is_in_backoffice'},
|
||||
),
|
||||
]
|
||||
formdef.store()
|
||||
formdef.data_class().wipe()
|
||||
|
@ -717,10 +864,41 @@ def test_tests_edit_data_live_url(formdef_class, pub):
|
|||
live_url = resp.html.find('form').attrs['data-live-url']
|
||||
live_resp = app.post(live_url, params=resp.form.submit_fields())
|
||||
assert live_resp.json['result']['2']['visible'] is True
|
||||
assert live_resp.json['result']['3']['visible'] is False
|
||||
|
||||
resp = resp.click('Switch to backoffice mode.').follow()
|
||||
resp.form['f1'] = 'nok'
|
||||
live_resp = app.post(live_url, params=resp.form.submit_fields())
|
||||
assert live_resp.json['result']['2']['visible'] is False
|
||||
assert live_resp.json['result']['3']['visible'] is True
|
||||
|
||||
|
||||
def test_tests_edit_data_numeric_field(pub):
|
||||
create_superuser(pub)
|
||||
|
||||
block = BlockDef()
|
||||
block.name = 'foobar'
|
||||
block.fields = [fields.NumericField(id='1', label='Numeric', varname='foo')]
|
||||
block.store()
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test title'
|
||||
formdef.fields = [
|
||||
fields.BlockField(id='1', label='Block Data', varname='blockdata', block_slug='foobar', max_items=3),
|
||||
]
|
||||
formdef.store()
|
||||
|
||||
formdata = formdef.data_class()()
|
||||
formdata.data['1'] = {'data': [{'1': decimal.Decimal(42)}]}
|
||||
|
||||
testdef = TestDef.create_from_formdata(formdef, formdata)
|
||||
testdef.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get(testdef.get_admin_url() + 'edit-data/')
|
||||
|
||||
resp = resp.click('Switch to backoffice mode.').follow()
|
||||
resp = resp.form.submit('submit').follow()
|
||||
|
||||
|
||||
def test_tests_manual_run(pub):
|
||||
|
@ -1006,12 +1184,16 @@ def test_tests_result_error_field(pub):
|
|||
|
||||
|
||||
def test_tests_result_inspect(pub):
|
||||
user = create_superuser(pub)
|
||||
create_superuser(pub)
|
||||
|
||||
role = pub.role_class(name='test role')
|
||||
role.store()
|
||||
user.roles = [role.id]
|
||||
user.store()
|
||||
|
||||
test_user = pub.user_class(name='new user')
|
||||
test_user.email = 'new@example.com'
|
||||
test_user.test_uuid = '42'
|
||||
test_user.roles = [role.id]
|
||||
test_user.store()
|
||||
|
||||
workflow = Workflow(name='Workflow One')
|
||||
workflow.backoffice_fields_formdef = WorkflowBackofficeFieldsFormDef(workflow)
|
||||
|
@ -1049,7 +1231,7 @@ def test_tests_result_inspect(pub):
|
|||
|
||||
testdef = TestDef.create_from_formdata(formdef, formdata)
|
||||
testdef.name = 'First test'
|
||||
testdef.agent_id = user.id
|
||||
testdef.agent_id = test_user.test_uuid
|
||||
testdef.is_in_backoffice = True
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.ButtonClick(id='1', button_name='Loop on status'),
|
||||
|
@ -1167,6 +1349,11 @@ def test_tests_duplicate(pub):
|
|||
response.name = 'Response xxx'
|
||||
response.store()
|
||||
|
||||
testdef.workflow_tests.actions.append(
|
||||
workflow_tests.AssertWebserviceCall(id='3', webservice_response_uuid=response.uuid),
|
||||
)
|
||||
testdef.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
|
||||
assert TestDef.count() == 1
|
||||
|
@ -1196,6 +1383,8 @@ def test_tests_duplicate(pub):
|
|||
assert testdef2.workflow_tests.actions[0].button_name == 'Go to end status'
|
||||
assert testdef1.get_webservice_responses()[0].name == 'Changed'
|
||||
assert testdef2.get_webservice_responses()[0].name == 'Response xxx'
|
||||
assert testdef1.workflow_tests.actions[2].details_label == 'Changed'
|
||||
assert testdef2.workflow_tests.actions[2].details_label == 'Response xxx'
|
||||
|
||||
resp = app.get('/backoffice/forms/1/tests/%s/' % testdef.id)
|
||||
resp = resp.click('Duplicate')
|
||||
|
@ -1423,3 +1612,142 @@ def test_tests_webservice_response(pub):
|
|||
resp = resp.form.submit()
|
||||
|
||||
assert 'must start with http://' in resp.text
|
||||
|
||||
resp = app.get('/backoffice/forms/1/tests/%s/webservice-responses/' % testdef.id)
|
||||
resp = resp.click('Import from other test')
|
||||
resp = resp.form.submit()
|
||||
|
||||
assert resp.pyquery('div.error').text() == 'required field'
|
||||
|
||||
testdef2 = TestDef.create_from_formdata(formdef, formdata)
|
||||
testdef.name = 'Second test'
|
||||
testdef2.store()
|
||||
|
||||
resp = app.get('/backoffice/forms/1/tests/%s/webservice-responses/' % testdef2.id)
|
||||
assert 'Test response' not in resp.text
|
||||
|
||||
resp = resp.click('Import from other test')
|
||||
resp.form['testdef_id'] = testdef.id
|
||||
resp = resp.form.submit().follow()
|
||||
|
||||
assert 'Test response' in resp.text
|
||||
assert len(testdef.get_webservice_responses()) == 1
|
||||
assert len(testdef2.get_webservice_responses()) == 1
|
||||
|
||||
|
||||
def test_tests_test_users_management(pub):
|
||||
create_superuser(pub)
|
||||
|
||||
role = pub.role_class(name='test role')
|
||||
role.store()
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test title'
|
||||
formdef.store()
|
||||
|
||||
user_formdef = UserFieldsFormDef(pub)
|
||||
user_formdef.fields = [
|
||||
fields.StringField(id='1', label='first_name', varname='first_name'),
|
||||
fields.StringField(id='2', label='last_name', varname='last_name'),
|
||||
fields.StringField(id='3', label='email', varname='email'),
|
||||
]
|
||||
user_formdef.store()
|
||||
pub.cfg['users'][
|
||||
'fullname_template'
|
||||
] = '{{ user_var_first_name|default:"" }} {{ user_var_last_name|default:"" }}'
|
||||
pub.cfg['users']['field_email'] = '3'
|
||||
pub.write_cfg()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/forms/%s/tests/' % formdef.id)
|
||||
|
||||
resp = resp.click('Test users')
|
||||
assert 'There are no test users yet.' in resp.text
|
||||
|
||||
resp = resp.click('New')
|
||||
resp.form['name'] = 'User test'
|
||||
resp = resp.form.submit().follow()
|
||||
|
||||
assert 'There are no test users yet.' not in resp.text
|
||||
|
||||
resp = resp.click('User test')
|
||||
resp.form['roles$element0'] = role.id
|
||||
resp.form['f1'] = 'Jon'
|
||||
resp.form['f2'] = 'Doe'
|
||||
resp.form['f3'] = 'jon@example.com'
|
||||
resp = resp.form.submit('submit').follow()
|
||||
|
||||
user = pub.user_class.select([NotNull('test_uuid')])[0]
|
||||
assert user.name == 'User test'
|
||||
assert user.email == 'jon@example.com'
|
||||
assert user.roles == [role.id]
|
||||
assert user.form_data['1'] == 'Jon'
|
||||
assert user.form_data['2'] == 'Doe'
|
||||
|
||||
real_user = pub.user_class(name='new user')
|
||||
real_user.email = 'jane@example.com'
|
||||
real_user.roles = [role.id]
|
||||
real_user.form_data = {
|
||||
'1': 'Jane',
|
||||
'2': 'Doe',
|
||||
'3': 'jane@example.com',
|
||||
}
|
||||
real_user.store()
|
||||
|
||||
resp = resp.click('New')
|
||||
resp.form['name'] = 'User test 2'
|
||||
resp.form['creation_mode'] = 'copy'
|
||||
resp.form['user_id'].force_value(real_user.id)
|
||||
resp = resp.form.submit().follow()
|
||||
|
||||
user = pub.user_class.select([NotNull('test_uuid')], order_by='id')[1]
|
||||
assert user.name == 'User test 2'
|
||||
assert user.email == 'jane@example.com'
|
||||
assert user.roles == [role.id]
|
||||
assert user.form_data['1'] == 'Jane'
|
||||
assert user.form_data['2'] == 'Doe'
|
||||
|
||||
resp = resp.click('New')
|
||||
resp.form['name'] = 'User test 3'
|
||||
resp.form['creation_mode'] = 'copy'
|
||||
resp.form['user_id'].force_value(real_user.id)
|
||||
resp = resp.form.submit()
|
||||
|
||||
assert 'A test user with this email already exists.' in resp.text
|
||||
|
||||
resp = app.get('/backoffice/forms/test-users/')
|
||||
resp = resp.click('User test 2')
|
||||
resp.form['f3'] = 'jon@example.com'
|
||||
resp = resp.form.submit('submit')
|
||||
|
||||
assert 'A test user with this email already exists.' in resp.text
|
||||
|
||||
user_test_2_export_resp = resp.click('Export')
|
||||
|
||||
resp = app.get('/backoffice/forms/test-users/')
|
||||
resp = resp.click('Remove', href=str(user.id))
|
||||
resp = resp.form.submit().follow()
|
||||
|
||||
assert 'User test 2' not in resp.text
|
||||
|
||||
resp = resp.click('Import')
|
||||
resp.form['file'] = Upload('export.json', user_test_2_export_resp.body, 'application/json')
|
||||
resp = resp.form.submit().follow()
|
||||
|
||||
assert 'Test users have been successfully imported.' in resp.text
|
||||
assert 'User test 2' in resp.text
|
||||
|
||||
user = pub.user_class.select([NotNull('test_uuid')], order_by='id')[1]
|
||||
assert user.name == 'User test 2'
|
||||
assert user.email == 'jane@example.com'
|
||||
assert user.roles == [role.id]
|
||||
assert user.form_data['1'] == 'Jane'
|
||||
assert user.form_data['2'] == 'Doe'
|
||||
|
||||
global_export_resp = resp.click('Export')
|
||||
|
||||
resp = resp.click('Import')
|
||||
resp.form['file'] = Upload('export.json', global_export_resp.body, 'application/json')
|
||||
resp = resp.form.submit().follow()
|
||||
|
||||
assert 'Some already existing users were not imported.' in resp.text
|
||||
|
|
|
@ -8,7 +8,7 @@ import responses
|
|||
from pyquery import PyQuery
|
||||
from webtest import Upload
|
||||
|
||||
from wcs import fields
|
||||
from wcs import fields, workflow_tests
|
||||
from wcs.blocks import BlockDef
|
||||
from wcs.carddef import CardDef
|
||||
from wcs.categories import WorkflowCategory
|
||||
|
@ -17,6 +17,7 @@ from wcs.mail_templates import MailTemplate
|
|||
from wcs.qommon.afterjobs import AfterJob
|
||||
from wcs.qommon.errors import ConnectionError
|
||||
from wcs.qommon.http_request import HTTPRequest
|
||||
from wcs.testdef import TestDef, TestResult
|
||||
from wcs.wf.create_formdata import CreateFormdataWorkflowStatusItem, Mapping
|
||||
from wcs.wf.form import WorkflowFormFieldsFormDef
|
||||
from wcs.workflows import (
|
||||
|
@ -633,7 +634,10 @@ def test_workflows_delete_status_reassign(pub, name):
|
|||
resp = resp.follow()
|
||||
assert formdefs[-1].data_class().get(formdata2.id).status == 'wf-%s' % wf_bar.id
|
||||
|
||||
assert AfterJob.count() == 3 # status change + rebuild_security + tests
|
||||
if name in ('forms', 'cards'):
|
||||
assert AfterJob.count() == 3 # status change + rebuild_security + form or card tests
|
||||
else:
|
||||
assert AfterJob.count() == 4 # status change + rebuild_security + card tests + form tests
|
||||
resp = resp.click('Back')
|
||||
assert resp.request.path == f'/backoffice/workflows/{workflow.id}/'
|
||||
|
||||
|
@ -2103,8 +2107,7 @@ def test_workflows_variables_edit(pub):
|
|||
assert resp.location == 'http://example.net/backoffice/workflows/1/variables/fields/'
|
||||
resp = resp.follow()
|
||||
resp = resp.click('Edit', href='%s/' % workflow.variables_formdef.fields[0].id)
|
||||
assert resp.forms[0]['varname$name'].value == 'foobar'
|
||||
assert 'varname$select' not in resp.forms[0].fields
|
||||
assert resp.forms[0]['varname'].value == 'foobar'
|
||||
|
||||
baz_status = workflow.add_status(name='baz')
|
||||
baz_status.add_action('displaymsg')
|
||||
|
@ -2112,24 +2115,7 @@ def test_workflows_variables_edit(pub):
|
|||
|
||||
resp = app.get('/backoffice/workflows/1/variables/fields/')
|
||||
resp = resp.click('Edit', href='%s/' % workflow.variables_formdef.fields[0].id)
|
||||
assert 'varname$select' not in resp.forms[0].fields
|
||||
|
||||
pub.site_options.set('options', 'enable-workflow-variable-parameter', 'true')
|
||||
with open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w') as fd:
|
||||
pub.site_options.write(fd)
|
||||
resp = app.get('/backoffice/workflows/1/variables/fields/')
|
||||
resp = resp.click('Edit', href='%s/' % workflow.variables_formdef.fields[0].id)
|
||||
assert 'varname$select' in resp.forms[0].fields
|
||||
resp.forms[0]['varname$select'].value = '1*1*message'
|
||||
assert (
|
||||
resp.pyquery('[data-widget-name="default_value"]')[0].attrib['data-dynamic-display-child-of']
|
||||
== 'varname$select'
|
||||
)
|
||||
resp = resp.forms[0].submit('submit')
|
||||
|
||||
workflow = Workflow.get(1)
|
||||
assert workflow.variables_formdef.fields[0].key == 'string'
|
||||
assert workflow.variables_formdef.fields[0].varname == '1*1*message'
|
||||
assert 'varname' in resp.forms[0].fields
|
||||
|
||||
|
||||
def test_workflows_variables_default_value(pub):
|
||||
|
@ -2183,20 +2169,6 @@ def test_workflows_variables_edit_with_all_action_types(pub):
|
|||
assert resp.location == 'http://example.net/backoffice/workflows/1/variables/fields/'
|
||||
resp = resp.follow()
|
||||
|
||||
pub.site_options.set('options', 'enable-workflow-variable-parameter', 'true')
|
||||
with open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w') as fd:
|
||||
pub.site_options.write(fd)
|
||||
workflow = Workflow.get(1)
|
||||
resp = app.get('/backoffice/workflows/1/variables/fields/')
|
||||
resp = resp.click('Edit', href='%s/' % workflow.variables_formdef.fields[0].id)
|
||||
assert 'varname$select' in resp.forms[0].fields
|
||||
resp.forms[0]['varname$name'].value = 'xxx'
|
||||
resp = resp.forms[0].submit('submit')
|
||||
|
||||
workflow = Workflow.get(1)
|
||||
assert workflow.variables_formdef.fields[0].key == 'string'
|
||||
assert workflow.variables_formdef.fields[0].varname == 'xxx'
|
||||
|
||||
|
||||
def test_workflows_variables_delete(pub):
|
||||
create_superuser(pub)
|
||||
|
@ -2234,55 +2206,6 @@ def test_workflows_variables_with_export_to_model_action(pub):
|
|||
resp = resp.click('Edit', href='%s/' % workflow.variables_formdef.fields[0].id)
|
||||
|
||||
|
||||
def test_workflows_variables_replacement(pub):
|
||||
create_superuser(pub)
|
||||
pub.site_options.set('options', 'enable-workflow-variable-parameter', 'true')
|
||||
with open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w') as fd:
|
||||
pub.site_options.write(fd)
|
||||
|
||||
Workflow.wipe()
|
||||
workflow = Workflow(name='foo')
|
||||
baz_status = workflow.add_status(name='baz')
|
||||
baz_status.add_action('displaymsg', id='1')
|
||||
workflow.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/workflows/%s/variables/fields/' % workflow.id)
|
||||
|
||||
# add a field
|
||||
resp.forms[0]['label'] = 'foobar'
|
||||
resp.forms[0]['type'] = 'string'
|
||||
resp = resp.forms[0].submit().follow()
|
||||
workflow = Workflow.get(1)
|
||||
|
||||
# edit
|
||||
resp = resp.click('Edit', href='%s/' % workflow.variables_formdef.fields[0].id)
|
||||
resp.form['varname$select'].value = '1*1*message'
|
||||
resp = resp.form.submit('submit').follow()
|
||||
|
||||
# make sure a wrong variable name is not displayed
|
||||
assert 'form_option_1*1*message' not in resp.text
|
||||
assert Workflow.get(workflow.id).variables_formdef.fields[0].varname == '1*1*message'
|
||||
|
||||
# and make sure it doesn't appear in formdata inspect page
|
||||
formdef = FormDef()
|
||||
formdef.name = 'Form title'
|
||||
formdef.workflow = workflow
|
||||
formdef.fields = []
|
||||
formdef.store()
|
||||
|
||||
data_class = formdef.data_class()
|
||||
data_class.wipe()
|
||||
|
||||
formdata = data_class()
|
||||
formdata.data = {}
|
||||
formdata.status = 'wf-new'
|
||||
formdata.store()
|
||||
|
||||
resp = app.get(formdata.get_backoffice_url() + 'inspect')
|
||||
assert 'form_option_1*1*message' not in resp.text
|
||||
|
||||
|
||||
def test_workflows_backoffice_fields(pub):
|
||||
create_superuser(pub)
|
||||
|
||||
|
@ -2375,6 +2298,19 @@ def test_workflows_backoffice_fields(pub):
|
|||
)
|
||||
assert 'prefill$type' not in resp.form.fields.keys()
|
||||
|
||||
# check display_locations
|
||||
resp.form['display_locations$element0'] = False
|
||||
resp.form['display_locations$element1'] = False
|
||||
resp = resp.form.submit('submit')
|
||||
assert (
|
||||
resp.location
|
||||
== 'http://example.net/backoffice/workflows/1/backoffice-fields/fields/#fieldId_%s'
|
||||
% workflow.backoffice_fields_formdef.fields[1].id
|
||||
)
|
||||
resp = resp.follow()
|
||||
workflow = Workflow.get(workflow.id)
|
||||
assert workflow.backoffice_fields_formdef.fields[1].display_locations is None
|
||||
|
||||
# add a title field
|
||||
resp = app.get('/backoffice/workflows/1/backoffice-fields/fields/')
|
||||
resp.forms[0]['label'] = 'foobar3'
|
||||
|
@ -2413,7 +2349,7 @@ def test_workflows_backoffice_fields(pub):
|
|||
'foobar Text (line)',
|
||||
'foobar2 Text (line)',
|
||||
'foobar3 Title',
|
||||
'foobar4 Field Block (Test Block)',
|
||||
'foobar4 Block of fields (Test Block)',
|
||||
]
|
||||
|
||||
workflow.refresh_from_storage()
|
||||
|
@ -2842,10 +2778,14 @@ def test_workflows_global_actions_timeout_triggers(pub):
|
|||
resp = resp.click(
|
||||
href='triggers/%s/' % Workflow.get(workflow.id).global_actions[0].triggers[0].id, index=0
|
||||
)
|
||||
for invalid_value in ('foobar', '-'):
|
||||
for invalid_value in ('foobar', '-', '0123'):
|
||||
resp.form['timeout'] = invalid_value
|
||||
resp = resp.form.submit('submit')
|
||||
assert 'wrong format' in resp.text
|
||||
for invalid_value in ('833333335', '-833333335'):
|
||||
resp.form['timeout'] = invalid_value
|
||||
resp = resp.form.submit('submit')
|
||||
assert 'invalid value, out of bounds' in resp.text
|
||||
resp.form['timeout'] = ''
|
||||
resp = resp.form.submit('submit')
|
||||
assert 'required field' in resp.text
|
||||
|
@ -3322,11 +3262,11 @@ def test_workflows_create_formdata_fields_with_same_label(pub):
|
|||
('', False, '---'),
|
||||
('0', False, 'string1 - Text (line) (foo)'),
|
||||
('1', True, 'string1 - Text (line) (bar)'),
|
||||
('2', False, 'block1 - Field Block (Test Block) (foo2)'),
|
||||
('2', False, 'block1 - Block of fields (Test Block) (foo2)'),
|
||||
('2$123', False, 'block1 (foo2) - Test - Text (line)'),
|
||||
('3', False, 'block1 - Field Block (Test Block) (bar2)'),
|
||||
('3', False, 'block1 - Block of fields (Test Block) (bar2)'),
|
||||
('3$123', False, 'block1 (bar2) - Test - Text (line)'),
|
||||
('4', False, 'block2 - Field Block (Test Block)'),
|
||||
('4', False, 'block2 - Block of fields (Test Block)'),
|
||||
('4$123', False, 'block2 - Test - Text (line)'),
|
||||
]
|
||||
|
||||
|
@ -4434,3 +4374,170 @@ def test_workflows_function_and_role_with_same_name(pub):
|
|||
(str(role1.id), False, 'Foo'),
|
||||
(str(role2.id), False, 'Foobar [role]'), # same name as function -> role suffix
|
||||
]
|
||||
|
||||
|
||||
def test_workflow_test_results(pub):
|
||||
create_superuser(pub)
|
||||
TestDef.wipe()
|
||||
TestResult.wipe()
|
||||
|
||||
Workflow.wipe()
|
||||
workflow = Workflow(name='Workflow One')
|
||||
workflow.add_status(name='New status')
|
||||
workflow.store()
|
||||
|
||||
FormDef.wipe()
|
||||
formdef = FormDef()
|
||||
formdef.workflow_id = workflow.id
|
||||
formdef.name = 'test title'
|
||||
formdef.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/workflows/1/edit')
|
||||
resp.form['name'] = 'test'
|
||||
resp = resp.form.submit('submit').follow()
|
||||
assert TestResult.count() == 0
|
||||
|
||||
formdata = formdef.data_class()()
|
||||
formdata.just_created()
|
||||
|
||||
testdef = TestDef.create_from_formdata(formdef, formdata)
|
||||
testdef.store()
|
||||
|
||||
resp = app.get('/backoffice/workflows/1/edit')
|
||||
resp.form['name'] = 'test 2'
|
||||
resp = resp.form.submit('submit').follow()
|
||||
assert TestResult.count() == 0
|
||||
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.AssertStatus(id='1'),
|
||||
]
|
||||
testdef.store()
|
||||
|
||||
resp = app.get('/backoffice/workflows/1/edit')
|
||||
resp.form['name'] = 'test 3'
|
||||
resp = resp.form.submit('submit').follow()
|
||||
|
||||
assert TestResult.count() == 1
|
||||
result = TestResult.select()[0]
|
||||
assert result.reason == 'Change in workflow'
|
||||
|
||||
resp = resp.click('add status')
|
||||
resp.forms[0]['name'] = 'new status'
|
||||
resp = resp.forms[0].submit()
|
||||
|
||||
assert TestResult.count() == 2
|
||||
result = TestResult.select(order_by='id')[1]
|
||||
assert result.reason == 'Workflow: New status "new status"'
|
||||
|
||||
|
||||
def test_workflow_documentation(pub):
|
||||
create_superuser(pub)
|
||||
|
||||
Workflow.wipe()
|
||||
workflow = Workflow(name='Workflow One')
|
||||
status = workflow.add_status(name='New status')
|
||||
status.add_action('anonymise')
|
||||
workflow.backoffice_fields_formdef = WorkflowBackofficeFieldsFormDef(workflow)
|
||||
workflow.backoffice_fields_formdef.fields = [
|
||||
fields.StringField(id='bo234', label='bo field 1'),
|
||||
]
|
||||
workflow.variables_formdef = WorkflowVariablesFieldsFormDef(workflow=workflow)
|
||||
workflow.variables_formdef.fields = [
|
||||
fields.StringField(id='va123', label='bo field 1'),
|
||||
]
|
||||
global_action = workflow.add_global_action('action1')
|
||||
workflow.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get(workflow.get_admin_url())
|
||||
assert resp.pyquery('.documentation[hidden]')
|
||||
assert app.post_json(workflow.get_admin_url() + 'update-documentation', {}).json.get('err') == 1
|
||||
resp = app.post_json(workflow.get_admin_url() + 'update-documentation', {'content': ''})
|
||||
assert resp.json == {'err': 0, 'empty': True, 'changed': False}
|
||||
resp = app.post_json(workflow.get_admin_url() + 'update-documentation', {'content': '<p>doc</p>'})
|
||||
assert resp.json == {'err': 0, 'empty': False, 'changed': True}
|
||||
workflow.refresh_from_storage()
|
||||
assert workflow.documentation == '<p>doc</p>'
|
||||
|
||||
# check forbidden HTML is cleaned
|
||||
resp = app.post_json(
|
||||
workflow.get_admin_url() + 'update-documentation',
|
||||
{'content': '<p>iframe</p><iframe src="xx"></iframe>'},
|
||||
)
|
||||
assert resp.json == {'err': 0, 'empty': False, 'changed': True}
|
||||
workflow.refresh_from_storage()
|
||||
assert workflow.documentation == '<p>iframe</p>'
|
||||
|
||||
resp = app.get(workflow.get_admin_url())
|
||||
assert resp.pyquery('.documentation:not([hidden])')
|
||||
|
||||
resp = app.get(workflow.get_admin_url() + 'variables/fields/')
|
||||
assert resp.pyquery('.documentation[hidden]')
|
||||
resp = app.post_json(
|
||||
workflow.get_admin_url() + 'variables/fields/update-documentation', {'content': '<p>doc</p>'}
|
||||
)
|
||||
assert resp.json == {'err': 0, 'empty': False, 'changed': True}
|
||||
workflow.refresh_from_storage()
|
||||
assert workflow.variables_formdef.documentation == '<p>doc</p>'
|
||||
resp = app.get(workflow.get_admin_url() + 'variables/fields/')
|
||||
assert resp.pyquery('.documentation:not([hidden])')
|
||||
|
||||
resp = app.get(workflow.get_admin_url() + 'variables/fields/va123/')
|
||||
assert resp.pyquery('.documentation[hidden]')
|
||||
assert resp.pyquery('#sidebar[hidden]')
|
||||
resp = app.post_json(
|
||||
workflow.get_admin_url() + 'variables/fields/va123/update-documentation', {'content': '<p>doc</p>'}
|
||||
)
|
||||
assert resp.json == {'err': 0, 'empty': False, 'changed': True}
|
||||
workflow.refresh_from_storage()
|
||||
assert workflow.variables_formdef.fields[0].documentation == '<p>doc</p>'
|
||||
resp = app.get(workflow.get_admin_url() + 'variables/fields/va123/')
|
||||
assert resp.pyquery('.documentation:not([hidden])')
|
||||
assert resp.pyquery('#sidebar:not([hidden])')
|
||||
|
||||
resp = app.get(workflow.get_admin_url() + 'backoffice-fields/fields/')
|
||||
assert resp.pyquery('.documentation[hidden]')
|
||||
resp = app.post_json(
|
||||
workflow.get_admin_url() + 'backoffice-fields/fields/update-documentation', {'content': '<p>doc</p>'}
|
||||
)
|
||||
assert resp.json == {'err': 0, 'empty': False, 'changed': True}
|
||||
workflow.refresh_from_storage()
|
||||
assert workflow.backoffice_fields_formdef.documentation == '<p>doc</p>'
|
||||
resp = app.get(workflow.get_admin_url() + 'backoffice-fields/fields/')
|
||||
assert resp.pyquery('.documentation:not([hidden])')
|
||||
|
||||
resp = app.get(workflow.get_admin_url() + 'backoffice-fields/fields/bo234/')
|
||||
assert resp.pyquery('.documentation[hidden]')
|
||||
assert resp.pyquery('#sidebar[hidden]')
|
||||
resp = app.post_json(
|
||||
workflow.get_admin_url() + 'backoffice-fields/fields/bo234/update-documentation',
|
||||
{'content': '<p>doc</p>'},
|
||||
)
|
||||
assert resp.json == {'err': 0, 'empty': False, 'changed': True}
|
||||
workflow.refresh_from_storage()
|
||||
assert workflow.backoffice_fields_formdef.fields[0].documentation == '<p>doc</p>'
|
||||
resp = app.get(workflow.get_admin_url() + 'backoffice-fields/fields/bo234/')
|
||||
assert resp.pyquery('.documentation:not([hidden])')
|
||||
assert resp.pyquery('#sidebar:not([hidden])')
|
||||
|
||||
resp = app.get(global_action.get_admin_url())
|
||||
assert resp.pyquery('.documentation[hidden]')
|
||||
resp = app.post_json(global_action.get_admin_url() + 'update-documentation', {'content': '<p>doc</p>'})
|
||||
assert resp.json == {'err': 0, 'empty': False, 'changed': True}
|
||||
workflow.refresh_from_storage()
|
||||
assert workflow.global_actions[0].documentation == '<p>doc</p>'
|
||||
resp = app.get(global_action.get_admin_url())
|
||||
assert resp.pyquery('.documentation:not([hidden])')
|
||||
|
||||
resp = app.get(status.get_admin_url())
|
||||
assert resp.pyquery('.documentation[hidden]')
|
||||
resp = app.post_json(status.get_admin_url() + 'update-documentation', {'content': '<p>doc</p>'})
|
||||
assert resp.json == {'err': 0, 'empty': False, 'changed': True}
|
||||
workflow.refresh_from_storage()
|
||||
assert workflow.possible_status[0].documentation == '<p>doc</p>'
|
||||
resp = app.get(status.get_admin_url())
|
||||
assert resp.pyquery('.documentation:not([hidden])')
|
||||
|
||||
resp = app.get(workflow.get_admin_url() + 'inspect')
|
||||
assert resp.pyquery('.documentation').length == 5
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import datetime
|
||||
import os
|
||||
|
||||
import pytest
|
||||
from django.utils.html import escape
|
||||
|
@ -9,9 +8,9 @@ from wcs import workflow_tests
|
|||
from wcs.formdef import FormDef, fields
|
||||
from wcs.qommon.http_request import HTTPRequest
|
||||
from wcs.testdef import TestDef, WebserviceResponse
|
||||
from wcs.workflows import Workflow, WorkflowBackofficeFieldsFormDef
|
||||
from wcs.workflows import Workflow, WorkflowBackofficeFieldsFormDef, WorkflowCriticalityLevel
|
||||
|
||||
from ..utilities import create_temporary_pub, get_app, login
|
||||
from ..utilities import clean_temporary_pub, create_temporary_pub, get_app, login
|
||||
from .test_all import create_superuser
|
||||
|
||||
|
||||
|
@ -24,50 +23,22 @@ def pub():
|
|||
pub.cfg['identification'] = {'methods': ['password']}
|
||||
pub.write_cfg()
|
||||
|
||||
if not pub.site_options.has_section('options'):
|
||||
pub.site_options.add_section('options')
|
||||
pub.site_options.set('options', 'enable-workflow-tests', 'true')
|
||||
with open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w') as fd:
|
||||
pub.site_options.write(fd)
|
||||
|
||||
pub.user_class.wipe()
|
||||
FormDef.wipe()
|
||||
TestDef.wipe()
|
||||
WebserviceResponse.wipe()
|
||||
return pub
|
||||
|
||||
|
||||
def test_workflow_tests_link_feature_flag(pub):
|
||||
create_superuser(pub)
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test title'
|
||||
formdef.store()
|
||||
|
||||
formdata = formdef.data_class()()
|
||||
formdata.just_created()
|
||||
formdata.store()
|
||||
|
||||
testdef = TestDef.create_from_formdata(formdef, formdata)
|
||||
testdef.name = 'First test'
|
||||
testdef.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
|
||||
resp = app.get('/backoffice/forms/1/tests/%s/' % testdef.id)
|
||||
assert 'Workflow tests' in resp.text
|
||||
|
||||
pub.site_options.set('options', 'enable-workflow-tests', 'false')
|
||||
with open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w') as fd:
|
||||
pub.site_options.write(fd)
|
||||
|
||||
resp = app.get('/backoffice/forms/1/tests/%s/' % testdef.id)
|
||||
assert 'Workflow tests' not in resp.text
|
||||
def teardown_module(module):
|
||||
clean_temporary_pub()
|
||||
|
||||
|
||||
def test_workflow_tests_options(pub):
|
||||
create_superuser(pub)
|
||||
user = pub.user_class(name='test user')
|
||||
user.email = 'test@example.com'
|
||||
user.test_uuid = '42'
|
||||
user.store()
|
||||
|
||||
formdef = FormDef()
|
||||
|
@ -87,44 +58,15 @@ def test_workflow_tests_options(pub):
|
|||
resp = app.get('/backoffice/forms/1/tests/%s/workflow/' % testdef.id)
|
||||
resp = resp.click('Options')
|
||||
|
||||
resp.form['agent'] = user.id
|
||||
resp.form['agent'] = user.test_uuid
|
||||
resp = resp.form.submit('submit').follow()
|
||||
|
||||
testdef = TestDef.get(testdef.id)
|
||||
assert testdef.agent_id == str(user.id)
|
||||
|
||||
|
||||
def test_workflow_tests_disabled_no_agent(pub):
|
||||
user = create_superuser(pub)
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test title'
|
||||
formdef.store()
|
||||
|
||||
formdata = formdef.data_class()()
|
||||
formdata.just_created()
|
||||
formdata.store()
|
||||
|
||||
testdef = TestDef.create_from_formdata(formdef, formdata)
|
||||
testdef.name = 'First test'
|
||||
testdef.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
|
||||
resp = app.get('/backoffice/forms/1/tests/%s/workflow/' % testdef.id)
|
||||
assert 'Backoffice user is not defined, workflow tests will not be executed.' in resp.text
|
||||
|
||||
resp = resp.click('Open test options')
|
||||
resp.form['agent'] = user.id
|
||||
resp.form.submit().follow()
|
||||
|
||||
resp = app.get('/backoffice/forms/1/tests/%s/workflow/' % testdef.id)
|
||||
assert 'Backoffice user is not defined' not in resp.text
|
||||
assert 'Open test options' not in resp.text
|
||||
assert testdef.agent_id == user.test_uuid
|
||||
|
||||
|
||||
def test_workflow_tests_edit_actions(pub):
|
||||
user = create_superuser(pub)
|
||||
create_superuser(pub)
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test title'
|
||||
|
@ -136,7 +78,6 @@ def test_workflow_tests_edit_actions(pub):
|
|||
|
||||
testdef = TestDef.create_from_formdata(formdef, formdata)
|
||||
testdef.name = 'First test'
|
||||
testdef.agent_id = user.id
|
||||
testdef.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
|
@ -170,22 +111,38 @@ def test_workflow_tests_edit_actions(pub):
|
|||
resp = resp.form.submit().follow()
|
||||
|
||||
assert 'not configured' not in resp.text
|
||||
assert resp.text.count(escape('Click on "Accept"')) == 1
|
||||
assert [x.text for x in resp.pyquery('ul li.workflow-test-action span.type')] == [
|
||||
'Click on "Accept" by backoffice user',
|
||||
]
|
||||
|
||||
resp = resp.click('Duplicate').follow()
|
||||
assert resp.text.count(escape('Click on "Accept"')) == 2
|
||||
assert [x.text for x in resp.pyquery('ul li.workflow-test-action span.type')] == [
|
||||
'Click on "Accept" by backoffice user',
|
||||
'Click on "Accept" by backoffice user',
|
||||
]
|
||||
|
||||
resp = resp.click('Edit', index=0)
|
||||
resp.form['button_name'] = 'Reject'
|
||||
resp = resp.form.submit().follow()
|
||||
|
||||
assert resp.text.count(escape('Click on "Accept"')) == 1
|
||||
assert resp.text.count(escape('Click on "Reject"')) == 1
|
||||
assert [x.text for x in resp.pyquery('ul li.workflow-test-action span.type')] == [
|
||||
'Click on "Reject" by backoffice user',
|
||||
'Click on "Accept" by backoffice user',
|
||||
]
|
||||
|
||||
resp = resp.click('Duplicate', index=0).follow()
|
||||
assert [x.text for x in resp.pyquery('ul li.workflow-test-action span.type')] == [
|
||||
'Click on "Reject" by backoffice user',
|
||||
'Click on "Reject" by backoffice user',
|
||||
'Click on "Accept" by backoffice user',
|
||||
]
|
||||
|
||||
resp = resp.click('Delete', index=0)
|
||||
resp = resp.form.submit().follow()
|
||||
assert resp.text.count(escape('Click on "Accept"')) == 1
|
||||
assert resp.text.count(escape('Click on "Reject"')) == 0
|
||||
assert [x.text for x in resp.pyquery('ul li.workflow-test-action span.type')] == [
|
||||
'Click on "Reject" by backoffice user',
|
||||
'Click on "Accept" by backoffice user',
|
||||
]
|
||||
|
||||
# simulate invalid action
|
||||
testdef = TestDef.get(testdef.id)
|
||||
|
@ -193,12 +150,15 @@ def test_workflow_tests_edit_actions(pub):
|
|||
testdef.store()
|
||||
|
||||
resp = app.get('/backoffice/forms/1/tests/%s/workflow/' % testdef.id)
|
||||
assert 'There are no workflow test actions yet.' in resp.text
|
||||
assert [x.text for x in resp.pyquery('ul li.workflow-test-action span.type')] == [
|
||||
'Click on "Accept" by backoffice user',
|
||||
]
|
||||
|
||||
|
||||
def test_workflow_tests_action_button_click(pub):
|
||||
create_superuser(pub)
|
||||
user = pub.user_class(name='test user')
|
||||
user.test_uuid = '42'
|
||||
user.store()
|
||||
|
||||
workflow = Workflow(name='Workflow One')
|
||||
|
@ -237,10 +197,16 @@ def test_workflow_tests_action_button_click(pub):
|
|||
jump = new_status.add_action('choice')
|
||||
jump.label = 'Button no target status'
|
||||
|
||||
workflow.add_global_action('Action 1')
|
||||
|
||||
interactive_action = workflow.add_global_action('Interactive action (should not be shown)')
|
||||
interactive_action.add_action('form')
|
||||
|
||||
workflow.store()
|
||||
|
||||
resp = app.get('/backoffice/forms/1/tests/%s/workflow/1/' % testdef.id)
|
||||
assert resp.form['button_name'].options == [
|
||||
('Action 1', False, 'Action 1'),
|
||||
('Button 1', False, 'Button 1'),
|
||||
('Button 2', False, 'Button 2'),
|
||||
('Button 4 (not available)', True, 'Button 4 (not available)'),
|
||||
|
@ -254,7 +220,7 @@ def test_workflow_tests_action_button_click(pub):
|
|||
|
||||
resp = app.get('/backoffice/forms/1/tests/%s/workflow/1/' % testdef.id)
|
||||
resp.form['who'] = 'other'
|
||||
resp.form['who_id'].force_value(user.id)
|
||||
resp.form['who_id'] = user.test_uuid
|
||||
resp = resp.form.submit().follow()
|
||||
|
||||
assert escape('Click on "Button 1" by test user') in resp.text
|
||||
|
@ -263,6 +229,21 @@ def test_workflow_tests_action_button_click(pub):
|
|||
resp = app.get('/backoffice/forms/1/tests/%s/workflow/' % testdef.id)
|
||||
assert escape('Click on "Button 1" by missing user') in resp.text
|
||||
|
||||
user.store()
|
||||
resp = app.get('/backoffice/forms/1/tests/%s/workflow/1/' % testdef.id)
|
||||
resp.form['who'] = 'receiver'
|
||||
resp = resp.form.submit().follow()
|
||||
|
||||
resp = app.get('/backoffice/forms/1/tests/%s/workflow/1/' % testdef.id)
|
||||
assert escape('Selected user is "Backoffice user" but it is not defined.') in resp.text
|
||||
|
||||
resp = resp.click('Open test options')
|
||||
resp.form['agent'] = user.test_uuid
|
||||
resp.form.submit().follow()
|
||||
|
||||
resp = app.get('/backoffice/forms/1/tests/%s/workflow/1/' % testdef.id)
|
||||
assert escape('Selected user is "Backoffice user" but it is not defined.') not in resp.text
|
||||
|
||||
|
||||
def test_workflow_tests_action_assert_status(pub):
|
||||
create_superuser(pub)
|
||||
|
@ -373,6 +354,20 @@ def test_workflow_tests_action_assert_email(pub):
|
|||
resp = app.get('/backoffice/forms/1/tests/%s/workflow/' % testdef.id)
|
||||
assert escape('Email to "a@entrouvert.com" (+2)') in resp.text
|
||||
|
||||
assert_email.addresses = []
|
||||
assert_email.subject_strings = ['Hello your form has been submitted']
|
||||
assert_email.parent.store()
|
||||
|
||||
resp = app.get('/backoffice/forms/1/tests/%s/workflow/' % testdef.id)
|
||||
assert escape('Subject must contain "Hello your form has been su(…)"') in resp.text
|
||||
|
||||
assert_email.subject_strings = []
|
||||
assert_email.body_strings = ['Hello your form has been submitted']
|
||||
assert_email.parent.store()
|
||||
|
||||
resp = app.get('/backoffice/forms/1/tests/%s/workflow/' % testdef.id)
|
||||
assert escape('Body must contain "Hello your form has been su(…)"') in resp.text
|
||||
|
||||
|
||||
def test_workflow_tests_action_assert_sms(pub):
|
||||
create_superuser(pub)
|
||||
|
@ -402,14 +397,14 @@ def test_workflow_tests_action_assert_sms(pub):
|
|||
|
||||
resp = resp.click('Edit')
|
||||
resp.form['phone_numbers$element0'] = '0123456789'
|
||||
resp.form['body'] = 'Hello'
|
||||
resp.form['body'] = 'Hello your form has been submitted'
|
||||
resp = resp.form.submit().follow()
|
||||
|
||||
assert 'SMS to 0123456789' in resp.text
|
||||
|
||||
assert_sms = TestDef.get(testdef.id).workflow_tests.actions[0]
|
||||
assert assert_sms.phone_numbers == ['0123456789']
|
||||
assert assert_sms.body == 'Hello'
|
||||
assert assert_sms.body == 'Hello your form has been submitted'
|
||||
|
||||
assert_sms.phone_numbers = ['0123456789', '0123456781', '0123456782']
|
||||
assert_sms.parent.store()
|
||||
|
@ -417,6 +412,164 @@ def test_workflow_tests_action_assert_sms(pub):
|
|||
resp = app.get('/backoffice/forms/1/tests/%s/workflow/' % testdef.id)
|
||||
assert escape('SMS to 0123456789 (+2)') in resp.text
|
||||
|
||||
assert_sms.phone_numbers = []
|
||||
assert_sms.parent.store()
|
||||
|
||||
resp = app.get('/backoffice/forms/1/tests/%s/workflow/' % testdef.id)
|
||||
assert 'Hello your form has been su(…)' in resp.text
|
||||
|
||||
|
||||
def test_workflow_tests_action_assert_anonymise(pub):
|
||||
create_superuser(pub)
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test title'
|
||||
formdef.store()
|
||||
|
||||
formdata = formdef.data_class()()
|
||||
formdata.just_created()
|
||||
|
||||
testdef = TestDef.create_from_formdata(formdef, formdata)
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.AssertAnonymise(id='1'),
|
||||
]
|
||||
testdef.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
|
||||
resp = app.get('/backoffice/forms/1/tests/%s/workflow/' % testdef.id)
|
||||
assert 'Edit' not in resp.text
|
||||
|
||||
|
||||
def test_workflow_tests_action_assert_redirect(pub):
|
||||
create_superuser(pub)
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test title'
|
||||
formdef.store()
|
||||
|
||||
formdata = formdef.data_class()()
|
||||
formdata.just_created()
|
||||
|
||||
testdef = TestDef.create_from_formdata(formdef, formdata)
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.AssertRedirect(id='1'),
|
||||
]
|
||||
testdef.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
|
||||
resp = app.get('/backoffice/forms/1/tests/%s/workflow/' % testdef.id)
|
||||
assert 'not configured' in resp.text
|
||||
|
||||
resp = resp.click('Edit')
|
||||
resp.form['url'] = 'http://example.com'
|
||||
resp = resp.form.submit().follow()
|
||||
|
||||
assert 'not configured' not in resp.text
|
||||
assert 'http://example.com' in resp.text
|
||||
|
||||
|
||||
def test_workflow_tests_action_assert_history_message(pub):
|
||||
create_superuser(pub)
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test title'
|
||||
formdef.store()
|
||||
|
||||
formdata = formdef.data_class()()
|
||||
formdata.just_created()
|
||||
|
||||
testdef = TestDef.create_from_formdata(formdef, formdata)
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.AssertHistoryMessage(id='1'),
|
||||
]
|
||||
testdef.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
|
||||
resp = app.get('/backoffice/forms/1/tests/%s/workflow/' % testdef.id)
|
||||
assert 'not configured' in resp.text
|
||||
|
||||
resp = resp.click('Edit')
|
||||
resp.form['message'] = 'Hello your form has been submitted'
|
||||
resp = resp.form.submit().follow()
|
||||
|
||||
assert 'not configured' not in resp.text
|
||||
assert 'Hello your form has been su(…)' in resp.text
|
||||
|
||||
|
||||
def test_workflow_tests_action_assert_alert(pub):
|
||||
create_superuser(pub)
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test title'
|
||||
formdef.store()
|
||||
|
||||
formdata = formdef.data_class()()
|
||||
formdata.just_created()
|
||||
|
||||
testdef = TestDef.create_from_formdata(formdef, formdata)
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.AssertAlert(id='1'),
|
||||
]
|
||||
testdef.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
|
||||
resp = app.get('/backoffice/forms/1/tests/%s/workflow/' % testdef.id)
|
||||
assert 'not configured' in resp.text
|
||||
|
||||
resp = resp.click('Edit')
|
||||
resp.form['message'] = 'Hello your form has been submitted'
|
||||
resp = resp.form.submit().follow()
|
||||
|
||||
assert 'not configured' not in resp.text
|
||||
assert 'Hello your form has been su(…)' in resp.text
|
||||
|
||||
|
||||
def test_workflow_tests_action_assert_criticality(pub):
|
||||
create_superuser(pub)
|
||||
|
||||
workflow = Workflow(name='Workflow One')
|
||||
workflow.add_status(name='New status')
|
||||
workflow.store()
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.workflow_id = workflow.id
|
||||
formdef.name = 'test title'
|
||||
formdef.store()
|
||||
|
||||
formdata = formdef.data_class()()
|
||||
formdata.just_created()
|
||||
|
||||
testdef = TestDef.create_from_formdata(formdef, formdata)
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.AssertCriticality(id='1'),
|
||||
]
|
||||
testdef.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
|
||||
resp = app.get('/backoffice/forms/1/tests/%s/workflow/' % testdef.id)
|
||||
assert 'not configured' in resp.text
|
||||
|
||||
resp = resp.click('Edit')
|
||||
assert 'Workflow has no criticality levels.' in resp.text
|
||||
|
||||
workflow.criticality_levels = [
|
||||
WorkflowCriticalityLevel(name='green'),
|
||||
WorkflowCriticalityLevel(name='red'),
|
||||
]
|
||||
workflow.store()
|
||||
|
||||
resp = app.get('/backoffice/forms/1/tests/%s/workflow/1/' % testdef.id)
|
||||
resp.form['level_id'].select(text='green')
|
||||
resp = resp.form.submit().follow()
|
||||
|
||||
assert 'not configured' not in resp.text
|
||||
assert escape('Criticality is "green"') in resp.text
|
||||
|
||||
|
||||
def test_workflow_tests_action_assert_backoffice_field(pub):
|
||||
create_superuser(pub)
|
||||
|
@ -508,13 +661,13 @@ def test_workflow_tests_action_assert_webservice_call(pub):
|
|||
response3.store()
|
||||
|
||||
resp = app.get('/backoffice/forms/1/tests/%s/workflow/1/' % testdef.id)
|
||||
assert resp.form['webservice_response_id'].options == [
|
||||
(str(response.id), False, 'Fake response'),
|
||||
(str(response2.id), False, 'Fake response 2'),
|
||||
assert resp.form['webservice_response_uuid'].options == [
|
||||
(str(response.uuid), False, 'Fake response'),
|
||||
(str(response2.uuid), False, 'Fake response 2'),
|
||||
]
|
||||
assert resp.form['call_count'].value == '1'
|
||||
|
||||
resp.form['webservice_response_id'] = 1
|
||||
resp.form['webservice_response_uuid'] = response.uuid
|
||||
resp.form['call_count'] = 2
|
||||
resp = resp.form.submit().follow()
|
||||
|
||||
|
@ -522,7 +675,7 @@ def test_workflow_tests_action_assert_webservice_call(pub):
|
|||
assert 'Broken' not in resp.text
|
||||
|
||||
assert_webservice_call = TestDef.get(testdef.id).workflow_tests.actions[0]
|
||||
assert assert_webservice_call.webservice_response_id == '1'
|
||||
assert assert_webservice_call.webservice_response_uuid == response.uuid
|
||||
assert assert_webservice_call.call_count == 2
|
||||
|
||||
response.remove_self()
|
||||
|
@ -583,12 +736,16 @@ def test_workflow_tests_actions_reorder(pub):
|
|||
|
||||
|
||||
def test_workflow_tests_run(pub):
|
||||
user = create_superuser(pub)
|
||||
create_superuser(pub)
|
||||
|
||||
role = pub.role_class(name='test role')
|
||||
role.store()
|
||||
user.roles = [role.id]
|
||||
user.store()
|
||||
|
||||
test_user = pub.user_class(name='test user')
|
||||
test_user.email = 'test@example.com'
|
||||
test_user.test_uuid = '42'
|
||||
test_user.roles = [role.id]
|
||||
test_user.store()
|
||||
|
||||
workflow = Workflow(name='Workflow One')
|
||||
new_status = workflow.add_status(name='New status')
|
||||
|
@ -614,7 +771,7 @@ def test_workflow_tests_run(pub):
|
|||
formdata.just_created()
|
||||
|
||||
testdef = TestDef.create_from_formdata(formdef, formdata)
|
||||
testdef.agent_id = user.id
|
||||
testdef.agent_id = test_user.test_uuid
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.ButtonClick(id='1', button_name='Loop on status'),
|
||||
]
|
||||
|
@ -668,6 +825,49 @@ def test_workflow_tests_run(pub):
|
|||
assert resp.pyquery('li#test-action').text() == 'Test action: Assert email is sent'
|
||||
|
||||
|
||||
def test_workflow_tests_run_webservice_call(pub):
|
||||
create_superuser(pub)
|
||||
|
||||
workflow = Workflow(name='Workflow One')
|
||||
new_status = workflow.add_status(name='New status')
|
||||
|
||||
wscall = new_status.add_action('webservice_call')
|
||||
wscall.url = 'http://example.com/json'
|
||||
|
||||
workflow.store()
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test title'
|
||||
formdef.workflow_id = workflow.id
|
||||
formdef.store()
|
||||
|
||||
testdef = TestDef.create_from_formdata(formdef, formdef.data_class()())
|
||||
testdef.store()
|
||||
|
||||
response = WebserviceResponse()
|
||||
response.testdef_id = testdef.id
|
||||
response.name = 'Fake response'
|
||||
response.url = 'http://example.com/json'
|
||||
response.payload = '{}'
|
||||
response.store()
|
||||
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.AssertWebserviceCall(webservice_response_uuid=response.uuid, call_count=1),
|
||||
]
|
||||
testdef.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
|
||||
resp = app.get('/backoffice/forms/1/tests/results/run').follow()
|
||||
assert 'Success!' in resp.text
|
||||
|
||||
wscall.response_type = 'attachment'
|
||||
workflow.store()
|
||||
|
||||
resp = app.get('/backoffice/forms/1/tests/results/run').follow()
|
||||
assert 'Workflow error: Webservice response Fake response was used 0 times' in resp.text
|
||||
|
||||
|
||||
def test_workfow_tests_creation_from_formdata(pub):
|
||||
create_superuser(pub)
|
||||
|
||||
|
|
|
@ -36,7 +36,6 @@ def teardown_module(module):
|
|||
def wscall():
|
||||
NamedWsCall.wipe()
|
||||
wscall = NamedWsCall(name='xxx')
|
||||
wscall.description = 'description'
|
||||
wscall.notify_on_errors = True
|
||||
wscall.record_on_errors = True
|
||||
wscall.request = {
|
||||
|
@ -68,7 +67,6 @@ def test_wscalls_new(pub, value):
|
|||
assert resp.form['notify_on_errors'].value is None
|
||||
assert resp.form['record_on_errors'].value == 'yes'
|
||||
resp.form['name'] = 'a new webservice call'
|
||||
resp.form['description'] = 'description'
|
||||
resp.form['notify_on_errors'] = value
|
||||
resp.form['record_on_errors'] = value
|
||||
resp.form['request$url'] = 'http://remote.example.net/json'
|
||||
|
@ -111,14 +109,12 @@ def test_wscalls_edit(pub, wscall):
|
|||
assert resp.form['notify_on_errors'].value == 'yes'
|
||||
assert resp.form['record_on_errors'].value == 'yes'
|
||||
assert 'slug' in resp.form.fields
|
||||
resp.form['description'] = 'bla bla bla'
|
||||
resp.form['notify_on_errors'] = False
|
||||
resp.form['record_on_errors'] = False
|
||||
resp = resp.form.submit('submit')
|
||||
assert resp.location == 'http://example.net/backoffice/settings/wscalls/xxx/'
|
||||
resp = resp.follow()
|
||||
|
||||
assert NamedWsCall.get('xxx').description == 'bla bla bla'
|
||||
assert NamedWsCall.get('xxx').notify_on_errors is False
|
||||
assert NamedWsCall.get('xxx').record_on_errors is False
|
||||
|
||||
|
@ -235,7 +231,6 @@ def test_wscalls_empty_param_values(pub):
|
|||
resp = app.get('/backoffice/settings/wscalls/')
|
||||
resp = resp.click('New webservice call')
|
||||
resp.form['name'] = 'a new webservice call'
|
||||
resp.form['description'] = 'description'
|
||||
resp.form['request$qs_data$element0key'] = 'foo'
|
||||
resp.form['request$post_data$element0key'] = 'bar'
|
||||
resp = resp.form.submit('submit').follow()
|
||||
|
@ -253,7 +248,6 @@ def test_wscalls_timeout(pub):
|
|||
resp = app.get('/backoffice/settings/wscalls/')
|
||||
resp = resp.click('New webservice call')
|
||||
resp.form['name'] = 'a new webservice call'
|
||||
resp.form['description'] = 'description'
|
||||
resp.form['request$timeout'] = 'plop'
|
||||
resp = resp.form.submit('submit')
|
||||
assert resp.pyquery('[data-widget-name="request$timeout"].widget-with-error')
|
||||
|
@ -300,3 +294,22 @@ def test_wscalls_usage(pub, wscall):
|
|||
formdef.store()
|
||||
resp = app.get(usage_url)
|
||||
assert 'No usage detected.' in resp.text
|
||||
|
||||
|
||||
def test_wscall_documentation(pub):
|
||||
create_superuser(pub)
|
||||
|
||||
NamedWsCall.wipe()
|
||||
wscall = NamedWsCall(name='foobar')
|
||||
wscall.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
|
||||
resp = app.get(wscall.get_admin_url())
|
||||
assert resp.pyquery('.documentation[hidden]')
|
||||
resp = app.post_json(wscall.get_admin_url() + 'update-documentation', {'content': '<p>doc</p>'})
|
||||
assert resp.json == {'err': 0, 'empty': False, 'changed': True}
|
||||
wscall.refresh_from_storage()
|
||||
assert wscall.documentation == '<p>doc</p>'
|
||||
resp = app.get(wscall.get_admin_url())
|
||||
assert resp.pyquery('.documentation:not([hidden])')
|
||||
|
|
|
@ -145,6 +145,26 @@ def test_validate_condition(pub):
|
|||
resp = get_app(pub).get('/api/validate-condition?type=unknown&value_unknown=2')
|
||||
assert resp.json['msg'] == 'unknown condition type'
|
||||
|
||||
resp = get_app(pub).get('/api/validate-condition?type=django&value_django=today > "2023"')
|
||||
assert resp.json == {'msg': ''}
|
||||
resp = get_app(pub).get(
|
||||
'/api/validate-condition?type=django&value_django=today > "2023"&warn-on-datetime=false'
|
||||
)
|
||||
assert resp.json == {'msg': ''}
|
||||
resp = get_app(pub).get(
|
||||
'/api/validate-condition?type=django&value_django=today > "2023"&warn-on-datetime=true'
|
||||
)
|
||||
assert resp.json['msg'].startswith('Warning: conditions are only evaluated when entering')
|
||||
|
||||
resp = get_app(pub).get(
|
||||
'/api/validate-condition?type=django&value_django=x|age_in_days > 10&warn-on-datetime=true'
|
||||
)
|
||||
assert resp.json['msg'].startswith('Warning: conditions are only evaluated when entering')
|
||||
resp = get_app(pub).get(
|
||||
'/api/validate-condition?type=django&value_django=x|age_in_days|abs > 10&warn-on-datetime=true'
|
||||
)
|
||||
assert resp.json['msg'].startswith('Warning: conditions are only evaluated when entering')
|
||||
|
||||
|
||||
def test_reverse_geocoding(pub):
|
||||
with responses.RequestsMock() as rsps:
|
||||
|
@ -285,3 +305,57 @@ def test_afterjobs_base_directory(pub):
|
|||
get_app(pub).get('/api/jobs/', status=403)
|
||||
# base directory is 404
|
||||
get_app(pub).get(sign_url('/api/jobs/?orig=coucou', '1234'), status=404)
|
||||
|
||||
|
||||
def test_preview_payload_structure(pub, admin_user):
|
||||
get_app(pub).get('/api/preview-payload-structure', status=403)
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/api/preview-payload-structure')
|
||||
|
||||
assert resp.pyquery('div.payload-preview').length == 1
|
||||
assert '<h2>Payload structure preview</h2>' in resp.text
|
||||
assert resp.pyquery('div.payload-preview').text() == '{}'
|
||||
params = {
|
||||
'request$post_data$added_elements': 1,
|
||||
'request$post_data$element1key': 'user/first_name',
|
||||
'request$post_data$element1value$value_template': 'Foo',
|
||||
'request$post_data$element1value$value_python': '',
|
||||
'request$post_data$element2key': 'user/last_name',
|
||||
'request$post_data$element2value$value_template': 'Bar',
|
||||
'request$post_data$element2value$value_python': '',
|
||||
'request$post_data$element3key': 'user/0',
|
||||
}
|
||||
resp = app.get('/api/preview-payload-structure', params=params)
|
||||
assert resp.pyquery('div.payload-preview div.errornotice').length == 0
|
||||
assert resp.pyquery('div.payload-preview').text() == '{"user": {"first_name": "Foo","last_name": "Bar"}}'
|
||||
params.update(
|
||||
{
|
||||
'request$post_data$element3value$value_template': 'value',
|
||||
'request$post_data$element3value$value_python': '',
|
||||
}
|
||||
)
|
||||
resp = app.get('/api/preview-payload-structure', params=params)
|
||||
|
||||
assert resp.pyquery('div.payload-preview div.errornotice').length == 1
|
||||
assert 'Unable to preview payload' in resp.pyquery('div.payload-preview div.errornotice').text()
|
||||
assert (
|
||||
'Following error occured: there is a mix between lists and dicts'
|
||||
in resp.pyquery('div.payload-preview div.errornotice').text()
|
||||
)
|
||||
|
||||
params = {
|
||||
'post_data$element1key': '0/0',
|
||||
'post_data$element1value$value_template': 'Foo',
|
||||
'post_data$element1value$value_python': '',
|
||||
'post_data$element2key': '0/1',
|
||||
'post_data$element2value$value_template': '{{ form_name }}',
|
||||
'post_data$element2value$value_python': '',
|
||||
'post_data$element3key': '1/0',
|
||||
'post_data$element3value$value_template': '',
|
||||
'post_data$element10key': '1/1',
|
||||
'post_data$element10value$value_template': '10',
|
||||
'post_data$element100key': '1/2',
|
||||
'post_data$element100value$value_template': '100',
|
||||
}
|
||||
resp = app.get('/api/preview-payload-structure', params=params)
|
||||
assert resp.pyquery('div.payload-preview').text() == '[["Foo",{{ form_name }}],["","10","100"]]'
|
||||
|
|
|
@ -9,6 +9,7 @@ import pytest
|
|||
|
||||
from wcs.api_export_import import BundleDeclareJob, BundleImportJob, klass_to_slug
|
||||
from wcs.applications import Application, ApplicationElement
|
||||
from wcs.backoffice.deprecations import DeprecationsScan
|
||||
from wcs.blocks import BlockDef
|
||||
from wcs.carddef import CardDef
|
||||
from wcs.categories import (
|
||||
|
@ -25,6 +26,7 @@ from wcs.data_sources import NamedDataSource
|
|||
from wcs.fields import BlockField, CommentField, ComputedField, PageField, StringField
|
||||
from wcs.formdef import FormDef
|
||||
from wcs.mail_templates import MailTemplate
|
||||
from wcs.qommon.afterjobs import AfterJob
|
||||
from wcs.sql import Equal
|
||||
from wcs.wf.form import WorkflowFormFieldsFormDef
|
||||
from wcs.workflows import Workflow, WorkflowBackofficeFieldsFormDef, WorkflowVariablesFieldsFormDef
|
||||
|
@ -322,6 +324,11 @@ def test_export_import_dependencies(pub):
|
|||
'value': '{{ forms|objects:"test-bis" }} {{ webservice.test_quinquies }}',
|
||||
},
|
||||
),
|
||||
BlockField(
|
||||
id='1bis',
|
||||
label='test_missing',
|
||||
block_slug='test-missing', # Unknown BlockDef
|
||||
),
|
||||
CommentField(
|
||||
id='2',
|
||||
label='X {{ webservice.test }} X {{ cards|objects:"test" }} X {{ forms|objects:"test-ter" }} X',
|
||||
|
@ -775,7 +782,9 @@ def test_export_import_bundle_import(pub):
|
|||
role = pub.role_class(name='test')
|
||||
role.store()
|
||||
|
||||
resp = get_app(pub).put(sign_uri('/api/export-import/bundle-import/'), bundles[0])
|
||||
resp = get_app(pub).post(
|
||||
sign_uri('/api/export-import/bundle-import/'), upload_files=[('bundle', 'bundle.tar', bundles[0])]
|
||||
)
|
||||
afterjob_url = resp.json['url']
|
||||
resp = get_app(pub).put(sign_uri(afterjob_url))
|
||||
assert resp.json['data']['status'] == 'completed'
|
||||
|
@ -870,7 +879,9 @@ def test_export_import_bundle_import(pub):
|
|||
element2.store()
|
||||
|
||||
# run new import to check it doesn't duplicate objects
|
||||
resp = get_app(pub).put(sign_uri('/api/export-import/bundle-import/'), bundles[1])
|
||||
resp = get_app(pub).post(
|
||||
sign_uri('/api/export-import/bundle-import/'), upload_files=[('bundle', 'bundle.tar', bundles[1])]
|
||||
)
|
||||
afterjob_url = resp.json['url']
|
||||
resp = get_app(pub).put(sign_uri(afterjob_url))
|
||||
assert resp.json['data']['status'] == 'completed'
|
||||
|
@ -931,7 +942,9 @@ def test_export_import_bundle_import(pub):
|
|||
formdef.disabled = True
|
||||
formdef.store()
|
||||
|
||||
resp = get_app(pub).put(sign_uri('/api/export-import/bundle-import/'), bundles[1])
|
||||
resp = get_app(pub).post(
|
||||
sign_uri('/api/export-import/bundle-import/'), upload_files=[('bundle', 'bundle.tar', bundles[1])]
|
||||
)
|
||||
afterjob_url = resp.json['url']
|
||||
resp = get_app(pub).put(sign_uri(afterjob_url))
|
||||
assert resp.json['data']['status'] == 'completed'
|
||||
|
@ -941,7 +954,9 @@ def test_export_import_bundle_import(pub):
|
|||
assert formdef.workflow_roles == {'_receiver': extra_role.id}
|
||||
|
||||
# bad file format
|
||||
resp = get_app(pub).put(sign_uri('/api/export-import/bundle-import/'), b'garbage')
|
||||
resp = get_app(pub).post(
|
||||
sign_uri('/api/export-import/bundle-import/'), upload_files=[('bundle', 'bundle.tar', b'garbage')]
|
||||
)
|
||||
afterjob_url = resp.json['url']
|
||||
resp = get_app(pub).put(sign_uri(afterjob_url))
|
||||
assert resp.json['data']['status'] == 'failed'
|
||||
|
@ -956,7 +971,10 @@ def test_export_import_bundle_import(pub):
|
|||
tarinfo = tarfile.TarInfo('foo.json')
|
||||
tarinfo.size = len(foo_fd.getvalue())
|
||||
tar.addfile(tarinfo, fileobj=foo_fd)
|
||||
resp = get_app(pub).put(sign_uri('/api/export-import/bundle-import/'), tar_io.getvalue())
|
||||
resp = get_app(pub).post(
|
||||
sign_uri('/api/export-import/bundle-import/'),
|
||||
upload_files=[('bundle', 'bundle.tar', tar_io.getvalue())],
|
||||
)
|
||||
afterjob_url = resp.json['url']
|
||||
resp = get_app(pub).put(sign_uri(afterjob_url))
|
||||
assert resp.json['data']['status'] == 'failed'
|
||||
|
@ -976,7 +994,10 @@ def test_export_import_bundle_import(pub):
|
|||
tarinfo = tarfile.TarInfo('manifest.json')
|
||||
tarinfo.size = len(manifest_fd.getvalue())
|
||||
tar.addfile(tarinfo, fileobj=manifest_fd)
|
||||
resp = get_app(pub).put(sign_uri('/api/export-import/bundle-import/'), tar_io.getvalue())
|
||||
resp = get_app(pub).post(
|
||||
sign_uri('/api/export-import/bundle-import/'),
|
||||
upload_files=[('bundle', 'bundle.tar', tar_io.getvalue())],
|
||||
)
|
||||
afterjob_url = resp.json['url']
|
||||
resp = get_app(pub).put(sign_uri(afterjob_url))
|
||||
assert resp.json['data']['status'] == 'failed'
|
||||
|
@ -1030,7 +1051,9 @@ def test_export_import_bundle_import_categories_ordering(pub, category_class):
|
|||
category.store()
|
||||
|
||||
# import bundle
|
||||
resp = get_app(pub).put(sign_uri('/api/export-import/bundle-import/'), bundle)
|
||||
resp = get_app(pub).post(
|
||||
sign_uri('/api/export-import/bundle-import/'), upload_files=[('bundle', 'bundle.tar', bundle)]
|
||||
)
|
||||
afterjob_url = resp.json['url']
|
||||
resp = get_app(pub).put(sign_uri(afterjob_url))
|
||||
assert resp.json['data']['status'] == 'completed'
|
||||
|
@ -1055,7 +1078,9 @@ def test_export_import_bundle_import_categories_ordering(pub, category_class):
|
|||
category.position = 3
|
||||
category.store()
|
||||
|
||||
resp = get_app(pub).put(sign_uri('/api/export-import/bundle-import/'), bundle)
|
||||
resp = get_app(pub).post(
|
||||
sign_uri('/api/export-import/bundle-import/'), upload_files=[('bundle', 'bundle.tar', bundle)]
|
||||
)
|
||||
afterjob_url = resp.json['url']
|
||||
resp = get_app(pub).put(sign_uri(afterjob_url))
|
||||
assert resp.json['data']['status'] == 'completed'
|
||||
|
@ -1080,7 +1105,9 @@ def test_export_import_bundle_import_categories_ordering(pub, category_class):
|
|||
category.position = 3
|
||||
category.store()
|
||||
|
||||
resp = get_app(pub).put(sign_uri('/api/export-import/bundle-import/'), bundle)
|
||||
resp = get_app(pub).post(
|
||||
sign_uri('/api/export-import/bundle-import/'), upload_files=[('bundle', 'bundle.tar', bundle)]
|
||||
)
|
||||
afterjob_url = resp.json['url']
|
||||
resp = get_app(pub).put(sign_uri(afterjob_url))
|
||||
assert resp.json['data']['status'] == 'completed'
|
||||
|
@ -1105,7 +1132,9 @@ def test_export_import_bundle_import_categories_ordering(pub, category_class):
|
|||
category.position = 3
|
||||
category.store()
|
||||
|
||||
resp = get_app(pub).put(sign_uri('/api/export-import/bundle-import/'), bundle)
|
||||
resp = get_app(pub).post(
|
||||
sign_uri('/api/export-import/bundle-import/'), upload_files=[('bundle', 'bundle.tar', bundle)]
|
||||
)
|
||||
afterjob_url = resp.json['url']
|
||||
resp = get_app(pub).put(sign_uri(afterjob_url))
|
||||
assert resp.json['data']['status'] == 'completed'
|
||||
|
@ -1133,7 +1162,9 @@ def test_export_import_bundle_import_categories_ordering(pub, category_class):
|
|||
category.position = 20
|
||||
category.store()
|
||||
|
||||
resp = get_app(pub).put(sign_uri('/api/export-import/bundle-import/'), bundle)
|
||||
resp = get_app(pub).post(
|
||||
sign_uri('/api/export-import/bundle-import/'), upload_files=[('bundle', 'bundle.tar', bundle)]
|
||||
)
|
||||
afterjob_url = resp.json['url']
|
||||
resp = get_app(pub).put(sign_uri(afterjob_url))
|
||||
assert resp.json['data']['status'] == 'completed'
|
||||
|
@ -1161,7 +1192,9 @@ def test_export_import_bundle_import_categories_ordering(pub, category_class):
|
|||
category.position = None # no position
|
||||
category.store()
|
||||
|
||||
resp = get_app(pub).put(sign_uri('/api/export-import/bundle-import/'), bundle)
|
||||
resp = get_app(pub).post(
|
||||
sign_uri('/api/export-import/bundle-import/'), upload_files=[('bundle', 'bundle.tar', bundle)]
|
||||
)
|
||||
afterjob_url = resp.json['url']
|
||||
resp = get_app(pub).put(sign_uri(afterjob_url))
|
||||
assert resp.json['data']['status'] == 'completed'
|
||||
|
@ -1195,7 +1228,9 @@ def test_export_import_formdef_do_not_overwrite_table_name(pub):
|
|||
|
||||
bundle = create_bundle([{'type': 'forms', 'slug': 'test', 'name': 'test'}], ('forms/test', formdef))
|
||||
|
||||
resp = get_app(pub).put(sign_uri('/api/export-import/bundle-import/'), bundle)
|
||||
resp = get_app(pub).post(
|
||||
sign_uri('/api/export-import/bundle-import/'), upload_files=[('bundle', 'bundle.tar', bundle)]
|
||||
)
|
||||
afterjob_url = resp.json['url']
|
||||
resp = get_app(pub).put(sign_uri(afterjob_url))
|
||||
assert resp.json['data']['status'] == 'completed'
|
||||
|
@ -1295,7 +1330,9 @@ def test_export_import_bundle_declare(pub):
|
|||
visible=False,
|
||||
)
|
||||
|
||||
resp = get_app(pub).put(sign_uri('/api/export-import/bundle-declare/'), bundle)
|
||||
resp = get_app(pub).post(
|
||||
sign_uri('/api/export-import/bundle-declare/'), upload_files=[('bundle', 'bundle.tar', bundle)]
|
||||
)
|
||||
afterjob_url = resp.json['url']
|
||||
resp = get_app(pub).put(sign_uri(afterjob_url))
|
||||
assert resp.json['data']['status'] == 'completed'
|
||||
|
@ -1328,7 +1365,9 @@ def test_export_import_bundle_declare(pub):
|
|||
# and remove an object to have an unkown reference in manifest
|
||||
MailTemplate.wipe()
|
||||
|
||||
resp = get_app(pub).put(sign_uri('/api/export-import/bundle-declare/'), bundle)
|
||||
resp = get_app(pub).post(
|
||||
sign_uri('/api/export-import/bundle-declare/'), upload_files=[('bundle', 'bundle.tar', bundle)]
|
||||
)
|
||||
afterjob_url = resp.json['url']
|
||||
resp = get_app(pub).put(sign_uri(afterjob_url))
|
||||
assert resp.json['data']['status'] == 'completed'
|
||||
|
@ -1366,7 +1405,9 @@ def test_export_import_bundle_declare(pub):
|
|||
)
|
||||
|
||||
# bad file format
|
||||
resp = get_app(pub).put(sign_uri('/api/export-import/bundle-declare/'), b'garbage')
|
||||
resp = get_app(pub).post(
|
||||
sign_uri('/api/export-import/bundle-declare/'), upload_files=[('bundle', 'bundle.tar', b'garbage')]
|
||||
)
|
||||
afterjob_url = resp.json['url']
|
||||
resp = get_app(pub).put(sign_uri(afterjob_url))
|
||||
assert resp.json['data']['status'] == 'failed'
|
||||
|
@ -1381,7 +1422,10 @@ def test_export_import_bundle_declare(pub):
|
|||
tarinfo = tarfile.TarInfo('foo.json')
|
||||
tarinfo.size = len(foo_fd.getvalue())
|
||||
tar.addfile(tarinfo, fileobj=foo_fd)
|
||||
resp = get_app(pub).put(sign_uri('/api/export-import/bundle-declare/'), tar_io.getvalue())
|
||||
resp = get_app(pub).post(
|
||||
sign_uri('/api/export-import/bundle-declare/'),
|
||||
upload_files=[('bundle', 'bundle.tar', tar_io.getvalue())],
|
||||
)
|
||||
afterjob_url = resp.json['url']
|
||||
resp = get_app(pub).put(sign_uri(afterjob_url))
|
||||
assert resp.json['data']['status'] == 'failed'
|
||||
|
@ -1631,13 +1675,21 @@ def test_export_import_bundle_check(pub):
|
|||
incomplete_bundles.append(tar_io.getvalue())
|
||||
|
||||
# incorrect bundles, missing information
|
||||
resp = get_app(pub).put(sign_uri('/api/export-import/bundle-check/'), incomplete_bundles[0])
|
||||
resp = get_app(pub).post(
|
||||
sign_uri('/api/export-import/bundle-check/'),
|
||||
upload_files=[('bundle', 'bundle.tar', incomplete_bundles[0])],
|
||||
)
|
||||
assert resp.json == {'data': {}}
|
||||
resp = get_app(pub).put(sign_uri('/api/export-import/bundle-check/'), incomplete_bundles[1])
|
||||
resp = get_app(pub).post(
|
||||
sign_uri('/api/export-import/bundle-check/'),
|
||||
upload_files=[('bundle', 'bundle.tar', incomplete_bundles[1])],
|
||||
)
|
||||
assert resp.json == {'data': {}}
|
||||
|
||||
# not yet imported
|
||||
resp = get_app(pub).put(sign_uri('/api/export-import/bundle-check/'), bundles[0])
|
||||
resp = get_app(pub).post(
|
||||
sign_uri('/api/export-import/bundle-check/'), upload_files=[('bundle', 'bundle.tar', bundles[0])]
|
||||
)
|
||||
assert resp.json == {
|
||||
'data': {
|
||||
'differences': [],
|
||||
|
@ -1664,7 +1716,9 @@ def test_export_import_bundle_check(pub):
|
|||
}
|
||||
|
||||
# import bundle
|
||||
resp = get_app(pub).put(sign_uri('/api/export-import/bundle-import/'), bundles[0])
|
||||
resp = get_app(pub).post(
|
||||
sign_uri('/api/export-import/bundle-import/'), upload_files=[('bundle', 'bundle.tar', bundles[0])]
|
||||
)
|
||||
afterjob_url = resp.json['url']
|
||||
resp = get_app(pub).put(sign_uri(afterjob_url))
|
||||
assert resp.json['data']['status'] == 'completed'
|
||||
|
@ -1675,7 +1729,9 @@ def test_export_import_bundle_check(pub):
|
|||
# remove application links
|
||||
Application.wipe()
|
||||
ApplicationElement.wipe()
|
||||
resp = get_app(pub).put(sign_uri('/api/export-import/bundle-check/'), bundles[0])
|
||||
resp = get_app(pub).post(
|
||||
sign_uri('/api/export-import/bundle-check/'), upload_files=[('bundle', 'bundle.tar', bundles[0])]
|
||||
)
|
||||
assert resp.json == {
|
||||
'data': {
|
||||
'differences': [],
|
||||
|
@ -1777,7 +1833,9 @@ def test_export_import_bundle_check(pub):
|
|||
}
|
||||
|
||||
# import bundle again, recreate links
|
||||
resp = get_app(pub).put(sign_uri('/api/export-import/bundle-import/'), bundles[0])
|
||||
resp = get_app(pub).post(
|
||||
sign_uri('/api/export-import/bundle-import/'), upload_files=[('bundle', 'bundle.tar', bundles[0])]
|
||||
)
|
||||
afterjob_url = resp.json['url']
|
||||
resp = get_app(pub).put(sign_uri(afterjob_url))
|
||||
assert resp.json['data']['status'] == 'completed'
|
||||
|
@ -1786,7 +1844,9 @@ def test_export_import_bundle_check(pub):
|
|||
assert ApplicationElement.count() == 15
|
||||
|
||||
# no changes since last import
|
||||
resp = get_app(pub).put(sign_uri('/api/export-import/bundle-check/'), bundles[0])
|
||||
resp = get_app(pub).post(
|
||||
sign_uri('/api/export-import/bundle-check/'), upload_files=[('bundle', 'bundle.tar', bundles[0])]
|
||||
)
|
||||
assert resp.json == {
|
||||
'data': {
|
||||
'differences': [],
|
||||
|
@ -1814,7 +1874,9 @@ def test_export_import_bundle_check(pub):
|
|||
assert len(new_snapshots) > len(old_snapshots)
|
||||
|
||||
# and check
|
||||
resp = get_app(pub).put(sign_uri('/api/export-import/bundle-check/'), bundles[0])
|
||||
resp = get_app(pub).post(
|
||||
sign_uri('/api/export-import/bundle-check/'), upload_files=[('bundle', 'bundle.tar', bundles[0])]
|
||||
)
|
||||
assert resp.json == {
|
||||
'data': {
|
||||
'differences': [
|
||||
|
@ -1916,14 +1978,18 @@ def test_export_import_bundle_check(pub):
|
|||
}
|
||||
|
||||
# update bundle
|
||||
resp = get_app(pub).put(sign_uri('/api/export-import/bundle-import/'), bundles[1])
|
||||
resp = get_app(pub).post(
|
||||
sign_uri('/api/export-import/bundle-import/'), upload_files=[('bundle', 'bundle.tar', bundles[1])]
|
||||
)
|
||||
afterjob_url = resp.json['url']
|
||||
resp = get_app(pub).put(sign_uri(afterjob_url))
|
||||
assert resp.json['data']['status'] == 'completed'
|
||||
assert resp.json['data']['completion_status'] == '34/34 (100%)'
|
||||
|
||||
# and check
|
||||
resp = get_app(pub).put(sign_uri('/api/export-import/bundle-check/'), bundles[1])
|
||||
resp = get_app(pub).post(
|
||||
sign_uri('/api/export-import/bundle-check/'), upload_files=[('bundle', 'bundle.tar', bundles[1])]
|
||||
)
|
||||
assert resp.json == {
|
||||
'data': {
|
||||
'differences': [],
|
||||
|
@ -1938,7 +2004,9 @@ def test_export_import_bundle_check(pub):
|
|||
snapshot.application_slug = None
|
||||
snapshot.application_version = None
|
||||
snapshot.store()
|
||||
resp = get_app(pub).put(sign_uri('/api/export-import/bundle-check/'), bundles[1])
|
||||
resp = get_app(pub).post(
|
||||
sign_uri('/api/export-import/bundle-check/'), upload_files=[('bundle', 'bundle.tar', bundles[1])]
|
||||
)
|
||||
assert resp.json == {
|
||||
'data': {
|
||||
'differences': [],
|
||||
|
@ -1965,7 +2033,9 @@ def test_export_import_bundle_check(pub):
|
|||
}
|
||||
|
||||
# bad file format
|
||||
resp = get_app(pub).put(sign_uri('/api/export-import/bundle-check/'), b'garbage')
|
||||
resp = get_app(pub).post(
|
||||
sign_uri('/api/export-import/bundle-check/'), upload_files=[('bundle', 'bundle.tar', b'garbage')]
|
||||
)
|
||||
assert resp.json['err_desc'] == 'Invalid tar file'
|
||||
|
||||
# missing manifest
|
||||
|
@ -1975,7 +2045,10 @@ def test_export_import_bundle_check(pub):
|
|||
tarinfo = tarfile.TarInfo('foo.json')
|
||||
tarinfo.size = len(foo_fd.getvalue())
|
||||
tar.addfile(tarinfo, fileobj=foo_fd)
|
||||
resp = get_app(pub).put(sign_uri('/api/export-import/bundle-check/'), tar_io.getvalue())
|
||||
resp = get_app(pub).post(
|
||||
sign_uri('/api/export-import/bundle-check/'),
|
||||
upload_files=[('bundle', 'bundle.tar', tar_io.getvalue())],
|
||||
)
|
||||
assert resp.json['err']
|
||||
assert resp.json['err_desc'] == 'Invalid tar file, missing manifest'
|
||||
|
||||
|
@ -1992,7 +2065,10 @@ def test_export_import_bundle_check(pub):
|
|||
tarinfo = tarfile.TarInfo('manifest.json')
|
||||
tarinfo.size = len(manifest_fd.getvalue())
|
||||
tar.addfile(tarinfo, fileobj=manifest_fd)
|
||||
resp = get_app(pub).put(sign_uri('/api/export-import/bundle-check/'), tar_io.getvalue())
|
||||
resp = get_app(pub).post(
|
||||
sign_uri('/api/export-import/bundle-check/'),
|
||||
upload_files=[('bundle', 'bundle.tar', tar_io.getvalue())],
|
||||
)
|
||||
assert resp.json['err']
|
||||
assert resp.json['err_desc'] == 'Invalid tar file, missing component forms/foo'
|
||||
|
||||
|
@ -2025,7 +2101,9 @@ def test_export_import_workflow_options(pub):
|
|||
FormDef.wipe()
|
||||
Workflow.wipe()
|
||||
|
||||
resp = get_app(pub).put(sign_uri('/api/export-import/bundle-import/'), bundle)
|
||||
resp = get_app(pub).post(
|
||||
sign_uri('/api/export-import/bundle-import/'), upload_files=[('bundle', 'bundle.tar', bundle)]
|
||||
)
|
||||
afterjob_url = resp.json['url']
|
||||
resp = get_app(pub).put(sign_uri(afterjob_url))
|
||||
assert resp.json['data']['status'] == 'completed'
|
||||
|
@ -2038,7 +2116,9 @@ def test_export_import_workflow_options(pub):
|
|||
# check workflow options are not reset on further installs
|
||||
formdef.workflow_options = {'foo': 'bar2'}
|
||||
formdef.store()
|
||||
resp = get_app(pub).put(sign_uri('/api/export-import/bundle-import/'), bundle)
|
||||
resp = get_app(pub).post(
|
||||
sign_uri('/api/export-import/bundle-import/'), upload_files=[('bundle', 'bundle.tar', bundle)]
|
||||
)
|
||||
afterjob_url = resp.json['url']
|
||||
resp = get_app(pub).put(sign_uri(afterjob_url))
|
||||
assert resp.json['data']['status'] == 'completed'
|
||||
|
@ -2047,6 +2127,7 @@ def test_export_import_workflow_options(pub):
|
|||
|
||||
|
||||
def test_export_import_with_deprecated(pub):
|
||||
AfterJob.wipe()
|
||||
pub.load_site_options()
|
||||
if not pub.site_options.has_section('options'):
|
||||
pub.site_options.add_section('options')
|
||||
|
@ -2066,10 +2147,15 @@ def test_export_import_with_deprecated(pub):
|
|||
],
|
||||
('forms/foo', formdef),
|
||||
)
|
||||
resp = get_app(pub).put(sign_uri('/api/export-import/bundle-import/'), bundle)
|
||||
resp = get_app(pub).post(
|
||||
sign_uri('/api/export-import/bundle-import/'), upload_files=[('bundle', 'bundle.tar', bundle)]
|
||||
)
|
||||
afterjob_url = resp.json['url']
|
||||
resp = get_app(pub).put(sign_uri(afterjob_url))
|
||||
assert resp.json['data']['status'] == 'failed'
|
||||
assert AfterJob.count() == 1
|
||||
job = AfterJob.select()[0]
|
||||
assert not isinstance(job, DeprecationsScan)
|
||||
|
||||
blockdef = BlockDef()
|
||||
blockdef.name = 'foo'
|
||||
|
@ -2083,7 +2169,9 @@ def test_export_import_with_deprecated(pub):
|
|||
],
|
||||
('blocks/foo', blockdef),
|
||||
)
|
||||
resp = get_app(pub).put(sign_uri('/api/export-import/bundle-import/'), bundle)
|
||||
resp = get_app(pub).post(
|
||||
sign_uri('/api/export-import/bundle-import/'), upload_files=[('bundle', 'bundle.tar', bundle)]
|
||||
)
|
||||
afterjob_url = resp.json['url']
|
||||
resp = get_app(pub).put(sign_uri(afterjob_url))
|
||||
assert resp.json['data']['status'] == 'failed'
|
||||
|
@ -2102,7 +2190,9 @@ def test_export_import_with_deprecated(pub):
|
|||
],
|
||||
('workflows/foo', workflow),
|
||||
)
|
||||
resp = get_app(pub).put(sign_uri('/api/export-import/bundle-import/'), bundle)
|
||||
resp = get_app(pub).post(
|
||||
sign_uri('/api/export-import/bundle-import/'), upload_files=[('bundle', 'bundle.tar', bundle)]
|
||||
)
|
||||
afterjob_url = resp.json['url']
|
||||
resp = get_app(pub).put(sign_uri(afterjob_url))
|
||||
assert resp.json['data']['status'] == 'failed'
|
||||
|
@ -2116,7 +2206,9 @@ def test_export_import_with_deprecated(pub):
|
|||
],
|
||||
('data-sources/foo', data_source),
|
||||
)
|
||||
resp = get_app(pub).put(sign_uri('/api/export-import/bundle-import/'), bundle)
|
||||
resp = get_app(pub).post(
|
||||
sign_uri('/api/export-import/bundle-import/'), upload_files=[('bundle', 'bundle.tar', bundle)]
|
||||
)
|
||||
afterjob_url = resp.json['url']
|
||||
resp = get_app(pub).put(sign_uri(afterjob_url))
|
||||
assert resp.json['data']['status'] == 'failed'
|
||||
|
@ -2131,7 +2223,9 @@ def test_export_import_with_deprecated(pub):
|
|||
],
|
||||
('wscalls/foo', wscall),
|
||||
)
|
||||
resp = get_app(pub).put(sign_uri('/api/export-import/bundle-import/'), bundle)
|
||||
resp = get_app(pub).post(
|
||||
sign_uri('/api/export-import/bundle-import/'), upload_files=[('bundle', 'bundle.tar', bundle)]
|
||||
)
|
||||
afterjob_url = resp.json['url']
|
||||
resp = get_app(pub).put(sign_uri(afterjob_url))
|
||||
assert resp.json['data']['status'] == 'failed'
|
||||
|
|
|
@ -10,6 +10,7 @@ import zipfile
|
|||
from contextlib import contextmanager
|
||||
|
||||
import pytest
|
||||
import responses
|
||||
from django.utils.encoding import force_bytes
|
||||
from django.utils.timezone import localtime, make_aware
|
||||
from quixote import get_publisher
|
||||
|
@ -28,6 +29,7 @@ from wcs.qommon import ods
|
|||
from wcs.qommon.http_request import HTTPRequest
|
||||
from wcs.qommon.upload_storage import PicklableUpload
|
||||
from wcs.wf.comment import WorkflowCommentPart
|
||||
from wcs.wf.form import WorkflowFormEvolutionPart, WorkflowFormFieldsFormDef
|
||||
from wcs.workflows import AttachmentEvolutionPart, Workflow, WorkflowBackofficeFieldsFormDef
|
||||
|
||||
from ..utilities import clean_temporary_pub, create_temporary_pub, get_app, login
|
||||
|
@ -49,6 +51,12 @@ def pub(emails):
|
|||
'''\
|
||||
[api-secrets]
|
||||
coucou = 1234
|
||||
|
||||
[variables]
|
||||
idp_api_url = https://authentic.example.invalid/api/'
|
||||
|
||||
[wscall-secrets]
|
||||
authentic.example.invalid = 4460cf12e156d841c116fbebd52d7ebe41282c63ac2605740068ba5fd89b7316
|
||||
'''
|
||||
)
|
||||
|
||||
|
@ -465,6 +473,106 @@ def test_formdata_backoffice_fields(pub, local_user):
|
|||
assert resp.json['workflow']['fields']['backoffice_blah'] == 'Hello world'
|
||||
|
||||
|
||||
@pytest.mark.parametrize('user', ['query-email', 'api-access'])
|
||||
@pytest.mark.parametrize('auth', ['signature', 'http-basic'])
|
||||
def test_formdata_workflow_form(pub, local_user, user, auth):
|
||||
app = get_app(pub)
|
||||
|
||||
if user == 'api-access':
|
||||
ApiAccess.wipe()
|
||||
access = ApiAccess()
|
||||
access.name = 'test'
|
||||
access.access_identifier = 'test'
|
||||
access.access_key = '12345'
|
||||
access.store()
|
||||
|
||||
if auth == 'http-basic':
|
||||
|
||||
def get_url(url, **kwargs):
|
||||
app.set_authorization(('Basic', ('test', '12345')))
|
||||
return app.get(url, **kwargs)
|
||||
|
||||
else:
|
||||
|
||||
def get_url(url, **kwargs):
|
||||
return app.get(sign_uri(url, orig=access.access_identifier, key=access.access_key), **kwargs)
|
||||
|
||||
else:
|
||||
if auth == 'http-basic':
|
||||
pytest.skip('http basic authentication requires ApiAccess')
|
||||
|
||||
def get_url(url, **kwargs):
|
||||
return app.get(sign_uri(url, user=local_user), **kwargs)
|
||||
|
||||
role = pub.role_class(name='test')
|
||||
role.id = '123'
|
||||
role.store()
|
||||
|
||||
Workflow.wipe()
|
||||
workflow = Workflow(name='foo')
|
||||
st = workflow.add_status('st1')
|
||||
form_action = st.add_action('form')
|
||||
form_action.varname = 'blah'
|
||||
form_action.formdef = WorkflowFormFieldsFormDef(item=form_action)
|
||||
form_action.formdef.fields = [
|
||||
fields.FileField(id='1', label='file', varname='file'),
|
||||
fields.StringField(id='2', label='str', varname='str'),
|
||||
]
|
||||
workflow.store()
|
||||
|
||||
FormDef.wipe()
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test'
|
||||
formdef.fields = []
|
||||
formdef.workflow_id = workflow.id
|
||||
formdef.workflow_roles = {'_receiver': role.id}
|
||||
formdef.store()
|
||||
|
||||
formdef.data_class().wipe()
|
||||
formdata = formdef.data_class()()
|
||||
formdata.just_created()
|
||||
data = {'1': PicklableUpload('test.txt', 'text/plain'), '2': 'text'}
|
||||
data['1'].receive([b'hello world wf form'])
|
||||
formdata.evolution[-1].parts = [
|
||||
WorkflowFormEvolutionPart(form_action, data),
|
||||
]
|
||||
formdata.store()
|
||||
|
||||
if user == 'api-access':
|
||||
access.roles = [role]
|
||||
access.store()
|
||||
else:
|
||||
local_user.roles = [role.id]
|
||||
local_user.store()
|
||||
|
||||
resp = get_url('/api/forms/test/%s/' % formdata.id, status=200)
|
||||
assert resp.json['evolution'][0]['parts'] == [
|
||||
{
|
||||
'data': {
|
||||
'file': 'test.txt',
|
||||
'file_raw': {
|
||||
'content': 'aGVsbG8gd29ybGQgd2YgZm9ybQ==',
|
||||
'content_type': 'text/plain',
|
||||
'filename': 'test.txt',
|
||||
},
|
||||
'file_url': None,
|
||||
'str': 'text',
|
||||
},
|
||||
'key': 'blah',
|
||||
'type': 'workflow-form',
|
||||
}
|
||||
]
|
||||
|
||||
resp = get_url('/api/forms/test/%s/?include-files-content=off' % formdata.id, status=200)
|
||||
assert resp.json['evolution'][0]['parts'] == [
|
||||
{
|
||||
'data': {'file': 'test.txt', 'file_url': None, 'str': 'text'},
|
||||
'key': 'blah',
|
||||
'type': 'workflow-form',
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_formdata_duplicated_varnames(pub, local_user):
|
||||
pub.role_class.wipe()
|
||||
role = pub.role_class(name='test')
|
||||
|
@ -1158,6 +1266,9 @@ def test_api_list_formdata(pub, local_user):
|
|||
resp = get_app(pub).get(sign_uri('/api/forms/test/list?full=on&order_by=-foobar', user=local_user))
|
||||
assert [d['fields']['foobar'] for d in resp.json] == ['FOO BAR %02d' % i for i in range(29, -1, -1)]
|
||||
|
||||
# check 400 on multiple order_by
|
||||
get_app(pub).get(sign_uri('/api/forms/test/list?full=on&order_by=f0,foobar', user=local_user), status=400)
|
||||
|
||||
# check fts
|
||||
resp = get_app(pub).get(sign_uri('/api/forms/test/list?full=on&q=foo', user=local_user))
|
||||
assert len(resp.json) == 30
|
||||
|
@ -2940,6 +3051,65 @@ def test_api_geojson_formdata_related_field(pub, local_user):
|
|||
assert properties['item - foobar'] == 'test.txt'
|
||||
|
||||
|
||||
def test_api_geojson_formdata_file_in_block_field(pub, local_user):
|
||||
pub.role_class.wipe()
|
||||
role = pub.role_class(name='test')
|
||||
role.store()
|
||||
|
||||
# add role to user
|
||||
local_user.roles = [role.id]
|
||||
local_user.store()
|
||||
|
||||
BlockDef.wipe()
|
||||
block = BlockDef()
|
||||
block.name = 'foobar'
|
||||
block.fields = [
|
||||
fields.FileField(id='123', label='file', varname='foo'),
|
||||
]
|
||||
block.store()
|
||||
|
||||
FormDef.wipe()
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test'
|
||||
formdef.workflow_roles = {'_receiver': role.id}
|
||||
formdef.fields = [
|
||||
fields.BlockField(id='1', label='test', varname='blockdata', block_slug='foobar', max_items=3),
|
||||
]
|
||||
formdef.geolocations = {'base': 'Location'}
|
||||
formdef.store()
|
||||
|
||||
data_class = formdef.data_class()
|
||||
data_class.wipe()
|
||||
upload = PicklableUpload('test.txt', 'text/plain', 'ascii')
|
||||
upload.receive([b'base64me'])
|
||||
|
||||
formdata = formdef.data_class()()
|
||||
formdata.data = {'1': {'data': [{'123': upload}], 'schema': {'123': 'file'}}, '1_display': 'test.txt'}
|
||||
formdata.geolocations = {'base': {'lat': 48, 'lon': 2}}
|
||||
formdata.just_created()
|
||||
formdata.store()
|
||||
|
||||
# get with blockfield
|
||||
resp = get_app(pub).get(sign_uri('/api/forms/test/geojson?filter=all&1=on', user=local_user))
|
||||
assert len(resp.json['features']) == 1
|
||||
assert resp.json['features'][0]['properties']['id'] == '1-1'
|
||||
assert resp.json['features'][0]['properties']['display_fields'][0]['value'] == 'test.txt'
|
||||
|
||||
# get with file field in block as property
|
||||
resp = get_app(pub).get(sign_uri('/api/forms/test/geojson?filter=all&1-123=on', user=local_user))
|
||||
assert len(resp.json['features']) == 1
|
||||
assert resp.json['features'][0]['properties']['id'] == '1-1'
|
||||
assert resp.json['features'][0]['properties']['display_fields'][0]['value'] == 'test.txt'
|
||||
assert 'download?f=1$0$123' in resp.json['features'][0]['properties']['display_fields'][0]['html_value']
|
||||
|
||||
# check full=on
|
||||
resp = get_app(pub).get(sign_uri('/api/forms/test/geojson?filter=all&full=on', user=local_user))
|
||||
assert len(resp.json['features']) == 1
|
||||
properties = {x['label']: x['value'] for x in resp.json['features'][0]['properties']['display_fields']}
|
||||
assert properties['test'] == 'test.txt'
|
||||
assert 'file' not in properties
|
||||
|
||||
|
||||
def test_api_distance_filter(pub, local_user):
|
||||
pub.role_class.wipe()
|
||||
role = pub.role_class(name='test')
|
||||
|
@ -2985,9 +3155,12 @@ def test_api_distance_filter(pub, local_user):
|
|||
get_app(pub).get(sign_uri('/api/forms/test/list?filter-distance=150000', user=local_user), status=400)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('user', ['query-email', 'api-access'])
|
||||
@pytest.mark.parametrize('user', ['query-email', 'api-access', 'idp-api-client'])
|
||||
@pytest.mark.parametrize('auth', ['signature', 'http-basic'])
|
||||
@responses.activate
|
||||
def test_api_ods_formdata(pub, local_user, user, auth):
|
||||
ApiAccess.wipe()
|
||||
|
||||
pub.role_class.wipe()
|
||||
role = pub.role_class(name='test')
|
||||
role.store()
|
||||
|
@ -3007,7 +3180,6 @@ def test_api_ods_formdata(pub, local_user, user, auth):
|
|||
data_class.wipe()
|
||||
|
||||
if user == 'api-access':
|
||||
ApiAccess.wipe()
|
||||
access = ApiAccess()
|
||||
access.name = 'test'
|
||||
access.access_identifier = 'test'
|
||||
|
@ -3025,6 +3197,29 @@ def test_api_ods_formdata(pub, local_user, user, auth):
|
|||
def get_url(url, **kwargs):
|
||||
return app.get(sign_uri(url, orig=access.access_identifier, key=access.access_key), **kwargs)
|
||||
|
||||
elif user == 'idp-api-client':
|
||||
if auth == 'signature':
|
||||
pytest.skip('signature authentication requires local user')
|
||||
|
||||
def get_url(url, **kwargs):
|
||||
app.set_authorization(('Basic', ('test', '12345')))
|
||||
return app.get(url, **kwargs)
|
||||
|
||||
responses.post(
|
||||
'https://authentic.example.invalid/api/check-api-client/',
|
||||
json={
|
||||
'err': 0,
|
||||
'data': {
|
||||
'is_active': True,
|
||||
'is_anonymous': False,
|
||||
'is_authenticated': True,
|
||||
'is_superuser': False,
|
||||
'restrict_to_anonymised_data': False,
|
||||
'roles': [],
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
else:
|
||||
if auth == 'http-basic':
|
||||
pytest.skip('http basic authentication requires ApiAccess')
|
||||
|
@ -3053,6 +3248,21 @@ def test_api_ods_formdata(pub, local_user, user, auth):
|
|||
if user == 'api-access':
|
||||
access.roles = [role]
|
||||
access.store()
|
||||
elif user == 'idp-api-client':
|
||||
responses.post(
|
||||
'https://authentic.example.invalid/api/check-api-client/',
|
||||
json={
|
||||
'err': 0,
|
||||
'data': {
|
||||
'is_active': True,
|
||||
'is_anonymous': False,
|
||||
'is_authenticated': True,
|
||||
'is_superuser': False,
|
||||
'restrict_to_anonymised_data': False,
|
||||
'roles': [role.id],
|
||||
},
|
||||
},
|
||||
)
|
||||
else:
|
||||
local_user.roles = [role.id]
|
||||
local_user.store()
|
||||
|
@ -3081,6 +3291,14 @@ def test_api_ods_formdata(pub, local_user, user, auth):
|
|||
formdef.store()
|
||||
get_url('/api/forms/test/ods', status=200)
|
||||
|
||||
if user == 'idp-api-client':
|
||||
# check a single api access object has been created
|
||||
assert ApiAccess.count() == 1
|
||||
api_access = ApiAccess.select()[0]
|
||||
assert api_access.idp_api_client
|
||||
assert api_access.access_identifier == '_idp_test'
|
||||
assert api_access.access_key is None
|
||||
|
||||
|
||||
def test_api_global_geojson(pub, local_user):
|
||||
pub.role_class.wipe()
|
||||
|
|
|
@ -1603,8 +1603,18 @@ def test_backoffice_map(pub):
|
|||
resp = app.get('/backoffice/management/form-title/')
|
||||
assert 'Plot on a Map' in resp.text
|
||||
resp = resp.click('Plot on a Map')
|
||||
assert 'data-geojson-url' in resp.text
|
||||
assert 'tiles.entrouvert.org/' in resp.text
|
||||
assert (
|
||||
resp.pyquery('.qommon-map')[0].attrib['data-geojson-url']
|
||||
== 'http://example.net/backoffice/management/form-title/geojson?'
|
||||
)
|
||||
assert (
|
||||
resp.pyquery('.qommon-map')[0].attrib['data-tile-urltemplate']
|
||||
== 'https://tiles.entrouvert.org/hdm/{z}/{x}/{y}.png'
|
||||
)
|
||||
assert (
|
||||
resp.pyquery('.qommon-map')[0].attrib['data-map-attribution']
|
||||
== 'Map data © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
||||
)
|
||||
|
||||
if not pub.site_options.has_section('options'):
|
||||
pub.site_options.add_section('options')
|
||||
|
@ -1615,7 +1625,10 @@ def test_backoffice_map(pub):
|
|||
|
||||
resp = app.get('/backoffice/management/form-title/')
|
||||
resp = resp.click('Plot on a Map')
|
||||
assert 'tile.example.net/' in resp.text
|
||||
assert (
|
||||
resp.pyquery('.qommon-map')[0].attrib['data-tile-urltemplate']
|
||||
== 'https://{s}.tile.example.net/{z}/{x}/{y}.png'
|
||||
)
|
||||
|
||||
# check query string is kept
|
||||
resp = app.get('/backoffice/management/form-title/map?filter=all')
|
||||
|
@ -2184,6 +2197,7 @@ def test_backoffice_download_as_zip(pub):
|
|||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/management/form-title/%s/' % number31.id)
|
||||
assert 'Download all files as .zip' not in resp
|
||||
formdef.management_sidebar_items = formdef.get_default_management_sidebar_items()
|
||||
formdef.management_sidebar_items.add('download-files')
|
||||
formdef.store()
|
||||
resp = app.get('/backoffice/management/form-title/%s/' % number31.id)
|
||||
|
@ -6192,3 +6206,50 @@ def test_backoffice_form_tracking_code_workflow_action(pub):
|
|||
formdata.refresh_from_storage()
|
||||
assert isinstance(formdata.evolution[-1].parts[0], WorkflowCommentPart)
|
||||
assert formdata.evolution[-1].who == str(user.id)
|
||||
|
||||
|
||||
def test_backoffice_compact_table_view(pub):
|
||||
user = create_user(pub)
|
||||
|
||||
workflow = Workflow(name='workflow')
|
||||
workflow.backoffice_fields_formdef = WorkflowBackofficeFieldsFormDef(workflow)
|
||||
workflow.backoffice_fields_formdef.fields = [
|
||||
fields.FileField(id='bo1', label='bo field 1'),
|
||||
]
|
||||
workflow.add_status('status1')
|
||||
workflow.store()
|
||||
|
||||
FormDef.wipe()
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test'
|
||||
formdef.fields = [fields.StringField(id='1', label='string')]
|
||||
formdef.workflow = workflow
|
||||
formdef.workflow_roles = {'_receiver': user.roles[0]}
|
||||
formdef.store()
|
||||
formdef.data_class().wipe()
|
||||
|
||||
formdata = formdef.data_class()()
|
||||
formdata.data = {'1': 'data', 'bo1': 'data'}
|
||||
formdata.just_created()
|
||||
formdata.store()
|
||||
|
||||
if not pub.site_options.has_section('options'):
|
||||
pub.site_options.add_section('options')
|
||||
pub.site_options.set('options', 'enable-compact-dataview', 'true')
|
||||
with open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w') as fd:
|
||||
pub.site_options.write(fd)
|
||||
|
||||
app = get_app(pub)
|
||||
|
||||
resp = login(app).get(formdata.get_backoffice_url())
|
||||
assert resp.pyquery('#compact-table-dataview-switch input')
|
||||
assert not resp.pyquery('#compact-table-dataview-switch input:checked')
|
||||
assert resp.pyquery('.dataview:not(.compact-dataview)')
|
||||
assert not resp.pyquery('.dataview.compact-dataview')
|
||||
|
||||
app.post_json('/api/user/preferences', {'use-compact-table-dataview': True}, status=200)
|
||||
resp = app.get(formdata.get_backoffice_url())
|
||||
assert resp.pyquery('#compact-table-dataview-switch input')
|
||||
assert resp.pyquery('#compact-table-dataview-switch input:checked')
|
||||
assert not resp.pyquery('.dataview:not(.compact-dataview)')
|
||||
assert resp.pyquery('.dataview.compact-dataview')
|
||||
|
|
|
@ -734,7 +734,7 @@ def test_blockdefs(pub, application_with_icon, application_without_icon, icon):
|
|||
)
|
||||
else:
|
||||
assert len(resp.pyquery('h3:contains("Applications") + .button-paragraph img')) == 0
|
||||
assert 'Field blocks outside applications' in resp
|
||||
assert 'Blocks of fields outside applications' in resp
|
||||
|
||||
# check application view
|
||||
resp = resp.click(href='application/%s/' % application.slug)
|
||||
|
@ -751,7 +751,7 @@ def test_blockdefs(pub, application_with_icon, application_without_icon, icon):
|
|||
|
||||
# check elements outside applications
|
||||
resp = resp.click(href='application/')
|
||||
assert resp.pyquery('h2').text() == 'Field blocks outside applications'
|
||||
assert resp.pyquery('h2').text() == 'Blocks of fields outside applications'
|
||||
assert len(resp.pyquery('ul.objects-list li')) == 1
|
||||
assert resp.pyquery('ul.objects-list li:nth-child(1)').text() == 'block1'
|
||||
|
||||
|
|
|
@ -398,6 +398,7 @@ def test_backoffice_card_item_link_id_template(pub):
|
|||
resp = resp.form.submit('submit')
|
||||
assert resp.location.endswith('/backoffice/data/foo/blah/')
|
||||
resp = resp.follow()
|
||||
assert resp.pyquery('.breadcrumbs a')[-1].attrib['href'] == '/backoffice/data/foo/blah/'
|
||||
resp = app.get('/backoffice/data/foo/')
|
||||
assert [x.attrib['href'] for x in resp.pyquery('table a')] == ['blah/', 'test/']
|
||||
|
||||
|
@ -681,6 +682,58 @@ def test_backoffice_cards_import_data_csv_no_backoffice_fields(pub):
|
|||
assert carddef.data_class().count() == 2
|
||||
|
||||
|
||||
def test_backoffice_cards_import_data_csv_custom_id_no_update(pub):
|
||||
user = create_user(pub)
|
||||
user.name_identifiers = [str(uuid.uuid4())]
|
||||
user.store()
|
||||
|
||||
Workflow.wipe()
|
||||
workflow = Workflow(name='form-title')
|
||||
workflow.backoffice_fields_formdef = WorkflowBackofficeFieldsFormDef(workflow)
|
||||
workflow.backoffice_fields_formdef.fields = [
|
||||
fields.StringField(id='bo0', varname='foo_bovar', label='bo variable'),
|
||||
]
|
||||
workflow.add_status('st0')
|
||||
workflow.store()
|
||||
|
||||
CardDef.wipe()
|
||||
carddef = CardDef()
|
||||
carddef.name = 'test'
|
||||
carddef.fields = [
|
||||
fields.StringField(id='1', label='String', varname='custom_id'),
|
||||
fields.ItemField(id='2', label='List', items=['item1', 'item2']),
|
||||
]
|
||||
carddef.backoffice_submission_roles = user.roles
|
||||
carddef.id_template = '{{form_var_custom_id}}'
|
||||
carddef.workflow = workflow
|
||||
carddef.store()
|
||||
carddef.data_class().wipe()
|
||||
|
||||
card = carddef.data_class()()
|
||||
card.data = {'1': 'plop', '2': 'test', '2_display': 'test', 'bo0': 'xxx'}
|
||||
card.just_created()
|
||||
card.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
data = b'''\
|
||||
"String","List"
|
||||
"plop","item1"
|
||||
"test","item2"
|
||||
'''
|
||||
resp = app.get('/backoffice/data/test/import-file')
|
||||
resp.forms[0]['file'] = Upload('test.csv', data, 'text/csv')
|
||||
resp.form['update_existing_cards'].checked = False
|
||||
resp = resp.forms[0].submit().follow()
|
||||
assert carddef.data_class().count() == 2
|
||||
|
||||
card.refresh_from_storage()
|
||||
assert card.data == {'1': 'plop', '2': 'test', '2_display': 'test', 'bo0': 'xxx'} # no change
|
||||
|
||||
other_card = carddef.data_class().select(order_by='-receipt_time')[0]
|
||||
assert other_card.data == {'1': 'test', '2': 'item2', '2_display': 'item2', 'bo0': None}
|
||||
assert other_card.id_display == 'test'
|
||||
|
||||
|
||||
def test_backoffice_cards_import_data_csv_custom_id_update(pub):
|
||||
user = create_user(pub)
|
||||
user.name_identifiers = [str(uuid.uuid4())]
|
||||
|
@ -721,6 +774,7 @@ def test_backoffice_cards_import_data_csv_custom_id_update(pub):
|
|||
'''
|
||||
resp = app.get('/backoffice/data/test/import-file')
|
||||
resp.forms[0]['file'] = Upload('test.csv', data, 'text/csv')
|
||||
resp.form['update_existing_cards'].checked = True
|
||||
resp = resp.forms[0].submit().follow()
|
||||
assert carddef.data_class().count() == 2
|
||||
|
||||
|
@ -760,7 +814,7 @@ def test_backoffice_cards_import_data_csv_blockfield(pub):
|
|||
assert sample_resp.text.splitlines()[0] == '"String","Block"'
|
||||
assert (
|
||||
sample_resp.text.splitlines()[1]
|
||||
== '"value","will be ignored - type Field Block (foobar) not supported"'
|
||||
== '"value","will be ignored - type Block of fields (foobar) not supported"'
|
||||
)
|
||||
|
||||
# block is required, error
|
||||
|
@ -1813,12 +1867,14 @@ def test_carddata_add_edit_related(pub):
|
|||
childdata = child.data_class().select()[0]
|
||||
assert len(childdata.get_workflow_traces()) == 1
|
||||
|
||||
AfterJob.wipe()
|
||||
resp = app.get('/backoffice/data/child/%s/wfedit-_editable?_popup=1' % childdata.id)
|
||||
assert resp.form['f1'].value == 'foo'
|
||||
assert resp.form['f2'].value == 'bar'
|
||||
resp.form['f1'] = 'foo2'
|
||||
resp.form['f2'] = 'bar2'
|
||||
resp = resp.form.submit('submit')
|
||||
assert AfterJob.count() == 1 # check a single job has been created to update relations
|
||||
childdata.refresh_from_storage()
|
||||
assert len(childdata.get_workflow_traces()) == 2
|
||||
|
||||
|
@ -2074,3 +2130,28 @@ def test_carddata_edit_items_display(pub):
|
|||
assert resp.status_int == 302
|
||||
resp = resp.follow()
|
||||
assert not resp.pyquery('#sect-dataview').text()
|
||||
|
||||
|
||||
def test_carddata_history_pane_default_mode(pub):
|
||||
CardDef.wipe()
|
||||
user = create_user(pub)
|
||||
|
||||
carddef = CardDef()
|
||||
carddef.name = 'foo'
|
||||
carddef.fields = []
|
||||
carddef.workflow_roles = {'_editor': user.roles[0]}
|
||||
carddef.store()
|
||||
carddef.data_class().wipe()
|
||||
|
||||
carddata = carddef.data_class()()
|
||||
carddata.just_created()
|
||||
carddata.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get(carddata.get_backoffice_url())
|
||||
assert resp.pyquery('#evolution-log.folded')
|
||||
|
||||
carddef.history_pane_default_mode = 'expanded'
|
||||
carddef.store()
|
||||
resp = app.get(carddata.get_backoffice_url())
|
||||
assert resp.pyquery('#evolution-log:not(.folded)')
|
||||
|
|
|
@ -149,14 +149,14 @@ def test_backoffice_submission_agent_column(pub):
|
|||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/management/form-title/')
|
||||
assert 'submission_agent' not in resp.forms['listing-settings'].fields
|
||||
assert 'submission-agent' not in resp.forms['listing-settings'].fields
|
||||
|
||||
formdef.backoffice_submission_roles = [role]
|
||||
formdef.store()
|
||||
|
||||
resp = app.get('/backoffice/management/form-title/')
|
||||
assert resp.text.count('</th>') == 6 # four columns
|
||||
resp.forms['listing-settings']['submission_agent'].checked = True
|
||||
resp.forms['listing-settings']['submission-agent'].checked = True
|
||||
resp = resp.forms['listing-settings'].submit()
|
||||
assert resp.text.count('</th>') == 7 # five columns
|
||||
assert resp.text.count('data-link') == 1 # 1 row
|
||||
|
@ -172,7 +172,7 @@ def test_backoffice_submission_agent_column(pub):
|
|||
formdata.store()
|
||||
|
||||
resp = app.get('/backoffice/management/form-title/')
|
||||
resp.forms['listing-settings']['submission_agent'].checked = True
|
||||
resp.forms['listing-settings']['submission-agent'].checked = True
|
||||
resp = resp.forms['listing-settings'].submit()
|
||||
assert resp.text.count('>agent<') == 1
|
||||
|
||||
|
|
|
@ -1426,52 +1426,102 @@ def test_backoffice_submission_agent_filter(pub):
|
|||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/management/form-title/?limit=100')
|
||||
# enable submission-agent column
|
||||
resp.forms['listing-settings']['submission_agent'].checked = True
|
||||
resp.forms['listing-settings']['submission-agent'].checked = True
|
||||
resp = resp.forms['listing-settings'].submit()
|
||||
assert resp.text.count('>userA<') > 0
|
||||
assert resp.text.count('>userB<') > 0
|
||||
# check the filter is hidden
|
||||
assert resp.pyquery.find('li[hidden] input[name=filter-submission-agent]')
|
||||
assert resp.text.count('>userA</td>') > 0
|
||||
assert resp.text.count('>userB</td>') > 0
|
||||
|
||||
base_url = resp.request.url
|
||||
resp = app.get(base_url + '&filter-submission-agent=on&filter-submission-agent-value=%s' % user1.id)
|
||||
assert resp.text.count('>userA<') > 0
|
||||
assert resp.text.count('>userB<') == 0
|
||||
assert resp.text.count('<tr') == 2
|
||||
assert resp.pyquery.find('input[value=userA]') # displayed in sidebar
|
||||
# check it persits on filter changes
|
||||
# enable submission-agent filter
|
||||
resp.forms['listing-settings']['filter-submission-agent'].checked = True
|
||||
resp = resp.forms['listing-settings'].submit()
|
||||
assert resp.text.count('>userA<') > 0
|
||||
assert resp.text.count('>userB<') == 0
|
||||
assert resp.text.count('<tr') == 2
|
||||
|
||||
resp = app.get(base_url + '&filter-submission-agent=on&filter-submission-agent-value=%s' % user2.id)
|
||||
assert resp.text.count('>userA<') == 0
|
||||
assert resp.text.count('>userB<') > 0
|
||||
assert resp.text.count('<tr') == 2
|
||||
# check everything is still displayed
|
||||
assert resp.forms['listing-settings']['filter-submission-agent-value'].value == ''
|
||||
assert resp.text.count('>userA</td>') > 0
|
||||
assert resp.text.count('>userB</td>') > 0
|
||||
|
||||
resp = app.get(
|
||||
'/backoffice/management/form-title/?limit=100&filter-submission-agent=on&filter-submission-agent-value=%s'
|
||||
% user2.id
|
||||
)
|
||||
assert resp.text.count('<tr') == 2
|
||||
# check available filter values
|
||||
assert [x.text for x in resp.pyquery('select[name="filter-submission-agent-value"] option')] == [
|
||||
None,
|
||||
'Current user',
|
||||
'admin',
|
||||
]
|
||||
|
||||
# add userA and userB to role for backoffice submission
|
||||
user1.roles = user.roles
|
||||
user1.store()
|
||||
user2.roles = user.roles
|
||||
user2.store()
|
||||
|
||||
# refresh
|
||||
resp = resp.forms['listing-settings'].submit()
|
||||
assert [x.text for x in resp.pyquery('select[name="filter-submission-agent-value"] option')] == [
|
||||
None,
|
||||
'Current user',
|
||||
'admin',
|
||||
'userA',
|
||||
'userB',
|
||||
]
|
||||
|
||||
resp.forms['listing-settings']['filter-submission-agent-value'].value = str(user1.id)
|
||||
resp = resp.forms['listing-settings'].submit()
|
||||
assert resp.text.count('>userA</td>') > 0
|
||||
assert resp.text.count('>userB</td>') == 0
|
||||
assert resp.pyquery('tbody tr').length == 1
|
||||
# check it persists on filter changes
|
||||
resp = resp.forms['listing-settings'].submit()
|
||||
assert resp.text.count('>userA</td>') > 0
|
||||
assert resp.text.count('>userB</td>') == 0
|
||||
assert resp.pyquery('tbody tr').length == 1
|
||||
|
||||
resp.forms['listing-settings']['filter-submission-agent-value'].value = str(user2.id)
|
||||
resp = resp.forms['listing-settings'].submit()
|
||||
assert resp.text.count('>userA</td>') == 0
|
||||
assert resp.text.count('>userB</td>') > 0
|
||||
assert resp.pyquery('tbody tr').length == 1
|
||||
|
||||
# filter on current user
|
||||
resp.forms['listing-settings']['filter-submission-agent-value'].value = '__current__'
|
||||
resp = resp.forms['listing-settings'].submit()
|
||||
assert resp.text.count('>userA</td>') == 0
|
||||
assert resp.text.count('>userB</td>') == 0
|
||||
assert resp.pyquery('tbody tr').length == 0
|
||||
|
||||
old_formdata_agent_id, formdata.submission_agent_id = formdata.submission_agent_id, user.id
|
||||
formdata.store()
|
||||
|
||||
resp = resp.forms['listing-settings'].submit()
|
||||
assert resp.text.count('>userA</td>') == 0
|
||||
assert resp.text.count('>admin</td>') == 1
|
||||
assert resp.pyquery('tbody tr').length == 1
|
||||
|
||||
# restore second formadata user
|
||||
formdata.submission_agent_id = old_formdata_agent_id
|
||||
formdata.store()
|
||||
|
||||
# filter on uuid
|
||||
user1.name_identifiers = ['0123456789']
|
||||
user1.store()
|
||||
resp = app.get(base_url + '&filter-submission-agent-uuid=0123456789')
|
||||
assert resp.text.count('>userA<') > 0
|
||||
assert resp.text.count('>userB<') == 0
|
||||
assert resp.pyquery.find('input[value=userA]') # displayed in sidebar
|
||||
resp = app.get(
|
||||
'/backoffice/management/form-title/?filter-submission-agent-uuid=0123456789&submission-agent=on'
|
||||
)
|
||||
assert resp.forms['listing-settings']['filter-submission-agent-value'].value == str(user1.id)
|
||||
assert resp.text.count('>userA</td>') > 0
|
||||
assert resp.text.count('>userB</td>') == 0
|
||||
# check it persists on filter changes
|
||||
resp = resp.forms['listing-settings'].submit()
|
||||
assert resp.text.count('>userA<') > 0
|
||||
assert resp.text.count('>userB<') == 0
|
||||
assert resp.text.count('>userA</td>') > 0
|
||||
assert resp.text.count('>userB</td>') == 0
|
||||
|
||||
# check with unknown uuid
|
||||
resp = app.get(base_url + '&filter-submission-agent-uuid=XXX')
|
||||
assert resp.text.count('>userA<') == 0
|
||||
assert resp.text.count('>userB<') == 0
|
||||
resp = app.get('/backoffice/management/form-title/?filter-submission-agent-uuid=XXX&submission-agent=on')
|
||||
assert resp.forms['listing-settings']['filter-submission-agent-value'].value == '-1'
|
||||
assert resp.text.count('>userA</td>') == 0
|
||||
assert resp.text.count('>userB</td>') == 0
|
||||
# check it persists on submits
|
||||
resp = resp.forms['listing-settings'].submit()
|
||||
assert resp.text.count('>userA</td>') == 0
|
||||
assert resp.text.count('>userB</td>') == 0
|
||||
|
||||
|
||||
def test_workflow_function_filter(pub):
|
||||
|
|
|
@ -22,6 +22,7 @@ from wcs.workflows import (
|
|||
Workflow,
|
||||
WorkflowBackofficeFieldsFormDef,
|
||||
WorkflowCriticalityLevel,
|
||||
WorkflowVariablesFieldsFormDef,
|
||||
)
|
||||
|
||||
from ..utilities import clean_temporary_pub, create_temporary_pub, get_app, login
|
||||
|
@ -1205,3 +1206,72 @@ def test_inspect_page_idp_role(pub):
|
|||
resp.pyquery('[data-function-key="_receiver"] a').attr.href
|
||||
== 'https://idp.example.net/manage/roles/uuid:d4b59e1ffb204dfd99fd3760f4952999/'
|
||||
)
|
||||
|
||||
|
||||
def test_inspect_page_form_option(pub):
|
||||
create_user(pub, is_admin=True)
|
||||
FormDef.wipe()
|
||||
|
||||
wf = Workflow(name='variables')
|
||||
wf.variables_formdef = WorkflowVariablesFieldsFormDef(workflow=wf)
|
||||
wf.add_status('st1')
|
||||
wf.store()
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'form title'
|
||||
formdef.fields = []
|
||||
formdef.workflow = wf
|
||||
formdef.store()
|
||||
formdef.data_class().wipe()
|
||||
|
||||
formdata = formdef.data_class()()
|
||||
formdata.just_created()
|
||||
formdata.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('%sinspect' % formdata.get_url(backoffice=True), status=200)
|
||||
assert 'form_option' not in resp.text
|
||||
|
||||
wf.variables_formdef.fields = [
|
||||
fields.StringField(label='String test', varname='string_test'),
|
||||
fields.DateField(label='Date test', varname='date_test'),
|
||||
]
|
||||
wf.store()
|
||||
resp = app.get('%sinspect' % formdata.get_url(backoffice=True), status=200)
|
||||
assert (
|
||||
resp.pyquery('[title="form_option_string_test"]').parents('li').children('div.value span').text()
|
||||
== 'None (no value)'
|
||||
)
|
||||
|
||||
wf.variables_formdef.fields[0].default_value = 'xxx'
|
||||
wf.variables_formdef.fields[1].default_value = '2024-03-20'
|
||||
wf.store()
|
||||
resp = app.get('%sinspect' % formdata.get_url(backoffice=True), status=200)
|
||||
assert (
|
||||
resp.pyquery('[title="form_option_string_test"]').parents('li').children('div.value span').text()
|
||||
== 'xxx'
|
||||
)
|
||||
assert (
|
||||
resp.pyquery('[title="form_option_date_test"]').parents('li').children('div.value span').text()
|
||||
== '2024-03-20'
|
||||
)
|
||||
assert (
|
||||
resp.pyquery('[title="form_option_date_test_year"]').parents('li').children('div.value span').text()
|
||||
== '2024 (integer number)'
|
||||
)
|
||||
|
||||
formdef.workflow_options = {'string_test': 'yyy', 'date_test': datetime.date(2024, 3, 21).timetuple()}
|
||||
formdef.store()
|
||||
resp = app.get('%sinspect' % formdata.get_url(backoffice=True), status=200)
|
||||
assert (
|
||||
resp.pyquery('[title="form_option_string_test"]').parents('li').children('div.value span').text()
|
||||
== 'yyy'
|
||||
)
|
||||
assert (
|
||||
resp.pyquery('[title="form_option_date_test"]').parents('li').children('div.value span').text()
|
||||
== '2024-03-21'
|
||||
)
|
||||
assert (
|
||||
resp.pyquery('[title="form_option_date_test_year"]').parents('li').children('div.value span').text()
|
||||
== '2024 (integer number)'
|
||||
)
|
||||
|
|
|
@ -516,7 +516,7 @@ def test_form_submit(pub):
|
|||
assert 'The form has been recorded' in next_page.text
|
||||
assert 'None' not in next_page.text
|
||||
assert formdef.data_class().count() == 1
|
||||
assert '<div class="section foldable folded" id="summary">' in next_page.text
|
||||
assert next_page.pyquery('#summary').attr['class'] == 'section foldable folded'
|
||||
assert next_page.pyquery('#summary .disclose-message')
|
||||
assert formdef.data_class().select()[0].submission_context['language'] == 'en'
|
||||
|
||||
|
@ -1362,7 +1362,7 @@ def test_form_submit_with_user(pub, emails):
|
|||
next_page = next_page.follow()
|
||||
assert 'The form has been recorded' in next_page.text
|
||||
assert formdef.data_class().count() == 1
|
||||
assert '<div class="section foldable folded" id="summary">' in next_page.text
|
||||
assert next_page.pyquery('#summary').attr['class'] == 'section foldable folded'
|
||||
# check the user received a copy by email
|
||||
assert emails.get('New form (test)')
|
||||
assert emails.get('New form (test)')['email_rcpt'] == ['foo@localhost']
|
||||
|
@ -3273,6 +3273,48 @@ def test_workflow_message_with_template_error(pub):
|
|||
assert logged_error.summary == "Error in template of workflow message ('int' object is not iterable)"
|
||||
|
||||
|
||||
def test_workflow_condition_on_message_age_in_hours(pub, freezer):
|
||||
create_user(pub)
|
||||
|
||||
formdef = create_formdef()
|
||||
formdef.fields = []
|
||||
formdef.store()
|
||||
|
||||
workflow = Workflow(name='test')
|
||||
st1 = workflow.add_status('Status1', 'st1')
|
||||
|
||||
display1 = st1.add_action('displaymsg')
|
||||
display1.message = 'message-to-all'
|
||||
display1.to = []
|
||||
|
||||
workflow.store()
|
||||
|
||||
formdef.workflow_id = workflow.id
|
||||
formdef.store()
|
||||
|
||||
formdef.data_class().wipe()
|
||||
|
||||
app = login(get_app(pub), username='foo', password='foo')
|
||||
page = app.get('/test/')
|
||||
page = page.forms[0].submit('submit') # form page
|
||||
page = page.forms[0].submit('submit') # confirmation page
|
||||
page = page.follow()
|
||||
assert 'message-to-all' in page.text
|
||||
|
||||
formdata = formdef.data_class().select()[0]
|
||||
page = app.get(formdata.get_url())
|
||||
assert 'message-to-all' in page.text
|
||||
|
||||
display1.condition = {'type': 'django', 'value': 'form_receipt_datetime|age_in_hours >= 1'}
|
||||
workflow.store()
|
||||
page = app.get(formdata.get_url())
|
||||
assert 'message-to-all' not in page.text
|
||||
|
||||
freezer.tick(60 * 60)
|
||||
page = app.get(formdata.get_url())
|
||||
assert 'message-to-all' in page.text
|
||||
|
||||
|
||||
def test_session_cookie_flags(pub):
|
||||
create_formdef()
|
||||
app = get_app(pub)
|
||||
|
@ -3949,6 +3991,22 @@ def test_email_actions(pub, emails):
|
|||
formdata.remove_self()
|
||||
app = get_app(pub)
|
||||
resp = app.get(action_url, status=404)
|
||||
assert 'This action link is no longer valid as the attached form has been removed.' in resp.text
|
||||
|
||||
# check action link referencing a formdata with an invalid/unknown status
|
||||
emails.empty()
|
||||
formdef.data_class().wipe()
|
||||
app = login(get_app(pub), username='foo', password='foo')
|
||||
resp = app.get(formdef.get_url())
|
||||
resp = resp.form.submit('submit')
|
||||
resp = resp.form.submit('submit')
|
||||
email_data = emails.get('New form2 (test email action)')
|
||||
action_url = re.findall(r'http.* ', email_data['payload'])[0].strip()
|
||||
formdata = formdef.data_class().select()[0]
|
||||
formdata.status = 'wf-abc'
|
||||
formdata.store()
|
||||
app = get_app(pub)
|
||||
resp = app.get(action_url, status=404)
|
||||
assert 'This action link is no longer valid' in resp.text
|
||||
|
||||
# two buttons on the same line, two urls
|
||||
|
@ -5139,10 +5197,34 @@ def test_form_honeypot(pub):
|
|||
resp.forms[0]['f0'] = 'plop'
|
||||
resp.forms[0]['f00'] = 'honey?'
|
||||
resp = resp.forms[0].submit('submit')
|
||||
assert 'Honey pot should be left untouched.' in resp
|
||||
assert 'Honey pots should be left untouched.' in resp
|
||||
assert formdef.data_class().count() == 0 # check no drafts have been saved
|
||||
|
||||
|
||||
def test_form_honeypot_level2(pub):
|
||||
pub.load_site_options()
|
||||
pub.site_options.set('options', 'honeypots', 'level2')
|
||||
with open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w') as fd:
|
||||
pub.site_options.write(fd)
|
||||
formdef = create_formdef()
|
||||
formdef.fields = [fields.StringField(id='0', label='string', required=False)]
|
||||
formdef.store()
|
||||
formdef.data_class().wipe()
|
||||
resp = get_app(pub).get('/test/')
|
||||
resp.forms[0]['f0'] = 'plop'
|
||||
assert resp.forms[0]['f002'].value == ''
|
||||
resp = resp.forms[0].submit('submit')
|
||||
assert 'Honey pots should be left untouched.' in resp
|
||||
assert formdef.data_class().count() == 0 # check no drafts have been saved
|
||||
|
||||
resp = get_app(pub).get('/test/')
|
||||
resp.forms[0]['f0'] = 'plop'
|
||||
resp.forms[0]['f002'].value = resp.pyquery('form')[0].attrib['data-honey-pot-value']
|
||||
resp = resp.forms[0].submit('submit') # -> validation
|
||||
resp = resp.forms[0].submit('submit') # -> submit
|
||||
assert formdef.data_class().count() == 1
|
||||
|
||||
|
||||
def test_structured_workflow_options(pub):
|
||||
create_user_and_admin(pub)
|
||||
|
||||
|
|
|
@ -77,6 +77,45 @@ def test_block_simple(pub):
|
|||
assert '>bar<' in resp
|
||||
|
||||
|
||||
def test_block_a11y(pub):
|
||||
FormDef.wipe()
|
||||
BlockDef.wipe()
|
||||
|
||||
block = BlockDef()
|
||||
block.name = 'foobar'
|
||||
block.fields = [
|
||||
fields.StringField(id='123', label='Test'),
|
||||
fields.StringField(id='234', label='Test2'),
|
||||
]
|
||||
block.store()
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'form title'
|
||||
formdef.fields = [
|
||||
fields.BlockField(id='1', label='test', block_slug='foobar'),
|
||||
]
|
||||
formdef.store()
|
||||
|
||||
app = get_app(pub)
|
||||
resp = app.get(formdef.get_url())
|
||||
assert resp.pyquery('.BlockWidget')[0].attrib.get('role') == 'group'
|
||||
assert resp.pyquery('.BlockWidget')[0].attrib.get('aria-labelledby')
|
||||
assert resp.pyquery('#' + resp.pyquery('.BlockWidget')[0].attrib.get('aria-labelledby'))
|
||||
|
||||
formdef.fields[0].label_display = 'subtitle'
|
||||
formdef.store()
|
||||
resp = app.get(formdef.get_url())
|
||||
assert resp.pyquery('.BlockWidget')[0].attrib.get('role')
|
||||
assert resp.pyquery('.BlockWidget')[0].attrib.get('aria-labelledby')
|
||||
assert resp.pyquery('#' + resp.pyquery('.BlockWidget')[0].attrib.get('aria-labelledby'))
|
||||
|
||||
formdef.fields[0].label_display = 'hidden'
|
||||
formdef.store()
|
||||
resp = app.get(formdef.get_url())
|
||||
assert not resp.pyquery('.BlockWidget')[0].attrib.get('role')
|
||||
assert not resp.pyquery('.BlockWidget')[0].attrib.get('aria-labelledby')
|
||||
|
||||
|
||||
def test_block_required(pub):
|
||||
FormDef.wipe()
|
||||
BlockDef.wipe()
|
||||
|
@ -2792,6 +2831,58 @@ def test_block_titles_and_empty_block_on_summary_page(pub, emails):
|
|||
assert 'Form Title' not in emails.get('New form (form title)')['msg'].get_payload()[0].get_payload()
|
||||
|
||||
|
||||
@pytest.mark.parametrize('logged_user', ['logged', 'anonymous'])
|
||||
@pytest.mark.parametrize('tracking_code', ['with-tracking-code', 'without-tracking-code'])
|
||||
def test_block_multiple_rows_single_draft(pub, logged_user, tracking_code):
|
||||
create_user(pub)
|
||||
FormDef.wipe()
|
||||
BlockDef.wipe()
|
||||
|
||||
block = BlockDef()
|
||||
block.name = 'foobar'
|
||||
block.fields = [fields.StringField(id='123', required=True, label='Test')]
|
||||
block.store()
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'form title'
|
||||
formdef.fields = [
|
||||
fields.BlockField(id='1', label='test', block_slug='foobar', max_items=5),
|
||||
]
|
||||
formdef.enable_tracking_codes = bool(tracking_code == 'with-tracking-code')
|
||||
formdef.store()
|
||||
formdef.data_class().wipe()
|
||||
|
||||
app = get_app(pub)
|
||||
if logged_user == 'logged':
|
||||
login(app, username='foo', password='foo')
|
||||
resp = app.get(formdef.get_url())
|
||||
resp.form['f1$element0$f123'].value = 'Hello World'
|
||||
resp = resp.form.submit('f1$add_element') # add second row
|
||||
|
||||
if logged_user == 'logged' or formdef.enable_tracking_codes:
|
||||
assert formdef.data_class().count() == 1
|
||||
assert formdef.data_class().select()[0].status == 'draft'
|
||||
else:
|
||||
assert formdef.data_class().count() == 0
|
||||
|
||||
resp.form['f1$element1$f123'].value = 'Something else'
|
||||
resp = resp.form.submit('f1$add_element') # add third row
|
||||
|
||||
if logged_user == 'logged' or formdef.enable_tracking_codes:
|
||||
assert formdef.data_class().count() == 1
|
||||
assert formdef.data_class().select()[0].status == 'draft'
|
||||
else:
|
||||
assert formdef.data_class().count() == 0
|
||||
|
||||
resp.form['f1$element2$f123'].value = 'Something else'
|
||||
resp = resp.form.submit('submit') # -> validation page
|
||||
resp = resp.form.submit('submit') # -> end page
|
||||
resp = resp.follow()
|
||||
|
||||
assert formdef.data_class().count() == 1
|
||||
assert formdef.data_class().select()[0].status == 'wf-new'
|
||||
|
||||
|
||||
def test_block_field_post_condition(pub):
|
||||
FormDef.wipe()
|
||||
BlockDef.wipe()
|
||||
|
|
|
@ -376,6 +376,42 @@ def test_form_recall_draft(pub):
|
|||
assert 'href="%s/"' % draft2.id in resp.text
|
||||
|
||||
|
||||
def test_form_recall_draft_digests(pub):
|
||||
user = create_user(pub)
|
||||
|
||||
formdef = create_formdef()
|
||||
formdef.fields = [fields.StringField(id='0', label='string', varname='name')]
|
||||
formdef.digest_templates = {'default': 'digest{{form_var_name}}digest'}
|
||||
formdef.store()
|
||||
formdef.data_class().wipe()
|
||||
|
||||
draft = formdef.data_class()()
|
||||
draft.user_id = user.id
|
||||
draft.status = 'draft'
|
||||
draft.data = {'0': 'DIGEST'}
|
||||
draft.store()
|
||||
|
||||
app = login(get_app(pub), username='foo', password='foo')
|
||||
resp = app.get('/test/')
|
||||
# single draft, digest is not displayed
|
||||
assert 'digestDIGESTdigest' not in resp.pyquery(f'[href="{draft.id}/"]').text()
|
||||
|
||||
draft2 = formdef.data_class()()
|
||||
draft2.user_id = user.id
|
||||
draft2.status = 'draft'
|
||||
draft2.data = {}
|
||||
draft2.store()
|
||||
|
||||
resp = app.get('/test/')
|
||||
# two drafts, the first one has its digest displayed
|
||||
assert 'digestDIGESTdigest' in resp.pyquery(f'[href="{draft.id}/"]').text()
|
||||
# the second doesn't have it as it contains "None"
|
||||
assert (
|
||||
resp.pyquery(f'[href="{draft2.id}/"]').text()
|
||||
and draft2.default_digest not in resp.pyquery(f'[href="{draft2.id}/"]').text()
|
||||
)
|
||||
|
||||
|
||||
def test_form_max_drafts(pub):
|
||||
user = create_user(pub)
|
||||
|
||||
|
@ -414,6 +450,14 @@ def test_form_max_drafts(pub):
|
|||
|
||||
assert not formdef.data_class().has_key(drafts[0].id) # oldest draft was removed
|
||||
|
||||
formdef.drafts_max_per_user = '3'
|
||||
formdef.store()
|
||||
|
||||
resp = app.get('/test/')
|
||||
resp.form['f0'] = 'hello2'
|
||||
resp = resp.form.submit('submit')
|
||||
assert formdef.data_class().count([Equal('status', 'draft')]) == 4
|
||||
|
||||
|
||||
def test_form_draft_temporary_access_url(pub):
|
||||
FormDef.wipe()
|
||||
|
@ -552,6 +596,92 @@ def test_nothing_to_update_add_row(pub):
|
|||
assert 'Technical error saving draft, please try again.' in resp.text
|
||||
|
||||
|
||||
def test_draft_with_block_data(pub):
|
||||
FormDef.wipe()
|
||||
BlockDef.wipe()
|
||||
|
||||
block = BlockDef()
|
||||
block.name = 'foobar'
|
||||
block.fields = [fields.StringField(id='123', required=True, label='Test1')]
|
||||
block.store()
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test'
|
||||
formdef.fields = [
|
||||
fields.PageField(id='0', label='1st page'),
|
||||
fields.PageField(id='2', label='2nd page'),
|
||||
fields.BlockField(id='3', label='block', block_slug='foobar', max_items=3),
|
||||
]
|
||||
formdef.store()
|
||||
|
||||
formdef.data_class().wipe()
|
||||
|
||||
create_user(pub)
|
||||
app = get_app(pub)
|
||||
resp = login(app, username='foo', password='foo').get('/test/')
|
||||
|
||||
resp = resp.form.submit('submit') # -> page 2
|
||||
resp.form['f3$element0$f123'] = 'foo'
|
||||
resp = resp.form.submit('submit') # -> confirmation page
|
||||
|
||||
resp = app.get('/test/')
|
||||
resp = resp.click('Continue with draft').follow()
|
||||
assert resp.forms[1]['f3$element0$f123'].value == 'foo'
|
||||
resp = resp.forms[1].submit('previous') # -> page 2
|
||||
assert resp.forms[1]['f3$element0$f123'].value == 'foo'
|
||||
resp = resp.forms[1].submit('submit') # -> confirmation page
|
||||
resp = resp.forms[1].submit('submit') # -> submit
|
||||
assert formdef.data_class().count() == 1
|
||||
formdata = formdef.data_class().select()[0]
|
||||
assert formdata.data == {
|
||||
'3': {'data': [{'123': 'foo'}], 'schema': {'123': 'string'}},
|
||||
'3_display': 'foobar',
|
||||
}
|
||||
|
||||
|
||||
def test_draft_with_block_data_tracking_code(pub):
|
||||
FormDef.wipe()
|
||||
BlockDef.wipe()
|
||||
|
||||
block = BlockDef()
|
||||
block.name = 'foobar'
|
||||
block.fields = [fields.StringField(id='123', required=True, label='Test1')]
|
||||
block.store()
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test'
|
||||
formdef.fields = [
|
||||
fields.PageField(id='0', label='1st page'),
|
||||
fields.PageField(id='2', label='2nd page'),
|
||||
fields.BlockField(id='3', label='block', block_slug='foobar', max_items=3),
|
||||
]
|
||||
formdef.enable_tracking_codes = True
|
||||
formdef.store()
|
||||
|
||||
formdef.data_class().wipe()
|
||||
|
||||
resp = get_app(pub).get('/test/')
|
||||
resp = resp.form.submit('submit') # -> page 2
|
||||
resp.form['f3$element0$f123'] = 'foo'
|
||||
resp = resp.form.submit('submit') # -> confirmation page
|
||||
tracking_code = get_displayed_tracking_code(resp)
|
||||
|
||||
resp = get_app(pub).get('/')
|
||||
resp.form['code'] = tracking_code
|
||||
resp = resp.form.submit().follow().follow().follow()
|
||||
assert resp.forms[1]['f3$element0$f123'].value == 'foo'
|
||||
resp = resp.forms[1].submit('previous') # -> page 2
|
||||
assert resp.forms[1]['f3$element0$f123'].value == 'foo'
|
||||
resp = resp.forms[1].submit('submit') # -> confirmation page
|
||||
resp = resp.forms[1].submit('submit') # -> submit
|
||||
assert formdef.data_class().count() == 1
|
||||
formdata = formdef.data_class().select()[0]
|
||||
assert formdata.data == {
|
||||
'3': {'data': [{'123': 'foo'}], 'schema': {'123': 'string'}},
|
||||
'3_display': 'foobar',
|
||||
}
|
||||
|
||||
|
||||
def test_draft_store_page_id(pub):
|
||||
formdef = create_formdef()
|
||||
formdef.enable_tracking_codes = True
|
||||
|
@ -813,3 +943,25 @@ def test_draft_store_page_id_when_no_page_and_no_confirmation(pub):
|
|||
assert formdef.data_class().count() == 1
|
||||
formdata = formdef.data_class().select()[0]
|
||||
assert formdata.status == 'wf-new'
|
||||
|
||||
|
||||
def test_draft_error_then_autosave(pub):
|
||||
formdef = create_formdef()
|
||||
formdef.enable_tracking_codes = True
|
||||
formdef.fields = [
|
||||
fields.PageField(id='0', label='1st page'),
|
||||
fields.StringField(id='1', label='string 1'),
|
||||
fields.PageField(id='2', label='2nd page'),
|
||||
]
|
||||
formdef.store()
|
||||
formdef.data_class().wipe()
|
||||
|
||||
app = get_app(pub)
|
||||
resp = app.get('/test/')
|
||||
resp = resp.form.submit('submit') # error
|
||||
assert formdef.data_class().count() == 1 # server roundtrip -> draft
|
||||
|
||||
resp.form['f1'] = 'test'
|
||||
app.post('/test/autosave', params=resp.form.submit_fields())
|
||||
assert formdef.data_class().count() == 1 # make sure same draft got reused
|
||||
assert formdef.data_class().select()[0].data['1'] == 'test'
|
||||
|
|
|
@ -11,6 +11,7 @@ from wcs.blocks import BlockDef
|
|||
from wcs.categories import Category
|
||||
from wcs.formdef import FormDef
|
||||
from wcs.qommon.errors import ConnectionError
|
||||
from wcs.wscalls import NamedWsCall
|
||||
|
||||
from ..utilities import clean_temporary_pub, create_temporary_pub, get_app, login
|
||||
from .test_all import create_user
|
||||
|
@ -463,9 +464,9 @@ def test_form_file_field_with_wrong_value(pub):
|
|||
assert pub.loggederror_class.count() == 1
|
||||
logged_error = pub.loggederror_class.select()[0]
|
||||
assert logged_error.formdef_id == formdef.id
|
||||
assert logged_error.summary == 'Failed to set value on field "file"'
|
||||
assert logged_error.exception_class == 'AttributeError'
|
||||
assert logged_error.exception_message == "'str' object has no attribute 'time'"
|
||||
assert logged_error.summary == 'Failed to convert value for field "file"'
|
||||
assert logged_error.exception_class == 'ValueError'
|
||||
assert logged_error.exception_message == "invalid data for file type ('foo bar wrong value')"
|
||||
|
||||
|
||||
def test_form_file_field_prefill(pub):
|
||||
|
@ -491,6 +492,72 @@ def test_form_file_field_prefill(pub):
|
|||
assert formdata.data['0'].get_content().startswith(b'\x89PNG')
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_form_file_field_dict_prefill(pub):
|
||||
NamedWsCall.wipe()
|
||||
wscall = NamedWsCall()
|
||||
wscall.name = 'Hello'
|
||||
wscall.request = {'url': 'http://example.net'}
|
||||
wscall.store()
|
||||
|
||||
FormDef.wipe()
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test'
|
||||
formdef.fields = [
|
||||
fields.FileField(
|
||||
id='0',
|
||||
label='file',
|
||||
prefill={'type': 'string', 'value': '{{ webservice.hello }}'},
|
||||
)
|
||||
]
|
||||
formdef.store()
|
||||
|
||||
responses.get(
|
||||
'http://example.net',
|
||||
json={'b64_content': 'aGVsbG8K', 'filename': 'hello.txt', 'content_type': 'text/plain'},
|
||||
)
|
||||
resp = get_app(pub).get('/test/')
|
||||
assert resp.form['f0$token']
|
||||
assert resp.click('hello.txt').content_type == 'text/plain'
|
||||
resp = resp.form.submit('submit') # -> validation
|
||||
resp = resp.form.submit('submit') # -> submit
|
||||
formdata = formdef.data_class().select()[0]
|
||||
assert formdata.data['0'].base_filename == 'hello.txt'
|
||||
assert formdata.data['0'].get_content() == b'hello\n'
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_form_file_field_url_prefill(pub):
|
||||
FormDef.wipe()
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test'
|
||||
formdef.fields = [
|
||||
fields.FileField(
|
||||
id='0',
|
||||
label='file',
|
||||
prefill={'type': 'string', 'value': 'http://example.net/hello.txt'},
|
||||
)
|
||||
]
|
||||
formdef.store()
|
||||
|
||||
responses.get('http://example.net/hello.txt', body=b'Hello\n', content_type='text/plain')
|
||||
resp = get_app(pub).get('/test/')
|
||||
assert resp.form['f0$token'].value
|
||||
assert resp.click('hello.txt').content_type == 'text/plain'
|
||||
resp = resp.form.submit('submit') # -> validation
|
||||
resp = resp.form.submit('submit') # -> submit
|
||||
formdata = formdef.data_class().select()[0]
|
||||
assert formdata.data['0'].base_filename == 'hello.txt'
|
||||
assert formdata.data['0'].get_content() == b'Hello\n'
|
||||
|
||||
pub.loggederror_class.wipe()
|
||||
responses.get('http://example.net/hello.txt', status=404)
|
||||
resp = get_app(pub).get('/test/')
|
||||
assert not resp.form['f0$token'].value
|
||||
assert 'hello.txt' not in resp.text
|
||||
assert [x.summary for x in pub.loggederror_class.select()] == ['Failed to convert value for field "file"']
|
||||
|
||||
|
||||
SVG_CONTENT = b'''<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 63.72 64.25" style="enable-background:new 0 0 63.72 64.25;" xml:space="preserve"> <g> </g> </svg>'''
|
||||
|
@ -592,3 +659,23 @@ def test_file_download_url_on_wrong_field(pub):
|
|||
resp = resp.form.submit('submit').follow() # -> submit
|
||||
formdata = formdef.data_class().select()[0]
|
||||
app.get(formdata.get_url() + 'files/1/', status=404)
|
||||
|
||||
|
||||
def test_file_auto_convert_heic(pub):
|
||||
FormDef.wipe()
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test'
|
||||
formdef.fields = [fields.FileField(id='0', label='field label')]
|
||||
formdef.store()
|
||||
formdef.data_class().wipe()
|
||||
|
||||
with open(os.path.join(os.path.dirname(__file__), '..', 'image.heic'), 'rb') as fd:
|
||||
upload = Upload('image.heic', fd.read(), 'image/heic')
|
||||
|
||||
resp = get_app(pub).get('/test/')
|
||||
resp.forms[0]['f0$file'] = upload
|
||||
resp = resp.forms[0].submit('submit') # -> validation
|
||||
resp = resp.forms[0].submit('submit') # -> submit
|
||||
resp = resp.follow()
|
||||
assert resp.click('image.jpeg').follow().content_type == 'image/jpeg'
|
||||
assert b'JFIF' in resp.click('image.jpeg').follow().body
|
||||
|
|
|
@ -1396,6 +1396,29 @@ def test_form_item_dynamic_map_data_source(pub, http_requests):
|
|||
assert len(resp_geojson.json['features']) == 2
|
||||
|
||||
|
||||
def test_form_item_map_data_source_initial_position(pub, http_requests):
|
||||
NamedDataSource.wipe()
|
||||
data_source = NamedDataSource(name='foobar')
|
||||
data_source.data_source = {
|
||||
'type': 'geojson',
|
||||
'value': 'http://remote.example.net/geojson',
|
||||
}
|
||||
data_source.id_property = 'id'
|
||||
data_source.label_template_property = '{{ text }}'
|
||||
data_source.cache_duration = '5'
|
||||
data_source.store()
|
||||
|
||||
formdef = create_formdef()
|
||||
formdef.fields = [
|
||||
fields.ItemField(id='1', label='map', display_mode='map', initial_position='geoloc'),
|
||||
]
|
||||
formdef.store()
|
||||
formdef.data_class().wipe()
|
||||
app = get_app(pub)
|
||||
resp = app.get('/test/')
|
||||
assert resp.pyquery('[data-init_with_geoloc="true"]')
|
||||
|
||||
|
||||
def test_form_item_timetable_data_source(pub, http_requests):
|
||||
NamedDataSource.wipe()
|
||||
data_source = NamedDataSource(name='foobar')
|
||||
|
|
|
@ -53,10 +53,19 @@ def test_form_map_field_back_and_submit(pub):
|
|||
),
|
||||
]
|
||||
formdef.store()
|
||||
resp = get_app(pub).get('/test/')
|
||||
formdef.data_class().wipe()
|
||||
resp = get_app(pub).get('/test/')
|
||||
assert 'qommon.map.js' in resp.text
|
||||
assert 'qommon.geolocation.js' in resp.text
|
||||
assert (
|
||||
resp.pyquery('.qommon-map')[0].attrib['data-tile-urltemplate']
|
||||
== 'https://tiles.entrouvert.org/hdm/{z}/{x}/{y}.png'
|
||||
)
|
||||
assert (
|
||||
resp.pyquery('.qommon-map')[0].attrib['data-map-attribution']
|
||||
== 'Map data © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
||||
)
|
||||
|
||||
# with a real user interaction this would get set by javascript
|
||||
resp.forms[0]['f0$latlng'].value = '1.234;-1.234'
|
||||
assert 'data-geolocation="road"' in resp.text
|
||||
|
|
|
@ -173,19 +173,10 @@ def test_form_draft_from_prefill(pub, field_type, logged_in):
|
|||
assert formdef.data_class().count() == 0
|
||||
formdef.data_class().wipe()
|
||||
|
||||
# draft created if there's been some prefilled fields
|
||||
# make sure no draft is created on prefilled fields
|
||||
formdef.fields[0].prefill = {'type': 'string', 'value': '{{request.GET.test|default:""}}'}
|
||||
formdef.store()
|
||||
app.get('/test/?test=hello')
|
||||
assert formdef.data_class().count() == 1
|
||||
formdef.data_class().wipe()
|
||||
|
||||
# unless the call was made from an application
|
||||
app.get('/test/?test=hello', headers={'User-agent': 'python-requests/0'})
|
||||
assert formdef.data_class().count() == 0
|
||||
|
||||
# or a bot
|
||||
app.get('/test/?test=hello', headers={'User-agent': 'Googlebot'})
|
||||
assert formdef.data_class().count() == 0
|
||||
|
||||
# check there's no leftover draft after submission
|
||||
|
@ -1683,3 +1674,78 @@ def test_form_page_prefill_and_tablerows_field(pub):
|
|||
resp = resp.form.submit('submit')
|
||||
assert resp.forms[0]['f1'].value == 'HELLO WORLD'
|
||||
assert not resp.pyquery('.widget-with-error')
|
||||
|
||||
|
||||
def test_form_page_user_data_source(pub):
|
||||
user = create_user(pub)
|
||||
|
||||
NamedDataSource.wipe()
|
||||
data_source = NamedDataSource(name='foobar')
|
||||
data_source.data_source = {'type': 'wcs:users'}
|
||||
data_source.store()
|
||||
|
||||
formdef = create_formdef()
|
||||
formdef.data_class().wipe()
|
||||
|
||||
for prefill_value in ('{{ session_user }}', '{{ session_user_id }}'):
|
||||
formdef.fields = [
|
||||
fields.ItemField(
|
||||
id='1',
|
||||
label='item',
|
||||
varname='item',
|
||||
hint='help text',
|
||||
required=False,
|
||||
data_source={'type': data_source.slug},
|
||||
prefill={'type': 'string', 'value': prefill_value},
|
||||
)
|
||||
]
|
||||
formdef.store()
|
||||
|
||||
resp = get_app(pub).get('/test/')
|
||||
assert resp.form['f1'].value == ''
|
||||
assert 'invalid value selected' not in resp.text
|
||||
|
||||
resp = login(get_app(pub), username='foo', password='foo').get('/test/')
|
||||
assert resp.form['f1'].value == str(user.id)
|
||||
assert 'invalid value selected' not in resp.text
|
||||
|
||||
|
||||
def test_form_page_template_block_rows_prefilled_with_form_data(pub):
|
||||
BlockDef.wipe()
|
||||
create_user(pub)
|
||||
|
||||
block = BlockDef()
|
||||
block.name = 'foobar'
|
||||
block.fields = [
|
||||
fields.StringField(
|
||||
id='123',
|
||||
required=True,
|
||||
label='Test',
|
||||
varname='test',
|
||||
prefill={'type': 'string', 'value': '{{ form_var_foo }}'},
|
||||
),
|
||||
]
|
||||
block.store()
|
||||
|
||||
formdef = create_formdef()
|
||||
formdef.data_class().wipe()
|
||||
formdef.fields = [
|
||||
fields.PageField(id='1', label='page1'),
|
||||
fields.StringField(id='2', label='text', varname='foo'),
|
||||
fields.PageField(id='3', label='page2'),
|
||||
fields.BlockField(id='4', label='test', block_slug='foobar', max_items=3),
|
||||
]
|
||||
formdef.store()
|
||||
|
||||
resp = get_app(pub).get('/test/')
|
||||
resp.form['f2'] = 'foo'
|
||||
resp = resp.form.submit('submit') # -> second page
|
||||
assert resp.form['f4$element0$f123'].value == 'foo'
|
||||
resp = resp.form.submit('f4$add_element')
|
||||
assert resp.form['f4$element1$f123'].value == 'foo'
|
||||
resp.form['f4$element0$f123'] = 'bar'
|
||||
resp = resp.form.submit('previous') # -> first page
|
||||
resp.form['f2'] = 'baz'
|
||||
resp = resp.form.submit('submit') # -> second page
|
||||
assert resp.form['f4$element0$f123'].value == 'bar' # not changed
|
||||
assert resp.form['f4$element1$f123'].value == 'baz' # updated
|
||||
|
|
Binary file not shown.
|
@ -624,6 +624,7 @@ def test_data_source_custom_view_digest(pub):
|
|||
'custom-view:view': '{{ form_var_foo }} Foo Bar',
|
||||
}
|
||||
carddef.store()
|
||||
pub.reset_caches()
|
||||
# rebuild digests
|
||||
carddata.store()
|
||||
carddata2.store()
|
||||
|
@ -761,6 +762,7 @@ def test_get_data_source_custom_view_order_by(pub):
|
|||
]
|
||||
carddef.digest_templates['custom-view:view'] = '{{ form_var_bar }}'
|
||||
carddef.store()
|
||||
pub.reset_caches()
|
||||
for carddata in carddef.data_class().select():
|
||||
carddata.store() # rebuild digests
|
||||
assert [i['text'] for i in CardDef.get_data_source_items('carddef:foo:view')] == [
|
||||
|
@ -1329,6 +1331,7 @@ def test_card_custom_id_format(pub):
|
|||
assert data_class.force_valid_id_characters('_Fôô bar-') == '_Foo-bar-'
|
||||
assert data_class.force_valid_id_characters('_Fôô bar☭-') == '_Foo-bar-'
|
||||
assert data_class.force_valid_id_characters('_Fôô bar❗') == '_Foo-bar'
|
||||
assert data_class.force_valid_id_characters(' Foo\'bar') == 'Foo-bar'
|
||||
|
||||
|
||||
def test_card_update_related(pub):
|
||||
|
@ -1386,6 +1389,7 @@ def test_card_update_related(pub):
|
|||
ItemsField(id='1', label='Test', data_source={'type': 'carddef:foo'}),
|
||||
]
|
||||
formdef.store()
|
||||
pub.reset_caches()
|
||||
|
||||
formdata = formdef.data_class()()
|
||||
formdata.data = {'1': ['1', '2']}
|
||||
|
@ -1419,6 +1423,7 @@ def test_card_update_related(pub):
|
|||
BlockField(id='2', label='Test2', block_slug=blockdef.slug), # left empty
|
||||
]
|
||||
formdef.store()
|
||||
pub.reset_caches()
|
||||
|
||||
formdata = formdef.data_class()()
|
||||
formdata.data = {
|
||||
|
@ -1515,6 +1520,80 @@ def test_card_update_related_with_custom_view(pub):
|
|||
assert formdata.data['1_display'] == 'view-card1-change1'
|
||||
|
||||
|
||||
def test_card_update_related_with_items_dynamic_custom_view(pub):
|
||||
CardDef.wipe()
|
||||
FormDef.wipe()
|
||||
pub.custom_view_class.wipe()
|
||||
|
||||
carddef = CardDef()
|
||||
carddef.name = 'foo'
|
||||
carddef.fields = [
|
||||
StringField(id='1', label='Test', varname='foo'),
|
||||
StringField(id='2', label='Test2'),
|
||||
]
|
||||
carddef.digest_templates = {
|
||||
'default': '{{ form_var_foo }}',
|
||||
'custom-view:view': 'view-{{ form_var_foo }}',
|
||||
}
|
||||
carddef.store()
|
||||
carddef.data_class().wipe()
|
||||
|
||||
carddata1 = carddef.data_class()()
|
||||
carddata1.data = {'1': 'card1', '2': 'ok'}
|
||||
carddata1.just_created()
|
||||
carddata1.store()
|
||||
|
||||
carddata2 = carddef.data_class()()
|
||||
carddata2.data = {'1': 'card2', '2': 'ok'}
|
||||
carddata2.just_created()
|
||||
carddata2.store()
|
||||
|
||||
custom_view = pub.custom_view_class()
|
||||
custom_view.title = 'view'
|
||||
custom_view.formdef = carddef
|
||||
custom_view.columns = {'list': [{'id': 'id'}]}
|
||||
custom_view.filters = {}
|
||||
custom_view.filters = {'filter-2': 'on', 'filter-2-value': '{{ form_var_data }}'}
|
||||
custom_view.visibility = 'datasource'
|
||||
custom_view.store()
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'foo'
|
||||
formdef.fields = [
|
||||
StringField(id='0', label='Foo', varname='data'),
|
||||
ItemsField(id='1', label='Test', data_source={'type': 'carddef:foo:view'}),
|
||||
]
|
||||
formdef.store()
|
||||
|
||||
formdata = formdef.data_class()()
|
||||
formdata.data = {'0': 'ok', '1': ['1']}
|
||||
formdata.data['1_display'] = 'view-card1'
|
||||
assert formdata.data['1_display'] == 'view-card1'
|
||||
formdata.just_created()
|
||||
formdata.store()
|
||||
|
||||
# check usual situation, carddata changed but is still present in the result set
|
||||
pub.cleanup()
|
||||
carddef = carddef.get(carddef.id)
|
||||
carddata1 = carddef.data_class().get(carddata1.id)
|
||||
carddata1.data = {'1': 'card1-change1', '2': 'ok'}
|
||||
carddata1.store()
|
||||
|
||||
formdata.refresh_from_storage()
|
||||
assert formdata.data['1'] == ['1']
|
||||
assert formdata.data['1_display'] == 'view-card1-change1'
|
||||
|
||||
# check with a card that will no longer be part of the custom view result set
|
||||
pub.cleanup()
|
||||
carddef = carddef.get(carddef.id)
|
||||
carddata1 = carddef.data_class().get(carddata1.id)
|
||||
carddata1.data = {'1': 'card1-change2', '2': 'ko'}
|
||||
carddata1.store()
|
||||
|
||||
formdata.refresh_from_storage()
|
||||
assert formdata.data['1_display'] == 'view-card1-change1' # no update, but data still here
|
||||
|
||||
|
||||
def test_card_update_related_cascading(pub):
|
||||
BlockDef.wipe()
|
||||
CardDef.wipe()
|
||||
|
@ -1626,6 +1705,57 @@ def test_card_update_related_cascading_loop(pub):
|
|||
assert carddata2.data['2_display'] == 'card1 card2 card1 None'
|
||||
|
||||
|
||||
def test_card_update_related_items_relation(pub):
|
||||
CardDef.wipe()
|
||||
FormDef.wipe()
|
||||
|
||||
carddef = CardDef()
|
||||
carddef.name = 'foo'
|
||||
carddef.fields = [
|
||||
StringField(id='1', label='Test', varname='foo'),
|
||||
]
|
||||
carddef.digest_templates = {'default': '{{ form_var_foo }}'}
|
||||
carddef.store()
|
||||
carddef.data_class().wipe()
|
||||
|
||||
carddata1 = carddef.data_class()()
|
||||
carddata1.data = {'1': 'card1'}
|
||||
carddata1.just_created()
|
||||
carddata1.store()
|
||||
|
||||
carddata2 = carddef.data_class()()
|
||||
carddata2.data = {'1': 'card2'}
|
||||
carddata2.just_created()
|
||||
carddata2.store()
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'foo'
|
||||
formdef.fields = [
|
||||
ItemField(id='1', label='Test', data_source={'type': 'carddef:foo'}),
|
||||
ItemsField(id='2', label='Test2', data_source={'type': 'carddef:foo'}),
|
||||
]
|
||||
formdef.store()
|
||||
|
||||
formdata = formdef.data_class()()
|
||||
formdata.data = {'1': '1', '2': ['1', '2']}
|
||||
formdata.data['1_display'] = formdef.fields[0].store_display_value(formdata.data, formdef.fields[0].id)
|
||||
formdata.data['2_display'] = formdef.fields[1].store_display_value(formdata.data, formdef.fields[1].id)
|
||||
assert formdata.data['1_display'] == 'card1'
|
||||
assert formdata.data['2_display'] == 'card1, card2'
|
||||
formdata.just_created()
|
||||
formdata.store()
|
||||
|
||||
pub.cleanup()
|
||||
carddef = carddef.get(carddef.id)
|
||||
carddata1 = carddef.data_class().get(carddata1.id)
|
||||
carddata1.data = {'1': 'card1-change1'}
|
||||
carddata1.store()
|
||||
|
||||
formdata.refresh_from_storage()
|
||||
assert formdata.data['1_display'] == 'card1-change1'
|
||||
assert formdata.data['2_display'] == 'card1-change1, card2'
|
||||
|
||||
|
||||
def test_card_update_related_deleted(pub):
|
||||
BlockDef.wipe()
|
||||
CardDef.wipe()
|
||||
|
|
|
@ -508,3 +508,40 @@ def test_comment_template_with_category(pub):
|
|||
export = ET.tostring(comment_template.export_to_xml(include_id=True))
|
||||
comment_template3 = CommentTemplate.import_from_xml_tree(ET.fromstring(export), include_id=True)
|
||||
assert comment_template3.category_id is None
|
||||
|
||||
|
||||
def test_comment_template_migration(pub):
|
||||
comment_template = CommentTemplate(name='test template')
|
||||
comment_template.description = 'hello'
|
||||
assert comment_template.migrate() is True
|
||||
assert not comment_template.description
|
||||
assert comment_template.documentation == 'hello'
|
||||
|
||||
|
||||
def test_comment_template_legacy_xml(pub):
|
||||
comment_template = CommentTemplate(name='test template')
|
||||
comment_template.documentation = 'hello'
|
||||
export = ET.tostring(export_to_indented_xml(comment_template))
|
||||
export = export.replace(b'documentation>', b'description>')
|
||||
|
||||
comment_template2 = CommentTemplate.import_from_xml_tree(ET.fromstring(export))
|
||||
comment_template2.store()
|
||||
comment_template2.refresh_from_storage()
|
||||
assert comment_template2.documentation
|
||||
|
||||
|
||||
def test_comment_template_documentation(pub, superuser):
|
||||
CommentTemplate.wipe()
|
||||
comment_template = CommentTemplate(name='foobar')
|
||||
comment_template.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
|
||||
resp = app.get(comment_template.get_admin_url())
|
||||
assert resp.pyquery('.documentation[hidden]')
|
||||
resp = app.post_json(comment_template.get_admin_url() + 'update-documentation', {'content': '<p>doc</p>'})
|
||||
assert resp.json == {'err': 0, 'empty': False, 'changed': True}
|
||||
comment_template.refresh_from_storage()
|
||||
assert comment_template.documentation == '<p>doc</p>'
|
||||
resp = app.get(comment_template.get_admin_url())
|
||||
assert resp.pyquery('.documentation:not([hidden])')
|
||||
|
|
|
@ -596,6 +596,26 @@ def test_backoffice_show_history(pub, user, formdef_class):
|
|||
}
|
||||
evo.add_part(part4)
|
||||
formdata.store()
|
||||
part5 = ContentSnapshotPart(formdata=formdata, old_data=copy.deepcopy(part4.new_data))
|
||||
part5.new_data = {
|
||||
'1': 'reset',
|
||||
'2': 'foo bar blah',
|
||||
'3': 'foo@bar.com',
|
||||
'4': True,
|
||||
'6': time.strptime('2022-11-06', '%Y-%m-%d'),
|
||||
'7': 'b',
|
||||
'7_display': 'b',
|
||||
'8': ['a', 'b'],
|
||||
'8_display': 'a, b',
|
||||
'9': '1.5;2.26',
|
||||
'10': {'cleartext': 'fooo'},
|
||||
'11': 'computed',
|
||||
# bad format, 12 is a block field
|
||||
'12': 'foobar',
|
||||
'bo1': 'foobar',
|
||||
}
|
||||
evo.add_part(part5)
|
||||
formdata.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get(formdata.get_backoffice_url())
|
||||
|
@ -741,6 +761,25 @@ def test_backoffice_show_history(pub, user, formdef_class):
|
|||
assert len(resp.pyquery('%s tr[data-field-id="12"]' % table4)) == 0
|
||||
assert len(resp.pyquery('%s tr[data-field-id="bo1"]' % table4)) == 0
|
||||
|
||||
assert resp.pyquery(
|
||||
'#evolutions fieldset[data-datetime="%s"] legend' % part5.datetime.isoformat()
|
||||
).text() == 'changed at %s' % localtime(part5.datetime).strftime('%Y-%m-%d %H:%M')
|
||||
table4 = '#evolutions table[data-datetime="%s"]' % part5.datetime.isoformat()
|
||||
assert len(resp.pyquery('%s tr[data-field-id="1"]' % table4)) == 0
|
||||
assert len(resp.pyquery('%s tr[data-field-id="2"]' % table4)) == 0
|
||||
assert len(resp.pyquery('%s tr[data-field-id="3"]' % table4)) == 0
|
||||
assert len(resp.pyquery('%s tr[data-field-id="4"]' % table4)) == 0
|
||||
assert len(resp.pyquery('%s tr[data-field-id="5"]' % table4)) == 0
|
||||
assert len(resp.pyquery('%s tr[data-field-id="6"]' % table4)) == 0
|
||||
assert len(resp.pyquery('%s tr[data-field-id="7"]' % table4)) == 0
|
||||
assert len(resp.pyquery('%s tr[data-field-id="8"]' % table4)) == 0
|
||||
assert len(resp.pyquery('%s tr[data-field-id="9"]' % table4)) == 0
|
||||
assert len(resp.pyquery('%s tr[data-field-id="10"]' % table4)) == 0
|
||||
assert len(resp.pyquery('%s tr[data-field-id="11"]' % table4)) == 0
|
||||
assert resp.pyquery('%s tr[data-field-id="12"] td' % table3).text() == 'Block'
|
||||
assert len(resp.pyquery('%s tr[data-block-id="12"]' % table3)) == 2
|
||||
assert len(resp.pyquery('%s tr[data-field-id="bo1"]' % table4)) == 0
|
||||
|
||||
# check user display
|
||||
part5 = ContentSnapshotPart(formdata=formdata, old_data=copy.deepcopy(part4.new_data))
|
||||
part5.new_data = copy.deepcopy(part4.new_data)
|
||||
|
|
|
@ -48,7 +48,7 @@ def test_get_admin_attributes():
|
|||
klass().get_admin_attributes()
|
||||
|
||||
|
||||
def test_add_to_form():
|
||||
def test_add_to_form(pub):
|
||||
for klass in fields.base.field_classes:
|
||||
form = Form(use_tokens=False)
|
||||
if klass is fields.PageField:
|
||||
|
@ -194,7 +194,7 @@ def test_bool():
|
|||
assert fields.BoolField().get_view_value(False) == 'No'
|
||||
|
||||
|
||||
def test_bool_stats():
|
||||
def test_bool_stats(pub):
|
||||
formdef = FormDef()
|
||||
formdef.name = 'title'
|
||||
formdef.url_name = 'title'
|
||||
|
@ -324,7 +324,7 @@ def test_file():
|
|||
assert fields.FileField().get_csv_value(upload) == ['/foo/bar']
|
||||
|
||||
|
||||
def test_page():
|
||||
def test_page(pub):
|
||||
formdef = FormDef()
|
||||
formdef.fields = []
|
||||
page = fields.PageField()
|
||||
|
@ -539,7 +539,7 @@ def test_map_set_value(pub):
|
|||
assert 'form_var_map_lon' not in keys
|
||||
|
||||
|
||||
def test_item_render():
|
||||
def test_item_render(pub):
|
||||
items_kwargs = []
|
||||
items_kwargs.append({'items': ['aa', 'ab', 'ac']})
|
||||
items_kwargs.append(
|
||||
|
@ -610,7 +610,7 @@ def test_item_render():
|
|||
assert str(form.render()).count('<option') == 1
|
||||
|
||||
|
||||
def test_item_render_as_autocomplete():
|
||||
def test_item_render_as_autocomplete(pub):
|
||||
field = fields.ItemField(id='1', label='Foobar', items=['aa', 'ab', 'ac'], display_mode='autocomplete')
|
||||
form = Form(use_tokens=False)
|
||||
field.add_to_form(form)
|
||||
|
@ -693,7 +693,7 @@ def test_item_render_as_list_with_hint(pub):
|
|||
assert len(PyQuery(str(form.render())).find('option')) == 3
|
||||
|
||||
|
||||
def test_item_render_as_radio():
|
||||
def test_item_render_as_radio(pub):
|
||||
items_kwargs = []
|
||||
items_kwargs.append({'items': ['aa', 'ab', 'ac']})
|
||||
items_kwargs.append(
|
||||
|
@ -760,7 +760,7 @@ def test_item_render_as_radio():
|
|||
assert str(form.render()).count('"radio"') == 1
|
||||
|
||||
|
||||
def test_item_radio_lengths():
|
||||
def test_item_radio_lengths(pub):
|
||||
field = fields.ItemField(id='1', label='Foobar', display_mode='radio', items=['aa', 'ab', 'ac'])
|
||||
form = Form(use_tokens=False)
|
||||
field.add_to_form(form)
|
||||
|
@ -786,7 +786,7 @@ def test_item_radio_lengths():
|
|||
assert 'widget-inline-radio' not in str(form.widgets[-1].render())
|
||||
|
||||
|
||||
def test_items_render():
|
||||
def test_items_render(pub):
|
||||
items_kwargs = []
|
||||
items_kwargs.append({'items': ['aa', 'ab', 'ac']})
|
||||
items_kwargs.append(
|
||||
|
@ -851,7 +851,7 @@ def test_table_rows():
|
|||
assert '<td>30.00</td>' in html_table
|
||||
|
||||
|
||||
def test_date():
|
||||
def test_date(pub):
|
||||
assert fields.DateField().convert_value_from_str('2015-01-04') is not None
|
||||
assert fields.DateField().convert_value_from_str('04/01/2015') is not None
|
||||
assert fields.DateField().convert_value_from_str('') is None
|
||||
|
@ -879,7 +879,7 @@ def test_date_anonymise(pub):
|
|||
assert formdata.data.get('0') == time.strptime('2023-03-28', '%Y-%m-%d')
|
||||
|
||||
|
||||
def test_file_convert_from_anything():
|
||||
def test_file_convert_from_anything(pub):
|
||||
assert fields.FileField().convert_value_from_anything(None) is None
|
||||
|
||||
value = fields.FileField().convert_value_from_anything({'content': 'hello', 'filename': 'test.txt'})
|
||||
|
|
|
@ -1347,6 +1347,14 @@ def test_objects_filter(pub):
|
|||
tmpl = Template('{{forms|objects:"form"|count}}')
|
||||
assert tmpl.render(context) == '1'
|
||||
|
||||
pub.loggederror_class.wipe()
|
||||
context = pub.substitutions.get_context_variables(mode='lazy')
|
||||
tmpl = Template('{{forms|objects:"form"|first|count}}')
|
||||
assert tmpl.render(context) == '0'
|
||||
assert pub.loggederror_class.count() == 1
|
||||
logged_error = pub.loggederror_class.select()[0]
|
||||
assert logged_error.summary == '|count used on uncountable value'
|
||||
|
||||
# called on invalid object
|
||||
pub.loggederror_class.wipe()
|
||||
tmpl = Template('{{xxx|objects:"form"|count}}')
|
||||
|
@ -1942,14 +1950,14 @@ def test_lazy_formdata_queryset_filter(pub, variable_test_data):
|
|||
assert tmpl.render(context) == 'None'
|
||||
assert pub.loggederror_class.count() == 1
|
||||
logged_error = pub.loggederror_class.select()[0]
|
||||
assert logged_error.summary == '|pending used on invalid queryset (\'\')'
|
||||
assert logged_error.summary == '|pending used on something else than a queryset (\'\')'
|
||||
|
||||
pub.loggederror_class.wipe()
|
||||
tmpl = Template('{{""|filter_value:"foo"}}')
|
||||
assert tmpl.render(context) == 'None'
|
||||
assert pub.loggederror_class.count() == 1
|
||||
logged_error = pub.loggederror_class.select()[0]
|
||||
assert logged_error.summary == '|filter_value used on invalid queryset (\'\')'
|
||||
assert logged_error.summary == '|filter_value used on something else than a queryset (\'\')'
|
||||
|
||||
|
||||
def test_lazy_formdata_queryset_filter_non_unique_varname(pub, variable_test_data):
|
||||
|
@ -1997,6 +2005,31 @@ def test_lazy_formdata_queryset_filter_non_unique_varname(pub, variable_test_dat
|
|||
assert tmpl.render(context) == '1'
|
||||
|
||||
|
||||
def test_filter_on_page_field(pub):
|
||||
pub.loggederror_class.wipe()
|
||||
|
||||
FormDef.wipe()
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test'
|
||||
formdef.fields = [
|
||||
fields.PageField(id='1', label='Page', varname='page'),
|
||||
]
|
||||
formdef.store()
|
||||
|
||||
data_class = formdef.data_class()
|
||||
formdata = data_class()
|
||||
formdata.just_created()
|
||||
formdata.store()
|
||||
|
||||
context = pub.substitutions.get_context_variables(mode='lazy')
|
||||
|
||||
tmpl = Template('{{forms|objects:"test"|filter_by:"page"|filter_value:"100"}}')
|
||||
tmpl.render(context)
|
||||
assert pub.loggederror_class.count() == 1
|
||||
logged_error = pub.loggederror_class.select()[0]
|
||||
assert logged_error.summary == 'Invalid filter "page"'
|
||||
|
||||
|
||||
def test_numeric_filter_on_string(pub):
|
||||
FormDef.wipe()
|
||||
formdef = FormDef()
|
||||
|
@ -2216,10 +2249,15 @@ def test_lazy_global_forms(pub):
|
|||
)
|
||||
assert tmpl.render(context) == '7,8,9,10,'
|
||||
|
||||
pub.loggederror_class.wipe()
|
||||
tmpl = Template('{{forms|objects:"foobarlazy"|with_custom_view:"private-form-view"|count}}')
|
||||
assert tmpl.render(context) == '0'
|
||||
assert [x.summary for x in pub.loggederror_class.select()] == ['Unknown custom view "private-form-view"']
|
||||
|
||||
pub.loggederror_class.wipe()
|
||||
tmpl = Template('{{forms|objects:"foobarlazy"|with_custom_view:"unknown"|count}}')
|
||||
assert tmpl.render(context) == '0'
|
||||
assert [x.summary for x in pub.loggederror_class.select()] == ['Unknown custom view "unknown"']
|
||||
|
||||
custom_view4 = pub.custom_view_class()
|
||||
custom_view4.title = 'unknown filter'
|
||||
|
@ -2228,6 +2266,8 @@ def test_lazy_global_forms(pub):
|
|||
custom_view4.filters = {'filter-42': 'on', 'filter-42-value': 'foo', 'filter-foobar': 'baz'}
|
||||
custom_view4.visibility = 'any'
|
||||
custom_view4.store()
|
||||
|
||||
pub.loggederror_class.wipe()
|
||||
tmpl = Template('{{forms|objects:"foobarlazy"|with_custom_view:"unknown-filter"|count}}')
|
||||
assert tmpl.render(context) == '0'
|
||||
assert pub.loggederror_class.count() == 2
|
||||
|
@ -4680,6 +4720,7 @@ def test_formdata_filtering_on_block_fields(pub):
|
|||
fields.DateField(id='4', label='Date', varname='date'),
|
||||
fields.EmailField(id='5', label='Email', varname='email'),
|
||||
fields.TextField(id='6', label='Text', varname='text'),
|
||||
fields.FileField(id='7', label='File', varname='file'),
|
||||
]
|
||||
block.store()
|
||||
|
||||
|
@ -4694,6 +4735,10 @@ def test_formdata_filtering_on_block_fields(pub):
|
|||
data_class = formdef.data_class()
|
||||
data_class.wipe()
|
||||
|
||||
upload = PicklableUpload('test.jpeg', 'image/jpeg')
|
||||
with open(os.path.join(os.path.dirname(__file__), 'image-with-gps-data.jpeg'), 'rb') as fd:
|
||||
upload.receive([fd.read()])
|
||||
|
||||
for i in range(14):
|
||||
formdata = data_class()
|
||||
formdata.data = {
|
||||
|
@ -5023,6 +5068,10 @@ def test_formdata_filtering_on_block_fields(pub):
|
|||
tmpl = Template('{{forms|objects:"test"|filter_by:"blockdata_text"|%s|count}}' % operator)
|
||||
assert tmpl.render(context) == result
|
||||
|
||||
# file
|
||||
tmpl = Template('{{forms|objects:"test"|filter_by:"blockdata_file"|absent|count}}')
|
||||
assert tmpl.render(context) == '0'
|
||||
|
||||
|
||||
def test_items_field_getlist(pub):
|
||||
NamedDataSource.wipe()
|
||||
|
@ -5735,6 +5784,7 @@ def test_reverse_links(pub):
|
|||
|
||||
# test reverse relation
|
||||
carddef1.store() # build & store reverse_relations
|
||||
pub.reset_caches()
|
||||
pub.substitutions.reset()
|
||||
pub.substitutions.feed(pub)
|
||||
pub.substitutions.feed(carddef1)
|
||||
|
@ -5749,6 +5799,7 @@ def test_reverse_links(pub):
|
|||
# test with natural id
|
||||
carddef1.id_template = 'X{{ form_var_name1 }}Y'
|
||||
carddef1.store()
|
||||
pub.reset_caches()
|
||||
carddata1.store()
|
||||
assert carddata1.id_display == 'Xfoo1Y'
|
||||
carddata2.data['1'] = carddata1.get_natural_key()
|
||||
|
|
|
@ -479,8 +479,8 @@ def test_unused_file_removal_job(pub):
|
|||
display_form.formdef = WorkflowFormFieldsFormDef(item=display_form)
|
||||
display_form.formdef.fields = []
|
||||
|
||||
data = {'blah_1': PicklableUpload('test.txt', 'text/plain')}
|
||||
data['blah_1'].receive([b'hello world wf form'])
|
||||
data = {'1': PicklableUpload('test.txt', 'text/plain')}
|
||||
data['1'].receive([b'hello world wf form'])
|
||||
formdata.evolution[-1].parts = [
|
||||
WorkflowFormEvolutionPart(display_form, data),
|
||||
]
|
||||
|
@ -647,7 +647,7 @@ def test_wipe_on_object(pub):
|
|||
formdef.wipe()
|
||||
|
||||
|
||||
def test_update_storage_all_formdefs(pub):
|
||||
def test_update_storage_all_formdefs(pub, capfd):
|
||||
CardDef.wipe()
|
||||
FormDef.wipe()
|
||||
|
||||
|
@ -664,6 +664,18 @@ def test_update_storage_all_formdefs(pub):
|
|||
update_storage_all_formdefs(pub)
|
||||
assert update_storage.call_count == 10
|
||||
|
||||
assert not capfd.readouterr().out
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'broken formdef'
|
||||
formdef.fields = [StringField(id='1', label='Test')]
|
||||
formdef.store()
|
||||
formdef.fields = [DateField(id='1', label='Test')]
|
||||
formdef.store()
|
||||
|
||||
update_storage_all_formdefs(pub)
|
||||
assert capfd.readouterr().out == '! Integrity errors in %s\n' % formdef.get_admin_url()
|
||||
|
||||
|
||||
def test_lazy_formdef(pub):
|
||||
FormDef.wipe()
|
||||
|
|
|
@ -87,7 +87,7 @@ def test_empty_display_locations_tag(pub):
|
|||
formdef = FormDef()
|
||||
formdef.name = 'Foo'
|
||||
formdef.fields = [
|
||||
fields.TitleField(label='title', display_locations=[]),
|
||||
fields.TitleField(label='title', display_locations=None),
|
||||
fields.SubtitleField(label='subtitle', display_locations=[]),
|
||||
fields.TextField(label='string', display_locations=[]),
|
||||
]
|
||||
|
|
|
@ -76,6 +76,12 @@ HOBO_JSON = {
|
|||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'service-id': 'lingo',
|
||||
'title': 'Lingo',
|
||||
'base_url': 'http://payment.example.net/',
|
||||
'secret_key': 'aaa',
|
||||
},
|
||||
],
|
||||
'profile': {
|
||||
'fields': [
|
||||
|
@ -293,6 +299,7 @@ def test_configure_site_options(setuptest, alt_tempdir):
|
|||
assert pub.get_site_option('xxx', 'variables') == 'HELLO WORLD'
|
||||
assert pub.get_site_option('portal_agent_url', 'variables') == 'http://agents.example.net/'
|
||||
assert pub.get_site_option('portal_url', 'variables') == 'http://portal.example.net/'
|
||||
assert pub.get_site_option('lingo_url', 'variables') == 'http://payment.example.net/'
|
||||
assert pub.get_site_option('test_wcs_url', 'variables') == 'http://wcs.example.net/'
|
||||
assert pub.get_site_option('disable_cron_jobs', 'variables') == 'True'
|
||||
assert pub.get_site_option('maintenance_page', 'variables') == 'True'
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
import pytest
|
||||
|
||||
from wcs.qommon.http_request import HTTPRequest
|
||||
from wcs.variables import LazyRequest
|
||||
|
||||
from .utilities import clean_temporary_pub, create_temporary_pub
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def pub():
|
||||
return create_temporary_pub()
|
||||
|
||||
|
||||
def teardown_module(module):
|
||||
clean_temporary_pub()
|
||||
|
||||
|
||||
def test_is_in_backoffice(pub):
|
||||
req = HTTPRequest(None, {'SCRIPT_NAME': '/', 'SERVER_NAME': 'example.net'})
|
||||
assert not req.is_in_backoffice()
|
||||
assert not LazyRequest(req).is_in_backoffice
|
||||
req = HTTPRequest(None, {'SCRIPT_NAME': '/backoffice/test', 'SERVER_NAME': 'example.net'})
|
||||
assert req.is_in_backoffice()
|
||||
assert LazyRequest(req).is_in_backoffice
|
||||
|
||||
|
||||
def test_is_from_mobile(pub):
|
||||
req = HTTPRequest(None, {'SCRIPT_NAME': '/', 'SERVER_NAME': 'example.net'})
|
||||
assert not req.is_from_mobile()
|
||||
assert not LazyRequest(req).is_from_mobile
|
||||
req = HTTPRequest(None, {'SCRIPT_NAME': '/', 'SERVER_NAME': 'example.net', 'HTTP_USER_AGENT': 'bot/1.0'})
|
||||
assert not req.is_from_mobile()
|
||||
assert not LazyRequest(req).is_from_mobile
|
||||
req = HTTPRequest(
|
||||
None,
|
||||
{'SCRIPT_NAME': '/', 'SERVER_NAME': 'example.net', 'HTTP_USER_AGENT': 'Mozilla/5.0 (Mobile) plop'},
|
||||
)
|
||||
assert req.is_from_mobile()
|
||||
assert LazyRequest(req).is_from_mobile
|
||||
req = HTTPRequest(
|
||||
None,
|
||||
{
|
||||
'SCRIPT_NAME': '/',
|
||||
'SERVER_NAME': 'example.net',
|
||||
'HTTP_USER_AGENT': 'Mozilla/5.0 (Chrome) Mobile Safari',
|
||||
},
|
||||
)
|
||||
assert req.is_from_mobile()
|
||||
assert LazyRequest(req).is_from_mobile
|
|
@ -542,3 +542,40 @@ def test_mail_template_with_category(pub):
|
|||
export = ET.tostring(mail_template.export_to_xml(include_id=True))
|
||||
mail_template3 = MailTemplate.import_from_xml_tree(ET.fromstring(export), include_id=True)
|
||||
assert mail_template3.category_id is None
|
||||
|
||||
|
||||
def test_mail_template_migration(pub):
|
||||
mail_template = MailTemplate(name='test template')
|
||||
mail_template.description = 'hello'
|
||||
assert mail_template.migrate() is True
|
||||
assert not mail_template.description
|
||||
assert mail_template.documentation == 'hello'
|
||||
|
||||
|
||||
def test_mail_template_legacy_xml(pub):
|
||||
mail_template = MailTemplate(name='test template')
|
||||
mail_template.documentation = 'hello'
|
||||
export = ET.tostring(export_to_indented_xml(mail_template))
|
||||
export = export.replace(b'documentation>', b'description>')
|
||||
|
||||
mail_template2 = MailTemplate.import_from_xml_tree(ET.fromstring(export))
|
||||
mail_template2.store()
|
||||
mail_template2.refresh_from_storage()
|
||||
assert mail_template2.documentation
|
||||
|
||||
|
||||
def test_mail_template_documentation(pub, superuser):
|
||||
MailTemplate.wipe()
|
||||
mail_template = MailTemplate(name='foobar')
|
||||
mail_template.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
|
||||
resp = app.get(mail_template.get_admin_url())
|
||||
assert resp.pyquery('.documentation[hidden]')
|
||||
resp = app.post_json(mail_template.get_admin_url() + 'update-documentation', {'content': '<p>doc</p>'})
|
||||
assert resp.json == {'err': 0, 'empty': False, 'changed': True}
|
||||
mail_template.refresh_from_storage()
|
||||
assert mail_template.documentation == '<p>doc</p>'
|
||||
resp = app.get(mail_template.get_admin_url())
|
||||
assert resp.pyquery('.documentation:not([hidden])')
|
||||
|
|
|
@ -20,13 +20,14 @@ from wcs.fields import StringField
|
|||
from wcs.qommon import evalutils, force_str
|
||||
from wcs.qommon.form import FileSizeWidget
|
||||
from wcs.qommon.http_request import HTTPRequest
|
||||
from wcs.qommon.humantime import humanduration2seconds, seconds2humanduration
|
||||
from wcs.qommon.humantime import humanduration2seconds, seconds2humanduration, timewords
|
||||
from wcs.qommon.misc import (
|
||||
_http_request,
|
||||
date_format,
|
||||
ellipsize,
|
||||
format_time,
|
||||
get_as_datetime,
|
||||
mark_spaces,
|
||||
normalize_geolocation,
|
||||
parse_decimal,
|
||||
parse_isotime,
|
||||
|
@ -108,6 +109,10 @@ def test_humantime_short(seconds, expected):
|
|||
assert seconds2humanduration(seconds, short=True) == expected
|
||||
|
||||
|
||||
def test_humantime_timewords():
|
||||
assert timewords() == ['day(s)', 'hour(s)', 'minute(s)', 'second(s)', 'month(s)', 'year(s)']
|
||||
|
||||
|
||||
def test_parse_mimetypes():
|
||||
assert FileTypesDirectory.parse_mimetypes('application/pdf') == ['application/pdf']
|
||||
assert FileTypesDirectory.parse_mimetypes('.pdf') == ['application/pdf']
|
||||
|
@ -511,7 +516,7 @@ def test_criteria_repr():
|
|||
|
||||
|
||||
def test_related_field_repr():
|
||||
from wcs.backoffice.management import RelatedField
|
||||
from wcs.backoffice.filter_fields import RelatedField
|
||||
|
||||
related_field = RelatedField(None, field=StringField(label='foo'), parent_field=StringField(label='bar'))
|
||||
assert 'foo' in repr(related_field)
|
||||
|
@ -755,3 +760,19 @@ def test_parse_decimal_keep_none(value, expected):
|
|||
def test_parse_decimal_do_raise(value, exception):
|
||||
with pytest.raises(exception):
|
||||
parse_decimal(value, do_raise=True)
|
||||
|
||||
|
||||
def test_mark_spaces():
|
||||
assert mark_spaces('test') == 'test'
|
||||
assert str(mark_spaces('<b>test</b>')) == '<b>test</b>'
|
||||
|
||||
button_code = (
|
||||
'<button class="toggle-escape-button" role="button" '
|
||||
'title="This line contains invisible characters."></button>'
|
||||
)
|
||||
space = '<span class="escaped-code-point" data-escaped="[U+0020]"><span class="char"> </span></span>'
|
||||
tab = '<span class="escaped-code-point" data-escaped="[U+0009]"><span class="char"> </span></span>'
|
||||
assert str(mark_spaces(' test ')) == button_code + space + 'test' + space
|
||||
assert str(mark_spaces(' test ')) == button_code + space + 'test' + space + space
|
||||
assert str(mark_spaces('test\t ')) == button_code + 'test' + tab + space
|
||||
assert str(mark_spaces(' <b>test</b>')) == button_code + space + '<b>test</b>'
|
||||
|
|
|
@ -26,7 +26,7 @@ from wcs.qommon.afterjobs import AfterJob
|
|||
from wcs.qommon.cron import CronJob
|
||||
from wcs.qommon.form import UploadedFile
|
||||
from wcs.qommon.http_request import HTTPRequest
|
||||
from wcs.qommon.publisher import Tenant
|
||||
from wcs.qommon.publisher import MaxSizeDict, Tenant
|
||||
from wcs.workflows import Workflow
|
||||
|
||||
from .utilities import clean_temporary_pub, create_temporary_pub
|
||||
|
@ -526,6 +526,65 @@ def test_cron_command_rewind_jobs(settings, freezer):
|
|||
assert sorted(jobs) == ['job1', 'job2', 'job3']
|
||||
|
||||
|
||||
def test_cron_command_job_exception(settings):
|
||||
create_temporary_pub()
|
||||
|
||||
def job1(pub, job=None):
|
||||
raise Exception('Error')
|
||||
|
||||
@classmethod
|
||||
def register_test_cronjobs(cls):
|
||||
cls.register_cronjob(CronJob(job1, name='job1', days=[10]))
|
||||
|
||||
get_publisher().set_tenant_by_hostname('example.net')
|
||||
sql.mark_cron_status('done')
|
||||
|
||||
with mock.patch('wcs.publisher.WcsPublisher.register_cronjobs', register_test_cronjobs):
|
||||
get_publisher_class().cronjobs = []
|
||||
clear_log_files()
|
||||
call_command('cron', job_name='job1', domain='example.net')
|
||||
assert get_logs('example.net') == [
|
||||
'start',
|
||||
"running jobs: ['job1']",
|
||||
'exception running job job1: Error',
|
||||
]
|
||||
|
||||
clean_temporary_pub()
|
||||
|
||||
|
||||
def test_cron_command_job_log(settings):
|
||||
pub = create_temporary_pub()
|
||||
|
||||
def job1(pub, job=None):
|
||||
job.log('hello')
|
||||
job.log_debug('debug')
|
||||
|
||||
@classmethod
|
||||
def register_test_cronjobs(cls):
|
||||
cls.register_cronjob(CronJob(job1, name='job1', days=[10]))
|
||||
|
||||
get_publisher().set_tenant_by_hostname('example.net')
|
||||
sql.mark_cron_status('done')
|
||||
|
||||
with mock.patch('wcs.publisher.WcsPublisher.register_cronjobs', register_test_cronjobs):
|
||||
get_publisher_class().cronjobs = []
|
||||
clear_log_files()
|
||||
call_command('cron', job_name='job1', domain='example.net')
|
||||
assert get_logs('example.net') == ['start', "running jobs: ['job1']", 'hello']
|
||||
|
||||
pub.load_site_options()
|
||||
pub.site_options.set('options', 'cron-log-level', 'debug')
|
||||
with open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w') as fd:
|
||||
pub.site_options.write(fd)
|
||||
|
||||
clear_log_files()
|
||||
call_command('cron', job_name='job1', domain='example.net')
|
||||
assert get_logs('example.net')[:3] == ['start', "running jobs: ['job1']", 'hello']
|
||||
assert re.match(r'\(mem: .*\) debug', get_logs('example.net')[3])
|
||||
|
||||
clean_temporary_pub()
|
||||
|
||||
|
||||
def test_clean_afterjobs():
|
||||
pub = create_temporary_pub()
|
||||
|
||||
|
@ -736,3 +795,17 @@ def test_get_site_language():
|
|||
|
||||
req.environ['HTTP_ACCEPT_LANGUAGE'] = 'xy,fr,en;q=0.7,es;q=0.3'
|
||||
assert pub.get_site_language() == 'fr'
|
||||
|
||||
|
||||
def test_maxsize_dict():
|
||||
d = MaxSizeDict()
|
||||
with pytest.raises(KeyError):
|
||||
d['a'] # noqa pylint: disable=pointless-statement
|
||||
for i in range(256):
|
||||
d[str(i)] = f'i : {i}'
|
||||
try:
|
||||
assert d['10'] # keep accessing low value
|
||||
except KeyError:
|
||||
pass
|
||||
# kept keys are the recently added one + '10' that we kept accessing
|
||||
assert set(d.keys()) == set(['10'] + [str(x) for x in range(129, 256)])
|
||||
|
|
|
@ -2,6 +2,7 @@ import io
|
|||
import os
|
||||
import shutil
|
||||
import xml.etree.ElementTree as ET
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
from quixote.http_request import Upload
|
||||
|
@ -11,7 +12,7 @@ from wcs.carddef import CardDef
|
|||
from wcs.categories import Category
|
||||
from wcs.comment_templates import CommentTemplate
|
||||
from wcs.data_sources import NamedDataSource
|
||||
from wcs.fields import CommentField, ItemField, PageField, StringField
|
||||
from wcs.fields import BlockField, CommentField, ItemField, PageField, StringField
|
||||
from wcs.formdef import FormDef
|
||||
from wcs.mail_templates import MailTemplate
|
||||
from wcs.qommon.form import UploadedFile
|
||||
|
@ -189,6 +190,14 @@ def test_snapshot_instance(pub):
|
|||
snapshots = pub.snapshot_class.select_object_history(carddef)
|
||||
assert len(snapshots) == 1
|
||||
|
||||
# check that DeprecationsScan is not run on instance load
|
||||
with mock.patch(
|
||||
'wcs.backoffice.deprecations.DeprecationsScan.check_deprecated_elements_in_object'
|
||||
) as check:
|
||||
snapshot = pub.snapshot_class.get_latest('formdef', formdef.id)
|
||||
assert snapshot.instance
|
||||
assert check.call_args_list == []
|
||||
|
||||
|
||||
def test_snapshot_user(pub):
|
||||
user = pub.user_class()
|
||||
|
@ -353,6 +362,61 @@ def test_form_snapshot_diff(pub):
|
|||
assert 'Snapshot <a href="%s/view/">%s</a> - (Version 42.0)' % (snapshot3.id, snapshot3.id) in resp
|
||||
|
||||
|
||||
def test_form_snapshot_diff_with_reference_error(pub):
|
||||
create_superuser(pub)
|
||||
create_role(pub)
|
||||
|
||||
BlockDef.wipe()
|
||||
blockdef = BlockDef()
|
||||
blockdef.name = 'testblock'
|
||||
blockdef.fields = []
|
||||
blockdef.store()
|
||||
|
||||
FormDef.wipe()
|
||||
formdef = FormDef()
|
||||
formdef.name = 'testform'
|
||||
formdef.fields = [
|
||||
BlockField(id='1', label='block1', varname='foo', block_slug=blockdef.slug),
|
||||
]
|
||||
formdef.store()
|
||||
assert pub.snapshot_class.count() == 2
|
||||
snapshot1 = pub.snapshot_class.get_latest('formdef', formdef.id)
|
||||
|
||||
formdef.fields.append(StringField(id=2, label='Test'))
|
||||
formdef.store()
|
||||
assert pub.snapshot_class.count() == 3
|
||||
|
||||
formdef.fields = formdef.fields[1:]
|
||||
formdef.store()
|
||||
assert pub.snapshot_class.count() == 4
|
||||
snapshot3 = pub.snapshot_class.get_latest('formdef', formdef.id)
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get(
|
||||
'/backoffice/forms/%s/history/compare?version1=%s&version2=%s'
|
||||
% (formdef.id, snapshot1.id, snapshot3.id)
|
||||
)
|
||||
assert resp.pyquery('h2').text() == 'Compare snapshots (XML)'
|
||||
resp = app.get(
|
||||
'/backoffice/forms/%s/history/compare?version1=%s&version2=%s&mode=inspect'
|
||||
% (formdef.id, snapshot1.id, snapshot3.id)
|
||||
)
|
||||
assert resp.pyquery('h2').text() == 'Compare snapshots (Inspect)'
|
||||
|
||||
BlockDef.wipe()
|
||||
resp = app.get(
|
||||
'/backoffice/forms/%s/history/compare?version1=%s&version2=%s'
|
||||
% (formdef.id, snapshot1.id, snapshot3.id)
|
||||
)
|
||||
assert resp.pyquery('h2').text() == 'Compare snapshots (XML)'
|
||||
resp = app.get(
|
||||
'/backoffice/forms/%s/history/compare?version1=%s&version2=%s&mode=inspect'
|
||||
% (formdef.id, snapshot1.id, snapshot3.id)
|
||||
)
|
||||
assert resp.pyquery('h2').text() == 'Error'
|
||||
assert 'Can not display snapshot (Unknown referenced objects)' in resp.text
|
||||
|
||||
|
||||
def test_form_snapshot_comments(pub):
|
||||
create_superuser(pub)
|
||||
create_role(pub)
|
||||
|
|
|
@ -17,10 +17,12 @@ import wcs.sql_criterias as st
|
|||
from wcs import fields, sql
|
||||
from wcs.blocks import BlockDef
|
||||
from wcs.carddef import CardDef
|
||||
from wcs.categories import Category
|
||||
from wcs.data_sources import NamedDataSource
|
||||
from wcs.formdata import Evolution
|
||||
from wcs.formdef import FormDef
|
||||
from wcs.qommon import force_str
|
||||
from wcs.testdef import TestDef
|
||||
from wcs.wf.register_comment import RegisterCommenterWorkflowStatusItem
|
||||
from wcs.workflows import (
|
||||
ActionsTracingEvolutionPart,
|
||||
|
@ -1576,6 +1578,51 @@ def test_all_forms_user_name_change(pub, formdef):
|
|||
conn.commit()
|
||||
|
||||
|
||||
def test_all_forms_category_change(pub, formdef):
|
||||
Category.wipe()
|
||||
FormDef.wipe()
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test'
|
||||
formdef.store()
|
||||
|
||||
formdata = formdef.data_class()()
|
||||
formdata.store()
|
||||
|
||||
conn, cur = sql.get_connection_and_cursor()
|
||||
cur.execute('SELECT category_id FROM wcs_all_forms WHERE formdef_id = %s', (formdef.id,))
|
||||
row = cur.fetchone()
|
||||
assert row[0] is None
|
||||
|
||||
category = Category()
|
||||
category.name = 'Test'
|
||||
category.store()
|
||||
|
||||
formdef.category_id = category.id
|
||||
formdef.store()
|
||||
cur.execute('SELECT category_id FROM wcs_all_forms WHERE formdef_id = %s', (formdef.id,))
|
||||
row = cur.fetchone()
|
||||
assert row[0] == int(category.id)
|
||||
|
||||
category2 = Category()
|
||||
category2.name = 'Test2'
|
||||
category2.store()
|
||||
formdef.category_id = category2.id
|
||||
formdef.store()
|
||||
cur.execute('SELECT category_id FROM wcs_all_forms WHERE formdef_id = %s', (formdef.id,))
|
||||
row = cur.fetchone()
|
||||
assert row[0] == int(category2.id)
|
||||
|
||||
formdef.category_id = None
|
||||
formdef.store()
|
||||
cur.execute('SELECT category_id FROM wcs_all_forms WHERE formdef_id = %s', (formdef.id,))
|
||||
row = cur.fetchone()
|
||||
assert row[0] is None
|
||||
|
||||
cur.close()
|
||||
conn.commit()
|
||||
|
||||
|
||||
def test_views_fts(pub):
|
||||
drop_formdef_tables()
|
||||
_, cur = sql.get_connection_and_cursor()
|
||||
|
@ -1692,6 +1739,26 @@ def test_load_all_evolutions_on_any_formdata(pub):
|
|||
assert len([x for x in objects if x._evolution is not None]) == 100
|
||||
|
||||
|
||||
def test_store_on_any_formdata(pub):
|
||||
drop_formdef_tables()
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test any store'
|
||||
formdef.fields = []
|
||||
formdef.store()
|
||||
|
||||
data_class = formdef.data_class(mode='sql')
|
||||
formdata = data_class()
|
||||
formdata.just_created()
|
||||
formdata.receipt_time = localtime()
|
||||
formdata.store()
|
||||
|
||||
objects = sql.AnyFormData.select()
|
||||
assert len(objects) == 1
|
||||
with pytest.raises(TypeError):
|
||||
objects[0].store()
|
||||
|
||||
|
||||
def test_geoloc_in_global_view(pub):
|
||||
drop_formdef_tables()
|
||||
|
||||
|
@ -2355,7 +2422,7 @@ def test_migration_59_all_forms_table(pub):
|
|||
formdata.store()
|
||||
|
||||
conn, cur = sql.get_connection_and_cursor()
|
||||
cur.execute('DROP TABLE wcs_all_forms')
|
||||
cur.execute('DROP TABLE wcs_all_forms CASCADE')
|
||||
cur.execute(
|
||||
'DROP TRIGGER %s ON %s' % (sql.get_formdef_trigger_name(formdef), sql.get_formdef_table_name(formdef))
|
||||
)
|
||||
|
@ -2917,3 +2984,77 @@ def test_sql_data_views(pub_with_views, formdef_class):
|
|||
assert column_exists_in_table(cur, f'{prefix}_test', 'geoloc_base_x')
|
||||
conn.commit()
|
||||
cur.close()
|
||||
|
||||
|
||||
def test_sql_integrity_errors(pub):
|
||||
FormDef.wipe()
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test'
|
||||
formdef.fields = [
|
||||
fields.StringField(id='1', label='string'),
|
||||
]
|
||||
formdef.store()
|
||||
assert not formdef.sql_integrity_errors
|
||||
|
||||
formdef.fields = [
|
||||
fields.FileField(id='1', label='string'),
|
||||
]
|
||||
formdef.store()
|
||||
assert formdef.sql_integrity_errors == {'1': {'got': 'character varying', 'expected': 'bytea'}}
|
||||
|
||||
|
||||
def test_testdef_user_uuid_migration(pub):
|
||||
pub.user_class.wipe()
|
||||
|
||||
user = pub.user_class(name='new user')
|
||||
user.email = 'new@example.com'
|
||||
user.store()
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test title'
|
||||
formdef.store()
|
||||
|
||||
formdata = formdef.data_class()()
|
||||
formdata.just_created()
|
||||
formdata.user_id = user.id
|
||||
|
||||
testdef = TestDef()
|
||||
testdef.name = 'First test'
|
||||
testdef.object_type = formdef.get_table_name()
|
||||
testdef.object_id = formdef.id
|
||||
testdef.data = {
|
||||
'data': [],
|
||||
'user': formdata.user.get_json_export_dict(),
|
||||
}
|
||||
testdef.store()
|
||||
|
||||
testdef2 = TestDef()
|
||||
testdef2.name = 'First test'
|
||||
testdef2.object_type = formdef.get_table_name()
|
||||
testdef2.object_id = formdef.id
|
||||
testdef2.data = {
|
||||
'data': [],
|
||||
'user': formdata.user.get_json_export_dict(),
|
||||
}
|
||||
testdef2.store()
|
||||
|
||||
conn, cur = sql.get_connection_and_cursor()
|
||||
cur.execute('UPDATE wcs_meta SET value = 106 WHERE key = %s', ('sql_level',))
|
||||
|
||||
sql.migrate()
|
||||
assert sql.is_reindex_needed('testdef', conn=conn, cur=cur) is True
|
||||
assert pub.user_class.count() == 1
|
||||
conn.commit()
|
||||
cur.close()
|
||||
sql.reindex()
|
||||
|
||||
assert pub.user_class.count() == 2
|
||||
test_user = pub.user_class.select([st.NotNull('test_uuid')])[0]
|
||||
|
||||
testdef = TestDef.get(testdef.id)
|
||||
assert not 'user' in testdef.data
|
||||
assert testdef.user_uuid == test_user.test_uuid
|
||||
|
||||
testdef2 = TestDef.get(testdef2.id)
|
||||
assert not 'user' in testdef2.data
|
||||
assert testdef2.user_uuid == test_user.test_uuid
|
||||
|
|
|
@ -31,6 +31,15 @@ class Foobar(StorableObject):
|
|||
unique_value = None
|
||||
|
||||
|
||||
class Foobar2(StorableObject):
|
||||
_names = 'tests%s' % random.randint(0, 100000)
|
||||
_indexes = ['unique_value']
|
||||
_hashed_indexes = ['value']
|
||||
|
||||
value = None
|
||||
unique_value = None
|
||||
|
||||
|
||||
def test_store():
|
||||
test = Foobar()
|
||||
test.value = 'value'
|
||||
|
@ -307,3 +316,34 @@ def test_umask():
|
|||
cache_umask()
|
||||
test.store()
|
||||
assert (os.stat(test.get_object_filename()).st_mode % 0o1000) == 0o664
|
||||
|
||||
|
||||
def test_publisher_cache():
|
||||
pub.reset_caches()
|
||||
|
||||
Foobar.wipe()
|
||||
Foobar2.wipe()
|
||||
|
||||
test = Foobar()
|
||||
test.value = 'value'
|
||||
test.unique_value = 'unique-value'
|
||||
test.store()
|
||||
|
||||
test2 = Foobar2()
|
||||
test2.value = 'value'
|
||||
test2.unique_value = 'unique-value'
|
||||
test2.store()
|
||||
|
||||
test = Foobar.cached_get('1')
|
||||
assert test.value == 'value'
|
||||
assert Foobar.cached_get('1') is test # same object
|
||||
|
||||
assert Foobar.get_on_index('unique-value', 'unique_value') is not test
|
||||
assert Foobar.get_on_index('unique-value', 'unique_value', use_cache=True) is test
|
||||
|
||||
assert Foobar2.cached_get('1') is not test
|
||||
assert Foobar2.cached_get('1') is Foobar2.get_on_index('unique-value', 'unique_value', use_cache=True)
|
||||
|
||||
with pytest.raises(KeyError):
|
||||
Foobar2.get_on_index('unique-value', 'invalid', use_cache=True)
|
||||
assert Foobar2.get_on_index('unique-value', 'invalid', use_cache=True, ignore_errors=True) is None
|
||||
|
|
|
@ -1819,3 +1819,22 @@ def test_temporary_access_url(pub):
|
|||
# removed formdata
|
||||
formdata.remove_self()
|
||||
assert Template('{% temporary_access_url %}').render(context) == ''
|
||||
|
||||
|
||||
def test_housenumber_templatefilters(pub):
|
||||
assert Template('{{ "42"|housenumber_number }}').render() == '42'
|
||||
assert Template('{{ "42"|housenumber_btq }}').render() == ''
|
||||
assert Template('{{ "42bis"|housenumber_number }}').render() == '42'
|
||||
assert Template('{{ "42bis"|housenumber_btq }}').render() == 'bis'
|
||||
assert Template('{{ " 42 bis "|housenumber_number }}').render() == '42'
|
||||
assert Template('{{ " 42 bis "|housenumber_btq }}').render() == 'bis'
|
||||
assert Template('{{ "42 3 t "|housenumber_number }}').render() == '42'
|
||||
assert Template('{{ "42 3 t "|housenumber_btq }}').render() == '3 t'
|
||||
assert Template('{{ " bis "|housenumber_number }}').render() == ''
|
||||
assert Template('{{ " bis "|housenumber_btq }}').render() == ''
|
||||
assert Template('{{ 42|housenumber_number }}').render() == '42'
|
||||
assert Template('{{ 42|housenumber_btq }}').render() == ''
|
||||
assert Template('{{ ""|housenumber_number }}').render() == ''
|
||||
assert Template('{{ ""|housenumber_btq }}').render() == ''
|
||||
assert Template('{{ null|housenumber_number }}').render({'null': None}) == ''
|
||||
assert Template('{{ null|housenumber_btq }}').render({'null': None}) == ''
|
||||
|
|
|
@ -78,10 +78,11 @@ def test_testdef_export_to_xml(pub):
|
|||
WebserviceResponse.wipe()
|
||||
|
||||
testdef2 = TestDef.import_from_xml(io.BytesIO(testdef_xml), formdef)
|
||||
testdef2.store()
|
||||
assert testdef2.name == 'test'
|
||||
assert testdef2.object_type == 'formdefs'
|
||||
assert testdef2.object_id == str(formdef.id)
|
||||
assert testdef2.data == {'fields': {'1': ['foo', 'baz'], '2': True}, 'user': None}
|
||||
assert testdef2.data == {'fields': {'1': ['foo', 'baz'], '2': True}}
|
||||
assert testdef2.expected_error == 'xxx'
|
||||
assert testdef2.is_in_backoffice is False
|
||||
|
||||
|
@ -1271,6 +1272,49 @@ def test_computed_field_forms_template_access(pub):
|
|||
assert testdef.recorded_errors == ['Invalid filter "unknown"']
|
||||
|
||||
|
||||
def test_numeric_field_support(pub):
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test title'
|
||||
formdef.fields = [
|
||||
fields.PageField(
|
||||
id='0',
|
||||
label='1st page',
|
||||
post_conditions=[
|
||||
{'condition': {'type': 'django', 'value': 'form_var_foo == 13.12'}, 'error_message': ''}
|
||||
],
|
||||
),
|
||||
fields.NumericField(
|
||||
id='1', label='Numeric', varname='foo', restrict_to_integers=False, min_value=decimal.Decimal(10)
|
||||
),
|
||||
]
|
||||
formdef.store()
|
||||
|
||||
formdata = formdef.data_class()()
|
||||
formdata.just_created()
|
||||
formdata.data['1'] = decimal.Decimal(13.12)
|
||||
|
||||
testdef = TestDef.create_from_formdata(formdef, formdata)
|
||||
testdef.store()
|
||||
testdef.run(formdef)
|
||||
|
||||
formdata.data['1'] = decimal.Decimal(9)
|
||||
testdef = TestDef.create_from_formdata(formdef, formdata)
|
||||
|
||||
with pytest.raises(TestError) as excinfo:
|
||||
testdef.run(formdef)
|
||||
assert (
|
||||
str(excinfo.value)
|
||||
== 'Invalid value "9" for field "Numeric": You should enter a number greater than or equal to 10.'
|
||||
)
|
||||
|
||||
formdata.data['1'] = decimal.Decimal(42)
|
||||
testdef = TestDef.create_from_formdata(formdef, formdata)
|
||||
|
||||
with pytest.raises(TestError) as excinfo:
|
||||
testdef.run(formdef)
|
||||
assert str(excinfo.value) == 'Page 1 post condition was not met (form_var_foo == 13.12).'
|
||||
|
||||
|
||||
def test_expected_error(pub):
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test title'
|
||||
|
|
|
@ -330,3 +330,24 @@ def test_clean_deleted_users(pub):
|
|||
call_command('cron', job_name='clean_deleted_users', domain='example.net')
|
||||
|
||||
assert User.count() == 0
|
||||
|
||||
|
||||
def test_normal_users_test_users_isolation(pub):
|
||||
pub.user_class.wipe()
|
||||
|
||||
user = pub.user_class()
|
||||
user.name = 'Jean'
|
||||
user.email = 'jean@example.com'
|
||||
user.store()
|
||||
|
||||
user = pub.user_class()
|
||||
user.name = 'Jean'
|
||||
user.email = 'jean@example.com'
|
||||
user.test_uuid = '42'
|
||||
user.store()
|
||||
|
||||
assert len(pub.user_class.select()) == 1
|
||||
assert pub.user_class.select()[0].test_uuid is None
|
||||
|
||||
assert len(pub.user_class.get_users_with_email('jean@example.com')) == 1
|
||||
assert pub.user_class.get_users_with_email('jean@example.com')[0].test_uuid is None
|
||||
|
|
|
@ -89,6 +89,17 @@ def test_status_forced_endpoint(pub):
|
|||
assert wf2.possible_status[1].forced_endpoint is False
|
||||
|
||||
|
||||
def test_status_with_loop(pub):
|
||||
wf = Workflow(name='status')
|
||||
st1 = wf.add_status('Status1', 'st1')
|
||||
st2 = wf.add_status('Status2', 'st2')
|
||||
st1.loop_items_template = '{{ "abc"|make_list }}'
|
||||
st1.after_loop_status = str(st2.id)
|
||||
wf2 = assert_import_export_works(wf)
|
||||
assert wf2.possible_status[0].loop_items_template == '{{ "abc"|make_list }}'
|
||||
assert wf2.possible_status[0].after_loop_status == wf2.possible_status[1].id
|
||||
|
||||
|
||||
def test_default_wf(pub):
|
||||
wf = Workflow.get_default_workflow()
|
||||
assert_import_export_works(wf)
|
||||
|
@ -494,9 +505,12 @@ def test_backoffice_fields(pub):
|
|||
wf = Workflow(name='bo fields')
|
||||
wf.backoffice_fields_formdef = WorkflowBackofficeFieldsFormDef(wf)
|
||||
wf.backoffice_fields_formdef.fields = [
|
||||
StringField(id='bo1', label='1st backoffice field', varname='backoffice_blah'),
|
||||
StringField(
|
||||
id='bo1', label='1st backoffice field', varname='backoffice_blah', display_locations=None
|
||||
),
|
||||
]
|
||||
assert_import_export_works(wf, True)
|
||||
wf2 = assert_import_export_works(wf)
|
||||
assert wf2.backoffice_fields_formdef.fields[0].display_locations == []
|
||||
|
||||
|
||||
def test_complex_dispatch_action(pub):
|
||||
|
@ -1087,3 +1101,38 @@ def test_import_root_node_error():
|
|||
excinfo.value.msg
|
||||
== 'Provided XML file is invalid, it starts with a <wrong_root_node> tag instead of <workflow>'
|
||||
)
|
||||
|
||||
|
||||
def test_documentation_attributes(pub):
|
||||
Workflow.wipe()
|
||||
workflow = Workflow(name='Workflow One')
|
||||
workflow.documentation = 'doc1'
|
||||
status = workflow.add_status(name='New status')
|
||||
status.documentation = 'doc2'
|
||||
action = status.add_action('anonymise')
|
||||
action.documentation = 'doc3'
|
||||
workflow.backoffice_fields_formdef = WorkflowBackofficeFieldsFormDef(workflow)
|
||||
workflow.backoffice_fields_formdef.documentation = 'doc4'
|
||||
workflow.backoffice_fields_formdef.fields = [
|
||||
StringField(id='bo234', label='bo field 1'),
|
||||
]
|
||||
workflow.backoffice_fields_formdef.fields[0].documentation = 'doc5'
|
||||
workflow.variables_formdef = WorkflowVariablesFieldsFormDef(workflow=workflow)
|
||||
workflow.variables_formdef.documentation = 'doc6'
|
||||
workflow.variables_formdef.fields = [
|
||||
StringField(id='va123', label='var field 1'),
|
||||
]
|
||||
workflow.variables_formdef.fields[0].documentation = 'doc7'
|
||||
global_action = workflow.add_global_action('action1')
|
||||
global_action.documentation = 'doc8'
|
||||
workflow.store()
|
||||
|
||||
wf2 = assert_import_export_works(workflow)
|
||||
assert wf2.documentation == 'doc1'
|
||||
assert wf2.possible_status[0].documentation == 'doc2'
|
||||
assert wf2.possible_status[0].items[0].documentation == 'doc3'
|
||||
assert wf2.backoffice_fields_formdef.documentation == 'doc4'
|
||||
assert wf2.backoffice_fields_formdef.fields[0].documentation == 'doc5'
|
||||
assert wf2.variables_formdef.documentation == 'doc6'
|
||||
assert wf2.variables_formdef.fields[0].documentation == 'doc7'
|
||||
assert wf2.global_actions[0].documentation == 'doc8'
|
||||
|
|
|
@ -9,10 +9,15 @@ from wcs.qommon.http_request import HTTPRequest
|
|||
from wcs.testdef import TestDef, WebserviceResponse
|
||||
from wcs.wf.jump import JumpWorkflowStatusItem, _apply_timeouts
|
||||
from wcs.workflow_tests import WorkflowTestError
|
||||
from wcs.workflows import Workflow, WorkflowBackofficeFieldsFormDef, WorkflowStatusItem
|
||||
from wcs.workflows import (
|
||||
Workflow,
|
||||
WorkflowBackofficeFieldsFormDef,
|
||||
WorkflowCriticalityLevel,
|
||||
WorkflowStatusItem,
|
||||
)
|
||||
|
||||
from .backoffice_pages.test_all import create_user
|
||||
from .utilities import create_temporary_pub, get_app, login
|
||||
from .utilities import clean_temporary_pub, create_temporary_pub, get_app, login
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
@ -31,10 +36,11 @@ def pub():
|
|||
return pub
|
||||
|
||||
|
||||
def test_workflow_tests_ignore_unsupported_items(pub, monkeypatch):
|
||||
user = pub.user_class(name='test user')
|
||||
user.store()
|
||||
def teardown_module(module):
|
||||
clean_temporary_pub()
|
||||
|
||||
|
||||
def test_workflow_tests_ignore_unsupported_items(pub, monkeypatch):
|
||||
workflow = Workflow(name='Workflow One')
|
||||
new_status = workflow.add_status(name='New status')
|
||||
end_status = workflow.add_status(name='End status')
|
||||
|
@ -53,7 +59,6 @@ def test_workflow_tests_ignore_unsupported_items(pub, monkeypatch):
|
|||
formdata.just_created()
|
||||
|
||||
testdef = TestDef.create_from_formdata(formdef, formdata)
|
||||
testdef.agent_id = user.id
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.AssertStatus(status_name='End status'),
|
||||
]
|
||||
|
@ -68,9 +73,6 @@ def test_workflow_tests_ignore_unsupported_items(pub, monkeypatch):
|
|||
|
||||
|
||||
def test_workflow_tests_no_actions(pub):
|
||||
user = pub.user_class(name='test user')
|
||||
user.store()
|
||||
|
||||
workflow = Workflow(name='Workflow One')
|
||||
workflow.add_status(name='New status')
|
||||
|
||||
|
@ -85,7 +87,6 @@ def test_workflow_tests_no_actions(pub):
|
|||
formdata.just_created()
|
||||
|
||||
testdef = TestDef.create_from_formdata(formdef, formdata)
|
||||
testdef.agent_id = user.id
|
||||
testdef.workflow_tests.actions = []
|
||||
|
||||
with mock.patch('wcs.workflow_tests.WorkflowTests.run') as mocked_run:
|
||||
|
@ -94,9 +95,6 @@ def test_workflow_tests_no_actions(pub):
|
|||
|
||||
|
||||
def test_workflow_tests_action_not_configured(pub):
|
||||
user = pub.user_class(name='test user')
|
||||
user.store()
|
||||
|
||||
workflow = Workflow(name='Workflow One')
|
||||
workflow.add_status(name='New status')
|
||||
|
||||
|
@ -111,7 +109,6 @@ def test_workflow_tests_action_not_configured(pub):
|
|||
formdata.just_created()
|
||||
|
||||
testdef = TestDef.create_from_formdata(formdef, formdata)
|
||||
testdef.agent_id = user.id
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.ButtonClick(),
|
||||
]
|
||||
|
@ -142,6 +139,7 @@ def test_workflow_tests_button_click(pub):
|
|||
role = pub.role_class(name='test role')
|
||||
role.store()
|
||||
user = pub.user_class(name='test user')
|
||||
user.test_uuid = '42'
|
||||
user.roles = [role.id]
|
||||
user.store()
|
||||
|
||||
|
@ -165,7 +163,7 @@ def test_workflow_tests_button_click(pub):
|
|||
formdata.just_created()
|
||||
|
||||
testdef = TestDef.create_from_formdata(formdef, formdata)
|
||||
testdef.agent_id = user.id
|
||||
testdef.agent_id = user.test_uuid
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.ButtonClick(button_name='Go to end status'),
|
||||
workflow_tests.AssertStatus(status_name='End status'),
|
||||
|
@ -204,15 +202,77 @@ def test_workflow_tests_button_click(pub):
|
|||
assert str(excinfo.value) == 'Button "Go to end status" is not displayed.'
|
||||
|
||||
|
||||
def test_workflow_tests_button_click_global_action(pub):
|
||||
role = pub.role_class(name='test role')
|
||||
role.store()
|
||||
user = pub.user_class(name='test user')
|
||||
user.test_uuid = '42'
|
||||
user.roles = [role.id]
|
||||
user.store()
|
||||
|
||||
workflow = Workflow(name='Workflow One')
|
||||
workflow.add_status(name='New status')
|
||||
end_status = workflow.add_status(name='End status')
|
||||
|
||||
global_action = workflow.add_global_action('Go to end status')
|
||||
global_action.triggers[0].roles = [role.id]
|
||||
|
||||
sendmail = global_action.add_action('sendmail')
|
||||
sendmail.to = ['test@example.org']
|
||||
sendmail.subject = 'In new status'
|
||||
sendmail.body = 'xxx'
|
||||
|
||||
jump = global_action.add_action('jump')
|
||||
jump.status = end_status.id
|
||||
|
||||
workflow.store()
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test title'
|
||||
formdef.workflow_id = workflow.id
|
||||
formdef.store()
|
||||
|
||||
formdata = formdef.data_class()()
|
||||
formdata.just_created()
|
||||
|
||||
testdef = TestDef.create_from_formdata(formdef, formdata)
|
||||
testdef.agent_id = user.test_uuid
|
||||
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.AssertStatus(status_name='End status'),
|
||||
]
|
||||
|
||||
with pytest.raises(WorkflowTestError) as excinfo:
|
||||
testdef.run(formdef)
|
||||
assert str(excinfo.value) == 'Form should be in status "End status" but is in status "New status".'
|
||||
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.ButtonClick(button_name='Go to end status'),
|
||||
workflow_tests.AssertEmail(),
|
||||
workflow_tests.AssertStatus(status_name='End status'),
|
||||
]
|
||||
testdef.run(formdef)
|
||||
|
||||
# hide button from test user
|
||||
user.roles = []
|
||||
user.store()
|
||||
|
||||
with pytest.raises(WorkflowTestError) as excinfo:
|
||||
testdef.run(formdef)
|
||||
assert str(excinfo.value) == 'Button "Go to end status" is not displayed.'
|
||||
|
||||
|
||||
def test_workflow_tests_button_click_who(pub):
|
||||
role = pub.role_class(name='test role')
|
||||
role.store()
|
||||
agent_user = pub.user_class(name='agent user')
|
||||
agent_user.test_uuid = '42'
|
||||
agent_user.roles = [role.id]
|
||||
agent_user.store()
|
||||
other_role = pub.role_class(name='other test role')
|
||||
other_role.store()
|
||||
other_user = pub.user_class(name='other user')
|
||||
other_user.test_uuid = '43'
|
||||
other_user.roles = [other_role.id]
|
||||
other_user.store()
|
||||
|
||||
|
@ -255,13 +315,18 @@ def test_workflow_tests_button_click_who(pub):
|
|||
formdata.just_created()
|
||||
|
||||
testdef = TestDef.create_from_formdata(formdef, formdata)
|
||||
testdef.agent_id = agent_user.id
|
||||
testdef.agent_id = agent_user.test_uuid
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.ButtonClick(button_name='Go to next status', who='receiver'),
|
||||
workflow_tests.AssertStatus(status_name='Jump by receiver'),
|
||||
]
|
||||
testdef.run(formdef)
|
||||
|
||||
testdef.agent_id = None
|
||||
with pytest.raises(WorkflowTestError) as excinfo:
|
||||
testdef.run(formdef)
|
||||
assert str(excinfo.value) == 'Broken, missing user'
|
||||
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.ButtonClick(button_name='Go to next status', who='submitter'),
|
||||
workflow_tests.AssertStatus(status_name='Jump by submitter'),
|
||||
|
@ -269,7 +334,7 @@ def test_workflow_tests_button_click_who(pub):
|
|||
testdef.run(formdef)
|
||||
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.ButtonClick(button_name='Go to next status', who='other', who_id=other_user.id),
|
||||
workflow_tests.ButtonClick(button_name='Go to next status', who='other', who_id=other_user.test_uuid),
|
||||
workflow_tests.AssertStatus(status_name='Jump by other user'),
|
||||
]
|
||||
testdef.run(formdef)
|
||||
|
@ -300,7 +365,7 @@ def test_workflow_tests_button_click_who(pub):
|
|||
formdata.user = submitter_user
|
||||
|
||||
testdef = TestDef.create_from_formdata(formdef, formdata)
|
||||
testdef.agent_id = agent_user.id
|
||||
testdef.agent_id = agent_user.test_uuid
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.ButtonClick(button_name='Go to next status', who='submitter'),
|
||||
workflow_tests.AssertStatus(status_name='Jump by submitter'),
|
||||
|
@ -309,9 +374,6 @@ def test_workflow_tests_button_click_who(pub):
|
|||
|
||||
|
||||
def test_workflow_tests_automatic_jump(pub):
|
||||
user = pub.user_class(name='test user')
|
||||
user.store()
|
||||
|
||||
workflow = Workflow(name='Workflow One')
|
||||
new_status = workflow.add_status(name='New status')
|
||||
end_status = workflow.add_status(name='End status')
|
||||
|
@ -330,7 +392,6 @@ def test_workflow_tests_automatic_jump(pub):
|
|||
formdata.just_created()
|
||||
|
||||
testdef = TestDef.create_from_formdata(formdef, formdata)
|
||||
testdef.agent_id = user.id
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.AssertStatus(status_name='End status'),
|
||||
]
|
||||
|
@ -350,9 +411,6 @@ def test_workflow_tests_automatic_jump(pub):
|
|||
|
||||
|
||||
def test_workflow_tests_automatic_jump_condition(pub):
|
||||
user = pub.user_class(name='test user')
|
||||
user.store()
|
||||
|
||||
workflow = Workflow(name='Workflow One')
|
||||
new_status = workflow.add_status(name='New status')
|
||||
frog_status = workflow.add_status(name='Frog status')
|
||||
|
@ -381,7 +439,6 @@ def test_workflow_tests_automatic_jump_condition(pub):
|
|||
formdata.data['1'] = 'frog'
|
||||
|
||||
testdef = TestDef.create_from_formdata(formdef, formdata)
|
||||
testdef.agent_id = user.id
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.AssertStatus(status_name='Frog status'),
|
||||
]
|
||||
|
@ -394,25 +451,11 @@ def test_workflow_tests_automatic_jump_condition(pub):
|
|||
assert str(excinfo.value) == 'Form should be in status "Frog status" but is in status "Bear status".'
|
||||
|
||||
|
||||
@pytest.mark.freeze_time('2024-02-19 12:00')
|
||||
def test_workflow_tests_automatic_jump_timeout(pub):
|
||||
user = pub.user_class(name='test user')
|
||||
user.store()
|
||||
|
||||
workflow = Workflow(name='Workflow One')
|
||||
new_status = workflow.add_status(name='New status')
|
||||
stalled_status = workflow.add_status(name='Stalled')
|
||||
|
||||
jump = new_status.add_action('jump')
|
||||
jump.status = stalled_status.id
|
||||
jump.timeout = 120 * 60 # 2 hours
|
||||
jump.condition = {'type': 'django', 'value': 'form_receipt_datetime|age_in_days >= 1'}
|
||||
|
||||
sendmail = new_status.add_action('sendmail')
|
||||
sendmail.to = ['test@example.org']
|
||||
sendmail.subject = 'In new status'
|
||||
sendmail.body = 'xxx'
|
||||
|
||||
workflow.store()
|
||||
|
||||
formdef = FormDef()
|
||||
|
@ -424,7 +467,27 @@ def test_workflow_tests_automatic_jump_timeout(pub):
|
|||
formdata.just_created()
|
||||
|
||||
testdef = TestDef.create_from_formdata(formdef, formdata)
|
||||
testdef.agent_id = user.id
|
||||
|
||||
# no jumps configured, try skipping time anyway
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.SkipTime(seconds=119 * 60),
|
||||
]
|
||||
testdef.run(formdef)
|
||||
|
||||
# configure jump
|
||||
jump = new_status.add_action('jump')
|
||||
jump.status = stalled_status.id
|
||||
jump.timeout = 120 * 60 # 2 hours
|
||||
jump.condition = {'type': 'django', 'value': 'form_receipt_datetime|age_in_days >= 1'}
|
||||
|
||||
sendmail = new_status.add_action('sendmail')
|
||||
sendmail.to = ['test@example.org']
|
||||
sendmail.subject = 'In new status'
|
||||
sendmail.body = 'xxx'
|
||||
|
||||
workflow.store()
|
||||
formdef.refresh_from_storage()
|
||||
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.AssertStatus(status_name='New status'),
|
||||
workflow_tests.SkipTime(seconds=119 * 60),
|
||||
|
@ -454,11 +517,101 @@ def test_workflow_tests_automatic_jump_timeout(pub):
|
|||
testdef.run(formdef)
|
||||
|
||||
|
||||
@pytest.mark.freeze_time('2024-02-19 12:00')
|
||||
def test_workflow_tests_global_action_timeout(pub):
|
||||
workflow = Workflow(name='Workflow One')
|
||||
new_status = workflow.add_status(name='New status')
|
||||
end_status = workflow.add_status(name='End status')
|
||||
|
||||
global_action = workflow.add_global_action('Go to end status')
|
||||
trigger = global_action.append_trigger('timeout')
|
||||
trigger.anchor = 'creation'
|
||||
trigger.timeout = 1
|
||||
|
||||
jump = global_action.add_action('jump')
|
||||
jump.status = end_status.id
|
||||
|
||||
# add choice so that new_status is not flagged as endpoint
|
||||
choice = new_status.add_action('choice')
|
||||
choice.label = 'Go to end status'
|
||||
choice.status = end_status.id
|
||||
|
||||
workflow.store()
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test title'
|
||||
formdef.workflow_id = workflow.id
|
||||
formdef.store()
|
||||
|
||||
formdata = formdef.data_class()()
|
||||
formdata.just_created()
|
||||
|
||||
testdef = TestDef.create_from_formdata(formdef, formdata)
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.AssertStatus(status_name='New status'),
|
||||
workflow_tests.SkipTime(seconds=60 * 60), # 1 hour
|
||||
workflow_tests.AssertStatus(status_name='New status'),
|
||||
workflow_tests.SkipTime(seconds=24 * 60 * 60), # 1 day
|
||||
workflow_tests.AssertStatus(status_name='End status'),
|
||||
]
|
||||
|
||||
testdef.run(formdef)
|
||||
|
||||
trigger.anchor = '1st-arrival'
|
||||
workflow.store()
|
||||
formdef.refresh_from_storage()
|
||||
|
||||
testdef.run(formdef)
|
||||
|
||||
trigger.anchor = 'latest-arrival'
|
||||
workflow.store()
|
||||
formdef.refresh_from_storage()
|
||||
|
||||
testdef.run(formdef)
|
||||
|
||||
trigger.anchor = 'template'
|
||||
trigger.anchor_template = '{{ form_receipt_date|date:"Y-m-d" }}'
|
||||
workflow.store()
|
||||
formdef.refresh_from_storage()
|
||||
|
||||
testdef.run(formdef)
|
||||
|
||||
trigger.anchor = 'finalized'
|
||||
workflow.store()
|
||||
formdef.refresh_from_storage()
|
||||
|
||||
with pytest.raises(WorkflowTestError) as excinfo:
|
||||
testdef.run(formdef)
|
||||
assert str(excinfo.value) == 'Form should be in status "End status" but is in status "New status".'
|
||||
|
||||
# remove choice so new status becomes endpoint
|
||||
new_status.items = [x for x in new_status.items if x.id != choice.id]
|
||||
workflow.store()
|
||||
formdef.refresh_from_storage()
|
||||
|
||||
testdef.run(formdef)
|
||||
|
||||
trigger.anchor = 'anonymisation'
|
||||
workflow.store()
|
||||
formdef.refresh_from_storage()
|
||||
|
||||
with pytest.raises(WorkflowTestError) as excinfo:
|
||||
testdef.run(formdef)
|
||||
assert str(excinfo.value) == 'Form should be in status "End status" but is in status "New status".'
|
||||
|
||||
new_status.add_action('anonymise')
|
||||
workflow.store()
|
||||
formdef.refresh_from_storage()
|
||||
|
||||
testdef.run(formdef)
|
||||
|
||||
|
||||
@mock.patch('wcs.qommon.emails.send_email')
|
||||
def test_workflow_tests_sendmail(mocked_send_email, pub):
|
||||
role = pub.role_class(name='test role')
|
||||
role.store()
|
||||
user = pub.user_class(name='test user')
|
||||
user.test_uuid = '42'
|
||||
user.roles = [role.id]
|
||||
user.store()
|
||||
|
||||
|
@ -467,7 +620,7 @@ def test_workflow_tests_sendmail(mocked_send_email, pub):
|
|||
end_status = workflow.add_status(name='End status')
|
||||
|
||||
sendmail = new_status.add_action('sendmail')
|
||||
sendmail.to = ['test@example.org']
|
||||
sendmail.to = ['test@example.org', 'test2@example.org']
|
||||
sendmail.subject = 'In new status'
|
||||
sendmail.body = 'xxx'
|
||||
|
||||
|
@ -492,7 +645,7 @@ def test_workflow_tests_sendmail(mocked_send_email, pub):
|
|||
formdata.just_created()
|
||||
|
||||
testdef = TestDef.create_from_formdata(formdef, formdata)
|
||||
testdef.agent_id = user.id
|
||||
testdef.agent_id = user.test_uuid
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.AssertEmail(
|
||||
addresses=['test@example.org'], subject_strings=['In new status'], body_strings=['xxx']
|
||||
|
@ -534,7 +687,7 @@ def test_workflow_tests_sendmail(mocked_send_email, pub):
|
|||
with pytest.raises(WorkflowTestError) as excinfo:
|
||||
testdef.run(formdef)
|
||||
assert str(excinfo.value) == 'Email was not sent to address "other@example.org".'
|
||||
assert 'Email addresses: test@example.org' in excinfo.value.details
|
||||
assert 'Email addresses: test2@example.org, test@example.org' in excinfo.value.details
|
||||
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.ButtonClick(button_name='Go to end status'),
|
||||
|
@ -550,9 +703,6 @@ def test_workflow_tests_sendmail(mocked_send_email, pub):
|
|||
def test_workflow_tests_sms(pub):
|
||||
pub.cfg['sms'] = {'sender': 'xxx', 'passerelle_url': 'http://passerelle.invalid/'}
|
||||
|
||||
user = pub.user_class(name='test user')
|
||||
user.store()
|
||||
|
||||
workflow = Workflow(name='Workflow One')
|
||||
new_status = workflow.add_status(name='New status')
|
||||
|
||||
|
@ -571,7 +721,12 @@ def test_workflow_tests_sms(pub):
|
|||
formdata.just_created()
|
||||
|
||||
testdef = TestDef.create_from_formdata(formdef, formdata)
|
||||
testdef.agent_id = user.id
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.AssertSMS(),
|
||||
]
|
||||
|
||||
testdef.run(formdef)
|
||||
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.AssertSMS(phone_numbers=['0123456789'], body='Hello'),
|
||||
]
|
||||
|
@ -603,10 +758,211 @@ def test_workflow_tests_sms(pub):
|
|||
assert 'SMS body: "Hello"' in excinfo.value.details
|
||||
|
||||
|
||||
def test_workflow_tests_backoffice_fields(pub):
|
||||
user = pub.user_class(name='test user')
|
||||
user.store()
|
||||
def test_workflow_tests_anonymise(pub):
|
||||
workflow = Workflow(name='Workflow One')
|
||||
new_status = workflow.add_status(name='New status')
|
||||
workflow.store()
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test title'
|
||||
formdef.workflow_id = workflow.id
|
||||
formdef.store()
|
||||
|
||||
formdata = formdef.data_class()()
|
||||
formdata.just_created()
|
||||
|
||||
testdef = TestDef.create_from_formdata(formdef, formdata)
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.AssertAnonymise(),
|
||||
]
|
||||
|
||||
with pytest.raises(WorkflowTestError) as excinfo:
|
||||
testdef.run(formdef)
|
||||
assert str(excinfo.value) == 'Form was not anonymised.'
|
||||
|
||||
anonymise_action = new_status.add_action('anonymise')
|
||||
workflow.store()
|
||||
formdef.refresh_from_storage()
|
||||
|
||||
testdef.run(formdef)
|
||||
|
||||
anonymise_action.mode = 'intermediate'
|
||||
workflow.store()
|
||||
formdef.refresh_from_storage()
|
||||
|
||||
testdef.run(formdef)
|
||||
|
||||
anonymise_action.mode = 'unlink_user'
|
||||
workflow.store()
|
||||
formdef.refresh_from_storage()
|
||||
|
||||
testdef.run(formdef)
|
||||
|
||||
|
||||
def test_workflow_tests_redirect(pub):
|
||||
workflow = Workflow(name='Workflow One')
|
||||
new_status = workflow.add_status(name='New status')
|
||||
workflow.store()
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test title'
|
||||
formdef.workflow_id = workflow.id
|
||||
formdef.store()
|
||||
|
||||
formdata = formdef.data_class()()
|
||||
formdata.just_created()
|
||||
|
||||
testdef = TestDef.create_from_formdata(formdef, formdata)
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.AssertRedirect(url='https://example.com/'),
|
||||
]
|
||||
|
||||
with pytest.raises(WorkflowTestError) as excinfo:
|
||||
testdef.run(formdef)
|
||||
assert str(excinfo.value) == 'No redirection occured.'
|
||||
|
||||
redirect_action = new_status.add_action('redirect_to_url')
|
||||
redirect_action.url = 'https://test.com/'
|
||||
workflow.store()
|
||||
formdef.refresh_from_storage()
|
||||
|
||||
with pytest.raises(WorkflowTestError) as excinfo:
|
||||
testdef.run(formdef)
|
||||
assert (
|
||||
str(excinfo.value)
|
||||
== 'Expected redirection to https://example.com/ but was redirected to https://test.com/.'
|
||||
)
|
||||
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.AssertRedirect(url='https://test.com/'),
|
||||
]
|
||||
|
||||
testdef.run(formdef)
|
||||
|
||||
|
||||
def test_workflow_tests_history_message(pub):
|
||||
workflow = Workflow(name='Workflow One')
|
||||
new_status = workflow.add_status(name='New status')
|
||||
workflow.store()
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test title'
|
||||
formdef.workflow_id = workflow.id
|
||||
formdef.store()
|
||||
|
||||
formdata = formdef.data_class()()
|
||||
formdata.just_created()
|
||||
|
||||
testdef = TestDef.create_from_formdata(formdef, formdata)
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.AssertHistoryMessage(message='Hello 42'),
|
||||
]
|
||||
|
||||
with pytest.raises(WorkflowTestError) as excinfo:
|
||||
testdef.run(formdef)
|
||||
assert str(excinfo.value) == 'No history message.'
|
||||
|
||||
register_comment = new_status.add_action('register-comment')
|
||||
register_comment.comment = 'Hello {{ 41|add:1 }}'
|
||||
workflow.store()
|
||||
formdef.refresh_from_storage()
|
||||
|
||||
testdef.run(formdef)
|
||||
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.AssertHistoryMessage(message='Hello 43'),
|
||||
]
|
||||
|
||||
with pytest.raises(WorkflowTestError) as excinfo:
|
||||
testdef.run(formdef)
|
||||
assert str(excinfo.value) == 'Wrong history message content.'
|
||||
assert 'Displayed history message: <div>Hello 42</div>' in excinfo.value.details
|
||||
assert 'Expected history message: Hello 43' in excinfo.value.details
|
||||
|
||||
|
||||
def test_workflow_tests_alert(pub):
|
||||
workflow = Workflow(name='Workflow One')
|
||||
new_status = workflow.add_status(name='New status')
|
||||
workflow.store()
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test title'
|
||||
formdef.workflow_id = workflow.id
|
||||
formdef.store()
|
||||
|
||||
formdata = formdef.data_class()()
|
||||
formdata.just_created()
|
||||
|
||||
testdef = TestDef.create_from_formdata(formdef, formdata)
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.AssertAlert(message='Héllo 42 abc'),
|
||||
]
|
||||
|
||||
with pytest.raises(WorkflowTestError) as excinfo:
|
||||
testdef.run(formdef)
|
||||
assert str(excinfo.value) == 'No alert matching message.'
|
||||
assert 'Displayed alerts: None' in excinfo.value.details
|
||||
assert 'Expected alert: Héllo 42 abc' in excinfo.value.details
|
||||
|
||||
alert = new_status.add_action('displaymsg')
|
||||
alert.message = 'Héllo <strong>{{ 41|add:1 }}</strong> abc'
|
||||
workflow.store()
|
||||
formdef.refresh_from_storage()
|
||||
|
||||
testdef.run(formdef)
|
||||
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.AssertAlert(message='Hello 43'),
|
||||
]
|
||||
|
||||
with pytest.raises(WorkflowTestError) as excinfo:
|
||||
testdef.run(formdef)
|
||||
assert str(excinfo.value) == 'No alert matching message.'
|
||||
assert 'Displayed alerts: Héllo 42 abc' in excinfo.value.details
|
||||
assert 'Expected alert: Hello 43' in excinfo.value.details
|
||||
|
||||
|
||||
def test_workflow_tests_criticality(pub):
|
||||
workflow = Workflow(name='Workflow One')
|
||||
new_status = workflow.add_status(name='New status')
|
||||
green_level = WorkflowCriticalityLevel(name='green')
|
||||
red_level = WorkflowCriticalityLevel(name='red')
|
||||
workflow.criticality_levels = [green_level, red_level]
|
||||
workflow.store()
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test title'
|
||||
formdef.workflow_id = workflow.id
|
||||
formdef.store()
|
||||
|
||||
formdata = formdef.data_class()()
|
||||
formdata.just_created()
|
||||
|
||||
testdef = TestDef.create_from_formdata(formdef, formdata)
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.AssertCriticality(level_id=red_level.id),
|
||||
]
|
||||
|
||||
with pytest.raises(WorkflowTestError) as excinfo:
|
||||
testdef.run(formdef)
|
||||
assert str(excinfo.value) == 'Form should have criticality level "red" but has level "green".'
|
||||
|
||||
new_status.add_action('modify_criticality')
|
||||
workflow.store()
|
||||
formdef.refresh_from_storage()
|
||||
|
||||
testdef.run(formdef)
|
||||
|
||||
workflow.criticality_levels = []
|
||||
workflow.store()
|
||||
formdef.refresh_from_storage()
|
||||
|
||||
with pytest.raises(WorkflowTestError) as excinfo:
|
||||
testdef.run(formdef)
|
||||
assert str(excinfo.value) == 'Broken, missing criticality level'
|
||||
|
||||
|
||||
def test_workflow_tests_backoffice_fields(pub):
|
||||
workflow = Workflow(name='Workflow One')
|
||||
workflow.backoffice_fields_formdef = WorkflowBackofficeFieldsFormDef(workflow)
|
||||
workflow.backoffice_fields_formdef.fields = [
|
||||
|
@ -633,7 +989,6 @@ def test_workflow_tests_backoffice_fields(pub):
|
|||
formdata.data['1'] = 'abc'
|
||||
|
||||
testdef = TestDef.create_from_formdata(formdef, formdata)
|
||||
testdef.agent_id = user.id
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.AssertBackofficeFieldValues(id='1', fields=[{'field_id': 'bo2', 'value': 'abc'}]),
|
||||
]
|
||||
|
@ -655,10 +1010,80 @@ def test_workflow_tests_backoffice_fields(pub):
|
|||
assert str(excinfo.value) == 'Field bo2 not found (expected value "abc").'
|
||||
|
||||
|
||||
def test_workflow_tests_webservice(pub):
|
||||
def test_workflow_tests_dispatch(pub):
|
||||
role = pub.role_class(name='test role')
|
||||
role.store()
|
||||
user = pub.user_class(name='test user')
|
||||
user.test_uuid = '42'
|
||||
user.roles = [role.id]
|
||||
user.store()
|
||||
|
||||
other_role = pub.role_class(name='test role')
|
||||
other_role.store()
|
||||
other_user = pub.user_class(name='test user')
|
||||
other_user.test_uuid = '43'
|
||||
other_user.roles = [other_role.id]
|
||||
other_user.store()
|
||||
|
||||
workflow = Workflow(name='Workflow One')
|
||||
new_status = workflow.add_status(name='New status')
|
||||
middle_status = workflow.add_status(name='Middle status')
|
||||
end_status = workflow.add_status(name='End status')
|
||||
|
||||
dispatch = new_status.add_action('dispatch')
|
||||
dispatch.dispatch_type = 'manual'
|
||||
dispatch.role_key = '_receiver'
|
||||
dispatch.role_id = role.id
|
||||
|
||||
choice = new_status.add_action('choice')
|
||||
choice.label = 'Go to middle status'
|
||||
choice.status = middle_status.id
|
||||
choice.by = ['_receiver']
|
||||
|
||||
dispatch = middle_status.add_action('dispatch')
|
||||
dispatch.dispatch_type = 'manual'
|
||||
dispatch.role_key = '_receiver'
|
||||
dispatch.role_id = other_role.id
|
||||
|
||||
choice = middle_status.add_action('choice')
|
||||
choice.label = 'Go to end status'
|
||||
choice.status = end_status.id
|
||||
choice.by = ['_receiver']
|
||||
|
||||
workflow.store()
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test title'
|
||||
formdef.workflow_id = workflow.id
|
||||
formdef.store()
|
||||
|
||||
formdata = formdef.data_class()()
|
||||
formdata.just_created()
|
||||
|
||||
testdef = TestDef.create_from_formdata(formdef, formdata)
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.AssertStatus(status_name='New status'),
|
||||
workflow_tests.ButtonClick(button_name='Go to middle status', who='other', who_id=user.test_uuid),
|
||||
workflow_tests.AssertStatus(status_name='Middle status'),
|
||||
workflow_tests.ButtonClick(button_name='Go to end status', who='other', who_id=other_user.test_uuid),
|
||||
workflow_tests.AssertStatus(status_name='End status'),
|
||||
]
|
||||
|
||||
testdef.run(formdef)
|
||||
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.AssertStatus(status_name='New status'),
|
||||
workflow_tests.ButtonClick(
|
||||
button_name='Go to middle status', who='other', who_id=other_user.test_uuid
|
||||
),
|
||||
]
|
||||
|
||||
with pytest.raises(WorkflowTestError) as excinfo:
|
||||
testdef.run(formdef)
|
||||
assert str(excinfo.value) == 'Button "Go to middle status" is not displayed.'
|
||||
|
||||
|
||||
def test_workflow_tests_webservice(pub):
|
||||
workflow = Workflow(name='Workflow One')
|
||||
new_status = workflow.add_status(name='New status')
|
||||
end_status = workflow.add_status(name='End status')
|
||||
|
@ -689,7 +1114,6 @@ def test_workflow_tests_webservice(pub):
|
|||
formdata.just_created()
|
||||
|
||||
testdef = TestDef.create_from_formdata(formdef, formdata)
|
||||
testdef.agent_id = user.id
|
||||
testdef.store()
|
||||
|
||||
response = WebserviceResponse()
|
||||
|
@ -701,7 +1125,7 @@ def test_workflow_tests_webservice(pub):
|
|||
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.AssertStatus(status_name='End status'),
|
||||
workflow_tests.AssertWebserviceCall(webservice_response_id=response.id, call_count=1),
|
||||
workflow_tests.AssertWebserviceCall(webservice_response_uuid=response.uuid, call_count=1),
|
||||
]
|
||||
|
||||
with pytest.raises(WorkflowTestError) as excinfo:
|
||||
|
@ -716,7 +1140,7 @@ def test_workflow_tests_webservice(pub):
|
|||
assert str(excinfo.value) == 'Webservice response Fake response was used 2 times (instead of 1).'
|
||||
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.AssertWebserviceCall(webservice_response_id=response.id, call_count=2),
|
||||
workflow_tests.AssertWebserviceCall(webservice_response_uuid=response.uuid, call_count=2),
|
||||
]
|
||||
|
||||
testdef.run(formdef)
|
||||
|
@ -733,8 +1157,8 @@ def test_workflow_tests_webservice(pub):
|
|||
response2.store()
|
||||
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.AssertWebserviceCall(webservice_response_id=response.id, call_count=1),
|
||||
workflow_tests.AssertWebserviceCall(webservice_response_id=response2.id, call_count=1),
|
||||
workflow_tests.AssertWebserviceCall(webservice_response_uuid=response.uuid, call_count=1),
|
||||
workflow_tests.AssertWebserviceCall(webservice_response_uuid=response2.uuid, call_count=1),
|
||||
]
|
||||
|
||||
testdef.run(formdef)
|
||||
|
@ -744,8 +1168,8 @@ def test_workflow_tests_webservice(pub):
|
|||
testdef.run(formdef)
|
||||
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.AssertWebserviceCall(webservice_response_id=response.id, call_count=1),
|
||||
workflow_tests.AssertWebserviceCall(webservice_response_id=response.id, call_count=1),
|
||||
workflow_tests.AssertWebserviceCall(webservice_response_uuid=response.uuid, call_count=1),
|
||||
workflow_tests.AssertWebserviceCall(webservice_response_uuid=response.uuid, call_count=1),
|
||||
]
|
||||
|
||||
with pytest.raises(WorkflowTestError) as excinfo:
|
||||
|
@ -753,7 +1177,7 @@ def test_workflow_tests_webservice(pub):
|
|||
assert str(excinfo.value) == 'Webservice response Fake response was used 0 times (instead of 1).'
|
||||
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.AssertWebserviceCall(webservice_response_id=response.id, call_count=0),
|
||||
workflow_tests.AssertWebserviceCall(webservice_response_uuid=response.uuid, call_count=0),
|
||||
]
|
||||
|
||||
with pytest.raises(WorkflowTestError) as excinfo:
|
||||
|
@ -761,7 +1185,7 @@ def test_workflow_tests_webservice(pub):
|
|||
assert str(excinfo.value) == 'Webservice response Fake response was used 1 times (instead of 0).'
|
||||
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.AssertWebserviceCall(webservice_response_id='xxx', call_count=1),
|
||||
workflow_tests.AssertWebserviceCall(webservice_response_uuid='xxx', call_count=1),
|
||||
]
|
||||
|
||||
with pytest.raises(WorkflowTestError) as excinfo:
|
||||
|
@ -770,9 +1194,6 @@ def test_workflow_tests_webservice(pub):
|
|||
|
||||
|
||||
def test_workflow_tests_webservice_status_jump(pub):
|
||||
user = pub.user_class(name='test user')
|
||||
user.store()
|
||||
|
||||
workflow = Workflow(name='Workflow One')
|
||||
new_status = workflow.add_status(name='New status')
|
||||
end_status = workflow.add_status(name='Error status')
|
||||
|
@ -793,7 +1214,6 @@ def test_workflow_tests_webservice_status_jump(pub):
|
|||
formdata.just_created()
|
||||
|
||||
testdef = TestDef.create_from_formdata(formdef, formdata)
|
||||
testdef.agent_id = user.id
|
||||
testdef.store()
|
||||
|
||||
response = WebserviceResponse()
|
||||
|
@ -820,9 +1240,13 @@ def test_workflow_tests_webservice_status_jump(pub):
|
|||
|
||||
|
||||
def test_workflow_tests_create_from_formdata(pub, http_requests, freezer):
|
||||
pub.cfg['sms'] = {'sender': 'xxx', 'passerelle_url': 'http://passerelle.invalid/'}
|
||||
pub.write_cfg()
|
||||
|
||||
role = pub.role_class(name='test role')
|
||||
role.store()
|
||||
user = create_user(pub, is_admin=True)
|
||||
user.test_uuid = '42'
|
||||
user.roles = [role.id]
|
||||
user.store()
|
||||
|
||||
|
@ -834,6 +1258,7 @@ def test_workflow_tests_create_from_formdata(pub, http_requests, freezer):
|
|||
status_with_timeout_jump = workflow.add_status('Status with timeout jump', 'status-with-timeout-jump')
|
||||
status_with_button = workflow.add_status('Status with button', 'status-with-button')
|
||||
transition_status = workflow.add_status('Transition status', 'transition-status')
|
||||
transition_status2 = workflow.add_status('Transition status 2', 'transition-status-2')
|
||||
end_status = workflow.add_status('End status', 'end-status')
|
||||
|
||||
jump = new_status.add_action('jump')
|
||||
|
@ -864,7 +1289,24 @@ def test_workflow_tests_create_from_formdata(pub, http_requests, freezer):
|
|||
sendsms.to = ['0123456789']
|
||||
sendsms.body = 'Hello'
|
||||
|
||||
jump = transition_status.add_action('jump')
|
||||
anonymise_action = transition_status.add_action('anonymise')
|
||||
anonymise_action.mode = 'intermediate'
|
||||
|
||||
redirect_action = transition_status.add_action('redirect_to_url')
|
||||
redirect_action.url = 'https://test.com/'
|
||||
|
||||
register_comment = transition_status.add_action('register-comment')
|
||||
register_comment.comment = 'Hello'
|
||||
|
||||
transition_status.add_action('modify_criticality')
|
||||
|
||||
global_action = workflow.add_global_action('Action 1')
|
||||
global_action.triggers[0].roles = [role.id]
|
||||
|
||||
jump = global_action.add_action('jump')
|
||||
jump.status = transition_status2.id
|
||||
|
||||
jump = transition_status2.add_action('jump')
|
||||
jump.status = end_status.id
|
||||
|
||||
workflow.store()
|
||||
|
@ -889,14 +1331,16 @@ def test_workflow_tests_create_from_formdata(pub, http_requests, freezer):
|
|||
app = login(get_app(pub))
|
||||
resp = app.get(formdata.get_url())
|
||||
resp.form.submit('button1').follow()
|
||||
resp.form.submit('button-action-1').follow()
|
||||
formdata.refresh_from_storage()
|
||||
assert formdata.status == 'wf-end-status'
|
||||
|
||||
testdef = TestDef.create_from_formdata(formdef, formdata, add_workflow_tests=True)
|
||||
testdef.agent_id = user.test_uuid
|
||||
testdef.run(formdef)
|
||||
|
||||
actions = testdef.workflow_tests.actions
|
||||
assert len(actions) == 9
|
||||
assert len(actions) == 15
|
||||
|
||||
assert actions[0].key == 'assert-status'
|
||||
assert actions[0].status_name == 'Status with timeout jump'
|
||||
|
@ -914,6 +1358,84 @@ def test_workflow_tests_create_from_formdata(pub, http_requests, freezer):
|
|||
assert actions[5].key == 'assert-email'
|
||||
assert actions[6].key == 'assert-backoffice-field'
|
||||
assert actions[7].key == 'assert-sms'
|
||||
assert actions[8].key == 'assert-anonymise'
|
||||
assert actions[9].key == 'assert-redirect'
|
||||
assert actions[10].key == 'assert-history-message'
|
||||
assert actions[11].key == 'assert-criticality'
|
||||
|
||||
assert actions[12].key == 'assert-status'
|
||||
assert actions[12].status_name == 'Transition status'
|
||||
|
||||
assert actions[13].key == 'button-click'
|
||||
assert actions[13].button_name == 'Action 1'
|
||||
|
||||
assert actions[-1].key == 'assert-status'
|
||||
assert actions[-1].status_name == 'End status'
|
||||
|
||||
|
||||
def test_workflow_tests_create_from_formdata_multiple_buttons(pub, http_requests):
|
||||
role = pub.role_class(name='test role')
|
||||
role.store()
|
||||
user = create_user(pub, is_admin=True)
|
||||
user.test_uuid = '42'
|
||||
user.roles = [role.id]
|
||||
user.store()
|
||||
|
||||
workflow = Workflow(name='Workflow One')
|
||||
new_status = workflow.add_status('New status', 'new-status')
|
||||
middle_status = workflow.add_status('Middle status', 'middle-status')
|
||||
end_status = workflow.add_status('End status', 'end-status')
|
||||
|
||||
choice = new_status.add_action('choice')
|
||||
choice.label = 'Go to middle status'
|
||||
choice.status = middle_status.id
|
||||
choice.by = [role.id]
|
||||
|
||||
choice = middle_status.add_action('choice')
|
||||
choice.label = 'Go to end status'
|
||||
choice.status = end_status.id
|
||||
choice.by = [role.id]
|
||||
|
||||
workflow.store()
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test title'
|
||||
formdef.workflow_id = workflow.id
|
||||
formdef.store()
|
||||
|
||||
formdata = formdef.data_class()()
|
||||
formdata.user_id = user.id
|
||||
formdata.just_created()
|
||||
formdata.store()
|
||||
|
||||
formdata.perform_workflow()
|
||||
formdata.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get(formdata.get_url())
|
||||
resp = resp.form.submit('button1').follow()
|
||||
resp = resp.form.submit('button1').follow()
|
||||
formdata.refresh_from_storage()
|
||||
assert formdata.status == 'wf-end-status'
|
||||
|
||||
testdef = TestDef.create_from_formdata(formdef, formdata, add_workflow_tests=True)
|
||||
testdef.agent_id = user.test_uuid
|
||||
testdef.run(formdef)
|
||||
|
||||
actions = testdef.workflow_tests.actions
|
||||
assert len(actions) == 5
|
||||
|
||||
assert actions[0].key == 'assert-status'
|
||||
assert actions[0].status_name == 'New status'
|
||||
|
||||
assert actions[1].key == 'button-click'
|
||||
assert actions[1].button_name == 'Go to middle status'
|
||||
|
||||
assert actions[2].key == 'assert-status'
|
||||
assert actions[2].status_name == 'Middle status'
|
||||
|
||||
assert actions[3].key == 'button-click'
|
||||
assert actions[3].button_name == 'Go to end status'
|
||||
|
||||
assert actions[4].key == 'assert-status'
|
||||
assert actions[4].status_name == 'End status'
|
||||
|
|
|
@ -289,6 +289,47 @@ def test_webservice_empty_param_values(http_requests, pub):
|
|||
assert http_requests.get_last('body') == '{"toto": ""}'
|
||||
|
||||
|
||||
def test_webservice_with_unflattened_payload_keys(http_requests, pub):
|
||||
NamedWsCall.wipe()
|
||||
|
||||
wscall = NamedWsCall()
|
||||
wscall.name = 'Hello world'
|
||||
wscall.request = {
|
||||
'method': 'POST',
|
||||
'url': 'http://remote.example.net/json',
|
||||
'post_data': {'foo/0': 'first', 'foo/1': 'second', 'bar': 'example', 'foo/2': ''},
|
||||
}
|
||||
wscall.store()
|
||||
|
||||
wscall.call()
|
||||
assert http_requests.get_last('url') == 'http://remote.example.net/json'
|
||||
assert http_requests.get_last('body') == '{"bar": "example", "foo": ["first", "second", ""]}'
|
||||
assert http_requests.count() == 1
|
||||
|
||||
wscall.request = {
|
||||
'method': 'POST',
|
||||
'url': 'http://remote.example.net/json',
|
||||
'post_data': {'foo/0': 'first', 'foo/1': 'second', 'foo/bar': 'example'},
|
||||
}
|
||||
wscall.store()
|
||||
|
||||
pub.loggederror_class.wipe()
|
||||
http_requests.empty()
|
||||
wscall.call()
|
||||
assert http_requests.count() == 0
|
||||
assert pub.loggederror_class.count() == 0
|
||||
|
||||
wscall.record_on_errors = True
|
||||
wscall.store()
|
||||
pub.loggederror_class.wipe()
|
||||
wscall.call()
|
||||
assert pub.loggederror_class.count() == 1
|
||||
assert (
|
||||
pub.loggederror_class.select()[0].summary
|
||||
== '[WSCALL] Webservice call failure because unable to unflatten payload keys (there is a mix between lists and dicts)'
|
||||
)
|
||||
|
||||
|
||||
def test_webservice_timeout(http_requests, pub):
|
||||
NamedWsCall.wipe()
|
||||
|
||||
|
|
|
@ -146,6 +146,7 @@ def create_temporary_pub(pickle_mode=False, lazy_mode=False):
|
|||
sql.Audit.wipe()
|
||||
sql_mark_current_test()
|
||||
pub.write_cfg()
|
||||
pub.reset_caches()
|
||||
return pub
|
||||
|
||||
os.symlink(os.path.join(os.path.dirname(__file__), 'templates'), os.path.join(pub.app_dir, 'templates'))
|
||||
|
|
|
@ -1468,7 +1468,7 @@ def test_set_backoffice_field_invalid_block_value(pub):
|
|||
logged_error = pub.loggederror_class.select()[0]
|
||||
assert (
|
||||
logged_error.summary
|
||||
== 'Failed to set Field Block (foobar) field (bo1), error: invalid value for block (field id: bo1)'
|
||||
== 'Failed to set Block of fields (foobar) field (bo1), error: invalid value for block (field id: bo1)'
|
||||
)
|
||||
|
||||
formdata = formdef.data_class().get(formdata.id)
|
||||
|
|
|
@ -1374,7 +1374,7 @@ def test_edit_carddata_partial_block_field(pub, admin_user):
|
|||
assert resp.form['mappings$element1$field_id'].options == [
|
||||
('', False, '---'),
|
||||
('0', False, 'foo - Text (line)'),
|
||||
('1', False, 'block field - Field Block (foobar)'),
|
||||
('1', False, 'block field - Block of fields (foobar)'),
|
||||
('1$123', True, 'block field - Test - Text (line)'),
|
||||
('1$234', False, 'block field - Test2 - Text (line)'),
|
||||
]
|
||||
|
|
|
@ -127,6 +127,7 @@ def test_create_formdata(pub):
|
|||
# now we want one
|
||||
target_formdef.enable_tracking_codes = True
|
||||
target_formdef.store()
|
||||
pub.reset_caches()
|
||||
target_formdef.data_class().wipe()
|
||||
formdata.perform_workflow()
|
||||
# and a tracking code is created
|
||||
|
@ -558,6 +559,7 @@ def test_recursive_create_formdata_with_subformdata(pub):
|
|||
|
||||
formdef.workflow_id = wf.id
|
||||
formdef.store()
|
||||
pub.reset_caches()
|
||||
|
||||
formdata = formdef.data_class()()
|
||||
formdata.data = {}
|
||||
|
@ -579,6 +581,7 @@ def test_recursive_create_formdata_with_subformdata(pub):
|
|||
StringField(id='0', label='string', varname='foo_string'),
|
||||
]
|
||||
subsubformdef.store()
|
||||
pub.reset_caches()
|
||||
|
||||
subwf = Workflow(name='create-formdata-again')
|
||||
subwf.possible_status = Workflow.get_default_workflow().possible_status[:]
|
||||
|
@ -591,6 +594,7 @@ def test_recursive_create_formdata_with_subformdata(pub):
|
|||
|
||||
subformdef.workflow_id = subwf.id
|
||||
subformdef.store()
|
||||
pub.reset_caches()
|
||||
|
||||
formdata = formdef.data_class()()
|
||||
formdata.data = {}
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import datetime
|
||||
import os
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
from django.core.management import call_command
|
||||
from pyquery import PyQuery
|
||||
from quixote import cleanup
|
||||
|
||||
|
@ -11,6 +13,7 @@ from wcs.qommon.http_request import HTTPRequest
|
|||
from wcs.wf.jump import JumpWorkflowStatusItem, _apply_timeouts
|
||||
from wcs.workflows import Workflow, perform_items
|
||||
|
||||
from ..test_publisher import get_logs
|
||||
from ..utilities import clean_temporary_pub, create_temporary_pub, get_app, login
|
||||
from .test_all import admin_user # noqa pylint: disable=unused-import
|
||||
|
||||
|
@ -629,3 +632,44 @@ def test_jump_self_timeout(pub):
|
|||
formdata.store()
|
||||
formdata.record_workflow_event('backoffice-created')
|
||||
_apply_timeouts(pub)
|
||||
|
||||
|
||||
def test_timeout_cron_debug_log(pub):
|
||||
FormDef.wipe()
|
||||
Workflow.wipe()
|
||||
|
||||
workflow = Workflow(name='timeout')
|
||||
st1 = workflow.add_status('Status1', 'st1')
|
||||
workflow.add_status('Status2', 'st2')
|
||||
|
||||
jump = st1.add_action('jump', id='_jump')
|
||||
jump.by = ['_submitter', '_receiver']
|
||||
jump.timeout = 30 * 60 # 30 minutes
|
||||
jump.status = 'st2'
|
||||
|
||||
workflow.store()
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'baz'
|
||||
formdef.fields = []
|
||||
formdef.workflow_id = workflow.id
|
||||
assert formdef.get_workflow().id == workflow.id
|
||||
formdef.store()
|
||||
|
||||
formdata = formdef.data_class()()
|
||||
formdata.just_created()
|
||||
rewind(formdata, seconds=40 * 60)
|
||||
formdata.store()
|
||||
formdata_id = formdata.id
|
||||
|
||||
pub.load_site_options()
|
||||
pub.site_options.set('options', 'cron-log-level', 'debug')
|
||||
with open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w') as fd:
|
||||
pub.site_options.write(fd)
|
||||
|
||||
call_command('cron', job_name='evaluate_jumps', domain='example.net', force_job=True)
|
||||
|
||||
assert formdef.data_class().get(formdata_id).status == 'wf-st2'
|
||||
assert get_logs('example.net')[:2] == ['start', "running jobs: ['evaluate_jumps']"]
|
||||
assert 'applying timeouts on baz' in get_logs('example.net')[2]
|
||||
assert 'event: timeout-jump' in get_logs('example.net')[3]
|
||||
|
|
|
@ -607,3 +607,22 @@ def test_register_comment_to_with_attachment(pub):
|
|||
assert 'to-role.txt' in display_parts()[2]
|
||||
assert 'to-submitter.txt' in display_parts()[4]
|
||||
assert 'to-role-or-submitter.txt' in display_parts()[6]
|
||||
|
||||
|
||||
def test_register_comment_fts(pub):
|
||||
pub.substitutions.feed(MockSubstitutionVariables())
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'baz'
|
||||
formdef.fields = []
|
||||
formdef.store()
|
||||
|
||||
formdata = formdef.data_class()()
|
||||
formdata.just_created()
|
||||
formdata.store()
|
||||
|
||||
item = RegisterCommenterWorkflowStatusItem()
|
||||
item.comment = 'Hello\x00\nworld'
|
||||
item.perform(formdata)
|
||||
assert formdata.evolution[-1].parts[-1].content == '<p>Hello\x00\nworld</p>' # kept
|
||||
assert formdata.evolution[-1].parts[-1].render_for_fts() == 'Hello world' # not kept
|
||||
|
|
|
@ -488,6 +488,26 @@ def test_webservice_call(http_requests, pub):
|
|||
assert isinstance(attachment, AttachmentEvolutionPart)
|
||||
assert attachment.base_filename == 'xxx.xml'
|
||||
assert attachment.content_type == 'text/xml'
|
||||
assert attachment.display_in_history is True
|
||||
attachment.fp.seek(0)
|
||||
assert attachment.fp.read(5) == b'<?xml'
|
||||
formdata.workflow_data = None
|
||||
|
||||
# check storing response as attachment, not displayed in history
|
||||
item = WebserviceCallStatusItem()
|
||||
item.url = 'http://remote.example.net/xml'
|
||||
item.varname = 'xxx'
|
||||
item.response_type = 'attachment'
|
||||
item.record_errors = True
|
||||
item.attach_file_to_history = False
|
||||
item.perform(formdata)
|
||||
assert formdata.workflow_data.get('xxx_status') == 200
|
||||
assert formdata.workflow_data.get('xxx_content_type') == 'text/xml'
|
||||
attachment = formdata.evolution[-1].parts[-1]
|
||||
assert isinstance(attachment, AttachmentEvolutionPart)
|
||||
assert attachment.base_filename == 'xxx.xml'
|
||||
assert attachment.content_type == 'text/xml'
|
||||
assert attachment.display_in_history is False
|
||||
attachment.fp.seek(0)
|
||||
assert attachment.fp.read(5) == b'<?xml'
|
||||
formdata.workflow_data = None
|
||||
|
@ -574,6 +594,82 @@ def test_webservice_call(http_requests, pub):
|
|||
assert payload == {'one': 1, 'str': 'abcd', 'evalme': formdata.get_display_id()}
|
||||
|
||||
|
||||
def test_webservice_with_unflattened_payload_keys(http_requests, pub):
|
||||
wf = Workflow(name='wf1')
|
||||
wf.add_status('Status1', 'st1')
|
||||
wf.store()
|
||||
|
||||
FormDef.wipe()
|
||||
formdef = FormDef()
|
||||
formdef.name = 'baz'
|
||||
formdef.fields = []
|
||||
formdef.workflow_id = wf.id
|
||||
formdef.store()
|
||||
|
||||
formdata = formdef.data_class()()
|
||||
formdata.just_created()
|
||||
formdata.store()
|
||||
|
||||
item = WebserviceCallStatusItem()
|
||||
item.url = 'http://remote.example.net'
|
||||
item.post_data = {
|
||||
'foo/0': 'first',
|
||||
'foo/1': 'second',
|
||||
'foo/2': '{{ form_name }}',
|
||||
'bar': 'example',
|
||||
'form//name': '{{ form_name }}',
|
||||
}
|
||||
pub.substitutions.feed(formdata)
|
||||
item.perform(formdata)
|
||||
assert http_requests.count() == 1
|
||||
assert http_requests.get_last('url') == 'http://remote.example.net/'
|
||||
assert http_requests.get_last('method') == 'POST'
|
||||
payload = json.loads(http_requests.get_last('body'))
|
||||
assert payload == {'foo': ['first', 'second', 'baz'], 'bar': 'example', 'form/name': 'baz'}
|
||||
|
||||
http_requests.empty()
|
||||
pub.loggederror_class.wipe()
|
||||
item = WebserviceCallStatusItem()
|
||||
item.url = 'http://remote.example.net'
|
||||
item.record_on_errors = True
|
||||
item.post_data = {'foo/1': 'first', 'foo/2': 'second'}
|
||||
|
||||
item.perform(formdata)
|
||||
assert http_requests.count() == 0
|
||||
assert pub.loggederror_class.count() == 1
|
||||
assert (
|
||||
pub.loggederror_class.select()[0].summary
|
||||
== '[WSCALL] Webservice call failure because unable to unflatten payload keys (incomplete array before key "foo/1")'
|
||||
)
|
||||
|
||||
http_requests.empty()
|
||||
pub.loggederror_class.wipe()
|
||||
item = WebserviceCallStatusItem()
|
||||
item.url = 'http://remote.example.net'
|
||||
item.record_on_errors = True
|
||||
item.post_data = {'0/foo': 'value', '1/bar': 'value', 'name': '{{ form_name }}'}
|
||||
|
||||
item.perform(formdata)
|
||||
assert (
|
||||
pub.loggederror_class.select()[0].summary
|
||||
== '[WSCALL] Webservice call failure because unable to unflatten payload keys (there is a mix between lists and dicts)'
|
||||
)
|
||||
|
||||
http_requests.empty()
|
||||
pub.loggederror_class.wipe()
|
||||
item = WebserviceCallStatusItem()
|
||||
item.url = 'http://remote.example.net'
|
||||
item.record_on_errors = True
|
||||
item.post_data = {'0/foo': 'value', '1/bar': 'value'}
|
||||
|
||||
pub.substitutions.feed(formdata)
|
||||
item.perform(formdata)
|
||||
|
||||
assert http_requests.count() == 1
|
||||
payload = json.loads(http_requests.get_last('body'))
|
||||
assert payload == [{'foo': 'value'}, {'bar': 'value'}]
|
||||
|
||||
|
||||
def test_webservice_waitpoint(pub):
|
||||
item = WebserviceCallStatusItem()
|
||||
assert item.waitpoint
|
||||
|
|
1
tox.ini
1
tox.ini
|
@ -77,6 +77,7 @@ deps =
|
|||
schwifty
|
||||
allowlist_externals =
|
||||
./getlasso3.sh
|
||||
./pylint.sh
|
||||
commands =
|
||||
./getlasso3.sh
|
||||
./pylint.sh wcs/ tests/
|
||||
|
|
|
@ -201,7 +201,7 @@ class ApiAccessDirectory(Directory):
|
|||
templates=['wcs/backoffice/api_accesses.html'],
|
||||
context={
|
||||
'view': self,
|
||||
'api_accesses': ApiAccess.select(order_by='name'),
|
||||
'api_accesses': [x for x in ApiAccess.select(order_by='name') if not x.idp_api_client],
|
||||
'api_manage_url': api_manage_url,
|
||||
},
|
||||
)
|
||||
|
|
|
@ -56,6 +56,7 @@ class BlockDirectory(FieldsDirectory):
|
|||
'duplicate',
|
||||
('history', 'snapshots_dir'),
|
||||
'overwrite',
|
||||
('update-documentation', 'update_documentation'),
|
||||
]
|
||||
field_def_page_class = BlockFieldDefPage
|
||||
blacklisted_types = ['page', 'table', 'table-select', 'tablerows', 'ranked-items', 'blocks', 'computed']
|
||||
|
@ -114,11 +115,13 @@ class BlockDirectory(FieldsDirectory):
|
|||
r += htmltext('<li><a href="export">%s</a></li>') % _('Export')
|
||||
r += htmltext('<li><a href="delete" rel="popup">%s</a></li>') % _('Delete')
|
||||
r += htmltext('</ul>')
|
||||
r += self.get_documentable_button()
|
||||
r += htmltext('<a href="settings" role="button">%s</a>') % _('Settings')
|
||||
r += htmltext('</span>')
|
||||
r += htmltext('</div>')
|
||||
r += utils.last_modification_block(obj=self.objectdef)
|
||||
r += get_session().display_message()
|
||||
r += self.get_documentable_zone()
|
||||
|
||||
if not self.objectdef.fields:
|
||||
r += htmltext('<div class="infonotice">%s</div>') % _('There are not yet any fields defined.')
|
||||
|
@ -153,7 +156,7 @@ class BlockDirectory(FieldsDirectory):
|
|||
'Save snapshot'
|
||||
)
|
||||
r += htmltext('<li><a class="button button-paragraph" rel="popup" href="overwrite">%s</a>') % _(
|
||||
'Overwrite'
|
||||
'Overwrite with new import'
|
||||
)
|
||||
r += htmltext('</ul>')
|
||||
r += htmltext('<h3>%s</h3>') % _('Navigation')
|
||||
|
@ -476,7 +479,7 @@ class BlocksDirectory(Directory):
|
|||
|
||||
error, reason = False, None
|
||||
try:
|
||||
blockdef = BlockDef.import_from_xml(fp)
|
||||
blockdef = BlockDef.import_from_xml(fp, check_deprecated=True)
|
||||
except BlockdefImportError as e:
|
||||
error = True
|
||||
reason = _(e.msg) % e.msg_args
|
||||
|
|
|
@ -469,7 +469,7 @@ class CategoriesDirectory(Directory):
|
|||
fp = form.get_widget('file').parse().fp
|
||||
|
||||
try:
|
||||
category = self.category_class.import_from_xml(fp)
|
||||
category = self.category_class.import_from_xml(fp, check_deprecated=True)
|
||||
get_session().message = ('info', _('This category has been successfully imported.'))
|
||||
except ValueError as e:
|
||||
form.set_error('file', _('Invalid File'))
|
||||
|
|
|
@ -20,6 +20,7 @@ from quixote.html import TemplateIO, htmltext
|
|||
|
||||
from wcs.admin import utils
|
||||
from wcs.admin.categories import CommentTemplateCategoriesDirectory, get_categories
|
||||
from wcs.admin.documentable import DocumentableMixin
|
||||
from wcs.backoffice.applications import ApplicationsDirectory
|
||||
from wcs.backoffice.snapshots import SnapshotsDirectory
|
||||
from wcs.categories import CommentTemplateCategory
|
||||
|
@ -150,7 +151,7 @@ class CommentTemplatesDirectory(Directory):
|
|||
|
||||
error = False
|
||||
try:
|
||||
comment_template = CommentTemplate.import_from_xml(fp)
|
||||
comment_template = CommentTemplate.import_from_xml(fp, check_deprecated=True)
|
||||
get_session().message = ('info', _('This comment template has been successfully imported.'))
|
||||
except ValueError:
|
||||
error = True
|
||||
|
@ -169,7 +170,7 @@ class CommentTemplatesDirectory(Directory):
|
|||
return redirect('%s/' % comment_template.id)
|
||||
|
||||
|
||||
class CommentTemplatePage(Directory):
|
||||
class CommentTemplatePage(Directory, DocumentableMixin):
|
||||
_q_exports = [
|
||||
'',
|
||||
'edit',
|
||||
|
@ -177,6 +178,7 @@ class CommentTemplatePage(Directory):
|
|||
'duplicate',
|
||||
'export',
|
||||
('history', 'snapshots_dir'),
|
||||
('update-documentation', 'update_documentation'),
|
||||
]
|
||||
do_not_call_in_templates = True
|
||||
|
||||
|
@ -187,6 +189,8 @@ class CommentTemplatePage(Directory):
|
|||
raise errors.TraversalError()
|
||||
get_response().breadcrumb.append((component + '/', self.comment_template.name))
|
||||
self.snapshots_dir = SnapshotsDirectory(self.comment_template)
|
||||
self.documented_object = self.comment_template
|
||||
self.documented_element = self.comment_template
|
||||
|
||||
def get_sidebar(self):
|
||||
r = TemplateIO(html=True)
|
||||
|
@ -247,14 +251,6 @@ class CommentTemplatePage(Directory):
|
|||
value=self.comment_template.category_id,
|
||||
)
|
||||
|
||||
form.add(
|
||||
TextWidget,
|
||||
'description',
|
||||
title=_('Description'),
|
||||
cols=80,
|
||||
rows=3,
|
||||
value=self.comment_template.description,
|
||||
)
|
||||
form.add(
|
||||
TextWidget,
|
||||
'comment',
|
||||
|
@ -307,7 +303,6 @@ class CommentTemplatePage(Directory):
|
|||
self.comment_template.name = name
|
||||
if form.get_widget('category_id'):
|
||||
self.comment_template.category_id = form.get_widget('category_id').parse()
|
||||
self.comment_template.description = form.get_widget('description').parse()
|
||||
self.comment_template.comment = form.get_widget('comment').parse()
|
||||
self.comment_template.attachments = form.get_widget('attachments').parse()
|
||||
if slug_widget:
|
||||
|
|
|
@ -20,6 +20,7 @@ from quixote.html import TemplateIO, htmltext
|
|||
|
||||
from wcs.admin import utils
|
||||
from wcs.admin.categories import DataSourceCategoriesDirectory, get_categories
|
||||
from wcs.admin.documentable import DocumentableMixin
|
||||
from wcs.backoffice.applications import ApplicationsDirectory
|
||||
from wcs.backoffice.snapshots import SnapshotsDirectory
|
||||
from wcs.carddef import CardDef
|
||||
|
@ -45,7 +46,6 @@ from wcs.qommon.form import (
|
|||
SingleSelectWidget,
|
||||
SlugWidget,
|
||||
StringWidget,
|
||||
TextWidget,
|
||||
WidgetDict,
|
||||
WidgetList,
|
||||
get_response,
|
||||
|
@ -73,14 +73,6 @@ class NamedDataSourceUI:
|
|||
options=category_options,
|
||||
value=self.datasource.category_id,
|
||||
)
|
||||
form.add(
|
||||
TextWidget,
|
||||
'description',
|
||||
title=_('Description'),
|
||||
cols=40,
|
||||
rows=5,
|
||||
value=self.datasource.description,
|
||||
)
|
||||
if not self.datasource or (
|
||||
self.datasource.type != 'wcs:users' and self.datasource.external != 'agenda_manual'
|
||||
):
|
||||
|
@ -297,7 +289,7 @@ class NamedDataSourceUI:
|
|||
self.datasource.store()
|
||||
|
||||
|
||||
class NamedDataSourcePage(Directory):
|
||||
class NamedDataSourcePage(Directory, DocumentableMixin):
|
||||
_q_exports = [
|
||||
'',
|
||||
'edit',
|
||||
|
@ -306,6 +298,7 @@ class NamedDataSourcePage(Directory):
|
|||
'duplicate',
|
||||
('history', 'snapshots_dir'),
|
||||
('preview-block', 'preview_block'),
|
||||
('update-documentation', 'update_documentation'),
|
||||
]
|
||||
do_not_call_in_templates = True
|
||||
|
||||
|
@ -319,6 +312,8 @@ class NamedDataSourcePage(Directory):
|
|||
self.datasource_ui = NamedDataSourceUI(self.datasource)
|
||||
get_response().breadcrumb.append((component + '/', self.datasource.name))
|
||||
self.snapshots_dir = SnapshotsDirectory(self.datasource)
|
||||
self.documented_object = self.datasource
|
||||
self.documented_element = self.datasource
|
||||
|
||||
def get_sidebar(self):
|
||||
r = TemplateIO(html=True)
|
||||
|
@ -670,7 +665,7 @@ class NamedDataSourcesDirectory(Directory):
|
|||
|
||||
error, reason = False, None
|
||||
try:
|
||||
datasource = NamedDataSource.import_from_xml(fp)
|
||||
datasource = NamedDataSource.import_from_xml(fp, check_deprecated=True)
|
||||
get_session().message = ('info', _('This datasource has been successfully imported.'))
|
||||
except NamedDataSourceImportError as e:
|
||||
error = True
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
# w.c.s. - web application for online forms
|
||||
# Copyright (C) 2005-2024 Entr'ouvert
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# 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 json
|
||||
|
||||
from quixote import get_request, get_response
|
||||
from quixote.html import htmltext
|
||||
|
||||
from wcs.qommon import _, template
|
||||
from wcs.qommon.form import RichTextWidget
|
||||
|
||||
|
||||
class DocumentableMixin:
|
||||
def get_documentable_button(self):
|
||||
return htmltext(template.render('wcs/backoffice/includes/documentation-editor-link.html', {}))
|
||||
|
||||
def get_documentable_zone(self):
|
||||
return htmltext('<span class="actions">%s</span>') % template.render(
|
||||
'wcs/backoffice/includes/documentation.html',
|
||||
{'element': self.documented_element, 'object': self.documented_object},
|
||||
)
|
||||
|
||||
def update_documentation(self):
|
||||
get_request().ignore_session = True
|
||||
get_response().set_content_type('application/json')
|
||||
try:
|
||||
content = get_request().json['content']
|
||||
except (KeyError, TypeError):
|
||||
return json.dumps({'err': 1})
|
||||
content = RichTextWidget('').clean_html(content) or None
|
||||
changed = False
|
||||
if content != self.documented_element.documentation:
|
||||
changed = True
|
||||
self.documented_element.documentation = content
|
||||
self.documented_object.store(_('Documentation update'))
|
||||
return json.dumps(
|
||||
{'err': 0, 'empty': not bool(self.documented_element.documentation), 'changed': changed}
|
||||
)
|
||||
|
||||
|
||||
class DocumentableFieldMixin:
|
||||
def documentation_part(self):
|
||||
if not self.field.documentation:
|
||||
get_response().filter['sidebar_attrs'] = 'hidden'
|
||||
return template.render(
|
||||
'wcs/backoffice/includes/documentation.html',
|
||||
{'element': self.documented_element, 'object': self.documented_object},
|
||||
)
|
|
@ -26,18 +26,21 @@ from wcs.admin import utils
|
|||
from wcs.carddef import CardDef
|
||||
from wcs.fields import BlockField, get_field_options
|
||||
from wcs.formdef import FormDef, UpdateStatisticsDataAfterJob
|
||||
from wcs.qommon import _, errors, get_cfg, misc
|
||||
from wcs.qommon import _, errors, get_cfg, misc, template
|
||||
from wcs.qommon.admin.menu import command_icon
|
||||
from wcs.qommon.form import CheckboxWidget, Form, HtmlWidget, OptGroup, SingleSelectWidget, StringWidget
|
||||
from wcs.qommon.substitution import CompatibilityNamesDict
|
||||
|
||||
from .documentable import DocumentableFieldMixin, DocumentableMixin
|
||||
|
||||
class FieldDefPage(Directory):
|
||||
_q_exports = ['', 'delete', 'duplicate']
|
||||
|
||||
class FieldDefPage(Directory, DocumentableMixin, DocumentableFieldMixin):
|
||||
_q_exports = ['', 'delete', 'duplicate', ('update-documentation', 'update_documentation')]
|
||||
|
||||
large = False
|
||||
page_id = None
|
||||
blacklisted_attributes = []
|
||||
is_documentable = True
|
||||
|
||||
def __init__(self, objectdef, field_id):
|
||||
self.objectdef = objectdef
|
||||
|
@ -47,6 +50,8 @@ class FieldDefPage(Directory):
|
|||
raise errors.TraversalError()
|
||||
if not self.field.label:
|
||||
self.field.label = str(_('None'))
|
||||
self.documented_object = objectdef
|
||||
self.documented_element = self.field
|
||||
label = misc.ellipsize(self.field.unhtmled_label, 40)
|
||||
last_breadcrumb_url_part, last_breadcrumb_label = get_response().breadcrumb[-1]
|
||||
get_response().breadcrumb = get_response().breadcrumb[:-1]
|
||||
|
@ -67,7 +72,11 @@ class FieldDefPage(Directory):
|
|||
return form
|
||||
|
||||
def get_sidebar(self):
|
||||
return None
|
||||
if not self.is_documentable:
|
||||
return None
|
||||
r = TemplateIO(html=True)
|
||||
r += self.documentation_part()
|
||||
return r.getvalue()
|
||||
|
||||
def _q_index(self):
|
||||
form = self.form()
|
||||
|
@ -94,15 +103,26 @@ class FieldDefPage(Directory):
|
|||
get_response().set_title(self.objectdef.name)
|
||||
get_response().filter['sidebar'] = self.get_sidebar() # noqa pylint: disable=assignment-from-none
|
||||
r = TemplateIO(html=True)
|
||||
r += htmltext('<div id="appbar" class="field-edit">')
|
||||
r += htmltext('<h2 class="field-edit--title">%s</h2>') % misc.ellipsize(
|
||||
self.field.unhtmled_label, 80
|
||||
)
|
||||
if isinstance(self.field, BlockField):
|
||||
r += htmltext('<h3 class="field-edit--subtitle">%s - <a href="%s">%s</a></h3>') % (
|
||||
_('Block of fields'),
|
||||
self.field.block.get_admin_url(),
|
||||
self.field.block.name,
|
||||
if self.is_documentable:
|
||||
r += htmltext('<span class="actions">%s</span>') % template.render(
|
||||
'wcs/backoffice/includes/documentation-editor-link.html', {}
|
||||
)
|
||||
r += htmltext('</div>')
|
||||
if isinstance(self.field, BlockField):
|
||||
try:
|
||||
block_field = self.field.block
|
||||
except KeyError:
|
||||
r += htmltext('<h3 class="field-edit--subtitle">%s</h3>') % self.field.get_type_label()
|
||||
else:
|
||||
r += htmltext('<h3 class="field-edit--subtitle">%s - <a href="%s">%s</a></h3>') % (
|
||||
_('Block of fields'),
|
||||
block_field.get_admin_url(),
|
||||
block_field.name,
|
||||
)
|
||||
else:
|
||||
r += htmltext('<h3 class="field-edit--subtitle">%s</h3>') % self.field.description
|
||||
existing_varnames = {
|
||||
|
@ -155,7 +175,7 @@ class FieldDefPage(Directory):
|
|||
self.objectdef.store(comment=_('Modification of field "%s"') % self.field.ellipsized_label)
|
||||
|
||||
def get_deletion_extra_warning(self):
|
||||
return _('Warning: this field data will be permanently deleted.')
|
||||
return {'level': 'warning', 'message': _('Warning: this field data will be permanently deleted.')}
|
||||
|
||||
def redirect_field_anchor(self, field):
|
||||
anchor = '#fieldId_%s' % field.id if field else ''
|
||||
|
@ -182,7 +202,7 @@ class FieldDefPage(Directory):
|
|||
if self.field.key not in ('page', 'subtitle', 'title', 'comment'):
|
||||
warning = self.get_deletion_extra_warning()
|
||||
if warning:
|
||||
form.widgets.append(HtmlWidget('<div class="warningnotice">%s</div>' % warning))
|
||||
form.widgets.append(HtmlWidget('<div class="%(level)snotice">%(message)s</div>' % warning))
|
||||
current_field_index = self.objectdef.fields.index(self.field)
|
||||
to_be_deleted = []
|
||||
if self.field.key == 'page':
|
||||
|
@ -196,7 +216,20 @@ class FieldDefPage(Directory):
|
|||
to_be_deleted.reverse()
|
||||
# add delete_fields checkbox only if the page has fields
|
||||
if to_be_deleted:
|
||||
form.add(CheckboxWidget, 'delete_fields', title=_('Also remove all fields from the page'))
|
||||
form.add(
|
||||
CheckboxWidget,
|
||||
'delete_fields',
|
||||
title=_('Also remove all fields from the page'),
|
||||
attrs={'data-dynamic-display-parent': 'true'},
|
||||
)
|
||||
form.widgets.append(
|
||||
HtmlWidget(
|
||||
'<div class="warningnotice" '
|
||||
'data-dynamic-display-child-of="delete_fields" '
|
||||
'data-dynamic-display-checked="true">%s</div>'
|
||||
% _('Warning: the page fields data will be permanently deleted.')
|
||||
)
|
||||
)
|
||||
form.add_submit('delete', _('Delete'))
|
||||
form.add_submit('cancel', _('Cancel'))
|
||||
if form.get_widget('cancel').parse():
|
||||
|
@ -311,8 +344,15 @@ class FieldsPagesDirectory(Directory):
|
|||
return directory
|
||||
|
||||
|
||||
class FieldsDirectory(Directory):
|
||||
_q_exports = ['', 'update_order', 'move_page_fields', 'new', 'pages']
|
||||
class FieldsDirectory(Directory, DocumentableMixin):
|
||||
_q_exports = [
|
||||
'',
|
||||
'update_order',
|
||||
'move_page_fields',
|
||||
'new',
|
||||
'pages',
|
||||
('update-documentation', 'update_documentation'),
|
||||
]
|
||||
field_def_page_class = FieldDefPage
|
||||
blacklisted_types = []
|
||||
page_id = None
|
||||
|
@ -327,6 +367,8 @@ class FieldsDirectory(Directory):
|
|||
|
||||
def __init__(self, objectdef):
|
||||
self.objectdef = objectdef
|
||||
self.documented_object = self.objectdef
|
||||
self.documented_element = self.objectdef
|
||||
self.pages = FieldsPagesDirectory(self)
|
||||
|
||||
def _q_traverse(self, path):
|
||||
|
@ -374,6 +416,18 @@ class FieldsDirectory(Directory):
|
|||
r += htmltext(' ')
|
||||
r += htmltext(_('It is close to the system limits and no new fields should be added.'))
|
||||
r += htmltext('</div>')
|
||||
elif (
|
||||
hasattr(self.objectdef, 'get_total_count_data_fields')
|
||||
and self.objectdef.get_total_count_data_fields() > 2000
|
||||
):
|
||||
# warn before DATA_UPLOAD_MAX_NUMBER_FIELDS
|
||||
r += htmltext('<div class="warningnotice">')
|
||||
r += htmltext('<p>%s %s</p>') % (
|
||||
_('There are at least %d data fields, including fields in blocks.')
|
||||
% self.objectdef.get_total_count_data_fields(),
|
||||
_('It is close to the system limits and no new fields should be added.'),
|
||||
)
|
||||
r += htmltext('</div>')
|
||||
|
||||
if [x for x in self.objectdef.fields if x.key == 'page']:
|
||||
if self.objectdef.fields[0].key != 'page':
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
# 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 difflib
|
||||
import io
|
||||
import xml.etree.ElementTree as ET
|
||||
|
@ -28,8 +29,10 @@ from wcs.backoffice.deprecations import DeprecationsDirectory
|
|||
from wcs.backoffice.snapshots import SnapshotsDirectory
|
||||
from wcs.carddef import CardDef
|
||||
from wcs.categories import Category
|
||||
from wcs.fields import PageField
|
||||
from wcs.formdef import (
|
||||
DRAFTS_DEFAULT_LIFESPAN,
|
||||
DRAFTS_DEFAULT_MAX_PER_USER,
|
||||
FormDef,
|
||||
FormdefImportError,
|
||||
FormdefImportRecoverableError,
|
||||
|
@ -59,13 +62,14 @@ from wcs.qommon.form import (
|
|||
)
|
||||
from wcs.qommon.misc import localstrftime
|
||||
from wcs.roles import get_user_roles, logged_users_role
|
||||
from wcs.sql_criterias import Equal, Null, StrictNotEqual
|
||||
from wcs.sql_criterias import Equal, GreaterOrEqual, Null, StrictNotEqual
|
||||
from wcs.workflows import Workflow
|
||||
|
||||
from . import utils
|
||||
from .blocks import BlocksDirectory
|
||||
from .categories import CategoriesDirectory, get_categories
|
||||
from .data_sources import NamedDataSourcesDirectory
|
||||
from .documentable import DocumentableMixin
|
||||
from .fields import FieldDefPage, FieldsDirectory
|
||||
from .logged_errors import LoggedErrorsDirectory
|
||||
|
||||
|
@ -211,7 +215,7 @@ class FormFieldDefPage(FieldDefPage):
|
|||
def get_deletion_extra_warning(self):
|
||||
if not self.objectdef.data_class().count():
|
||||
return None
|
||||
return self.deletion_extra_warning_message
|
||||
return {'level': 'warning', 'message': self.deletion_extra_warning_message}
|
||||
|
||||
|
||||
class FormFieldsDirectory(FieldsDirectory):
|
||||
|
@ -290,6 +294,23 @@ class OptionsDirectory(Directory):
|
|||
widget.validation_function = check_lifespan
|
||||
widget.validation_function_error_message = _('Lifespan must be between 2 and 100 days.')
|
||||
|
||||
widget = form.add(
|
||||
WcsExtraStringWidget,
|
||||
'drafts_max_per_user',
|
||||
title=_('Maximum number of drafts per user (between 2 and 100)'),
|
||||
value=self.formdef.drafts_max_per_user,
|
||||
hint=_('%s drafts per user by default') % DRAFTS_DEFAULT_MAX_PER_USER,
|
||||
)
|
||||
|
||||
def check_max_per_user(value):
|
||||
try:
|
||||
return bool(int(value) >= 2 and int(value) <= 100)
|
||||
except (ValueError, TypeError):
|
||||
return False
|
||||
|
||||
widget.validation_function = check_max_per_user
|
||||
widget.validation_function_error_message = _('Maximum must be between 2 and 100 drafts.')
|
||||
|
||||
form.widgets.append(HtmlWidget(htmltext('<h3>%s</h3>') % _('Tracking Code')))
|
||||
form.add(
|
||||
CheckboxWidget,
|
||||
|
@ -495,8 +516,10 @@ class OptionsDirectory(Directory):
|
|||
'id_template',
|
||||
'submission_lateral_template',
|
||||
'drafts_lifespan',
|
||||
'drafts_max_per_user',
|
||||
'user_support',
|
||||
'management_sidebar_items',
|
||||
'history_pane_default_mode',
|
||||
]
|
||||
for attr in attrs:
|
||||
widget = form.get_widget(attr)
|
||||
|
@ -507,8 +530,8 @@ class OptionsDirectory(Directory):
|
|||
continue
|
||||
new_value = widget.parse()
|
||||
if attr == 'management_sidebar_items':
|
||||
new_value = set(new_value)
|
||||
if new_value == self.formdef.__class__.management_sidebar_items:
|
||||
new_value = set(new_value or [])
|
||||
if new_value == self.formdef.get_default_management_sidebar_items():
|
||||
new_value = {'__default__'}
|
||||
if attr == 'digest_template':
|
||||
if self.formdef.default_digest_template != new_value:
|
||||
|
@ -599,7 +622,7 @@ class WorkflowRoleDirectory(Directory):
|
|||
return redirect('..')
|
||||
|
||||
|
||||
class FormDefPage(Directory, TempfileDirectoryMixin):
|
||||
class FormDefPage(Directory, TempfileDirectoryMixin, DocumentableMixin):
|
||||
do_not_call_in_templates = True
|
||||
_q_exports = [
|
||||
'',
|
||||
|
@ -611,7 +634,6 @@ class FormDefPage(Directory, TempfileDirectoryMixin):
|
|||
'enable',
|
||||
'workflow',
|
||||
'role',
|
||||
('workflow-options', 'workflow_options'),
|
||||
('workflow-variables', 'workflow_variables'),
|
||||
('workflow-status-remapping', 'workflow_status_remapping'),
|
||||
'roles',
|
||||
|
@ -627,6 +649,7 @@ class FormDefPage(Directory, TempfileDirectoryMixin):
|
|||
('backoffice-submission-roles', 'backoffice_submission_roles'),
|
||||
('logged-errors', 'logged_errors_dir'),
|
||||
('history', 'snapshots_dir'),
|
||||
('update-documentation', 'update_documentation'),
|
||||
]
|
||||
|
||||
formdef_class = FormDef
|
||||
|
@ -670,6 +693,8 @@ class FormDefPage(Directory, TempfileDirectoryMixin):
|
|||
parent_dir=self, formdef_class=self.formdef_class, formdef_id=self.formdef.id
|
||||
)
|
||||
self.snapshots_dir = SnapshotsDirectory(self.formdef)
|
||||
self.documented_object = self.formdef
|
||||
self.documented_element = self.formdef
|
||||
|
||||
def add_option_line(self, link, label, current_value, popup=True):
|
||||
return htmltext(
|
||||
|
@ -786,7 +811,8 @@ class FormDefPage(Directory, TempfileDirectoryMixin):
|
|||
_('Custom')
|
||||
if (
|
||||
self.formdef.skip_from_360_view
|
||||
or self.formdef.management_sidebar_items != {'__default__'}
|
||||
or self.formdef.management_sidebar_items
|
||||
not in ({'__default__'}, self.formdef.get_default_management_sidebar_items())
|
||||
)
|
||||
else _('Default'),
|
||||
),
|
||||
|
@ -836,17 +862,8 @@ class FormDefPage(Directory, TempfileDirectoryMixin):
|
|||
)
|
||||
|
||||
options['workflow_options'] = ''
|
||||
if self.formdef.workflow_id:
|
||||
pristine_workflow = Workflow.get(self.formdef.workflow_id, ignore_errors=True)
|
||||
if pristine_workflow and pristine_workflow.variables_formdef:
|
||||
options['workflow_options'] = self.add_option_line('workflow-variables', _('Options'), '')
|
||||
elif self.formdef.workflow_options and get_publisher().has_site_option(
|
||||
'enable-workflow-variable-parameter'
|
||||
):
|
||||
# there are no variables defined but there are some values
|
||||
# in workflow_options, this is probably the legacy stuff.
|
||||
if any(x for x in self.formdef.workflow_options if '*' in x):
|
||||
options['workflow_options'] = self.add_option_line('workflow-options', _('Options'), '')
|
||||
if self.formdef.workflow and self.formdef.workflow.variables_formdef:
|
||||
options['workflow_options'] = self.add_option_line('workflow-variables', _('Options'), '')
|
||||
|
||||
options['workflow_roles_list'] = []
|
||||
if self.formdef.workflow.roles:
|
||||
|
@ -1435,7 +1452,7 @@ class FormDefPage(Directory, TempfileDirectoryMixin):
|
|||
|
||||
error, reason = False, None
|
||||
try:
|
||||
new_formdef = self.formdef_class.import_from_xml(fp, include_id=True)
|
||||
new_formdef = self.formdef_class.import_from_xml(fp, include_id=True, check_deprecated=True)
|
||||
except FormdefImportError as e:
|
||||
error = True
|
||||
reason = _(e.msg) % e.msg_args
|
||||
|
@ -1682,55 +1699,6 @@ class FormDefPage(Directory, TempfileDirectoryMixin):
|
|||
r += form.render()
|
||||
return r.getvalue()
|
||||
|
||||
def workflow_options(self):
|
||||
request = get_request()
|
||||
if request.get_method() == 'GET' and request.form.get('file'):
|
||||
value = self.formdef.workflow_options.get(request.form.get('file'))
|
||||
if value:
|
||||
return value.build_response()
|
||||
|
||||
get_response().set_title(title=_('Workflow Options'))
|
||||
form = Form(enctype='multipart/form-data')
|
||||
pristine_workflow = Workflow.get(self.formdef.workflow_id)
|
||||
for status in self.formdef.workflow.possible_status:
|
||||
had_options = False
|
||||
for item in status.items:
|
||||
prefix = '%s*%s*' % (status.id, item.id)
|
||||
pristine_item = pristine_workflow.get_status(status.id).get_item(item.id)
|
||||
parameters = [x for x in item.get_parameters() if not getattr(pristine_item, x)]
|
||||
if not parameters:
|
||||
continue
|
||||
if not had_options:
|
||||
form.widgets.append(HtmlWidget('<h3>%s</h3>' % status.name))
|
||||
had_options = True
|
||||
label = getattr(item, 'label', None) or _(item.description)
|
||||
form.widgets.append(HtmlWidget('<h4>%s</h4>' % label))
|
||||
item.add_parameters_widgets(form, parameters, prefix=prefix, formdef=self.formdef)
|
||||
form.add_submit('submit', _('Submit'))
|
||||
form.add_submit('cancel', _('Cancel'))
|
||||
|
||||
if form.get_widget('cancel').parse():
|
||||
return redirect('.')
|
||||
|
||||
if form.is_submitted() and not form.has_errors():
|
||||
self.workflow_options_submit(form)
|
||||
return redirect('.')
|
||||
|
||||
r = TemplateIO(html=True)
|
||||
r += htmltext('<h2>%s</h2>') % _('Workflow Options')
|
||||
r += form.render()
|
||||
return r.getvalue()
|
||||
|
||||
def workflow_options_submit(self, form):
|
||||
self.formdef.workflow_options = {}
|
||||
for widget in form.get_all_widgets():
|
||||
if widget in form.get_submit_widgets():
|
||||
continue
|
||||
if widget.name.startswith('_'):
|
||||
continue
|
||||
self.formdef.workflow_options[widget.name] = widget.parse()
|
||||
self.formdef.store(comment=_('Change in workflow options'))
|
||||
|
||||
def inspect(self):
|
||||
get_response().set_title(self.formdef.name)
|
||||
get_response().breadcrumb.append(('inspect', _('Inspector')))
|
||||
|
@ -1786,13 +1754,65 @@ class FormDefPage(Directory, TempfileDirectoryMixin):
|
|||
else:
|
||||
role_label = '-'
|
||||
view.role = role_label
|
||||
|
||||
context['custom_views'] = sorted(custom_views, key=lambda x: getattr(x, 'title'))
|
||||
context['is_carddef'] = isinstance(self.formdef, CardDef)
|
||||
|
||||
if not hasattr(self.formdef, 'snapshot_object'):
|
||||
deprecations = DeprecationsDirectory()
|
||||
context['deprecations'] = deprecations.get_deprecations(
|
||||
f'{self.formdef.xml_root_node}:{self.formdef.id}'
|
||||
)
|
||||
context['deprecation_titles'] = deprecations.titles
|
||||
|
||||
receipt_time_criteria = GreaterOrEqual(
|
||||
'receipt_time',
|
||||
datetime.datetime.now() - datetime.timedelta(days=self.formdef.get_drafts_lifespan()),
|
||||
)
|
||||
|
||||
temp_drafts = defaultdict(int)
|
||||
for formdata in self.formdef.data_class().select_iterator(
|
||||
clause=[Equal('status', 'draft'), receipt_time_criteria], itersize=200
|
||||
):
|
||||
page_id = formdata.page_id if formdata.page_id is not None else '_unknown'
|
||||
temp_drafts[page_id] += 1
|
||||
|
||||
total_drafts = sum(temp_drafts.values()) if temp_drafts else 0
|
||||
drafts = {}
|
||||
special_page_index_mapping = {
|
||||
'_first_page': -1000, # first
|
||||
'_unknown': 1000, # last
|
||||
'_confirmation_page': 999, # second to last
|
||||
}
|
||||
if total_drafts:
|
||||
for page_id, page_index in special_page_index_mapping.items():
|
||||
try:
|
||||
page_total = temp_drafts.pop(page_id)
|
||||
except KeyError:
|
||||
page_total = 0
|
||||
drafts[page_id] = {'total': page_total, 'field': None, 'page_index': page_index}
|
||||
for page_id, page_total in temp_drafts.items():
|
||||
for index, field in enumerate(self.formdef.iter_fields(with_backoffice_fields=False)):
|
||||
if page_id == field.id and isinstance(field, PageField):
|
||||
drafts[page_id] = {
|
||||
'total': page_total,
|
||||
'field': field,
|
||||
'page_index': index,
|
||||
}
|
||||
break
|
||||
else:
|
||||
drafts['_unknown']['total'] += page_total
|
||||
|
||||
for draft_data in drafts.values():
|
||||
draft_data['percent'] = 100 * draft_data['total'] / total_drafts
|
||||
|
||||
total_formdata = self.formdef.data_class().count([receipt_time_criteria])
|
||||
context['drafts'] = sorted(drafts.items(), key=lambda x: x[1]['page_index'])
|
||||
context['percent_submitted_formdata'] = 100 * (total_formdata - total_drafts) / total_formdata
|
||||
context['total_formdata'] = total_formdata
|
||||
|
||||
context['total_drafts'] = total_drafts
|
||||
|
||||
return template.QommonTemplateResponse(
|
||||
templates=[self.inspect_template_name],
|
||||
context=context,
|
||||
|
@ -1820,6 +1840,7 @@ class FormsDirectory(AccessControlled, Directory):
|
|||
'categories',
|
||||
('data-sources', 'data_sources'),
|
||||
('application', 'applications_dir'),
|
||||
('test-users', 'test_users'),
|
||||
]
|
||||
|
||||
category_class = Category
|
||||
|
@ -1852,6 +1873,12 @@ class FormsDirectory(AccessControlled, Directory):
|
|||
super().__init__(*args, **kwargs)
|
||||
self.applications_dir = ApplicationsDirectory(self.formdef_class)
|
||||
|
||||
@property
|
||||
def test_users(self):
|
||||
from wcs.admin.tests import TestUsersDirectory
|
||||
|
||||
return TestUsersDirectory()
|
||||
|
||||
def _q_traverse(self, path):
|
||||
get_response().breadcrumb.append(('%s/' % self.section, self.top_title))
|
||||
get_response().set_backoffice_section(self.section)
|
||||
|
@ -2005,7 +2032,7 @@ class FormsDirectory(AccessControlled, Directory):
|
|||
error, reason = False, None
|
||||
try:
|
||||
try:
|
||||
formdef = self.formdef_class.import_from_xml(fp)
|
||||
formdef = self.formdef_class.import_from_xml(fp, check_deprecated=True)
|
||||
get_session().message = ('info', str(self.import_success_message))
|
||||
except FormdefImportRecoverableError:
|
||||
fp.seek(0)
|
||||
|
|
|
@ -317,7 +317,12 @@ class LoggedErrorsDirectory(AccessControlled, Directory):
|
|||
if 'others' in form.get_widget('types').parse():
|
||||
criterias.append(Null('formdef_class'))
|
||||
criterias = [Or(criterias)]
|
||||
criterias.append(Less('latest_occurence_timestamp', form.get_widget('latest_occurence').parse()))
|
||||
criterias.append(
|
||||
Less(
|
||||
'latest_occurence_timestamp',
|
||||
misc.get_as_datetime(form.get_widget('latest_occurence').parse()),
|
||||
)
|
||||
)
|
||||
get_publisher().loggederror_class.wipe(clause=criterias)
|
||||
return redirect('.')
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ from quixote.html import TemplateIO, htmltext
|
|||
|
||||
from wcs.admin import utils
|
||||
from wcs.admin.categories import MailTemplateCategoriesDirectory, get_categories
|
||||
from wcs.admin.documentable import DocumentableMixin
|
||||
from wcs.backoffice.applications import ApplicationsDirectory
|
||||
from wcs.backoffice.snapshots import SnapshotsDirectory
|
||||
from wcs.categories import MailTemplateCategory
|
||||
|
@ -150,7 +151,7 @@ class MailTemplatesDirectory(Directory):
|
|||
|
||||
error = False
|
||||
try:
|
||||
mail_template = MailTemplate.import_from_xml(fp)
|
||||
mail_template = MailTemplate.import_from_xml(fp, check_deprecated=True)
|
||||
get_session().message = ('info', _('This mail template has been successfully imported.'))
|
||||
except ValueError:
|
||||
error = True
|
||||
|
@ -167,7 +168,7 @@ class MailTemplatesDirectory(Directory):
|
|||
return redirect('%s/' % mail_template.id)
|
||||
|
||||
|
||||
class MailTemplatePage(Directory):
|
||||
class MailTemplatePage(Directory, DocumentableMixin):
|
||||
_q_exports = [
|
||||
'',
|
||||
'edit',
|
||||
|
@ -175,6 +176,7 @@ class MailTemplatePage(Directory):
|
|||
'duplicate',
|
||||
'export',
|
||||
('history', 'snapshots_dir'),
|
||||
('update-documentation', 'update_documentation'),
|
||||
]
|
||||
do_not_call_in_templates = True
|
||||
|
||||
|
@ -185,6 +187,8 @@ class MailTemplatePage(Directory):
|
|||
raise errors.TraversalError()
|
||||
get_response().breadcrumb.append((component + '/', self.mail_template.name))
|
||||
self.snapshots_dir = SnapshotsDirectory(self.mail_template)
|
||||
self.documented_object = self.mail_template
|
||||
self.documented_element = self.mail_template
|
||||
|
||||
def get_sidebar(self):
|
||||
r = TemplateIO(html=True)
|
||||
|
@ -242,15 +246,6 @@ class MailTemplatePage(Directory):
|
|||
options=category_options,
|
||||
value=self.mail_template.category_id,
|
||||
)
|
||||
|
||||
form.add(
|
||||
TextWidget,
|
||||
'description',
|
||||
title=_('Description'),
|
||||
cols=80,
|
||||
rows=3,
|
||||
value=self.mail_template.description,
|
||||
)
|
||||
form.add(
|
||||
StringWidget,
|
||||
'subject',
|
||||
|
@ -312,7 +307,6 @@ class MailTemplatePage(Directory):
|
|||
self.mail_template.name = name
|
||||
if form.get_widget('category_id'):
|
||||
self.mail_template.category_id = form.get_widget('category_id').parse()
|
||||
self.mail_template.description = form.get_widget('description').parse()
|
||||
self.mail_template.subject = form.get_widget('subject').parse()
|
||||
self.mail_template.body = form.get_widget('body').parse()
|
||||
self.mail_template.attachments = form.get_widget('attachments').parse()
|
||||
|
|
|
@ -131,6 +131,7 @@ authentication is unavailable. Lasso must be installed to use it.'
|
|||
|
||||
class UserFieldDefPage(FieldDefPage):
|
||||
blacklisted_attributes = ['condition']
|
||||
is_documentable = False
|
||||
|
||||
|
||||
class UserFieldsDirectory(FieldsDirectory):
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
import collections
|
||||
import copy
|
||||
import json
|
||||
import uuid
|
||||
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.timezone import now
|
||||
|
@ -24,10 +25,14 @@ from quixote import get_publisher, get_request, get_response, get_session, redir
|
|||
from quixote.directory import Directory
|
||||
from quixote.html import TemplateIO, htmltext
|
||||
|
||||
from wcs.admin import utils
|
||||
from wcs.admin.workflow_tests import WorkflowTestsDirectory
|
||||
from wcs.api import posted_json_data_to_formdata_data
|
||||
from wcs.backoffice.management import FormBackofficeEditPage, FormBackOfficeStatusPage
|
||||
from wcs.backoffice.pagination import pagination_links
|
||||
from wcs.backoffice.snapshots import SnapshotsDirectory
|
||||
from wcs.carddef import CardDef
|
||||
from wcs.formdef import FormDef
|
||||
from wcs.forms.common import FormStatusPage
|
||||
from wcs.qommon import _, misc, template
|
||||
from wcs.qommon.afterjobs import AfterJob
|
||||
|
@ -35,14 +40,16 @@ from wcs.qommon.errors import TraversalError
|
|||
from wcs.qommon.form import (
|
||||
FileWidget,
|
||||
Form,
|
||||
JsonpSingleSelectWidget,
|
||||
RadiobuttonsWidget,
|
||||
SingleSelectWidget,
|
||||
StringWidget,
|
||||
TextWidget,
|
||||
UrlWidget,
|
||||
WidgetDict,
|
||||
WidgetList,
|
||||
)
|
||||
from wcs.sql_criterias import Equal, Null, StrictNotEqual
|
||||
from wcs.sql_criterias import Equal, NotNull, Null, StrictNotEqual
|
||||
from wcs.testdef import TestDef, TestError, TestResult, WebserviceResponse
|
||||
from wcs.workflow_tests import WorkflowTestError
|
||||
from wcs.workflow_traces import WorkflowTrace
|
||||
|
@ -68,11 +75,8 @@ class TestEditPage(FormBackofficeEditPage):
|
|||
return super()._q_index()
|
||||
|
||||
def create_form(self, *args, **kwargs):
|
||||
# FormBackofficeEditPage.create_form is relevant only for forms, skip it for cards
|
||||
if self.testdef.object_type == 'formdefs':
|
||||
form = super().create_form(*args, **kwargs)
|
||||
else:
|
||||
form = super(FormBackofficeEditPage, self).create_form(*args, **kwargs)
|
||||
form = super().create_form(*args, **kwargs)
|
||||
form.attrs['data-live-url'] = self.testdef.get_admin_url() + 'edit-data/live'
|
||||
return form
|
||||
|
||||
def modify_filling_context(self, context, *args, **kwargs):
|
||||
|
@ -130,12 +134,12 @@ class TestEditPage(FormBackofficeEditPage):
|
|||
self.testdef.data = testdef.data
|
||||
|
||||
self.testdef.expected_error = get_request().form.get('error')
|
||||
self.testdef.store()
|
||||
self.testdef.store(comment=_('Mark test as failing'))
|
||||
return redirect('..')
|
||||
|
||||
def change_submission_mode(self):
|
||||
self.testdef.is_in_backoffice = not self.testdef.is_in_backoffice
|
||||
self.testdef.store()
|
||||
self.testdef.store(comment=_('Change submission mode'))
|
||||
return redirect('.')
|
||||
|
||||
|
||||
|
@ -148,19 +152,27 @@ class TestPage(FormBackOfficeStatusPage):
|
|||
'duplicate',
|
||||
('workflow', 'workflow_tests'),
|
||||
('webservice-responses', 'webservice_responses'),
|
||||
('history', 'snapshots_dir'),
|
||||
]
|
||||
|
||||
def __init__(self, component, objectdef):
|
||||
def __init__(self, component, objectdef=None, instance=None):
|
||||
try:
|
||||
self.testdef = TestDef.get(component)
|
||||
self.testdef = instance or TestDef.get(component)
|
||||
except KeyError:
|
||||
raise TraversalError()
|
||||
|
||||
if not objectdef:
|
||||
klass = FormDef if self.testdef.object_type == 'formdefs' else CardDef
|
||||
objectdef = klass.get(self.testdef.object_id)
|
||||
|
||||
self.testdef.formdef = objectdef
|
||||
|
||||
filled = self.testdef.build_formdata(objectdef, include_fields=True)
|
||||
super().__init__(objectdef, filled)
|
||||
|
||||
self.workflow_tests = WorkflowTestsDirectory(self.testdef, self.formdef)
|
||||
self.webservice_responses = WebserviceResponseDirectory(self.testdef)
|
||||
self.snapshots_dir = SnapshotsDirectory(self.testdef)
|
||||
|
||||
@property
|
||||
def edit_data(self):
|
||||
|
@ -178,16 +190,28 @@ class TestPage(FormBackOfficeStatusPage):
|
|||
return False
|
||||
|
||||
def get_extra_context_bar(self, parent=None):
|
||||
return render_to_string('wcs/backoffice/test_sidebar.html', context={})
|
||||
if self.testdef.is_readonly():
|
||||
r = TemplateIO(html=True)
|
||||
r += htmltext('<div class="infonotice"><p>%s</p></div>') % _('This test is readonly.')
|
||||
r += utils.snapshot_info_block(self.testdef.snapshot_object)
|
||||
r += htmltext('<h3>%s</h3>') % _('Navigation')
|
||||
r += htmltext(
|
||||
'<li><a class="button button-paragraph" href="webservice-responses/">%s</a></li>'
|
||||
) % _('Webservice responses')
|
||||
r += htmltext('<li><a class="button button-paragraph" href="inspect">%s</a></li>') % _('Inspect')
|
||||
r += htmltext('</h3>')
|
||||
return r.getvalue()
|
||||
else:
|
||||
return render_to_string('wcs/backoffice/test_sidebar.html', context={})
|
||||
|
||||
def status(self):
|
||||
r = TemplateIO(html=True)
|
||||
r += htmltext('<div id="appbar">')
|
||||
r += htmltext('<h2>%s</h2>') % self.testdef
|
||||
r += htmltext('<span class="actions">')
|
||||
r += htmltext('<a href="edit-data/">%s</a>') % _('Edit data')
|
||||
if get_publisher().has_site_option('enable-workflow-tests'):
|
||||
r += htmltext('<a href="workflow/">%s</a>') % _('Workflow tests')
|
||||
if not self.testdef.is_readonly():
|
||||
r += htmltext('<a href="edit-data/">%s</a>') % _('Edit data')
|
||||
r += htmltext('<a href="workflow/">%s</a>') % _('Workflow tests')
|
||||
r += htmltext('</span>')
|
||||
r += htmltext('</div>')
|
||||
if self.testdef.expected_error:
|
||||
|
@ -227,13 +251,14 @@ class TestPage(FormBackOfficeStatusPage):
|
|||
form.add(StringWidget, 'name', title=_('Name'), required=True, size=50, value=self.testdef.name)
|
||||
|
||||
user_options = [('', '---', '')] + [
|
||||
(x.id, str(x), x.id) for x in get_publisher().user_class.select(order_by='name')
|
||||
(x.test_uuid, str(x), x.test_uuid)
|
||||
for x in get_publisher().user_class.select([NotNull('test_uuid')], order_by='name')
|
||||
]
|
||||
form.add(
|
||||
SingleSelectWidget,
|
||||
'user',
|
||||
title=_('User'),
|
||||
value=self.testdef.data['user'].get('id', '') if self.testdef.data['user'] else '',
|
||||
value=self.testdef.user_uuid or '',
|
||||
options=user_options,
|
||||
**{'data-autocomplete': 'true'},
|
||||
)
|
||||
|
@ -251,15 +276,9 @@ class TestPage(FormBackOfficeStatusPage):
|
|||
return r.getvalue()
|
||||
else:
|
||||
self.testdef.name = form.get_widget('name').parse()
|
||||
self.testdef.user_uuid = form.get_widget('user').parse()
|
||||
|
||||
user_id = form.get_widget('user').parse()
|
||||
if user_id:
|
||||
user = get_publisher().user_class.get(user_id)
|
||||
self.testdef.data['user'] = user.get_json_export_dict()
|
||||
else:
|
||||
self.testdef.data['user'] = None
|
||||
|
||||
self.testdef.store()
|
||||
self.testdef.store(comment=_('Change in options'))
|
||||
return redirect('.')
|
||||
|
||||
def duplicate(self):
|
||||
|
@ -290,7 +309,7 @@ class TestPage(FormBackOfficeStatusPage):
|
|||
|
||||
self.testdef.name = form.get_widget('name').parse()
|
||||
self.testdef = TestDef.import_from_xml_tree(self.testdef.export_to_xml(), self.formdef)
|
||||
self.testdef.store()
|
||||
self.testdef.store(comment=_('Creation (from duplication)'))
|
||||
|
||||
return redirect(self.testdef.get_admin_url())
|
||||
|
||||
|
@ -347,11 +366,8 @@ class TestsDirectory(Directory):
|
|||
creation_options = [
|
||||
('empty', _('Fill data manually'), 'empty'),
|
||||
('formdata', _('Import data from form'), 'formdata'),
|
||||
('formdata-wf', _('Import data from form (and initialise workflow tests)'), 'formdata-wf'),
|
||||
]
|
||||
if get_publisher().has_site_option('enable-workflow-tests'):
|
||||
creation_options.append(
|
||||
('formdata-wf', _('Import data from form (and initialise workflow tests)'), 'formdata-wf')
|
||||
)
|
||||
form.add(
|
||||
RadiobuttonsWidget,
|
||||
'creation_mode',
|
||||
|
@ -386,12 +402,15 @@ class TestsDirectory(Directory):
|
|||
r += form.render()
|
||||
return r.getvalue()
|
||||
|
||||
agent_user = get_publisher().user_class.get(get_session().user)
|
||||
test_agent_user, dummy = TestDef.get_or_create_test_user(agent_user)
|
||||
|
||||
creation_mode_widget = form.get_widget('creation_mode')
|
||||
if not creation_mode_widget or creation_mode_widget.parse() == 'empty':
|
||||
testdef = TestDef.create_from_formdata(self.objectdef, self.objectdef.data_class()())
|
||||
testdef.name = form.get_widget('name').parse()
|
||||
testdef.agent_id = get_session().user
|
||||
testdef.store()
|
||||
testdef.agent_id = test_agent_user.test_uuid
|
||||
testdef.store(comment=_('Creation (empty)'))
|
||||
return redirect(testdef.get_admin_url() + 'edit-data/')
|
||||
else:
|
||||
formdata_id = form.get_widget('formdata').parse()
|
||||
|
@ -403,8 +422,8 @@ class TestsDirectory(Directory):
|
|||
add_workflow_tests=bool(creation_mode_widget.parse() == 'formdata-wf'),
|
||||
)
|
||||
testdef.name = form.get_widget('name').parse()
|
||||
testdef.agent_id = get_session().user
|
||||
testdef.store()
|
||||
testdef.agent_id = test_agent_user.test_uuid
|
||||
testdef.store(comment=_('Creation (from formdata)'))
|
||||
return redirect(testdef.get_admin_url())
|
||||
|
||||
def p_import(self):
|
||||
|
@ -427,9 +446,6 @@ class TestsDirectory(Directory):
|
|||
get_response().set_title(_('Import Test'))
|
||||
r = TemplateIO(html=True)
|
||||
r += htmltext('<h2>%s</h2>') % _('Import Test')
|
||||
r += htmltext('<p>%s</p>') % _(
|
||||
'You can add a new test or update an existing one by importing a JSON file.'
|
||||
)
|
||||
r += form.render()
|
||||
return r.getvalue()
|
||||
|
||||
|
@ -442,6 +458,7 @@ class TestsDirectory(Directory):
|
|||
form.set_error('file', _('Invalid File'))
|
||||
raise e
|
||||
|
||||
testdef.store(comment=_('Creation (from import)'))
|
||||
get_session().message = ('info', _('Test "%s" has been successfully imported.') % testdef.name)
|
||||
return redirect('.')
|
||||
|
||||
|
@ -650,12 +667,13 @@ class TestResultsDirectory(Directory):
|
|||
|
||||
|
||||
class TestsAfterJob(AfterJob):
|
||||
def __init__(self, objectdef, reason, snapshot=None, **kwargs):
|
||||
def __init__(self, objectdef, reason, snapshot=None, triggered_by='', **kwargs):
|
||||
super().__init__(
|
||||
objectdef_class=objectdef.__class__,
|
||||
objectdef_id=objectdef.id,
|
||||
reason=reason,
|
||||
reason=str(reason or ''),
|
||||
snapshot_id=snapshot.id if snapshot else None,
|
||||
triggered_by=triggered_by,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
@ -666,7 +684,7 @@ class TestsAfterJob(AfterJob):
|
|||
return
|
||||
reason = self.kwargs['reason']
|
||||
|
||||
result = self.run_tests(objectdef, reason)
|
||||
result = self.run_tests(objectdef, reason, self.kwargs.get('triggered_by', ''))
|
||||
|
||||
if result and self.kwargs['snapshot_id'] is not None:
|
||||
snapshot = get_publisher().snapshot_class.get(self.kwargs['snapshot_id'])
|
||||
|
@ -674,11 +692,14 @@ class TestsAfterJob(AfterJob):
|
|||
snapshot.store()
|
||||
|
||||
@staticmethod
|
||||
def run_tests(objectdef, reason):
|
||||
def run_tests(objectdef, reason, triggered_by=''):
|
||||
testdefs = TestDef.select_for_objectdef(objectdef)
|
||||
if not testdefs:
|
||||
return
|
||||
|
||||
if triggered_by == 'workflow-change' and not any(x.workflow_tests.actions for x in testdefs):
|
||||
return
|
||||
|
||||
for test in testdefs:
|
||||
try:
|
||||
test.run(objectdef)
|
||||
|
@ -822,7 +843,8 @@ class WebserviceResponsePage(Directory):
|
|||
},
|
||||
)
|
||||
|
||||
form.add_submit('submit', _('Submit'))
|
||||
if not self.testdef.is_readonly():
|
||||
form.add_submit('submit', _('Submit'))
|
||||
form.add_submit('cancel', _('Cancel'))
|
||||
form.add_media()
|
||||
|
||||
|
@ -844,6 +866,7 @@ class WebserviceResponsePage(Directory):
|
|||
self.webservice_response.method = form.get_widget('method').parse()
|
||||
self.webservice_response.post_data = form.get_widget('post_data').parse()
|
||||
self.webservice_response.store()
|
||||
self.testdef.store(comment=_('Change webservice response "%s"') % self.webservice_response.name)
|
||||
|
||||
return redirect('..')
|
||||
|
||||
|
@ -869,11 +892,14 @@ class WebserviceResponsePage(Directory):
|
|||
new_webservice_response.id = None
|
||||
new_webservice_response.name = '%s %s' % (new_webservice_response.name, _('(copy)'))
|
||||
new_webservice_response.store()
|
||||
self.testdef.store(
|
||||
comment=_('Duplication of webservice response "%s"') % self.webservice_response.name
|
||||
)
|
||||
return redirect('..')
|
||||
|
||||
|
||||
class WebserviceResponseDirectory(Directory):
|
||||
_q_exports = ['', 'new']
|
||||
_q_exports = ['', 'new', ('import', 'p_import')]
|
||||
|
||||
def __init__(self, testdef):
|
||||
self.testdef = testdef
|
||||
|
@ -888,7 +914,8 @@ class WebserviceResponseDirectory(Directory):
|
|||
def _q_index(self):
|
||||
context = {
|
||||
'webservice_responses': self.testdef.get_webservice_responses(),
|
||||
'has_sidebar': True,
|
||||
'has_sidebar': bool(not self.testdef.is_readonly()),
|
||||
'testdef': self.testdef,
|
||||
}
|
||||
get_response().add_javascript(['popup.js'])
|
||||
get_response().set_title(_('Webservice responses'))
|
||||
|
@ -920,5 +947,287 @@ class WebserviceResponseDirectory(Directory):
|
|||
webservice_response.testdef_id = self.testdef.id
|
||||
webservice_response.name = form.get_widget('name').parse()
|
||||
webservice_response.store()
|
||||
self.testdef.store(comment=_('New webservice response "%s"') % webservice_response.name)
|
||||
|
||||
return redirect(self.testdef.get_admin_url() + 'webservice-responses/%s/' % webservice_response.id)
|
||||
|
||||
def p_import(self):
|
||||
form = Form(enctype='multipart/form-data')
|
||||
|
||||
testdef_options = [
|
||||
(x.id, x, x.id)
|
||||
for x in TestDef.select_for_objectdef(self.testdef.formdef)
|
||||
if x.id != self.testdef.id
|
||||
]
|
||||
form.add(
|
||||
SingleSelectWidget,
|
||||
'testdef_id',
|
||||
required=True,
|
||||
options=[(None, '---', None)] + testdef_options,
|
||||
**{'data-autocomplete': 'true'},
|
||||
)
|
||||
|
||||
form.add_submit('submit', _('Import'))
|
||||
form.add_submit('cancel', _('Cancel'))
|
||||
|
||||
if form.get_submit() == 'cancel':
|
||||
return redirect('.')
|
||||
|
||||
if not form.is_submitted() or form.has_errors():
|
||||
get_response().breadcrumb.append(('import', _('Import')))
|
||||
get_response().set_title(_('Import webservice responses'))
|
||||
r = TemplateIO(html=True)
|
||||
r += htmltext('<h2>%s</h2>') % _('Import webservice responses')
|
||||
r += form.render()
|
||||
return r.getvalue()
|
||||
|
||||
testdef_id = form.get_widget('testdef_id').parse()
|
||||
testdef = TestDef.get(testdef_id)
|
||||
|
||||
for response in testdef.get_webservice_responses():
|
||||
response.id = None
|
||||
response.testdef_id = self.testdef.id
|
||||
response.store()
|
||||
|
||||
return redirect('.')
|
||||
|
||||
|
||||
class TestUserPage(Directory):
|
||||
_q_exports = ['', 'delete', 'export']
|
||||
|
||||
def __init__(self, component):
|
||||
try:
|
||||
self.user = get_publisher().user_class.get(component)
|
||||
except IndexError:
|
||||
raise TraversalError()
|
||||
|
||||
if not self.user.test_uuid:
|
||||
raise TraversalError()
|
||||
|
||||
def _q_index(self):
|
||||
form = Form(enctype='multipart/form-data')
|
||||
|
||||
formdef = get_publisher().user_class.get_formdef()
|
||||
form.add(
|
||||
StringWidget, 'name', title=_('Test user label'), required=True, size=30, value=self.user.name
|
||||
)
|
||||
roles = list(get_publisher().role_class.select(order_by='name'))
|
||||
form.add(
|
||||
WidgetList,
|
||||
'roles',
|
||||
title=_('Roles'),
|
||||
element_type=SingleSelectWidget,
|
||||
value=self.user.roles,
|
||||
add_element_label=_('Add Role'),
|
||||
element_kwargs={
|
||||
'render_br': False,
|
||||
'options': [(None, '---', None)]
|
||||
+ [(x.id, x.name, x.id) for x in roles if not x.is_internal()],
|
||||
},
|
||||
)
|
||||
formdef.add_fields_to_form(form, form_data=self.user.form_data)
|
||||
|
||||
form.add_submit('submit', _('Submit'))
|
||||
form.add_submit('cancel', _('Cancel'))
|
||||
form.add_media()
|
||||
|
||||
if form.get_widget('cancel').parse():
|
||||
return redirect('.')
|
||||
|
||||
if form.get_submit() == 'submit' and not form.has_errors():
|
||||
formdef = get_publisher().user_class.get_formdef()
|
||||
data = formdef.get_data(form)
|
||||
self.user.set_attributes_from_formdata(data)
|
||||
self.user.form_data = data
|
||||
|
||||
if get_publisher().user_class.count(
|
||||
[Equal('email', self.user.email), NotNull('test_uuid'), StrictNotEqual('id', self.user.id)]
|
||||
):
|
||||
form.add_global_errors([_('A test user with this email already exists.')])
|
||||
else:
|
||||
self.user.name = form.get_widget('name').parse()
|
||||
self.user.roles = form.get_widget('roles').parse()
|
||||
self.user.store()
|
||||
|
||||
return redirect('..')
|
||||
|
||||
get_response().breadcrumb.append(('edit', _('Edit test user')))
|
||||
r = TemplateIO(html=True)
|
||||
r += htmltext('<div id="appbar">')
|
||||
r += htmltext('<h2>%s</h2>') % (_('Edit test user'))
|
||||
r += htmltext('<span class="actions">')
|
||||
r += htmltext('<a href="export">%s</a>') % _('Export')
|
||||
r += htmltext('</span>')
|
||||
r += htmltext('</div>')
|
||||
r += form.render()
|
||||
return r.getvalue()
|
||||
|
||||
def delete(self):
|
||||
form = Form(enctype='multipart/form-data')
|
||||
form.add_submit('delete', _('Delete'))
|
||||
form.add_submit('cancel', _('Cancel'))
|
||||
|
||||
if form.get_widget('cancel').parse():
|
||||
return redirect('.')
|
||||
if not form.is_submitted() or form.has_errors():
|
||||
get_response().breadcrumb.append(('delete', _('Delete')))
|
||||
r = TemplateIO(html=True)
|
||||
r += htmltext('<h2>%s %s</h2>') % (_('Deleting:'), self.user)
|
||||
r += form.render()
|
||||
return r.getvalue()
|
||||
|
||||
self.user.remove_object(self.user.id)
|
||||
return redirect('..')
|
||||
|
||||
def export(self):
|
||||
get_response().set_content_type('application/json')
|
||||
get_response().set_header(
|
||||
'content-disposition', 'attachment; filename=wcs_test_user_%s.json' % self.user.name
|
||||
)
|
||||
return json.dumps({'test-users': [self.user.get_json_export_dict(full=True, include_roles=True)]})
|
||||
|
||||
|
||||
class TestUsersDirectory(Directory):
|
||||
_q_exports = ['', 'new', 'export', ('import', 'p_import')]
|
||||
|
||||
def _q_traverse(self, path):
|
||||
get_response().breadcrumb.append(('test-users/', _('Test users')))
|
||||
return super()._q_traverse(path)
|
||||
|
||||
def _q_lookup(self, component):
|
||||
return TestUserPage(component)
|
||||
|
||||
def _q_index(self):
|
||||
context = {
|
||||
'users': get_publisher().user_class.select([NotNull('test_uuid')]),
|
||||
'has_sidebar': True,
|
||||
}
|
||||
get_response().add_javascript(['popup.js', 'select2.js'])
|
||||
get_response().set_title(_('Test users'))
|
||||
return template.QommonTemplateResponse(
|
||||
templates=['wcs/backoffice/test-users.html'],
|
||||
context=context,
|
||||
is_django_native=True,
|
||||
)
|
||||
|
||||
def new(self):
|
||||
form = Form(enctype='multipart/form-data')
|
||||
form.add(StringWidget, 'name', title=_('Name'), required=True, size=50)
|
||||
|
||||
creation_options = [
|
||||
('empty', _('Empty user'), 'empty'),
|
||||
('copy', _('Copy existing user'), 'copy'),
|
||||
]
|
||||
form.add(
|
||||
RadiobuttonsWidget,
|
||||
'creation_mode',
|
||||
options=creation_options,
|
||||
value='empty',
|
||||
attrs={'data-dynamic-display-parent': 'true'},
|
||||
)
|
||||
form.attrs['data-enable-select2'] = 'on'
|
||||
form.add(
|
||||
JsonpSingleSelectWidget,
|
||||
'user_id',
|
||||
url='/api/users/',
|
||||
attrs={
|
||||
'data-dynamic-display-child-of': 'creation_mode',
|
||||
'data-dynamic-display-value-in': 'copy',
|
||||
},
|
||||
)
|
||||
|
||||
form.add_submit('submit', _('Submit'))
|
||||
form.add_submit('cancel', _('Cancel'))
|
||||
|
||||
if form.get_widget('cancel').parse():
|
||||
return redirect('.')
|
||||
|
||||
if form.is_submitted() and not form.has_errors():
|
||||
if form.get_widget('creation_mode').parse() == 'empty':
|
||||
user = get_publisher().user_class()
|
||||
user.test_uuid = str(uuid.uuid4())
|
||||
else:
|
||||
user = get_publisher().user_class.get(form.get_widget('user_id').parse())
|
||||
user, created = TestDef.get_or_create_test_user(user)
|
||||
if not created:
|
||||
form.get_widget('user_id').set_error(_('A test user with this email already exists.'))
|
||||
|
||||
if not form.has_errors():
|
||||
user.name = form.get_widget('name').parse()
|
||||
user.store()
|
||||
return redirect('.')
|
||||
|
||||
get_response().breadcrumb.append(('new', _('New')))
|
||||
get_response().set_title(_('New test user'))
|
||||
r = TemplateIO(html=True)
|
||||
r += htmltext('<h2>%s</h2>') % _('New test user')
|
||||
r += form.render()
|
||||
return r.getvalue()
|
||||
|
||||
def export(self):
|
||||
get_response().set_content_type('application/json')
|
||||
get_response().set_header('content-disposition', 'attachment; filename=wcs_test_users.json')
|
||||
users = get_publisher().user_class.select([NotNull('test_uuid')])
|
||||
return json.dumps(
|
||||
{'test-users': [x.get_json_export_dict(full=True, include_roles=True) for x in users]}
|
||||
)
|
||||
|
||||
def p_import(self):
|
||||
form = Form(enctype='multipart/form-data')
|
||||
|
||||
form.add(FileWidget, 'file', title=_('File'), required=True)
|
||||
form.add_submit('submit', _('Import'))
|
||||
form.add_submit('cancel', _('Cancel'))
|
||||
|
||||
if form.get_submit() == 'cancel':
|
||||
return redirect('.')
|
||||
|
||||
if form.is_submitted() and not form.has_errors():
|
||||
try:
|
||||
return self.import_submit(form)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
get_response().breadcrumb.append(('import', _('Import')))
|
||||
get_response().set_title(_('Import test users'))
|
||||
r = TemplateIO(html=True)
|
||||
r += htmltext('<h2>%s</h2>') % _('Import test users')
|
||||
r += form.render()
|
||||
return r.getvalue()
|
||||
|
||||
def import_submit(self, form):
|
||||
fp = form.get_widget('file').parse().fp
|
||||
try:
|
||||
data = json.loads(fp.read())
|
||||
except ValueError:
|
||||
form.set_error('file', _('Invalid JSON file'))
|
||||
raise ValueError
|
||||
|
||||
existing_users = get_publisher().user_class.select([NotNull('test_uuid')])
|
||||
existing_uuids = {x.test_uuid for x in existing_users}
|
||||
existing_emails = {x.email for x in existing_users}
|
||||
|
||||
users = []
|
||||
users_were_ignored = False
|
||||
for user_dict in data.get('test-users', []):
|
||||
try:
|
||||
user = get_publisher().user_class.import_from_json(user_dict)
|
||||
except KeyError:
|
||||
form.set_error('file', _('Invalid File'))
|
||||
raise ValueError
|
||||
|
||||
if user.test_uuid in existing_uuids or user.email in existing_emails:
|
||||
users_were_ignored = True
|
||||
continue
|
||||
|
||||
users.append(user)
|
||||
|
||||
for user in users:
|
||||
user.store()
|
||||
|
||||
if users_were_ignored:
|
||||
get_session().message = ('warning', _('Some already existing users were not imported.'))
|
||||
else:
|
||||
get_session().message = ('success', _('Test users have been successfully imported.'))
|
||||
|
||||
return redirect('.')
|
||||
|
|
|
@ -24,6 +24,7 @@ from quixote.html import TemplateIO, htmltext
|
|||
from wcs.qommon import _, template
|
||||
from wcs.qommon.errors import TraversalError
|
||||
from wcs.qommon.form import Form, SingleSelectWidget
|
||||
from wcs.sql_criterias import NotNull
|
||||
from wcs.workflow_tests import get_test_action_class_by_type, get_test_action_options
|
||||
|
||||
|
||||
|
@ -46,7 +47,8 @@ class WorkflowTestActionPage(Directory):
|
|||
if not form.widgets:
|
||||
form.add_global_errors([htmltext(self.action.empty_form_error)])
|
||||
else:
|
||||
form.add_submit('submit', _('Submit'))
|
||||
if not self.testdef.is_readonly():
|
||||
form.add_submit('submit', _('Submit'))
|
||||
form.add_submit('cancel', _('Cancel'))
|
||||
|
||||
if form.get_widget('cancel').parse():
|
||||
|
@ -68,7 +70,7 @@ class WorkflowTestActionPage(Directory):
|
|||
|
||||
setattr(self.action, widget.name, value)
|
||||
|
||||
self.testdef.store()
|
||||
self.testdef.store(comment=_('Change in workflow test action "%s"') % self.action.label)
|
||||
return redirect('..')
|
||||
|
||||
def delete(self):
|
||||
|
@ -89,14 +91,15 @@ class WorkflowTestActionPage(Directory):
|
|||
self.testdef.workflow_tests.actions = [
|
||||
x for x in self.testdef.workflow_tests.actions if x.id != self.action.id
|
||||
]
|
||||
self.testdef.store()
|
||||
self.testdef.store(comment=_('Deletion of workflow test action "%s"') % self.action.label)
|
||||
return redirect('..')
|
||||
|
||||
def duplicate(self):
|
||||
new_action = copy.deepcopy(self.action)
|
||||
new_action.id = self.testdef.workflow_tests.get_new_action_id()
|
||||
self.testdef.workflow_tests.actions.append(new_action)
|
||||
self.testdef.store()
|
||||
action_position = self.testdef.workflow_tests.actions.index(self.action)
|
||||
self.testdef.workflow_tests.actions.insert(action_position + 1, new_action)
|
||||
self.testdef.store(comment=_('Duplication of workflow test action "%s"') % self.action.label)
|
||||
return redirect('..')
|
||||
|
||||
|
||||
|
@ -118,7 +121,7 @@ class WorkflowTestsDirectory(Directory):
|
|||
def _q_index(self):
|
||||
context = {
|
||||
'testdef': self.testdef,
|
||||
'has_sidebar': True,
|
||||
'has_sidebar': bool(not self.testdef.is_readonly()),
|
||||
'sidebar_form': self.get_sidebar_form(),
|
||||
}
|
||||
|
||||
|
@ -147,7 +150,8 @@ class WorkflowTestsDirectory(Directory):
|
|||
form = Form(enctype='multipart/form-data')
|
||||
|
||||
user_options = [('', '---', '')] + [
|
||||
(str(x.id), str(x), str(x.id)) for x in get_publisher().user_class.select(order_by='name')
|
||||
(str(x.test_uuid), str(x), str(x.test_uuid))
|
||||
for x in get_publisher().user_class.select([NotNull('test_uuid')], order_by='name')
|
||||
]
|
||||
form.add(
|
||||
SingleSelectWidget,
|
||||
|
@ -158,7 +162,8 @@ class WorkflowTestsDirectory(Directory):
|
|||
**{'data-autocomplete': 'true'},
|
||||
)
|
||||
|
||||
form.add_submit('submit', _('Submit'))
|
||||
if not self.testdef.is_readonly():
|
||||
form.add_submit('submit', _('Submit'))
|
||||
form.add_submit('cancel', _('Cancel'))
|
||||
|
||||
if form.get_widget('cancel').parse():
|
||||
|
@ -173,7 +178,7 @@ class WorkflowTestsDirectory(Directory):
|
|||
return r.getvalue()
|
||||
|
||||
self.testdef.agent_id = form.get_widget('agent').parse()
|
||||
self.testdef.store()
|
||||
self.testdef.store(comment=_('Change in workflow test options'))
|
||||
return redirect('.')
|
||||
|
||||
def new(self):
|
||||
|
@ -187,7 +192,7 @@ class WorkflowTestsDirectory(Directory):
|
|||
action_type = form.get_widget('type').parse()
|
||||
action_class = get_test_action_class_by_type(action_type)
|
||||
self.testdef.workflow_tests.add_action(action_class)
|
||||
self.testdef.store()
|
||||
self.testdef.store(comment=_('New test action "%s"') % action_class.label)
|
||||
|
||||
return redirect('.')
|
||||
|
||||
|
@ -216,7 +221,7 @@ class WorkflowTestsDirectory(Directory):
|
|||
return json.dumps({'success': 'ko'})
|
||||
|
||||
self.testdef.workflow_tests.actions = new_actions
|
||||
self.testdef.store()
|
||||
self.testdef.store(comment=_('Change in workflow test actions order'))
|
||||
|
||||
return json.dumps(
|
||||
{
|
||||
|
|
|
@ -40,7 +40,6 @@ from wcs.qommon.afterjobs import AfterJob
|
|||
from wcs.qommon.form import (
|
||||
CheckboxWidget,
|
||||
ColourWidget,
|
||||
CompositeWidget,
|
||||
ComputedExpressionWidget,
|
||||
FileWidget,
|
||||
Form,
|
||||
|
@ -51,7 +50,6 @@ from wcs.qommon.form import (
|
|||
SlugWidget,
|
||||
StringWidget,
|
||||
UrlWidget,
|
||||
VarnameWidget,
|
||||
)
|
||||
from wcs.sql_criterias import Equal
|
||||
from wcs.workflows import (
|
||||
|
@ -69,6 +67,7 @@ from wcs.workflows import (
|
|||
from . import utils
|
||||
from .comment_templates import CommentTemplatesDirectory
|
||||
from .data_sources import NamedDataSourcesDirectory
|
||||
from .documentable import DocumentableFieldMixin, DocumentableMixin
|
||||
from .fields import FieldDefPage, FieldsDirectory
|
||||
from .logged_errors import LoggedErrorsDirectory
|
||||
from .mail_templates import MailTemplatesDirectory
|
||||
|
@ -442,8 +441,14 @@ class WorkflowUI:
|
|||
return workflow
|
||||
|
||||
|
||||
class WorkflowItemPage(Directory):
|
||||
_q_exports = ['', 'delete', 'copy']
|
||||
class WorkflowItemPage(Directory, DocumentableMixin):
|
||||
_q_exports = [
|
||||
'',
|
||||
'delete',
|
||||
'copy',
|
||||
('update-documentation', 'update_documentation'),
|
||||
]
|
||||
do_not_call_in_templates = True
|
||||
|
||||
def __init__(self, workflow, parent, component):
|
||||
try:
|
||||
|
@ -452,6 +457,8 @@ class WorkflowItemPage(Directory):
|
|||
raise errors.TraversalError()
|
||||
self.workflow = workflow
|
||||
self.parent = parent
|
||||
self.documented_object = self.workflow
|
||||
self.documented_element = self.item
|
||||
get_response().breadcrumb.append(('items/%s/' % component, self.item.description))
|
||||
|
||||
def _q_index(self):
|
||||
|
@ -493,12 +500,20 @@ class WorkflowItemPage(Directory):
|
|||
return redirect('..')
|
||||
|
||||
get_response().set_title('%s - %s' % (_('Workflow'), self.workflow.name))
|
||||
r = TemplateIO(html=True)
|
||||
r += htmltext('<h2>%s</h2>') % self.item.description
|
||||
r += form.render()
|
||||
if self.item.support_substitution_variables:
|
||||
r += get_publisher().substitutions.get_substitution_html_table()
|
||||
return r.getvalue()
|
||||
get_response().add_javascript(['jquery-ui.js'])
|
||||
context = {
|
||||
'view': self,
|
||||
'html_form': form,
|
||||
'workflow': self.workflow,
|
||||
'has_sidebar': True,
|
||||
'action': self.item,
|
||||
'get_substitution_html_table': get_publisher().substitutions.get_substitution_html_table,
|
||||
}
|
||||
return template.QommonTemplateResponse(
|
||||
templates=['wcs/backoffice/workflow-action.html'],
|
||||
context=context,
|
||||
is_django_native=True,
|
||||
)
|
||||
|
||||
def delete(self):
|
||||
form = Form(enctype='multipart/form-data')
|
||||
|
@ -669,7 +684,7 @@ class GlobalActionItemsDir(ToChildDirectory):
|
|||
klass = WorkflowItemPage
|
||||
|
||||
|
||||
class WorkflowStatusPage(Directory):
|
||||
class WorkflowStatusPage(Directory, DocumentableMixin):
|
||||
_q_exports = [
|
||||
'',
|
||||
'delete',
|
||||
|
@ -683,6 +698,7 @@ class WorkflowStatusPage(Directory):
|
|||
'fullscreen',
|
||||
('schema.svg', 'svg'),
|
||||
'svg',
|
||||
('update-documentation', 'update_documentation'),
|
||||
]
|
||||
do_not_call_in_templates = True
|
||||
|
||||
|
@ -694,6 +710,8 @@ class WorkflowStatusPage(Directory):
|
|||
raise errors.TraversalError()
|
||||
|
||||
self.items_dir = WorkflowItemsDir(workflow, self.status)
|
||||
self.documented_object = self.workflow
|
||||
self.documented_element = self.status
|
||||
get_response().breadcrumb.append(('status/%s/' % status_id, self.status.name))
|
||||
|
||||
def _q_index(self):
|
||||
|
@ -1083,80 +1101,22 @@ class WorkflowStatusDirectory(Directory):
|
|||
return r.getvalue()
|
||||
|
||||
|
||||
class WorkflowVariableWidget(CompositeWidget):
|
||||
def __init__(self, name, value=None, workflow=None, **kwargs):
|
||||
CompositeWidget.__init__(self, name, **kwargs)
|
||||
if value and '*' in value:
|
||||
varname = None
|
||||
else:
|
||||
varname = value
|
||||
self.add(VarnameWidget, 'name', render_br=False, value=varname)
|
||||
if not get_publisher().has_site_option('enable-workflow-variable-parameter'):
|
||||
return
|
||||
options = []
|
||||
if workflow:
|
||||
excluded_parameters = ['backoffice_info_text']
|
||||
for status in workflow.possible_status:
|
||||
for item in status.items:
|
||||
prefix = '%s*%s*' % (status.id, item.id)
|
||||
parameters = [
|
||||
x
|
||||
for x in item.get_parameters()
|
||||
if not getattr(item, x) and x not in excluded_parameters
|
||||
]
|
||||
label = getattr(item, 'label', None) or item.description
|
||||
for parameter in parameters:
|
||||
key = prefix + parameter
|
||||
fake_form = Form()
|
||||
item.add_parameters_widgets(fake_form, [parameter], orig='variable_widget')
|
||||
if not fake_form.widgets:
|
||||
continue
|
||||
parameter_label = fake_form.widgets[0].title
|
||||
option_value = '%s / %s / %s' % (status.name, label, parameter_label)
|
||||
options.append((key, option_value, key))
|
||||
if not options:
|
||||
return
|
||||
options = [('', '---', '')] + options
|
||||
self.widgets.append(
|
||||
HtmlWidget(_('or you can use this field to directly replace a workflow parameter:'))
|
||||
)
|
||||
self.add(
|
||||
SingleSelectWidget,
|
||||
'select',
|
||||
options=options,
|
||||
value=value,
|
||||
hint=_('This takes priority over a variable name'),
|
||||
attrs={'data-dynamic-display-parent': 'true'},
|
||||
render_br=False,
|
||||
)
|
||||
|
||||
def _parse(self, request):
|
||||
super()._parse(request)
|
||||
if self.get('select'):
|
||||
self.value = self.get('select')
|
||||
elif self.get('name'):
|
||||
self.value = self.get('name')
|
||||
|
||||
|
||||
class WorkflowVariablesFieldDefPage(FieldDefPage):
|
||||
class WorkflowVariablesFieldDefPage(FieldDefPage, DocumentableFieldMixin):
|
||||
section = 'workflows'
|
||||
blacklisted_attributes = ['condition', 'prefill', 'display_locations', 'anonymise']
|
||||
has_documentation = True
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.workflow = self.objectdef.workflow
|
||||
self.documented_object = self.workflow
|
||||
self.documented_element = self.field
|
||||
|
||||
def form(self):
|
||||
form = super().form()
|
||||
form.remove('varname')
|
||||
form.add(
|
||||
WorkflowVariableWidget,
|
||||
'varname',
|
||||
title=_('Variable'),
|
||||
value=self.field.varname,
|
||||
advanced=False,
|
||||
required=True,
|
||||
workflow=self.objectdef.workflow,
|
||||
)
|
||||
# add default value widget
|
||||
if self.field.key in ('string', 'email', 'text', 'date'):
|
||||
widget = form.add(
|
||||
form.add(
|
||||
self.field.widget_class,
|
||||
'default_value',
|
||||
title=_('Default Value'),
|
||||
|
@ -1167,11 +1127,6 @@ class WorkflowVariablesFieldDefPage(FieldDefPage):
|
|||
),
|
||||
value=getattr(self.field, 'default_value', None),
|
||||
)
|
||||
if get_publisher().has_site_option('enable-workflow-variable-parameter'):
|
||||
widget.attrs = {
|
||||
'data-dynamic-display-child-of': 'varname$select',
|
||||
'data-dynamic-display-value': '',
|
||||
}
|
||||
return form
|
||||
|
||||
def submit(self, form):
|
||||
|
@ -1181,7 +1136,7 @@ class WorkflowVariablesFieldDefPage(FieldDefPage):
|
|||
super().submit(form)
|
||||
|
||||
|
||||
class WorkflowBackofficeFieldDefPage(FieldDefPage):
|
||||
class WorkflowBackofficeFieldDefPage(FieldDefPage, DocumentableFieldMixin):
|
||||
section = 'workflows'
|
||||
blacklisted_attributes = ['condition']
|
||||
|
||||
|
@ -1194,21 +1149,23 @@ class WorkflowBackofficeFieldDefPage(FieldDefPage):
|
|||
continue
|
||||
if any(x.get('field_id') == self.field.id for x in action.fields or []):
|
||||
usage_actions.append(action)
|
||||
if not usage_actions:
|
||||
return
|
||||
|
||||
r = TemplateIO(html=True)
|
||||
r += htmltext('<div class="actions-using-this-field">')
|
||||
r += htmltext('<h3>%s</h3>') % _('Actions using this field')
|
||||
r += htmltext('<ul>')
|
||||
for action in usage_actions:
|
||||
label = _('"%s" action') % action.label if action.label else _('Action')
|
||||
if isinstance(action.parent, WorkflowGlobalAction):
|
||||
location = _('in global action "%s"') % action.parent.name
|
||||
else:
|
||||
location = _('in status "%s"') % action.parent.name
|
||||
r += htmltext(f'<li><a href="{action.get_admin_url()}">%s %s</a></li>') % (label, location)
|
||||
r += htmltext('</ul>')
|
||||
r += htmltext('<div>')
|
||||
r += self.documentation_part()
|
||||
if usage_actions:
|
||||
get_response().filter['sidebar_attrs'] = ''
|
||||
r += htmltext('<div class="actions-using-this-field">')
|
||||
r += htmltext('<h3>%s</h3>') % _('Actions using this field')
|
||||
r += htmltext('<ul>')
|
||||
for action in usage_actions:
|
||||
label = _('"%s" action') % action.label if action.label else _('Action')
|
||||
if isinstance(action.parent, WorkflowGlobalAction):
|
||||
location = _('in global action "%s"') % action.parent.name
|
||||
else:
|
||||
location = _('in status "%s"') % action.parent.name
|
||||
r += htmltext(f'<li><a href="{action.get_admin_url()}">%s %s</a></li>') % (label, location)
|
||||
r += htmltext('</ul>')
|
||||
r += htmltext('<div>')
|
||||
return r.getvalue()
|
||||
|
||||
def form(self):
|
||||
|
@ -1230,7 +1187,7 @@ class WorkflowBackofficeFieldDefPage(FieldDefPage):
|
|||
|
||||
|
||||
class WorkflowVariablesFieldsDirectory(FieldsDirectory):
|
||||
_q_exports = ['', 'update_order', 'new']
|
||||
_q_exports = ['', 'update_order', 'new', ('update-documentation', 'update_documentation')]
|
||||
|
||||
section = 'workflows'
|
||||
field_def_page_class = WorkflowVariablesFieldDefPage
|
||||
|
@ -1247,8 +1204,12 @@ class WorkflowVariablesFieldsDirectory(FieldsDirectory):
|
|||
|
||||
def index_top(self):
|
||||
r = TemplateIO(html=True)
|
||||
r += htmltext('<div id="appbar">')
|
||||
r += htmltext('<h2>%s - %s - %s</h2>') % (_('Workflow'), self.objectdef.name, _('Variables'))
|
||||
r += htmltext('<span class="actions">%s</span>') % self.get_documentable_button()
|
||||
r += htmltext('</div>')
|
||||
r += get_session().display_message()
|
||||
r += self.get_documentable_zone()
|
||||
if not self.objectdef.fields:
|
||||
r += htmltext('<p>%s</p>') % _('There are not yet any variables.')
|
||||
return r.getvalue()
|
||||
|
@ -1258,7 +1219,7 @@ class WorkflowVariablesFieldsDirectory(FieldsDirectory):
|
|||
|
||||
|
||||
class WorkflowBackofficeFieldsDirectory(FieldsDirectory):
|
||||
_q_exports = ['', 'update_order', 'new']
|
||||
_q_exports = ['', 'update_order', 'new', ('update-documentation', 'update_documentation')]
|
||||
|
||||
section = 'workflows'
|
||||
field_def_page_class = WorkflowBackofficeFieldDefPage
|
||||
|
@ -1273,10 +1234,19 @@ class WorkflowBackofficeFieldsDirectory(FieldsDirectory):
|
|||
fields_count_total_soft_limit = 40
|
||||
fields_count_total_hard_limit = 80
|
||||
|
||||
def __init__(self, objectdef):
|
||||
super().__init__(objectdef)
|
||||
self.documented_object = objectdef
|
||||
self.documented_element = objectdef
|
||||
|
||||
def index_top(self):
|
||||
r = TemplateIO(html=True)
|
||||
r += htmltext('<div id="appbar">')
|
||||
r += htmltext('<h2>%s - %s - %s</h2>') % (_('Workflow'), self.objectdef.name, _('Backoffice Fields'))
|
||||
r += htmltext('<span class="actions">%s</span>') % self.get_documentable_button()
|
||||
r += htmltext('</div>')
|
||||
r += get_session().display_message()
|
||||
r += self.get_documentable_zone()
|
||||
if not self.objectdef.fields:
|
||||
r += htmltext('<p>%s</p>') % _('There are not yet any backoffice fields.')
|
||||
return r.getvalue()
|
||||
|
@ -1497,6 +1467,7 @@ class GlobalActionPage(WorkflowStatusPage):
|
|||
('triggers', 'triggers_dir'),
|
||||
'update_triggers_order',
|
||||
'options',
|
||||
('update-documentation', 'update_documentation'),
|
||||
]
|
||||
|
||||
def __init__(self, workflow, action_id):
|
||||
|
@ -1508,6 +1479,8 @@ class GlobalActionPage(WorkflowStatusPage):
|
|||
self.status = self.action
|
||||
self.items_dir = GlobalActionItemsDir(workflow, self.action)
|
||||
self.triggers_dir = GlobalActionTriggersDir(workflow, self.action)
|
||||
self.documented_object = self.workflow
|
||||
self.documented_element = self.action
|
||||
|
||||
def _q_traverse(self, path):
|
||||
get_response().breadcrumb.append(
|
||||
|
@ -1685,7 +1658,7 @@ class GlobalActionsDirectory(Directory):
|
|||
return r.getvalue()
|
||||
|
||||
|
||||
class WorkflowPage(Directory):
|
||||
class WorkflowPage(Directory, DocumentableMixin):
|
||||
_q_exports = [
|
||||
'',
|
||||
'edit',
|
||||
|
@ -1709,6 +1682,7 @@ class WorkflowPage(Directory):
|
|||
('logged-errors', 'logged_errors_dir'),
|
||||
('history', 'snapshots_dir'),
|
||||
('fullscreen'),
|
||||
('update-documentation', 'update_documentation'),
|
||||
]
|
||||
do_not_call_in_templates = True
|
||||
|
||||
|
@ -1731,6 +1705,8 @@ class WorkflowPage(Directory):
|
|||
self.criticality_levels_dir = CriticalityLevelsDirectory(self.workflow)
|
||||
self.logged_errors_dir = LoggedErrorsDirectory(parent_dir=self, workflow_id=self.workflow.id)
|
||||
self.snapshots_dir = SnapshotsDirectory(self.workflow)
|
||||
self.documented_object = self.workflow
|
||||
self.documented_element = self.workflow
|
||||
if component:
|
||||
get_response().breadcrumb.append((component + '/', self.workflow.name))
|
||||
|
||||
|
@ -2222,7 +2198,7 @@ class WorkflowsDirectory(Directory):
|
|||
|
||||
error, reason = False, None
|
||||
try:
|
||||
workflow = Workflow.import_from_xml(fp)
|
||||
workflow = Workflow.import_from_xml(fp, check_deprecated=True)
|
||||
except WorkflowImportError as e:
|
||||
error = True
|
||||
reason = _(e.msg) % e.msg_args
|
||||
|
|
|
@ -21,10 +21,11 @@ from quixote.directory import Directory
|
|||
from quixote.html import TemplateIO, htmltext
|
||||
|
||||
from wcs.admin import utils
|
||||
from wcs.admin.documentable import DocumentableMixin
|
||||
from wcs.backoffice.applications import ApplicationsDirectory
|
||||
from wcs.backoffice.snapshots import SnapshotsDirectory
|
||||
from wcs.qommon import _, errors, misc, template
|
||||
from wcs.qommon.form import CheckboxWidget, FileWidget, Form, HtmlWidget, SlugWidget, StringWidget, TextWidget
|
||||
from wcs.qommon.form import CheckboxWidget, FileWidget, Form, HtmlWidget, SlugWidget, StringWidget
|
||||
from wcs.utils import grep_strings
|
||||
from wcs.wscalls import NamedWsCall, NamedWsCallImportError, WsCallRequestWidget
|
||||
|
||||
|
@ -38,9 +39,6 @@ class NamedWsCallUI:
|
|||
def get_form(self):
|
||||
form = Form(enctype='multipart/form-data', use_tabs=True)
|
||||
form.add(StringWidget, 'name', title=_('Name'), required=True, size=30, value=self.wscall.name)
|
||||
form.add(
|
||||
TextWidget, 'description', title=_('Description'), cols=40, rows=5, value=self.wscall.description
|
||||
)
|
||||
if self.wscall.slug:
|
||||
form.add(
|
||||
SlugWidget,
|
||||
|
@ -100,7 +98,6 @@ class NamedWsCallUI:
|
|||
raise ValueError()
|
||||
|
||||
self.wscall.name = name
|
||||
self.wscall.description = form.get_widget('description').parse()
|
||||
self.wscall.notify_on_errors = form.get_widget('notify_on_errors').parse()
|
||||
self.wscall.record_on_errors = form.get_widget('record_on_errors').parse()
|
||||
self.wscall.request = form.get_widget('request').parse()
|
||||
|
@ -109,7 +106,7 @@ class NamedWsCallUI:
|
|||
self.wscall.store()
|
||||
|
||||
|
||||
class NamedWsCallPage(Directory):
|
||||
class NamedWsCallPage(Directory, DocumentableMixin):
|
||||
do_not_call_in_templates = True
|
||||
_q_exports = [
|
||||
'',
|
||||
|
@ -118,6 +115,7 @@ class NamedWsCallPage(Directory):
|
|||
'export',
|
||||
('history', 'snapshots_dir'),
|
||||
'usage',
|
||||
('update-documentation', 'update_documentation'),
|
||||
]
|
||||
|
||||
def __init__(self, component, instance=None):
|
||||
|
@ -128,6 +126,8 @@ class NamedWsCallPage(Directory):
|
|||
self.wscall_ui = NamedWsCallUI(self.wscall)
|
||||
get_response().breadcrumb.append((component + '/', self.wscall.name))
|
||||
self.snapshots_dir = SnapshotsDirectory(self.wscall)
|
||||
self.documented_object = self.wscall
|
||||
self.documented_element = self.wscall
|
||||
|
||||
def get_sidebar(self):
|
||||
r = TemplateIO(html=True)
|
||||
|
@ -203,6 +203,7 @@ class NamedWsCallPage(Directory):
|
|||
return redirect('../%s/' % self.wscall.id)
|
||||
|
||||
get_response().breadcrumb.append(('edit', _('Edit')))
|
||||
get_response().add_javascript(['jquery-ui.js'])
|
||||
get_response().set_title(_('Edit webservice call'))
|
||||
r = TemplateIO(html=True)
|
||||
r += htmltext('<h2>%s</h2>') % _('Edit webservice call')
|
||||
|
@ -319,7 +320,7 @@ class NamedWsCallsDirectory(Directory):
|
|||
|
||||
error, reason = False, None
|
||||
try:
|
||||
wscall = NamedWsCall.import_from_xml(fp)
|
||||
wscall = NamedWsCall.import_from_xml(fp, check_deprecated=True)
|
||||
get_session().message = ('info', _('This webservice call has been successfully imported.'))
|
||||
except NamedWsCallImportError as e:
|
||||
error = True
|
||||
|
|
89
wcs/api.py
89
wcs/api.py
|
@ -18,6 +18,7 @@ import base64
|
|||
import copy
|
||||
import datetime
|
||||
import json
|
||||
import re
|
||||
import urllib.parse
|
||||
|
||||
from django.http import HttpResponse, HttpResponseBadRequest, JsonResponse
|
||||
|
@ -26,6 +27,7 @@ from django.utils.timezone import localtime, make_naive
|
|||
from quixote import get_publisher, get_request, get_response, get_session, redirect
|
||||
from quixote.directory import Directory
|
||||
from quixote.errors import MethodNotAllowedError, RequestError
|
||||
from quixote.html import TemplateIO, htmltext
|
||||
|
||||
import wcs.qommon.storage as st
|
||||
from wcs.admin.settings import UserFieldsFormDef
|
||||
|
@ -56,6 +58,7 @@ from wcs.sql_criterias import (
|
|||
StrictNotEqual,
|
||||
)
|
||||
from wcs.workflows import ContentSnapshotPart
|
||||
from wcs.wscalls import UnflattenKeysException, unflatten_keys
|
||||
|
||||
from .backoffice.data_management import CardPage as BackofficeCardPage
|
||||
from .backoffice.management import FormPage as BackofficeFormPage
|
||||
|
@ -67,6 +70,7 @@ from .qommon.template import Template, TemplateError
|
|||
|
||||
|
||||
def posted_json_data_to_formdata_data(formdef, data):
|
||||
data = copy.deepcopy(data)
|
||||
# remap fields from varname to field id
|
||||
for field in formdef.get_all_fields():
|
||||
if not field.varname:
|
||||
|
@ -1406,6 +1410,7 @@ class ApiDirectory(Directory):
|
|||
'geojson',
|
||||
'jobs',
|
||||
('card-file-by-token', 'card_file_by_token'),
|
||||
('preview-payload-structure', 'preview_payload_structure'),
|
||||
('sign-url-token', 'sign_url_token'),
|
||||
]
|
||||
|
||||
|
@ -1433,6 +1438,81 @@ class ApiDirectory(Directory):
|
|||
get_response().set_content_type('application/json')
|
||||
return json.dumps({'err': 0, 'data': list_roles})
|
||||
|
||||
def preview_payload_structure(self):
|
||||
if not (get_request().user and get_request().user.can_go_in_admin()):
|
||||
raise AccessForbiddenError('user has no access to backoffice')
|
||||
|
||||
def parse_payload():
|
||||
payload = {}
|
||||
for param, value in get_request().form.items():
|
||||
# skip elements which are not part of payload
|
||||
if 'post_data$element' not in param or param.endswith('value_python'):
|
||||
continue
|
||||
prefix, order, field = re.split(r'(\d+)(?!\d)', param) # noqa pylint: disable=unused-variable
|
||||
# skip elements that aren't ordered
|
||||
if not order:
|
||||
continue
|
||||
|
||||
if order not in payload:
|
||||
payload[order] = []
|
||||
|
||||
if field == 'key':
|
||||
# skip empty keys
|
||||
if not value:
|
||||
continue
|
||||
# insert key on first position
|
||||
payload[order].insert(0, value)
|
||||
else:
|
||||
payload[order].append(value)
|
||||
return dict([v for v in payload.values() if len(v) > 1])
|
||||
|
||||
def format_payload(o, html=htmltext(''), last_element=True):
|
||||
if isinstance(o, (list, tuple)):
|
||||
html += htmltext('[<span class="payload-preview--obj">')
|
||||
while True:
|
||||
try:
|
||||
head, tail = o[0], o[1:]
|
||||
except IndexError:
|
||||
break
|
||||
html = format_payload(head, html=html, last_element=len(tail) < 1)
|
||||
o = tail
|
||||
html += htmltext('</span>]')
|
||||
elif isinstance(o, dict):
|
||||
html += htmltext('{<span class="payload-preview--obj">')
|
||||
for i, (k, v) in enumerate(o.items()):
|
||||
html += htmltext('<span class="payload-preview--key">"%s"</span>: ' % k)
|
||||
html = format_payload(v, html=html, last_element=i == len(o) - 1)
|
||||
html += htmltext('</span>}')
|
||||
else:
|
||||
# check if it's empty string, a template with text around or just text
|
||||
if not o or re.sub('^({[{|%]).+([%|}]})$', '', o):
|
||||
# and add double quotes
|
||||
html += htmltext('<span class="payload-preview--value">"%s"</span>' % o)
|
||||
else:
|
||||
html += htmltext('<span class="payload-preview--template-value">%s</span>' % o)
|
||||
# last element doesn't need separator
|
||||
if not last_element:
|
||||
html += htmltext('<span class="payload-preview--item-separator">,</span>')
|
||||
return html
|
||||
|
||||
payload = parse_payload()
|
||||
r = TemplateIO(html=True)
|
||||
r += htmltext('<h2>%s</h2>') % _('Payload structure preview')
|
||||
r += htmltext('<div class="payload-preview">')
|
||||
try:
|
||||
unflattened_payload = unflatten_keys(payload)
|
||||
r += htmltext('<div class="payload-preview--structure">')
|
||||
r += format_payload(unflattened_payload)
|
||||
r += htmltext('</div>')
|
||||
except UnflattenKeysException as e:
|
||||
r += htmltext('<div class="errornotice"><p>%s</p><p>%s %s</p></div>') % (
|
||||
_('Unable to preview payload.'),
|
||||
_('Following error occured: '),
|
||||
e,
|
||||
)
|
||||
r += htmltext('</div>')
|
||||
return r.getvalue()
|
||||
|
||||
def _q_traverse(self, path):
|
||||
get_request().is_json_marker = True
|
||||
return super()._q_traverse(path)
|
||||
|
@ -1476,6 +1556,15 @@ def validate_condition(request, *args, **kwargs):
|
|||
Condition(condition).validate()
|
||||
except ValidationError as e:
|
||||
hint['msg'] = str(e)
|
||||
else:
|
||||
if request.GET.get('warn-on-datetime') == 'true' and condition['type'] == 'django':
|
||||
variables = re.compile(r'\b(today|now)\b')
|
||||
filters = re.compile(r'\|age_in_(years|months|days|hours)')
|
||||
if variables.search(condition['value']) or filters.search(condition['value']):
|
||||
hint['msg'] = _(
|
||||
'Warning: conditions are only evaluated when entering the action, '
|
||||
'you may need to set a timeout if you want it to be evaluated regularly.'
|
||||
)
|
||||
return JsonResponse(hint)
|
||||
|
||||
|
||||
|
|
|
@ -33,6 +33,7 @@ class ApiAccess(XmlStorableObject):
|
|||
access_key = None
|
||||
description = None
|
||||
restrict_to_anonymised_data = False
|
||||
idp_api_client = False
|
||||
_roles = None
|
||||
_role_ids = Ellipsis
|
||||
|
||||
|
@ -44,6 +45,7 @@ class ApiAccess(XmlStorableObject):
|
|||
('access_key', 'str'),
|
||||
('restrict_to_anonymised_data', 'bool'),
|
||||
('roles', 'roles'),
|
||||
('idp_api_client', 'bool'),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
|
@ -98,7 +100,7 @@ class ApiAccess(XmlStorableObject):
|
|||
@classmethod
|
||||
def get_with_credentials(cls, username, password):
|
||||
api_access = cls.get_by_identifier(username)
|
||||
if not api_access or api_access.access_key != password:
|
||||
if not api_access or api_access.access_key != password or api_access.idp_api_client:
|
||||
api_access = cls.get_from_idp(username, password)
|
||||
if not api_access:
|
||||
raise KeyError
|
||||
|
@ -143,11 +145,18 @@ class ApiAccess(XmlStorableObject):
|
|||
if data.get('err', 1) != 0:
|
||||
return None
|
||||
|
||||
api_access = cls.volatile()
|
||||
# cache api client locally, it is necessary for serialization for afterjobs
|
||||
# in uwsgi spooler.
|
||||
access_identifier = f'_idp_{username}'
|
||||
api_access = cls.get_by_identifier(access_identifier) or cls()
|
||||
api_access.idp_api_client = True
|
||||
api_access.access_identifier = access_identifier
|
||||
role_class = get_publisher().role_class
|
||||
try:
|
||||
api_access.restrict_to_anonymised_data = data['data']['restrict_to_anonymised_data']
|
||||
api_access._role_ids = data['data']['roles']
|
||||
api_access.roles = [role_class.get(x, ignore_errors=True) for x in data['data']['roles']]
|
||||
api_access.roles = [x for x in api_access.roles if x is not None]
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
api_access.store()
|
||||
return api_access
|
||||
|
|
|
@ -269,9 +269,9 @@ def object_dependencies(request, objects, slug):
|
|||
|
||||
@signature_required
|
||||
def bundle_check(request):
|
||||
tar_io = io.BytesIO(request.body)
|
||||
bundle = request.FILES['bundle']
|
||||
try:
|
||||
with tarfile.open(fileobj=tar_io) as tar:
|
||||
with tarfile.open(fileobj=bundle) as tar:
|
||||
try:
|
||||
manifest = json.loads(tar.extractfile('manifest.json').read().decode())
|
||||
except KeyError:
|
||||
|
@ -529,7 +529,10 @@ class BundleImportJob(AfterJob):
|
|||
'Invalid tar file, missing component %s/%s.' % (element['type'], element['slug'])
|
||||
)
|
||||
new_object = element_klass.import_from_xml_tree(
|
||||
ET.fromstring(element_content), include_id=False, check_datasources=False
|
||||
ET.fromstring(element_content),
|
||||
include_id=False,
|
||||
check_datasources=False,
|
||||
check_deprecated=True,
|
||||
)
|
||||
if not finalize and element_klass in category_classes:
|
||||
# for categories, keep positions of imported objects
|
||||
|
@ -631,7 +634,7 @@ class BundleImportJob(AfterJob):
|
|||
|
||||
@signature_required
|
||||
def bundle_import(request):
|
||||
job = BundleImportJob(tar_content=request.body)
|
||||
job = BundleImportJob(tar_content=request.FILES['bundle'].read())
|
||||
job.store()
|
||||
job.run(spool=True)
|
||||
return JsonResponse({'err': 0, 'url': job.get_api_status_url()})
|
||||
|
@ -689,7 +692,7 @@ class BundleDeclareJob(BundleImportJob):
|
|||
|
||||
@signature_required
|
||||
def bundle_declare(request):
|
||||
job = BundleDeclareJob(tar_content=request.body)
|
||||
job = BundleDeclareJob(tar_content=request.FILES['bundle'].read())
|
||||
job.store()
|
||||
job.run(spool=True)
|
||||
return JsonResponse({'err': 0, 'url': job.get_api_status_url()})
|
||||
|
|
|
@ -31,7 +31,7 @@ from wcs.categories import CardDefCategory
|
|||
from wcs.sql_criterias import Null, StrictNotEqual
|
||||
|
||||
from ..qommon import _, pgettext_lazy
|
||||
from ..qommon.form import ComputedExpressionWidget, StringWidget
|
||||
from ..qommon.form import CheckboxesWidget, ComputedExpressionWidget, Form, RadiobuttonsWidget, StringWidget
|
||||
|
||||
|
||||
class CardDefUI(FormDefUI):
|
||||
|
@ -71,6 +71,26 @@ class CardDefOptionsDirectory(OptionsDirectory):
|
|||
)
|
||||
return form
|
||||
|
||||
def management(self):
|
||||
form = Form(enctype='multipart/form-data')
|
||||
form.add(
|
||||
CheckboxesWidget,
|
||||
'management_sidebar_items',
|
||||
title=_('Sidebar elements'),
|
||||
options=[(x[0], x[1], x[0]) for x in self.formdef.get_management_sidebar_available_items()],
|
||||
value=self.formdef.get_management_sidebar_items(),
|
||||
inline=False,
|
||||
)
|
||||
form.add(
|
||||
RadiobuttonsWidget,
|
||||
'history_pane_default_mode',
|
||||
title=_('History pane default mode'),
|
||||
options=[('collapsed', _('Collapsed'), 'collapsed'), ('expanded', _('Expanded'), 'expanded')],
|
||||
value=self.formdef.history_pane_default_mode,
|
||||
extra_css_class='widget-inline-radio',
|
||||
)
|
||||
return self.handle(form, pgettext_lazy('cards', 'Management'))
|
||||
|
||||
|
||||
class CardFieldDefPage(FormFieldDefPage):
|
||||
section = 'cards'
|
||||
|
@ -78,6 +98,21 @@ class CardFieldDefPage(FormFieldDefPage):
|
|||
'Warning: this field data will be permanently deleted from existing cards.'
|
||||
)
|
||||
|
||||
def get_deletion_extra_warning(self):
|
||||
warning = super().get_deletion_extra_warning()
|
||||
if warning and self.field.varname and self.objectdef.id_template:
|
||||
varnames = self.field.get_referenced_varnames(self.objectdef, self.objectdef.id_template)
|
||||
if self.field.varname in varnames:
|
||||
warning['level'] = 'error'
|
||||
warning['message'] = htmltext('%s<br>%s') % (
|
||||
warning['message'],
|
||||
_(
|
||||
'This field may be used in the card custom identifiers, '
|
||||
'its removal may render cards unreachable.'
|
||||
),
|
||||
)
|
||||
return warning
|
||||
|
||||
|
||||
class CardFieldsDirectory(FormFieldsDirectory):
|
||||
field_def_page_class = CardFieldDefPage
|
||||
|
@ -140,6 +175,15 @@ class CardDefPage(FormDefPage):
|
|||
options['user_support'] = self.add_option_line(
|
||||
'options/user_support', _('User support'), user_support_status
|
||||
)
|
||||
options['management'] = self.add_option_line(
|
||||
'options/management',
|
||||
pgettext_lazy('cards', 'Management'),
|
||||
_('Custom')
|
||||
if self.formdef.history_pane_default_mode != 'collapsed'
|
||||
or self.formdef.management_sidebar_items
|
||||
not in ({'__default__'}, self.formdef.get_default_management_sidebar_items())
|
||||
else _('Default'),
|
||||
)
|
||||
return options
|
||||
|
||||
def get_sorted_usage_in_formdefs(self):
|
||||
|
|
|
@ -228,10 +228,16 @@ class CardPage(FormPage):
|
|||
form.add(
|
||||
CheckboxWidget,
|
||||
'update_existing_cards',
|
||||
title=_('Update existing cards (only for JSON imports)'),
|
||||
title=_('Update existing cards (only for JSON imports)')
|
||||
if not self.formdef.id_template
|
||||
else _('Update existing cards'),
|
||||
hint=_('Cards will be matched using their unique identifier ("uuid" property).')
|
||||
if not self.formdef.id_template
|
||||
else _('Cards will be matched using their custom identifier ("id" property).'),
|
||||
else _(
|
||||
'Cards will be matched using their custom identifier ("id" property). '
|
||||
'If this option is enabled cards with the same identifiers will be updated, '
|
||||
'otherwise they will be skipped.'
|
||||
),
|
||||
value=False,
|
||||
)
|
||||
form.add_submit('submit', _('Submit'))
|
||||
|
@ -241,19 +247,22 @@ class CardPage(FormPage):
|
|||
|
||||
if form.is_submitted() and not form.has_errors():
|
||||
file_content = form.get_widget('file').parse().fp.read()
|
||||
update_existing_cards = form.get_widget('update_existing_cards').parse()
|
||||
try:
|
||||
json_content = json.loads(file_content)
|
||||
except ValueError:
|
||||
# not json -> CSV
|
||||
try:
|
||||
return self.import_csv_submit(file_content, submission_agent_id=get_request().user.id)
|
||||
return self.import_csv_submit(
|
||||
file_content,
|
||||
update_existing_cards=update_existing_cards,
|
||||
submission_agent_id=get_request().user.id,
|
||||
)
|
||||
except ValueError as e:
|
||||
form.set_error('file', e)
|
||||
else:
|
||||
try:
|
||||
return self.import_json_submit(
|
||||
json_content, update_existing_cards=form.get_widget('update_existing_cards').parse()
|
||||
)
|
||||
return self.import_json_submit(json_content, update_existing_cards=update_existing_cards)
|
||||
except ValueError as e:
|
||||
form.set_error('file', e)
|
||||
|
||||
|
@ -275,7 +284,9 @@ class CardPage(FormPage):
|
|||
impossible_fields.append(field.label)
|
||||
return impossible_fields
|
||||
|
||||
def import_csv_submit(self, content, afterjob=True, api=False, submission_agent_id=None):
|
||||
def import_csv_submit(
|
||||
self, content, afterjob=True, api=False, update_existing_cards=False, submission_agent_id=None
|
||||
):
|
||||
if b'\0' in content:
|
||||
raise ValueError(_('Invalid file format.'))
|
||||
|
||||
|
@ -328,7 +339,10 @@ class CardPage(FormPage):
|
|||
raise ValueError(error_message)
|
||||
|
||||
job = ImportFromCsvAfterJob(
|
||||
carddef=self.formdef, data_lines=data_lines, submission_agent_id=submission_agent_id
|
||||
carddef=self.formdef,
|
||||
data_lines=data_lines,
|
||||
update_existing_cards=update_existing_cards,
|
||||
submission_agent_id=submission_agent_id,
|
||||
)
|
||||
if afterjob:
|
||||
get_response().add_after_job(job)
|
||||
|
@ -427,17 +441,15 @@ class CardBackOfficeStatusPage(FormBackOfficeStatusPage):
|
|||
def should_fold_summary(self, mine, request_user):
|
||||
return False
|
||||
|
||||
def should_fold_history(self):
|
||||
return True
|
||||
|
||||
|
||||
class ImportFromCsvAfterJob(AfterJob):
|
||||
def __init__(self, carddef, data_lines, submission_agent_id):
|
||||
def __init__(self, carddef, data_lines, update_existing_cards, submission_agent_id):
|
||||
super().__init__(
|
||||
label=_('Importing data into cards'),
|
||||
carddef_class=carddef.__class__,
|
||||
carddef_id=carddef.id,
|
||||
data_lines=data_lines,
|
||||
update_existing_cards=update_existing_cards,
|
||||
submission_agent_id=submission_agent_id,
|
||||
)
|
||||
|
||||
|
@ -448,6 +460,7 @@ class ImportFromCsvAfterJob(AfterJob):
|
|||
|
||||
def execute(self):
|
||||
self.carddef = self.kwargs['carddef_class'].get(self.kwargs['carddef_id'])
|
||||
update_existing_cards = self.kwargs['update_existing_cards']
|
||||
carddata_class = self.carddef.data_class()
|
||||
self.submission_agent_id = self.kwargs['submission_agent_id']
|
||||
self.total_count = len(self.kwargs['data_lines'])
|
||||
|
@ -507,6 +520,9 @@ class ImportFromCsvAfterJob(AfterJob):
|
|||
except KeyError:
|
||||
pass # unique id, fine
|
||||
else:
|
||||
if not update_existing_cards:
|
||||
self.increment_count()
|
||||
continue
|
||||
# overwrite (only fields from CSV columns, not unsupported or backoffice fields)
|
||||
new_card = False
|
||||
orig_data = copy.copy(carddata_with_same_id.data)
|
||||
|
|
|
@ -416,6 +416,7 @@ class DeprecationsScan(AfterJob):
|
|||
)
|
||||
|
||||
def check_deprecated_elements_in_object(self, obj):
|
||||
self.id = None # to avoid store of afterjob
|
||||
if not get_publisher().has_site_option('forbid-new-python-expressions'):
|
||||
# for perfs, don't check object if nothing is forbidden
|
||||
return
|
||||
|
|
|
@ -0,0 +1,519 @@
|
|||
# w.c.s. - web application for online forms
|
||||
# Copyright (C) 2005-2024 Entr'ouvert
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# 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
|
||||
|
||||
from django.utils.encoding import force_str
|
||||
from quixote import get_publisher
|
||||
from quixote.html import TemplateIO, htmltext
|
||||
|
||||
from wcs.qommon import _, misc, pgettext_lazy
|
||||
from wcs.qommon.form import DateWidget, SingleSelectWidget, StringWidget
|
||||
from wcs.sql_criterias import ArrayContains, Or
|
||||
|
||||
|
||||
def render_filter_widget(filter_widget, operators, filter_field_operator_key, filter_field_operator):
|
||||
result = htmltext('<div class="widget operator-and-value-widget">')
|
||||
result += htmltext('<div class="title-and-operator">')
|
||||
result += filter_widget.render_title(filter_widget.get_title())
|
||||
if operators:
|
||||
result += htmltext('<div class="operator">')
|
||||
operator_widget = SingleSelectWidget(
|
||||
filter_field_operator_key,
|
||||
options=[(o[0], o[1], o[0]) for o in operators],
|
||||
value=filter_field_operator,
|
||||
render_br=False,
|
||||
)
|
||||
result += operator_widget.render_content()
|
||||
result += htmltext('</div>')
|
||||
result += htmltext('</div>')
|
||||
result += htmltext('<div class="value">')
|
||||
result += filter_widget.render_content()
|
||||
result += htmltext('</div>')
|
||||
result += htmltext('</div>')
|
||||
return result
|
||||
|
||||
|
||||
class FilterField:
|
||||
can_include_in_listing = True
|
||||
id = None
|
||||
key = None
|
||||
label = None
|
||||
available_for_filter = False
|
||||
include_in_statistics = False
|
||||
geojson_label = None
|
||||
store_display_value = None
|
||||
store_structured_value = None
|
||||
|
||||
def __init__(self, formdef):
|
||||
self.formdef = formdef
|
||||
self.varname = self.id.replace('-', '_')
|
||||
self.contextual_id = self.id
|
||||
self.contextual_varname = self.varname
|
||||
self.label = force_str(self.label) # so it can be pickled
|
||||
self.geojson_label = force_str(self.geojson_label or self.label)
|
||||
self.filter_field_key = 'filter-%s-value' % self.contextual_id
|
||||
self.filter_field_operator_key = '%s-operator' % self.filter_field_key.replace('-value', '')
|
||||
self.filters_dict = {}
|
||||
|
||||
def get_allowed_operators(self):
|
||||
from wcs.variables import LazyFormDefObjectsManager
|
||||
|
||||
lazy_manager = LazyFormDefObjectsManager(formdef=self.formdef)
|
||||
return lazy_manager.get_field_allowed_operators(self) or []
|
||||
|
||||
def get_view_value(self, value):
|
||||
# just here to quack like a duck
|
||||
return None
|
||||
|
||||
def get_csv_heading(self):
|
||||
return [self.label]
|
||||
|
||||
def get_csv_value(self, element, **kwargs):
|
||||
return [element]
|
||||
|
||||
@property
|
||||
def has_relations(self):
|
||||
return bool(self.id == 'user-label')
|
||||
|
||||
def get_filter_field_value(self):
|
||||
return self.filters_dict.get(self.filter_field_key)
|
||||
|
||||
def get_filter_field_operator(self):
|
||||
return self.filters_dict.get(self.filter_field_operator_key) or 'eq'
|
||||
|
||||
def render_filter_widget(self, widget):
|
||||
return render_filter_widget(
|
||||
widget,
|
||||
operators=self.get_allowed_operators(),
|
||||
filter_field_operator_key=self.filter_field_operator_key,
|
||||
filter_field_operator=self.get_filter_field_operator(),
|
||||
)
|
||||
|
||||
|
||||
class RelatedField:
|
||||
is_related_field = True
|
||||
key = 'related-field'
|
||||
varname = None
|
||||
related_field = None
|
||||
can_include_in_listing = True
|
||||
available_for_filter = False
|
||||
|
||||
def __init__(self, carddef, field, parent_field):
|
||||
self.carddef = carddef
|
||||
self.related_field = field
|
||||
self.parent_field = parent_field
|
||||
self.parent_field_id = parent_field.id
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return '%s$%s' % (self.parent_field_id, self.related_field.id)
|
||||
|
||||
@property
|
||||
def contextual_id(self):
|
||||
return self.id
|
||||
|
||||
@property
|
||||
def label(self):
|
||||
return '%s - %s' % (self.parent_field.label, self.related_field.label)
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s (card: %r, parent: %r, related: %r)>' % (
|
||||
self.__class__.__name__,
|
||||
self.carddef,
|
||||
self.parent_field.label,
|
||||
self.related_field.label,
|
||||
)
|
||||
|
||||
@property
|
||||
def store_display_value(self):
|
||||
return self.related_field.store_display_value
|
||||
|
||||
@property
|
||||
def store_structured_value(self):
|
||||
return self.related_field.store_structured_value
|
||||
|
||||
def get_view_value(self, value, **kwargs):
|
||||
if value is None:
|
||||
return ''
|
||||
if isinstance(value, bool):
|
||||
return _('Yes') if value else _('No')
|
||||
if isinstance(value, datetime.date):
|
||||
return misc.strftime(misc.date_format(), value)
|
||||
return value
|
||||
|
||||
def get_view_short_value(self, value, max_len=30, **kwargs):
|
||||
return self.get_view_value(value)
|
||||
|
||||
def get_csv_heading(self):
|
||||
if self.related_field:
|
||||
return self.related_field.get_csv_heading()
|
||||
return [self.label]
|
||||
|
||||
def get_csv_value(self, value, **kwargs):
|
||||
if self.related_field:
|
||||
return self.related_field.get_csv_value(value, **kwargs)
|
||||
return [self.get_view_value(value)]
|
||||
|
||||
def get_column_field_id(self):
|
||||
from wcs.sql import get_field_id
|
||||
|
||||
return get_field_id(self.related_field)
|
||||
|
||||
|
||||
class UserRelatedField(RelatedField):
|
||||
# it is named 'user-label' and not 'user' for compatibility with existing
|
||||
# listings, as the 'classic' user column is named 'user-label'.
|
||||
parent_field_id = 'user-label'
|
||||
store_display_value = None
|
||||
store_structured_value = None
|
||||
|
||||
def __init__(self, field):
|
||||
self.related_field = field
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s (field: %r)>' % (
|
||||
self.__class__.__name__,
|
||||
self.related_field.label,
|
||||
)
|
||||
|
||||
@property
|
||||
def label(self):
|
||||
return _('%s of User') % self.related_field.label
|
||||
|
||||
|
||||
class UserLabelRelatedField(UserRelatedField):
|
||||
# custom user-label column, targetting the "name" (= full name) column
|
||||
# of the users table
|
||||
id = 'user-label'
|
||||
key = 'user-label'
|
||||
varname = 'user_label'
|
||||
has_relations = True
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def __repr__(self):
|
||||
return '<UserLabelRelatedField>'
|
||||
|
||||
def get_column_field_id(self):
|
||||
return 'name'
|
||||
|
||||
@property
|
||||
def label(self):
|
||||
return _('User Label')
|
||||
|
||||
|
||||
class DisplayNameFilterField(FilterField):
|
||||
id = 'name'
|
||||
key = 'display_name'
|
||||
label = _('Name')
|
||||
|
||||
|
||||
class StatusFilterField(FilterField):
|
||||
id = 'status'
|
||||
key = 'status'
|
||||
label = _('Status')
|
||||
include_in_statistics = True
|
||||
|
||||
def __init__(self, formdef):
|
||||
super().__init__(formdef=formdef)
|
||||
if self.formdef:
|
||||
self.waitpoint_status = self.formdef.workflow.get_waitpoint_status()
|
||||
|
||||
@property
|
||||
def available_for_filter(self):
|
||||
return bool(self.formdef is None or self.waitpoint_status)
|
||||
|
||||
def get_filter_widget(self, mode=None):
|
||||
filter_field_value = self.get_filter_field_value()
|
||||
r = TemplateIO(html=True)
|
||||
operators = [
|
||||
('eq', '='),
|
||||
('ne', '!='),
|
||||
]
|
||||
r += htmltext('<div class="widget operator-and-value-widget">')
|
||||
r += htmltext('<div class="title-and-operator">')
|
||||
r += htmltext('<div class="title">%s</div>') % _('Status to display')
|
||||
if mode != 'stats':
|
||||
r += htmltext('<div class="operator">')
|
||||
operator_widget = SingleSelectWidget(
|
||||
'filter-operator',
|
||||
options=[(o[0], o[1], o[0]) for o in operators],
|
||||
value=self.get_filter_field_operator(),
|
||||
render_br=False,
|
||||
)
|
||||
r += operator_widget.render_content()
|
||||
r += htmltext('</div>')
|
||||
r += htmltext('</div>')
|
||||
r += htmltext('<div class="value content">')
|
||||
r += htmltext('<select name="filter">')
|
||||
filters = [
|
||||
('waiting', _('Waiting for an action'), None),
|
||||
('all', _('All'), None),
|
||||
('pending', pgettext_lazy('formdata', 'Open'), None),
|
||||
('done', _('Done'), None),
|
||||
]
|
||||
for status in self.waitpoint_status:
|
||||
filters.append((status.id, status.name, status.colour))
|
||||
for filter_id, filter_label, filter_colour in filters:
|
||||
if filter_id == filter_field_value:
|
||||
selected = ' selected="selected"'
|
||||
else:
|
||||
selected = ''
|
||||
style = ''
|
||||
if filter_colour and filter_colour != '#FFFFFF':
|
||||
fg_colour = misc.get_foreground_colour(filter_colour)
|
||||
style = 'style="background: %s; color: %s;"' % (filter_colour, fg_colour)
|
||||
r += htmltext('<option value="%s"%s %s>' % (filter_id, selected, style))
|
||||
r += htmltext('%s</option>') % filter_label
|
||||
r += htmltext('</select>')
|
||||
r += htmltext('</div>')
|
||||
r += htmltext('</div>')
|
||||
return r.getvalue()
|
||||
|
||||
|
||||
class UserVisibleStatusField(FilterField):
|
||||
id = 'user-visible-status'
|
||||
key = 'user-visible-status'
|
||||
label = _('Status (for user)')
|
||||
geolabel_status = _('Status')
|
||||
|
||||
|
||||
class InternalIdFilterField(FilterField):
|
||||
id = 'internal-id'
|
||||
key = 'internal-id'
|
||||
label = _('Identifier')
|
||||
available_for_filter = True
|
||||
|
||||
def get_filter_widget(self, **kwargs):
|
||||
widget = StringWidget(
|
||||
self.filter_field_key,
|
||||
title=self.label,
|
||||
value=self.get_filter_field_value(),
|
||||
render_br=False,
|
||||
)
|
||||
return self.render_filter_widget(widget)
|
||||
|
||||
|
||||
class AbstractPeriodFilterField(FilterField):
|
||||
available_for_filter = True
|
||||
|
||||
def get_filter_widget(self, **kwargs):
|
||||
return DateWidget(
|
||||
self.filter_field_key, title=self.label, value=self.get_filter_field_value(), render_br=False
|
||||
).render()
|
||||
|
||||
|
||||
class PeriodStartFilterField(AbstractPeriodFilterField):
|
||||
id = 'start'
|
||||
key = 'period-date'
|
||||
label = _('Start')
|
||||
|
||||
|
||||
class PeriodEndFilterField(AbstractPeriodFilterField):
|
||||
id = 'end'
|
||||
key = 'period-date'
|
||||
label = _('End')
|
||||
|
||||
|
||||
class PeriodStartUpdateTimeFilterField(AbstractPeriodFilterField):
|
||||
id = 'start-mtime'
|
||||
key = 'period-date'
|
||||
label = _('Start (modification time)')
|
||||
|
||||
|
||||
class PeriodEndUpdateTimeFilterField(AbstractPeriodFilterField):
|
||||
id = 'end-mtime'
|
||||
key = 'period-date'
|
||||
label = _('End (modification time)')
|
||||
|
||||
|
||||
class UserIdFilterField(FilterField):
|
||||
id = 'user'
|
||||
key = 'user-id'
|
||||
label = _('User')
|
||||
available_for_filter = True
|
||||
|
||||
def get_allowed_operators(self):
|
||||
return []
|
||||
|
||||
def get_filter_widget(self, **kwargs):
|
||||
filter_field_value = self.get_filter_field_value()
|
||||
options = [
|
||||
('', _('None'), ''),
|
||||
('__current__', _('Current user'), '__current__'),
|
||||
]
|
||||
if filter_field_value and filter_field_value != '__current__':
|
||||
try:
|
||||
filtered_user = get_publisher().user_class.get(filter_field_value)
|
||||
except KeyError:
|
||||
filtered_user = None
|
||||
filtered_user_value = filtered_user.display_name if filtered_user else _('Unknown')
|
||||
options += [(filter_field_value, filtered_user_value, filter_field_value)]
|
||||
widget = SingleSelectWidget(
|
||||
self.filter_field_key,
|
||||
title=self.label,
|
||||
options=options,
|
||||
value=filter_field_value,
|
||||
render_br=False,
|
||||
)
|
||||
return self.render_filter_widget(widget)
|
||||
|
||||
|
||||
class UserFunctionFilterField(FilterField):
|
||||
id = 'user-function'
|
||||
key = 'user-function'
|
||||
label = _('Current User Function')
|
||||
available_for_filter = True
|
||||
|
||||
def get_allowed_operators(self):
|
||||
return []
|
||||
|
||||
def get_filter_widget(self, **kwargs):
|
||||
options = [('', '', '')] + [(x[0], x[1], x[0]) for x in self.formdef.workflow.get_sorted_functions()]
|
||||
widget = SingleSelectWidget(
|
||||
self.filter_field_key,
|
||||
title=self.label,
|
||||
options=options,
|
||||
value=self.get_filter_field_value(),
|
||||
render_br=False,
|
||||
)
|
||||
return self.render_filter_widget(widget)
|
||||
|
||||
|
||||
class SubmissionAgentFilterField(FilterField):
|
||||
id = 'submission-agent'
|
||||
key = 'submission-agent'
|
||||
label = _('Submission Agent')
|
||||
|
||||
@property
|
||||
def available_for_filter(self):
|
||||
return bool(self.formdef.backoffice_submission_roles)
|
||||
|
||||
def get_filter_widget(self, **kwargs):
|
||||
filter_field_value = self.get_filter_field_value()
|
||||
options = [
|
||||
('', '', ''),
|
||||
('__current__', _('Current user'), '__current__'),
|
||||
]
|
||||
if filter_field_value == '-1':
|
||||
# this happens when ?filter-submission-agent-uuid is given with an unknown uuid,
|
||||
# an option for "invalid user" is added so refreshs or new filters won't reset
|
||||
# this filter.
|
||||
options.append(('-1', _('Invalid user'), '-1'))
|
||||
options.extend(
|
||||
[
|
||||
(str(x.id), x.display_name, str(x.id))
|
||||
for x in get_publisher().user_class.select(
|
||||
[Or([ArrayContains('roles', [str(y)]) for y in self.formdef.backoffice_submission_roles])]
|
||||
)
|
||||
]
|
||||
)
|
||||
widget = SingleSelectWidget(
|
||||
self.filter_field_key,
|
||||
title=self.label,
|
||||
options=options,
|
||||
value=filter_field_value,
|
||||
render_br=False,
|
||||
)
|
||||
return self.render_filter_widget(widget)
|
||||
|
||||
|
||||
class SubmissionChannelFilterField(FilterField):
|
||||
id = 'submission_channel'
|
||||
key = 'submission_channel'
|
||||
label = _('Channel')
|
||||
|
||||
|
||||
class CriticalityLevelFilterFiled(FilterField):
|
||||
id = 'criticality-level'
|
||||
key = 'criticality-level'
|
||||
label = _('Criticality Level')
|
||||
|
||||
@property
|
||||
def available_for_filter(self):
|
||||
return bool(self.formdef.workflow.criticality_levels)
|
||||
|
||||
def get_allowed_operators(self):
|
||||
return []
|
||||
|
||||
def get_filter_widget(self, **kwargs):
|
||||
options = [('', pgettext_lazy('criticality-level', 'All'), '')] + [
|
||||
(str(i), x.name, str(i)) for i, x in enumerate(self.formdef.workflow.criticality_levels)
|
||||
]
|
||||
widget = SingleSelectWidget(
|
||||
self.filter_field_key,
|
||||
title=self.label,
|
||||
options=options,
|
||||
value=self.get_filter_field_value(),
|
||||
render_br=False,
|
||||
)
|
||||
return self.render_filter_widget(widget)
|
||||
|
||||
|
||||
class DigestFilterField(FilterField):
|
||||
id = 'digest'
|
||||
key = 'digest'
|
||||
label = _('Digest')
|
||||
|
||||
|
||||
class IdFilterField(FilterField):
|
||||
id = 'id'
|
||||
key = 'id'
|
||||
|
||||
def __init__(self, formdef):
|
||||
super().__init__(formdef=formdef)
|
||||
self.label = force_str(_('Identifier') if self.formdef.id_template else _('Number'))
|
||||
|
||||
|
||||
class TimeFilterField(FilterField):
|
||||
id = 'time'
|
||||
key = 'time'
|
||||
label = _('Created')
|
||||
|
||||
|
||||
class LastUpdateFilterField(FilterField):
|
||||
id = 'last_update_time'
|
||||
key = 'last_update_time'
|
||||
label = _('Last Modified')
|
||||
|
||||
|
||||
class AnonymisedFilterField(FilterField):
|
||||
id = 'anonymised'
|
||||
key = 'anonymised'
|
||||
label = _('Anonymised')
|
||||
|
||||
|
||||
class NumberFilterField(FilterField):
|
||||
id = 'number'
|
||||
key = 'number'
|
||||
label = _('Number')
|
||||
available_for_filter = True
|
||||
|
||||
|
||||
class IdentifierFilterField(FilterField):
|
||||
id = 'identifier'
|
||||
key = 'identifier'
|
||||
label = _('Identifier')
|
||||
available_for_filter = True
|
||||
|
||||
|
||||
class DistanceFilterField(FilterField):
|
||||
id = 'distance'
|
||||
key = 'distance'
|
||||
label = _('Distance')
|
||||
available_for_filter = True
|
|
@ -34,6 +34,7 @@ from quixote.http_request import parse_query
|
|||
|
||||
from wcs.api_access import ApiAccess
|
||||
from wcs.api_utils import get_query_flag, get_user_from_api_query_string
|
||||
from wcs.backoffice import filter_fields
|
||||
from wcs.backoffice.pagination import pagination_links
|
||||
from wcs.carddef import CardDef
|
||||
from wcs.categories import Category
|
||||
|
@ -123,6 +124,9 @@ def geojson_formdatas(formdatas, geoloc_key='base', fields=None):
|
|||
if hasattr(html_value, 'replace'):
|
||||
html_value = html_value.replace('[download]', '%sdownload' % formdata_backoffice_url)
|
||||
value = formdata.get_field_view_value(field)
|
||||
if field.key == 'block':
|
||||
# return display value for block fields, not the internal structure
|
||||
value = formdata.data.get(f'{field.id}_display')
|
||||
if not html_value and not value:
|
||||
continue
|
||||
|
||||
|
@ -132,7 +136,7 @@ def geojson_formdatas(formdatas, geoloc_key='base', fields=None):
|
|||
'value': str(value),
|
||||
'html_value': str(htmlescape(html_value)),
|
||||
}
|
||||
if field.key == 'file':
|
||||
if field.key == 'file' and not getattr(field, 'block_field', None):
|
||||
raw_value = formdata.data.get(field.id)
|
||||
if raw_value.has_redirect_url():
|
||||
geojson_infos['file_url'] = field.get_download_url(file_value=raw_value)
|
||||
|
@ -761,8 +765,8 @@ class ManagementDirectory(Directory):
|
|||
criterias = self.get_global_listing_criterias()
|
||||
formdatas = sql.AnyFormData.select(criterias + [NotNull('geoloc_base_x'), Null('anonymised')])
|
||||
fields = [
|
||||
FakeField('name', 'display_name', _('Name')),
|
||||
FakeField('status', 'status', _('Status')),
|
||||
filter_fields.DisplayNameFilterField(formdef=None),
|
||||
filter_fields.StatusFilterField(formdef=None),
|
||||
]
|
||||
get_response().set_content_type('application/json')
|
||||
return json.dumps(geojson_formdatas(formdatas, fields=fields), cls=misc.JSONEncoder)
|
||||
|
@ -1132,28 +1136,6 @@ class FormPage(Directory, TempfileDirectoryMixin):
|
|||
{'err': 0, 'data': [{'id': x[0], 'text': x[1]} for x in options]}, cls=misc.JSONEncoder
|
||||
)
|
||||
|
||||
def get_filterable_field_types(self):
|
||||
types = [
|
||||
'string',
|
||||
'text',
|
||||
'email',
|
||||
'item',
|
||||
'bool',
|
||||
'numeric',
|
||||
'items',
|
||||
'internal-id',
|
||||
'identifier',
|
||||
'number',
|
||||
'period-date',
|
||||
'user-id',
|
||||
'user-function',
|
||||
'submission-agent-id',
|
||||
'date',
|
||||
'distance',
|
||||
'criticality-level',
|
||||
]
|
||||
return types
|
||||
|
||||
def get_filter_sidebar(
|
||||
self,
|
||||
selected_filter=None,
|
||||
|
@ -1164,27 +1146,26 @@ class FormPage(Directory, TempfileDirectoryMixin):
|
|||
):
|
||||
r = TemplateIO(html=True)
|
||||
|
||||
waitpoint_status = self.formdef.workflow.get_waitpoint_status()
|
||||
fake_fields = [
|
||||
FakeField('internal-id', 'internal-id', _('Identifier')),
|
||||
FakeField('start', 'period-date', _('Start')),
|
||||
FakeField('end', 'period-date', _('End')),
|
||||
FakeField('user', 'user-id', _('User')),
|
||||
FakeField('user-function', 'user-function', _('Current User Function')),
|
||||
FakeField('submission-agent', 'submission-agent-id', _('Submission Agent'), addable=False),
|
||||
klass(formdef=self.formdef)
|
||||
for klass in (
|
||||
filter_fields.InternalIdFilterField,
|
||||
filter_fields.PeriodStartFilterField,
|
||||
filter_fields.PeriodEndFilterField,
|
||||
filter_fields.UserIdFilterField,
|
||||
filter_fields.UserFunctionFilterField,
|
||||
filter_fields.CriticalityLevelFilterFiled,
|
||||
)
|
||||
]
|
||||
if self.formdef.workflow.criticality_levels:
|
||||
fake_fields.append(FakeField('criticality-level', 'criticality-level', _('Criticality Level')))
|
||||
default_filters = self.get_default_filters(mode)
|
||||
|
||||
filter_fields = []
|
||||
available_fields = []
|
||||
for field in fake_fields + list(self.get_formdef_fields()):
|
||||
field.enabled = False
|
||||
if field.key not in self.get_filterable_field_types() + ['status']:
|
||||
field.formdef = self.formdef
|
||||
if not field.available_for_filter:
|
||||
continue
|
||||
if field.key == 'status' and not waitpoint_status:
|
||||
continue
|
||||
filter_fields.append(field)
|
||||
available_fields.append(field)
|
||||
|
||||
if getattr(field, 'block_field', None):
|
||||
field.label = '%s / %s' % (field.block_field.label, field.label)
|
||||
|
@ -1242,31 +1223,21 @@ class FormPage(Directory, TempfileDirectoryMixin):
|
|||
filters_dict.update(self.view.get_filters_dict())
|
||||
filters_dict.update(get_request().form)
|
||||
|
||||
def render_widget(filter_widget, operators):
|
||||
result = htmltext('<div class="widget operator-and-value-widget">')
|
||||
result += htmltext('<div class="title-and-operator">')
|
||||
result += filter_widget.render_title(filter_widget.get_title())
|
||||
if operators:
|
||||
result += htmltext('<div class="operator">')
|
||||
operator_widget = SingleSelectWidget(
|
||||
filter_field_operator_key,
|
||||
options=[(o[0], o[1], o[0]) for o in operators],
|
||||
value=filter_field_operator,
|
||||
render_br=False,
|
||||
)
|
||||
result += operator_widget.render_content()
|
||||
result += htmltext('</div>')
|
||||
result += htmltext('</div>')
|
||||
result += htmltext('<div class="value">')
|
||||
result += filter_widget.render_content()
|
||||
result += htmltext('</div>')
|
||||
result += htmltext('</div>')
|
||||
return result
|
||||
if selected_filter:
|
||||
filters_dict['filter-status-value'] = selected_filter
|
||||
filters_dict['filter-status-operator'] = selected_filter_operator
|
||||
|
||||
for filter_field in filter_fields:
|
||||
def render_widget(filter_widget, operators):
|
||||
return filter_fields.render_filter_widget(
|
||||
filter_widget, operators, filter_field_operator_key, filter_field_operator
|
||||
)
|
||||
|
||||
for filter_field in available_fields:
|
||||
if not filter_field.enabled:
|
||||
continue
|
||||
|
||||
filter_field.filters_dict = filters_dict
|
||||
|
||||
filter_field_key = 'filter-%s-value' % filter_field.contextual_id
|
||||
filter_field_value = filters_dict.get(filter_field_key)
|
||||
|
||||
|
@ -1276,124 +1247,8 @@ class FormPage(Directory, TempfileDirectoryMixin):
|
|||
lazy_manager = LazyFormDefObjectsManager(formdef=self.formdef)
|
||||
operators = lazy_manager.get_field_allowed_operators(filter_field) or []
|
||||
|
||||
if filter_field.key == 'status':
|
||||
operators = [
|
||||
('eq', '='),
|
||||
('ne', '!='),
|
||||
]
|
||||
r += htmltext('<div class="widget operator-and-value-widget">')
|
||||
r += htmltext('<div class="title-and-operator">')
|
||||
r += htmltext('<div class="title">%s</div>') % _('Status to display')
|
||||
if mode != 'stats':
|
||||
r += htmltext('<div class="operator">')
|
||||
operator_widget = SingleSelectWidget(
|
||||
'filter-operator',
|
||||
options=[(o[0], o[1], o[0]) for o in operators],
|
||||
value=selected_filter_operator,
|
||||
render_br=False,
|
||||
)
|
||||
r += operator_widget.render_content()
|
||||
r += htmltext('</div>')
|
||||
r += htmltext('</div>')
|
||||
r += htmltext('<div class="value content">')
|
||||
r += htmltext('<select name="filter">')
|
||||
filters = [
|
||||
('waiting', _('Waiting for an action'), None),
|
||||
('all', _('All'), None),
|
||||
('pending', pgettext_lazy('formdata', 'Open'), None),
|
||||
('done', _('Done'), None),
|
||||
]
|
||||
for status in waitpoint_status:
|
||||
filters.append((status.id, status.name, status.colour))
|
||||
for filter_id, filter_label, filter_colour in filters:
|
||||
if filter_id == selected_filter:
|
||||
selected = ' selected="selected"'
|
||||
else:
|
||||
selected = ''
|
||||
style = ''
|
||||
if filter_colour and filter_colour != '#FFFFFF':
|
||||
fg_colour = misc.get_foreground_colour(filter_colour)
|
||||
style = 'style="background: %s; color: %s;"' % (filter_colour, fg_colour)
|
||||
r += htmltext('<option value="%s"%s %s>' % (filter_id, selected, style))
|
||||
r += htmltext('%s</option>') % filter_label
|
||||
r += htmltext('</select>')
|
||||
r += htmltext('</div>')
|
||||
r += htmltext('</div>')
|
||||
|
||||
elif filter_field.key == 'period-date':
|
||||
r += DateWidget(
|
||||
filter_field_key, title=filter_field.label, value=filter_field_value, render_br=False
|
||||
).render()
|
||||
|
||||
elif filter_field.key == 'user-id':
|
||||
options = [
|
||||
('', _('None'), ''),
|
||||
('__current__', _('Current user'), '__current__'),
|
||||
]
|
||||
if filter_field_value and filter_field_value != '__current__':
|
||||
try:
|
||||
filtered_user = get_publisher().user_class.get(filter_field_value)
|
||||
except KeyError:
|
||||
filtered_user = None
|
||||
filtered_user_value = filtered_user.display_name if filtered_user else _('Unknown')
|
||||
options += [(filter_field_value, filtered_user_value, filter_field_value)]
|
||||
widget = SingleSelectWidget(
|
||||
filter_field_key,
|
||||
title=filter_field.label,
|
||||
options=options,
|
||||
value=filter_field_value,
|
||||
render_br=False,
|
||||
)
|
||||
r += render_widget(widget, operators=[])
|
||||
|
||||
elif filter_field.key == 'submission-agent-id':
|
||||
r += HiddenWidget(filter_field_key, value=filter_field_value).render()
|
||||
if filter_field_value:
|
||||
filtered_user = get_publisher().user_class.get(filter_field_value, ignore_errors=True)
|
||||
widget = StringWidget(
|
||||
'_' + filter_field_key,
|
||||
title=filter_field.label,
|
||||
value=filtered_user.display_name if filtered_user else _('Unknown'),
|
||||
readonly=True,
|
||||
render_br=False,
|
||||
)
|
||||
widget._parsed = True # make sure value is not replaced by request query
|
||||
r += widget.render()
|
||||
|
||||
elif filter_field.key == 'user-function':
|
||||
options = [('', '', '')] + [
|
||||
(x[0], x[1], x[0]) for x in self.formdef.workflow.get_sorted_functions()
|
||||
]
|
||||
widget = SingleSelectWidget(
|
||||
filter_field_key,
|
||||
title=filter_field.label,
|
||||
options=options,
|
||||
value=filter_field_value,
|
||||
render_br=False,
|
||||
)
|
||||
r += render_widget(widget, operators=[])
|
||||
|
||||
elif filter_field.key == 'internal-id':
|
||||
widget = StringWidget(
|
||||
filter_field_key,
|
||||
title=filter_field.label,
|
||||
value=filter_field_value,
|
||||
render_br=False,
|
||||
)
|
||||
r += render_widget(widget, operators)
|
||||
|
||||
elif filter_field.key == 'criticality-level':
|
||||
options = [('', pgettext_lazy('criticality-level', 'All'), '')] + [
|
||||
(str(i), x.name, str(i)) for i, x in enumerate(self.formdef.workflow.criticality_levels)
|
||||
]
|
||||
widget = SingleSelectWidget(
|
||||
filter_field_key,
|
||||
title=filter_field.label,
|
||||
options=options,
|
||||
value=filter_field_value,
|
||||
render_br=False,
|
||||
)
|
||||
r += render_widget(widget, operators=[])
|
||||
if hasattr(filter_field, 'get_filter_widget'):
|
||||
r += filter_field.get_filter_widget(mode=mode)
|
||||
|
||||
elif filter_field.key in ('item', 'items'):
|
||||
filter_field.required = False
|
||||
|
@ -1485,9 +1340,8 @@ class FormPage(Directory, TempfileDirectoryMixin):
|
|||
# field filter dialog content
|
||||
r += htmltext('<div style="display: none;">')
|
||||
r += htmltext('<ul id="field-filter" class="objects-list">')
|
||||
for field in filter_fields:
|
||||
addable = getattr(field, 'addable', True)
|
||||
r += htmltext('<li %s>') % ('' if addable else 'hidden')
|
||||
for field in available_fields:
|
||||
r += htmltext('<li>')
|
||||
r += htmltext('<label for="fields-filter-%s">') % field.contextual_id
|
||||
r += htmltext('<input type="checkbox" name="filter-%s"') % field.contextual_id
|
||||
if field.enabled:
|
||||
|
@ -1591,7 +1445,7 @@ class FormPage(Directory, TempfileDirectoryMixin):
|
|||
classnames = 'has-relations-field'
|
||||
attrs = 'data-field-id="%s"' % field.id
|
||||
seen_parents.add(field.id)
|
||||
elif isinstance(field, RelatedField):
|
||||
elif isinstance(field, filter_fields.RelatedField):
|
||||
classnames = 'related-field'
|
||||
if field.parent_field_id in seen_parents:
|
||||
classnames += ' collapsed'
|
||||
|
@ -1873,27 +1727,27 @@ class FormPage(Directory, TempfileDirectoryMixin):
|
|||
self.view.remove_self()
|
||||
return redirect('..')
|
||||
|
||||
def get_formdef_fields(self, include_block_items_fields=False):
|
||||
yield FakeField('id', 'id', _('Identifier') if self.formdef.id_template else _('Number'))
|
||||
def get_formdef_fields(self, include_block_fields=True, include_block_items_fields=False):
|
||||
yield filter_fields.IdFilterField(formdef=self.formdef)
|
||||
if self.formdef.default_digest_template:
|
||||
yield FakeField('digest', 'digest', _('Digest'))
|
||||
yield FakeField('submission_channel', 'submission_channel', _('Channel'))
|
||||
yield filter_fields.DigestFilterField(formdef=self.formdef)
|
||||
yield filter_fields.SubmissionChannelFilterField(formdef=self.formdef)
|
||||
if self.formdef.backoffice_submission_roles:
|
||||
yield FakeField('submission_agent', 'submission_agent', _('Submission By'))
|
||||
yield FakeField('time', 'time', _('Created'))
|
||||
yield FakeField('last_update_time', 'last_update_time', _('Last Modified'))
|
||||
yield filter_fields.SubmissionAgentFilterField(formdef=self.formdef)
|
||||
yield filter_fields.TimeFilterField(formdef=self.formdef)
|
||||
yield filter_fields.LastUpdateFilterField(formdef=self.formdef)
|
||||
|
||||
# user fields
|
||||
# user-label field but as a custom field, to get full name of user
|
||||
# using a sql join clause.
|
||||
yield UserLabelRelatedField()
|
||||
yield filter_fields.UserLabelRelatedField()
|
||||
for field in get_publisher().user_class.get_fields():
|
||||
if not field.can_include_in_listing:
|
||||
continue
|
||||
field.has_relations = True
|
||||
yield UserRelatedField(field)
|
||||
yield filter_fields.UserRelatedField(field)
|
||||
|
||||
for field in self.formdef.iter_fields(include_block_fields=True):
|
||||
for field in self.formdef.iter_fields(include_block_fields=include_block_fields):
|
||||
if getattr(field, 'block_field', None):
|
||||
if field.key == 'items' and not include_block_items_fields:
|
||||
# not yet
|
||||
|
@ -1917,17 +1771,12 @@ class FormPage(Directory, TempfileDirectoryMixin):
|
|||
if not card_field.can_include_in_listing:
|
||||
continue
|
||||
field.has_relations = True
|
||||
yield RelatedField(carddef, card_field, field)
|
||||
yield filter_fields.RelatedField(carddef, card_field, field)
|
||||
|
||||
yield FakeField('status', 'status', _('Status'), include_in_statistics=True)
|
||||
yield filter_fields.StatusFilterField(formdef=self.formdef)
|
||||
if any(x.get_visibility_mode() != 'all' for x in self.formdef.workflow.possible_status):
|
||||
yield FakeField(
|
||||
'user-visible-status',
|
||||
'user-visible-status',
|
||||
_('Status (for user)'),
|
||||
geojson_label=_('Status'),
|
||||
)
|
||||
yield FakeField('anonymised', 'anonymised', _('Anonymised'))
|
||||
yield filter_fields.UserVisibleStatusField(formdef=self.formdef)
|
||||
yield filter_fields.AnonymisedFilterField(formdef=self.formdef)
|
||||
|
||||
def get_default_columns(self):
|
||||
if self.view:
|
||||
|
@ -2004,18 +1853,21 @@ class FormPage(Directory, TempfileDirectoryMixin):
|
|||
statistics_fields_only=False,
|
||||
):
|
||||
fake_fields = [
|
||||
FakeField('internal-id', 'internal-id', _('Identifier')),
|
||||
FakeField('number', 'number', _('Number')),
|
||||
FakeField('identifier', 'identifier', _('Identifier')),
|
||||
FakeField('start', 'period-date', _('Start')),
|
||||
FakeField('end', 'period-date', _('End')),
|
||||
FakeField('start-mtime', 'period-date', _('Start (modification time)')),
|
||||
FakeField('end-mtime', 'period-date', _('End (modification time)')),
|
||||
FakeField('user', 'user-id', _('User')),
|
||||
FakeField('user-function', 'user-function', _('Current User Function')),
|
||||
FakeField('submission-agent', 'submission-agent-id', _('Submission Agent')),
|
||||
FakeField('distance', 'distance', _('Distance')),
|
||||
FakeField('criticality-level', 'criticality-level', _('Criticality Level')),
|
||||
klass(formdef=self.formdef)
|
||||
for klass in (
|
||||
filter_fields.InternalIdFilterField,
|
||||
filter_fields.NumberFilterField,
|
||||
filter_fields.IdentifierFilterField,
|
||||
filter_fields.PeriodStartFilterField,
|
||||
filter_fields.PeriodEndFilterField,
|
||||
filter_fields.PeriodStartUpdateTimeFilterField,
|
||||
filter_fields.PeriodEndUpdateTimeFilterField,
|
||||
filter_fields.UserIdFilterField,
|
||||
filter_fields.UserFunctionFilterField,
|
||||
filter_fields.SubmissionAgentFilterField,
|
||||
filter_fields.DistanceFilterField,
|
||||
filter_fields.CriticalityLevelFilterFiled,
|
||||
)
|
||||
]
|
||||
criterias = []
|
||||
|
||||
|
@ -2052,7 +1904,7 @@ class FormPage(Directory, TempfileDirectoryMixin):
|
|||
criterias.append(Nothing())
|
||||
|
||||
for filter_field in fake_fields + list(self.get_formdef_fields(include_block_items_fields=True)):
|
||||
if filter_field.key not in self.get_filterable_field_types():
|
||||
if not filter_field.available_for_filter:
|
||||
continue
|
||||
|
||||
if statistics_fields_only and not getattr(filter_field, 'include_in_statistics', False):
|
||||
|
@ -2108,7 +1960,7 @@ class FormPage(Directory, TempfileDirectoryMixin):
|
|||
# allow for short form, with a single query parameter
|
||||
filters_dict['filter-user-function-value'] = filters_dict.get('filter-user-function')
|
||||
|
||||
if filter_field.key == 'submission-agent-id':
|
||||
if filter_field.key == 'submission-agent':
|
||||
# convert uuid based filter into local id filter
|
||||
name_id = filters_dict.get('filter-submission-agent-uuid')
|
||||
if name_id:
|
||||
|
@ -2284,7 +2136,7 @@ class FormPage(Directory, TempfileDirectoryMixin):
|
|||
elif filter_field.id == 'end-mtime':
|
||||
criterias.append(LessOrEqual('last_update_time', filter_date_value))
|
||||
criterias[-1]._label = '%s: %s' % (filter_field.label, filter_field_value)
|
||||
elif filter_field.key == 'user-id':
|
||||
elif filter_field.key in ('submission-agent', 'user-id'):
|
||||
if filter_field_value == '__current__':
|
||||
context_vars = get_publisher().substitutions.get_context_variables(mode='lazy')
|
||||
if request and request.is_in_backoffice() and context_vars.get('form'):
|
||||
|
@ -2299,10 +2151,10 @@ class FormPage(Directory, TempfileDirectoryMixin):
|
|||
filter_field_value = None
|
||||
if filter_field_value in ('__current__', None):
|
||||
criterias.append(Nothing())
|
||||
else:
|
||||
elif filter_field.key == 'user-id':
|
||||
criterias.append(Equal('user_id', filter_field_value))
|
||||
elif filter_field.key == 'submission-agent-id':
|
||||
criterias.append(Equal('submission_agent_id', filter_field_value))
|
||||
elif filter_field.key == 'submission-agent':
|
||||
criterias.append(Equal('submission_agent_id', filter_field_value))
|
||||
elif filter_field.key == 'user-function':
|
||||
user_object = None
|
||||
if ':' in filter_field_value:
|
||||
|
@ -2840,7 +2692,7 @@ class FormPage(Directory, TempfileDirectoryMixin):
|
|||
selected_filter = self.get_filter_from_query()
|
||||
selected_filter_operator = self.get_filter_operator_from_query()
|
||||
if get_request().form.get('full') == 'on':
|
||||
fields = list(self.get_formdef_fields())
|
||||
fields = list(self.get_formdef_fields(include_block_fields=False))
|
||||
else:
|
||||
fields = self.get_fields_from_query()
|
||||
criterias = self.get_criterias_from_query()
|
||||
|
@ -4062,7 +3914,7 @@ class FormBackOfficeStatusPage(FormStatusPage):
|
|||
if field_url:
|
||||
r += htmltext(' <a title="%s" href="%s"></a>' % (v._field.label, field_url))
|
||||
r += htmltext('</code>')
|
||||
r += htmltext(' <div class="value"><span>%s</span>') % v
|
||||
r += htmltext(' <div class="value"><span>%s</span>') % misc.mark_spaces(v)
|
||||
if isinstance(v, NoneFieldVar):
|
||||
r += htmltext(' <span class="type">(%s)</span>') % _('no value')
|
||||
elif isinstance(v, (types.FunctionType, types.MethodType)):
|
||||
|
@ -4103,7 +3955,13 @@ class FormBackOfficeStatusPage(FormStatusPage):
|
|||
r += ', '.join(custom_repr(x) for x in v)
|
||||
r += htmltext(']</span>')
|
||||
else:
|
||||
r += htmltext(' <div class="value"><span>%s</span>') % ellipsize(safe(v), 10000)
|
||||
if k in ('form_details', 'form_evolution'):
|
||||
# do not mark spaces in those variables
|
||||
r += htmltext(' <div class="value"><span>%s</span>') % ellipsize(safe(v), 10000)
|
||||
else:
|
||||
r += htmltext(' <div class="value"><span>%s</span>') % misc.mark_spaces(
|
||||
ellipsize(safe(v), 10000)
|
||||
)
|
||||
if not isinstance(v, str):
|
||||
r += htmltext(' <span class="type">(%s)</span>') % get_type_name(v)
|
||||
r += htmltext('</div></li>')
|
||||
|
@ -4202,148 +4060,6 @@ class FormBackOfficeStatusPage(FormStatusPage):
|
|||
return self.test_tool_result()
|
||||
|
||||
|
||||
class FakeField:
|
||||
can_include_in_listing = True
|
||||
|
||||
def __init__(self, id, type_key, label, addable=True, include_in_statistics=False, geojson_label=None):
|
||||
self.id = id
|
||||
self.contextual_id = self.id
|
||||
self.key = type_key
|
||||
self.label = force_str(label)
|
||||
self.fake = True
|
||||
self.varname = id.replace('-', '_')
|
||||
self.contextual_varname = self.varname
|
||||
self.store_display_value = None
|
||||
self.store_structured_value = None
|
||||
self.addable = addable
|
||||
self.include_in_statistics = include_in_statistics
|
||||
self.geojson_label = force_str(geojson_label or self.label)
|
||||
|
||||
def get_view_value(self, value):
|
||||
# just here to quack like a duck
|
||||
return None
|
||||
|
||||
def get_csv_heading(self):
|
||||
return [self.label]
|
||||
|
||||
def get_csv_value(self, element, **kwargs):
|
||||
return [element]
|
||||
|
||||
@property
|
||||
def has_relations(self):
|
||||
return bool(self.id == 'user-label')
|
||||
|
||||
|
||||
class RelatedField:
|
||||
is_related_field = True
|
||||
key = 'related-field'
|
||||
varname = None
|
||||
related_field = None
|
||||
can_include_in_listing = True
|
||||
|
||||
def __init__(self, carddef, field, parent_field):
|
||||
self.carddef = carddef
|
||||
self.related_field = field
|
||||
self.parent_field = parent_field
|
||||
self.parent_field_id = parent_field.id
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return '%s$%s' % (self.parent_field_id, self.related_field.id)
|
||||
|
||||
@property
|
||||
def contextual_id(self):
|
||||
return self.id
|
||||
|
||||
@property
|
||||
def label(self):
|
||||
return '%s - %s' % (self.parent_field.label, self.related_field.label)
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s (card: %r, parent: %r, related: %r)>' % (
|
||||
self.__class__.__name__,
|
||||
self.carddef,
|
||||
self.parent_field.label,
|
||||
self.related_field.label,
|
||||
)
|
||||
|
||||
@property
|
||||
def store_display_value(self):
|
||||
return self.related_field.store_display_value
|
||||
|
||||
@property
|
||||
def store_structured_value(self):
|
||||
return self.related_field.store_structured_value
|
||||
|
||||
def get_view_value(self, value, **kwargs):
|
||||
if value is None:
|
||||
return ''
|
||||
if isinstance(value, bool):
|
||||
return _('Yes') if value else _('No')
|
||||
if isinstance(value, datetime.date):
|
||||
return misc.strftime(misc.date_format(), value)
|
||||
return value
|
||||
|
||||
def get_view_short_value(self, value, max_len=30, **kwargs):
|
||||
return self.get_view_value(value)
|
||||
|
||||
def get_csv_heading(self):
|
||||
if self.related_field:
|
||||
return self.related_field.get_csv_heading()
|
||||
return [self.label]
|
||||
|
||||
def get_csv_value(self, value, **kwargs):
|
||||
if self.related_field:
|
||||
return self.related_field.get_csv_value(value, **kwargs)
|
||||
return [self.get_view_value(value)]
|
||||
|
||||
def get_column_field_id(self):
|
||||
return get_field_id(self.related_field)
|
||||
|
||||
|
||||
class UserRelatedField(RelatedField):
|
||||
# it is named 'user-label' and not 'user' for compatibility with existing
|
||||
# listings, as the 'classic' user column is named 'user-label'.
|
||||
parent_field_id = 'user-label'
|
||||
store_display_value = None
|
||||
store_structured_value = None
|
||||
|
||||
def __init__(self, field):
|
||||
self.related_field = field
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s (field: %r)>' % (
|
||||
self.__class__.__name__,
|
||||
self.related_field.label,
|
||||
)
|
||||
|
||||
@property
|
||||
def label(self):
|
||||
return _('%s of User') % self.related_field.label
|
||||
|
||||
|
||||
class UserLabelRelatedField(UserRelatedField):
|
||||
# custom user-label column, targetting the "name" (= full name) column
|
||||
# of the users table
|
||||
id = 'user-label'
|
||||
key = 'user-label'
|
||||
varname = 'user_label'
|
||||
has_relations = True
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def __repr__(self):
|
||||
return '<UserLabelRelatedField>'
|
||||
|
||||
def get_column_field_id(self):
|
||||
return 'name'
|
||||
|
||||
@property
|
||||
def label(self):
|
||||
return _('User Label')
|
||||
|
||||
|
||||
def do_graphs_section(period_start=None, period_end=None, criterias=None):
|
||||
from wcs import sql
|
||||
|
||||
|
@ -4513,11 +4229,10 @@ class MassActionAfterJob(AfterJob):
|
|||
# action not found
|
||||
return
|
||||
|
||||
if item_ids:
|
||||
oldest_lazy_form = formdef.data_class().get(item_ids[0]).get_as_lazy()
|
||||
self.total_count = len(item_ids)
|
||||
self.store()
|
||||
|
||||
oldest_lazy_form = None
|
||||
publisher = get_publisher()
|
||||
for i, formdata_id in enumerate(item_ids):
|
||||
# do not load all formdatas at once as they can be modified during the loop
|
||||
|
@ -4525,6 +4240,8 @@ class MassActionAfterJob(AfterJob):
|
|||
formdata = formdef.data_class().get(formdata_id, ignore_errors=True)
|
||||
if not formdata:
|
||||
continue
|
||||
if oldest_lazy_form is None:
|
||||
oldest_lazy_form = formdata.get_as_lazy()
|
||||
publisher.reset_formdata_state()
|
||||
publisher.substitutions.feed(user)
|
||||
publisher.substitutions.feed(formdef)
|
||||
|
@ -4845,3 +4562,10 @@ class JsonFileExportAfterJob(CsvExportAfterJob):
|
|||
)
|
||||
self.content_type = 'application/json'
|
||||
self.store()
|
||||
|
||||
|
||||
class FakeField:
|
||||
# 2024-04-12, legacy class, for transition from FakeField to filter_fields.*
|
||||
# can be removed once all afterjobs referencing fake fields are removed.
|
||||
# (= 2 days after update)
|
||||
pass
|
||||
|
|
|
@ -52,7 +52,8 @@ class SnapshotsDirectory(Directory):
|
|||
templates=['wcs/backoffice/snapshots.html'],
|
||||
context={
|
||||
'view': self,
|
||||
'form_has_tests': bool(TestDef.select_for_objectdef(self.obj)),
|
||||
'form_has_tests': self.object_type in ('formdef', 'carddef')
|
||||
and bool(TestDef.select_for_objectdef(self.obj)),
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -135,7 +136,13 @@ class SnapshotsDirectory(Directory):
|
|||
if mode == 'inspect' and not has_inspect:
|
||||
raise errors.TraversalError()
|
||||
|
||||
context = getattr(self, 'get_compare_%s_context' % mode)(snapshot1, snapshot2)
|
||||
from wcs.blocks import BlockdefImportError
|
||||
|
||||
try:
|
||||
context = getattr(self, 'get_compare_%s_context' % mode)(snapshot1, snapshot2)
|
||||
except (BlockdefImportError, FormdefImportError, WorkflowImportError) as e:
|
||||
return template.error_page(_('Can not display snapshot (%s)') % e)
|
||||
|
||||
context.update(
|
||||
{
|
||||
'mode': mode,
|
||||
|
@ -144,6 +151,8 @@ class SnapshotsDirectory(Directory):
|
|||
'snapshot2': snapshot2,
|
||||
}
|
||||
)
|
||||
get_response().add_javascript(['gadjo.snapshotdiff.js'])
|
||||
get_response().add_css_include('gadjo.snapshotdiff.css')
|
||||
return template.QommonTemplateResponse(
|
||||
templates=['wcs/backoffice/snapshots_compare.html'],
|
||||
context=context,
|
||||
|
@ -261,7 +270,7 @@ class SnapshotsDirectory(Directory):
|
|||
current_date = None
|
||||
snapshots = get_publisher().snapshot_class.select_object_history(self.obj)
|
||||
test_results = TestResult.select(
|
||||
[Equal('object_type', self.obj.get_table_name()), Equal('object_id', self.obj.id)]
|
||||
[Equal('object_type', self.obj.get_table_name()), Equal('object_id', str(self.obj.id))]
|
||||
)
|
||||
test_results_by_id = {x.id: x for x in test_results}
|
||||
day_snapshot = None
|
||||
|
|
|
@ -98,7 +98,7 @@ class StudioDirectory(Directory):
|
|||
backoffice_root = get_publisher().get_backoffice_root()
|
||||
object_types = []
|
||||
if backoffice_root.is_accessible('forms'):
|
||||
extra_links.append(('../forms/blocks/', pgettext('studio', 'Field blocks')))
|
||||
extra_links.append(('../forms/blocks/', pgettext('studio', 'Blocks of fields')))
|
||||
if backoffice_root.is_accessible('workflows'):
|
||||
extra_links.append(('../workflows/mail-templates/', pgettext('studio', 'Mail templates')))
|
||||
extra_links.append(('../workflows/comment-templates/', pgettext('studio', 'Comment templates')))
|
||||
|
|
|
@ -504,7 +504,6 @@ class SubmissionDirectory(Directory):
|
|||
if redirect_url:
|
||||
return redirect(redirect_url)
|
||||
|
||||
get_response().breadcrumb.append(('submission/', _('Submission')))
|
||||
get_response().set_title(_('Submission'))
|
||||
|
||||
list_forms = self.get_submittable_formdefs(prefetch=False)
|
||||
|
@ -587,7 +586,7 @@ class SubmissionDirectory(Directory):
|
|||
|
||||
if get_request().form.get('ajax') == 'true':
|
||||
get_request().ignore_session = True
|
||||
get_response().filter = {'raw': True}
|
||||
get_response().raw = True
|
||||
return r.getvalue()
|
||||
|
||||
rt = TemplateIO(html=True)
|
||||
|
|
|
@ -52,8 +52,8 @@ class BlockDef(StorableObject, PostConditionsXmlMixin):
|
|||
_indexes = ['slug']
|
||||
backoffice_class = 'wcs.admin.blocks.BlockDirectory'
|
||||
xml_root_node = 'block'
|
||||
verbose_name = _('Field block')
|
||||
verbose_name_plural = _('Field blocks')
|
||||
verbose_name = _('Block of fields')
|
||||
verbose_name_plural = _('Blocks of fields')
|
||||
var_prefixes = ['block']
|
||||
|
||||
name = None
|
||||
|
@ -61,12 +61,13 @@ class BlockDef(StorableObject, PostConditionsXmlMixin):
|
|||
fields = None
|
||||
digest_template = None
|
||||
category_id = None
|
||||
documentation = None
|
||||
post_conditions = None
|
||||
|
||||
SLUG_DASH = '_'
|
||||
|
||||
# declarations for serialization
|
||||
TEXT_ATTRIBUTES = ['name', 'slug', 'digest_template']
|
||||
TEXT_ATTRIBUTES = ['name', 'slug', 'digest_template', 'documentation']
|
||||
|
||||
def __init__(self, name=None, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
@ -183,7 +184,7 @@ class BlockDef(StorableObject, PostConditionsXmlMixin):
|
|||
return root
|
||||
|
||||
@classmethod
|
||||
def import_from_xml(cls, fd, include_id=False, check_datasources=True, check_deprecated=True):
|
||||
def import_from_xml(cls, fd, include_id=False, check_datasources=True, check_deprecated=False):
|
||||
try:
|
||||
tree = ET.parse(fd)
|
||||
except Exception:
|
||||
|
@ -207,7 +208,7 @@ class BlockDef(StorableObject, PostConditionsXmlMixin):
|
|||
|
||||
@classmethod
|
||||
def import_from_xml_tree(
|
||||
cls, tree, include_id=False, check_datasources=True, check_deprecated=True, **kwargs
|
||||
cls, tree, include_id=False, check_datasources=True, check_deprecated=False, **kwargs
|
||||
):
|
||||
from wcs.backoffice.deprecations import DeprecatedElementsDetected, DeprecationsScan
|
||||
|
||||
|
@ -523,6 +524,17 @@ class BlockWidget(WidgetList):
|
|||
**kwargs,
|
||||
)
|
||||
|
||||
@property
|
||||
def a11y_labelledby(self):
|
||||
return bool(self.a11y_role)
|
||||
|
||||
@property
|
||||
def a11y_role(self):
|
||||
# don't mark block as a group if it has no label
|
||||
if self.label_display != 'hidden':
|
||||
return 'group'
|
||||
return None
|
||||
|
||||
def set_value(self, value):
|
||||
from .fields.block import BlockRowValue
|
||||
|
||||
|
@ -595,7 +607,9 @@ class BlockWidget(WidgetList):
|
|||
def render_title(self, title):
|
||||
attrs = {'id': 'form_label_%s' % self.get_name_for_id()}
|
||||
if not title or self.label_display == 'hidden':
|
||||
return htmltag('span', **attrs) + htmltext('</span>')
|
||||
# add a tag even if there's no label to display as it's used as an anchor point
|
||||
# for links to errors.
|
||||
return htmltag('div', **attrs) + htmltext('</div>')
|
||||
|
||||
if self.label_display == 'normal':
|
||||
return super().render_title(title)
|
||||
|
|
|
@ -29,7 +29,7 @@ class CardData(FormData):
|
|||
def get_data_source_structured_item(
|
||||
self, digest_key='default', group_by=None, with_related_urls=False, with_files_urls=False
|
||||
):
|
||||
if self.digests is None:
|
||||
if not self.digests:
|
||||
if digest_key == 'default':
|
||||
summary = _('Digest (default) not defined')
|
||||
else:
|
||||
|
@ -122,11 +122,18 @@ class CardData(FormData):
|
|||
return '/api/card-file-by-token/%s' % token.id
|
||||
|
||||
def update_related(self):
|
||||
if self.is_draft():
|
||||
return
|
||||
if self.formdef.reverse_relations:
|
||||
job = UpdateRelationsAfterJob(carddata=self)
|
||||
if get_response():
|
||||
job.store()
|
||||
get_response().add_after_job(job)
|
||||
job._update_key = (self._formdef.id, self.id)
|
||||
# do not register/run job if an identical job is already planned
|
||||
if job._update_key not in (
|
||||
getattr(x, '_update_key', None) for x in get_response().after_jobs or []
|
||||
):
|
||||
job.store()
|
||||
get_response().add_after_job(job)
|
||||
else:
|
||||
job.execute()
|
||||
self._has_changed_digest = False
|
||||
|
@ -149,20 +156,21 @@ class UpdateRelationsAfterJob(AfterJob):
|
|||
update_related_seen = get_publisher()._update_related_seen
|
||||
|
||||
try:
|
||||
carddef = CardDef.get(self.kwargs['carddef_id'])
|
||||
carddef = CardDef.cached_get(self.kwargs['carddef_id'])
|
||||
carddata = carddef.data_class().get(self.kwargs['carddata_id'])
|
||||
except KeyError:
|
||||
# card got removed (probably the afterjob met some unexpected delay), ignore.
|
||||
return
|
||||
|
||||
klass = {'carddef': CardDef, 'formdef': FormDef}
|
||||
publisher = get_publisher()
|
||||
|
||||
# check all known reverse relations
|
||||
for obj_ref in {x['obj'] for x in carddef.reverse_relations}:
|
||||
obj_type, obj_slug = obj_ref.split(':')
|
||||
obj_class = klass.get(obj_type)
|
||||
try:
|
||||
objdef = obj_class.get_by_slug(obj_slug)
|
||||
objdef = obj_class.get_by_slug(obj_slug, use_cache=True)
|
||||
except KeyError:
|
||||
continue
|
||||
criterias = []
|
||||
|
@ -200,6 +208,11 @@ class UpdateRelationsAfterJob(AfterJob):
|
|||
if objdata_seen_key in update_related_seen:
|
||||
# do not allow updates to cycle back
|
||||
continue
|
||||
|
||||
publisher.reset_formdata_state()
|
||||
publisher.substitutions.feed(objdata.formdef)
|
||||
publisher.substitutions.feed(objdata)
|
||||
|
||||
objdata_changed = False
|
||||
for field in fields:
|
||||
if getattr(field, 'block_field', None):
|
||||
|
|
|
@ -49,6 +49,7 @@ class CardDef(FormDef):
|
|||
item_name_plural = pgettext_lazy('item', 'cards')
|
||||
|
||||
confirmation = False
|
||||
history_pane_default_mode = 'collapsed'
|
||||
|
||||
# users are not allowed to access carddata where they're submitter.
|
||||
user_allowed_to_access_own_data = False
|
||||
|
@ -143,6 +144,10 @@ class CardDef(FormDef):
|
|||
self.roles = self.backoffice_submission_roles
|
||||
return super().store(comment=comment, *args, **kwargs)
|
||||
|
||||
def update_category_reference(self):
|
||||
# only relevant for formdefs
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def get_carddefs_as_data_source(cls):
|
||||
carddefs_by_id = {}
|
||||
|
@ -199,7 +204,7 @@ class CardDef(FormDef):
|
|||
assert data_source_id.startswith('carddef:')
|
||||
parts = data_source_id.split(':')
|
||||
try:
|
||||
carddef = cls.get_by_urlname(parts[1])
|
||||
carddef = cls.get_by_urlname(parts[1], use_cache=True)
|
||||
except KeyError:
|
||||
return []
|
||||
criterias = [StrictNotEqual('status', 'draft'), Null('anonymised')]
|
||||
|
@ -294,7 +299,7 @@ class CardDef(FormDef):
|
|||
if len(parts) != 3:
|
||||
return []
|
||||
try:
|
||||
carddef = cls.get_by_urlname(parts[1])
|
||||
carddef = cls.get_by_urlname(parts[1], use_cache=True)
|
||||
except KeyError:
|
||||
return []
|
||||
custom_view = cls.get_data_source_custom_view(data_source_id, carddef=carddef)
|
||||
|
@ -311,6 +316,24 @@ class CardDef(FormDef):
|
|||
return True
|
||||
return False
|
||||
|
||||
def get_default_management_sidebar_items(self):
|
||||
management_sidebar_items = {
|
||||
'general',
|
||||
'submission-context',
|
||||
'user',
|
||||
'geolocation',
|
||||
'custom-template',
|
||||
}
|
||||
if not self.user_support:
|
||||
management_sidebar_items.remove('user')
|
||||
return management_sidebar_items
|
||||
|
||||
def get_management_sidebar_available_items(self):
|
||||
excluded_parts = ['pending-forms']
|
||||
if not self.user_support:
|
||||
excluded_parts.append('user')
|
||||
return [x for x in super().get_management_sidebar_available_items() if x[0] not in excluded_parts]
|
||||
|
||||
|
||||
def get_cards_graph(category=None, show_orphans=False):
|
||||
out = io.StringIO()
|
||||
|
|
|
@ -34,7 +34,7 @@ class CommentTemplate(XmlStorableObject):
|
|||
|
||||
name = None
|
||||
slug = None
|
||||
description = None
|
||||
documentation = None
|
||||
comment = None
|
||||
attachments = []
|
||||
category_id = None
|
||||
|
@ -43,7 +43,8 @@ class CommentTemplate(XmlStorableObject):
|
|||
XML_NODES = [
|
||||
('name', 'str'),
|
||||
('slug', 'str'),
|
||||
('description', 'str'),
|
||||
('description', 'str'), # legacy
|
||||
('documentation', 'str'),
|
||||
('comment', 'str'),
|
||||
('attachments', 'str_list'),
|
||||
]
|
||||
|
@ -52,6 +53,16 @@ class CommentTemplate(XmlStorableObject):
|
|||
XmlStorableObject.__init__(self)
|
||||
self.name = name
|
||||
|
||||
def migrate(self):
|
||||
changed = False
|
||||
if getattr(self, 'description', None): # 2024-04-07
|
||||
self.documentation = getattr(self, 'description')
|
||||
self.description = None
|
||||
changed = True
|
||||
if changed:
|
||||
self.store(comment=_('Automatic update'), snapshot_store_user=False)
|
||||
return changed
|
||||
|
||||
@property
|
||||
def category(self):
|
||||
return CommentTemplateCategory.get(self.category_id, ignore_errors=True)
|
||||
|
@ -67,14 +78,16 @@ class CommentTemplate(XmlStorableObject):
|
|||
base_url = get_publisher().get_backoffice_url()
|
||||
return '%s/workflows/comment-templates/%s/' % (base_url, self.id)
|
||||
|
||||
def store(self, comment=None, application=None, *args, **kwargs):
|
||||
def store(self, comment=None, snapshot_store_user=True, application=None, *args, **kwargs):
|
||||
assert not self.is_readonly()
|
||||
if self.slug is None:
|
||||
# set slug if it's not yet there
|
||||
self.slug = self.get_new_slug()
|
||||
super().store(*args, **kwargs)
|
||||
if get_publisher().snapshot_class:
|
||||
get_publisher().snapshot_class.snap(instance=self, comment=comment, application=application)
|
||||
get_publisher().snapshot_class.snap(
|
||||
instance=self, store_user=snapshot_store_user, comment=comment, application=application
|
||||
)
|
||||
|
||||
def get_places_of_use(self):
|
||||
from wcs.workflows import Workflow
|
||||
|
|
|
@ -427,7 +427,7 @@ class Command(TenantCommand):
|
|||
|
||||
def configure_site_options(self, current_service, pub, ignore_timestamp=False):
|
||||
# configure site-options.cfg
|
||||
config = configparser.RawConfigParser()
|
||||
config = configparser.ConfigParser(interpolation=None)
|
||||
site_options_filepath = os.path.join(pub.app_dir, 'site-options.cfg')
|
||||
if os.path.exists(site_options_filepath):
|
||||
config.read(site_options_filepath)
|
||||
|
@ -495,6 +495,10 @@ class Command(TenantCommand):
|
|||
variables['portal_user_url'] = service_url
|
||||
variables['portal_user_title'] = service.get('title')
|
||||
config.set('options', 'theme_skeleton_url', service.get('base_url') + '__skeleton__/')
|
||||
|
||||
if service.get('service-id') == 'lingo':
|
||||
variables['lingo_url'] = urllib.parse.urljoin(service_url, '/')
|
||||
|
||||
for legacy_url in service.get('legacy_urls', []):
|
||||
legacy_domain = urllib.parse.urlparse(legacy_url['base_url']).netloc.split(':')[0]
|
||||
legacy_urls[legacy_domain] = domain
|
||||
|
|
|
@ -47,6 +47,17 @@ class CustomView(StorableObject):
|
|||
|
||||
xml_root_node = 'custom_view'
|
||||
|
||||
def migrate(self):
|
||||
changed = False
|
||||
# 2024-04-10
|
||||
if self.columns and 'submission_agent' in [x['id'] for x in self.columns['list']]:
|
||||
self.columns['list'] = [
|
||||
{'id': x['id'].replace('submission_agent', 'submission-agent')} for x in self.columns['list']
|
||||
]
|
||||
changed = True
|
||||
if changed:
|
||||
self.store()
|
||||
|
||||
@property
|
||||
def user(self):
|
||||
return get_publisher().user_class.get(self.user_id)
|
||||
|
@ -58,9 +69,9 @@ class CustomView(StorableObject):
|
|||
@property
|
||||
def formdef(self):
|
||||
if self.formdef_type == 'formdef':
|
||||
return FormDef.get(self.formdef_id)
|
||||
return FormDef.cached_get(self.formdef_id)
|
||||
else:
|
||||
return CardDef.get(self.formdef_id)
|
||||
return CardDef.cached_get(self.formdef_id)
|
||||
|
||||
@formdef.setter
|
||||
def formdef(self, value):
|
||||
|
|
|
@ -676,7 +676,7 @@ class NamedDataSource(XmlStorableObject):
|
|||
|
||||
name = None
|
||||
slug = None
|
||||
description = None
|
||||
documentation = None
|
||||
data_source = None
|
||||
cache_duration = None
|
||||
query_parameter = None
|
||||
|
@ -702,7 +702,8 @@ class NamedDataSource(XmlStorableObject):
|
|||
XML_NODES = [
|
||||
('name', 'str'),
|
||||
('slug', 'str'),
|
||||
('description', 'str'),
|
||||
('description', 'str'), # legacy
|
||||
('documentation', 'str'),
|
||||
('cache_duration', 'str'),
|
||||
('query_parameter', 'str'),
|
||||
('id_parameter', 'str'),
|
||||
|
@ -737,6 +738,11 @@ class NamedDataSource(XmlStorableObject):
|
|||
self.data_source['value'] = translate_url(publisher, url)
|
||||
changed = True
|
||||
|
||||
if getattr(self, 'description', None): # 2024-04-07
|
||||
self.documentation = getattr(self, 'description')
|
||||
self.description = None
|
||||
changed = True
|
||||
|
||||
if changed:
|
||||
self.store(comment=_('Automatic update'), snapshot_store_user=False)
|
||||
|
||||
|
@ -917,7 +923,7 @@ class NamedDataSource(XmlStorableObject):
|
|||
return root
|
||||
|
||||
@classmethod
|
||||
def import_from_xml_tree(cls, tree, include_id=False, check_deprecated=True, **kwargs):
|
||||
def import_from_xml_tree(cls, tree, include_id=False, check_deprecated=False, **kwargs):
|
||||
from wcs.backoffice.deprecations import DeprecatedElementsDetected, DeprecationsScan
|
||||
|
||||
data_source = super().import_from_xml_tree(
|
||||
|
@ -940,8 +946,7 @@ class NamedDataSource(XmlStorableObject):
|
|||
data_source = super().get_by_slug(slug, ignore_errors=ignore_errors)
|
||||
if data_source is None:
|
||||
if stub_fallback:
|
||||
if slug != 'inspect_collapse':
|
||||
get_logger().warning("data source '%s' does not exist" % slug)
|
||||
get_logger().warning("data source '%s' does not exist" % slug)
|
||||
return StubNamedDataSource(name=slug)
|
||||
return data_source
|
||||
|
||||
|
@ -1118,6 +1123,8 @@ class NamedDataSource(XmlStorableObject):
|
|||
elif self.type == 'json' and self.id_parameter:
|
||||
value = self.get_value_by_id(self.id_parameter, option_id)
|
||||
elif self.type == 'wcs:users':
|
||||
if isinstance(option_id, get_publisher().user_class):
|
||||
option_id = option_id.id
|
||||
value = get_publisher().user_class.get_user_with_roles(
|
||||
option_id,
|
||||
included_roles=self.users_included_roles,
|
||||
|
@ -1219,6 +1226,8 @@ class StubNamedDataSource(NamedDataSource):
|
|||
|
||||
class DataSourcesSubstitutionProxy:
|
||||
def __getattr__(self, attr):
|
||||
if attr == 'inspect_collapse':
|
||||
return True
|
||||
return DataSourceProxy(attr)
|
||||
|
||||
def inspect_keys(self):
|
||||
|
|
|
@ -222,6 +222,8 @@ class Field:
|
|||
condition = None
|
||||
is_no_data_field = False
|
||||
can_include_in_listing = False
|
||||
available_for_filter = False
|
||||
documentation = None
|
||||
|
||||
# flag a field for removal by AnonymiseWorkflowStatusItem
|
||||
# possible values are final, intermediate, no.
|
||||
|
@ -231,7 +233,7 @@ class Field:
|
|||
|
||||
# declarations for serialization, they are mostly for legacy files,
|
||||
# new exports directly include typing attributes.
|
||||
TEXT_ATTRIBUTES = ['label', 'type', 'hint', 'varname', 'extra_css_class']
|
||||
TEXT_ATTRIBUTES = ['label', 'type', 'hint', 'varname', 'extra_css_class', 'documentation']
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
for k, v in kwargs.items():
|
||||
|
@ -319,7 +321,7 @@ class Field:
|
|||
|
||||
def export_to_xml(self, include_id=False):
|
||||
field = ET.Element('field')
|
||||
extra_fields = ['default_value'] # specific to workflow variables
|
||||
extra_fields = ['default_value', 'documentation'] # default_value is specific to workflow variables
|
||||
if include_id:
|
||||
extra_fields.append('id')
|
||||
ET.SubElement(field, 'type').text = self.key
|
||||
|
@ -358,7 +360,7 @@ class Field:
|
|||
return field
|
||||
|
||||
def init_with_xml(self, elem, include_id=False, snapshot=False):
|
||||
extra_fields = ['default_value'] # specific to workflow variables
|
||||
extra_fields = ['documentation', 'default_value'] # default_value is specific to workflow variables
|
||||
for attribute in self.get_admin_attributes() + extra_fields:
|
||||
el = elem.find(attribute)
|
||||
if hasattr(self, '%s_init_with_xml' % attribute):
|
||||
|
@ -443,6 +445,11 @@ class Field:
|
|||
if xml_node_text(node.find('locked')) == 'True':
|
||||
self.prefill['locked'] = True
|
||||
|
||||
def display_locations_export_to_xml(self, node, include_id=False):
|
||||
display_locations_node = ET.SubElement(node, 'display_locations')
|
||||
for v in self.display_locations or []:
|
||||
ET.SubElement(display_locations_node, 'item').text = force_str(v)
|
||||
|
||||
def get_rst_view_value(self, value, indent=''):
|
||||
return indent + self.get_view_value(value)
|
||||
|
||||
|
|
|
@ -136,13 +136,16 @@ class BlockField(WidgetField):
|
|||
|
||||
def get_type_label(self):
|
||||
try:
|
||||
return _('Field Block (%s)') % self.block.name
|
||||
return _('Block of fields (%s)') % self.block.name
|
||||
except KeyError:
|
||||
return _('Field Block (%s, missing)') % self.block_slug
|
||||
return _('Block of fields (%s, missing)') % self.block_slug
|
||||
|
||||
def get_dependencies(self):
|
||||
yield from super().get_dependencies()
|
||||
yield self.block
|
||||
try:
|
||||
yield self.block
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def add_to_form(self, form, value=None):
|
||||
try:
|
||||
|
|
|
@ -32,6 +32,7 @@ class BoolField(WidgetField):
|
|||
description = _('Check Box (single choice)')
|
||||
allow_complex = True
|
||||
allow_statistics = True
|
||||
available_for_filter = True
|
||||
|
||||
widget_class = CheckboxWidget
|
||||
required = False
|
||||
|
|
|
@ -29,6 +29,7 @@ from .base import WidgetField, register_field_class
|
|||
class DateField(WidgetField):
|
||||
key = 'date'
|
||||
description = _('Date')
|
||||
available_for_filter = True
|
||||
|
||||
widget_class = DateWidget
|
||||
minimum_date = None
|
||||
|
|
|
@ -30,6 +30,7 @@ class EmailField(WidgetField):
|
|||
key = 'email'
|
||||
description = _('Email')
|
||||
use_live_server_validation = True
|
||||
available_for_filter = True
|
||||
|
||||
widget_class = EmailWidget
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
import base64
|
||||
import os
|
||||
import urllib.parse
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from django.utils.encoding import force_bytes, force_str
|
||||
|
@ -139,6 +140,20 @@ class FileField(WidgetField):
|
|||
upload = PicklableUpload(value.filename, value.content_type)
|
||||
upload.receive([value.content])
|
||||
return upload
|
||||
|
||||
value = misc.unlazy(value)
|
||||
if isinstance(value, str) and urllib.parse.urlparse(value).scheme in ('http', 'https'):
|
||||
try:
|
||||
response, dummy, data, dummy = misc.http_get_page(value, raise_on_http_errors=True)
|
||||
except misc.ConnectionError:
|
||||
pass
|
||||
else:
|
||||
value = {
|
||||
'filename': os.path.basename(urllib.parse.urlparse(value).path) or _('file.bin'),
|
||||
'content': data,
|
||||
'content_type': response.headers.get('content-type'),
|
||||
}
|
||||
|
||||
if isinstance(value, dict):
|
||||
# if value is a dictionary we expect it to have a content or
|
||||
# b64_content key and a filename keys and an optional
|
||||
|
|
|
@ -257,6 +257,7 @@ class ItemField(WidgetField, MapOptionsMixin, ItemFieldMixin, ItemWithImageField
|
|||
description = _('List')
|
||||
allow_complex = True
|
||||
allow_statistics = True
|
||||
available_for_filter = True
|
||||
|
||||
items = []
|
||||
show_as_radio = None
|
||||
|
@ -612,6 +613,7 @@ class ItemField(WidgetField, MapOptionsMixin, ItemFieldMixin, ItemWithImageField
|
|||
title=_('Initial Position'),
|
||||
options=(
|
||||
('', _('Default position (from markers)'), ''),
|
||||
('geoloc', _('Device geolocation'), 'geoloc'),
|
||||
('template', _('From template'), 'template'),
|
||||
),
|
||||
value=self.initial_position or '',
|
||||
|
|
|
@ -41,6 +41,7 @@ class ItemsField(WidgetField, ItemFieldMixin, ItemWithImageFieldMixin):
|
|||
description = _('Multiple choice list')
|
||||
allow_complex = True
|
||||
allow_statistics = True
|
||||
available_for_filter = True
|
||||
|
||||
items = []
|
||||
min_choices = 0
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue