Compare commits
276 Commits
a3d05511b7
...
e84240387d
Author | SHA1 | Date |
---|---|---|
Serghei Mihai | e84240387d | |
Serghei Mihai | 72beb659bc | |
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 | |
Frédéric Péters | 6de8f10127 | |
Frédéric Péters | 0ed6455a65 | |
Frédéric Péters | 76b94d7ee8 | |
Valentin Deniaud | 09018961dd | |
Frédéric Péters | df546eb981 | |
Frédéric Péters | 0294c31667 | |
Frédéric Péters | 0667660357 | |
Frédéric Péters | bd8d750953 | |
Valentin Deniaud | 6838b2a135 | |
Valentin Deniaud | 17ae2751a2 | |
Valentin Deniaud | fdc8154527 | |
Valentin Deniaud | 2c62dd8196 | |
Valentin Deniaud | 783f3a8bb4 | |
Valentin Deniaud | a11facd293 | |
Valentin Deniaud | 46610bb775 | |
Valentin Deniaud | ea21213f93 | |
Valentin Deniaud | b51d025422 | |
Frédéric Péters | e9a20e4de9 | |
Frédéric Péters | 3c0e04afe7 | |
Frédéric Péters | 2399c72d27 | |
Frédéric Péters | 75030a2bd7 | |
Frédéric Péters | 5bfc33eb62 | |
Lauréline Guérin | 3477ee2f29 | |
Lauréline Guérin | b4c4181cde | |
Lauréline Guérin | eba79fdc77 | |
Lauréline Guérin | 86f28b8037 | |
Lauréline Guérin | 78f2796266 | |
Lauréline Guérin | aa917a59c4 | |
Corentin Sechet | cf0ee0ca29 | |
Frédéric Péters | 7b45d83bd2 | |
Frédéric Péters | 8efea827a1 | |
Frédéric Péters | 68712c8cd0 | |
Frédéric Péters | e30798deb5 | |
Lauréline Guérin | bb73f23502 | |
Frédéric Péters | 1a4fdc71cf | |
Frédéric Péters | d2b95ce0d0 | |
Benjamin Dauvergne | 2d619766b7 | |
Frédéric Péters | 87e3e9aa51 | |
Frédéric Péters | 0d76883638 | |
Frédéric Péters | 783dab9978 | |
Frédéric Péters | 33d243f6e3 | |
Frédéric Péters | 2954998e48 | |
Frédéric Péters | 48593b4e86 | |
Frédéric Péters | f5422ddef0 | |
Frédéric Péters | 96af0663eb | |
Frédéric Péters | f1471ca20c | |
Frédéric Péters | 8598a77b4e | |
Frédéric Péters | a80dc1f54f | |
Frédéric Péters | bff0dc5d83 | |
Frédéric Péters | 8273b31537 | |
Frédéric Péters | 29026b4c72 | |
Frédéric Péters | 9e2743234d | |
Frédéric Péters | e608131d7a | |
Frédéric Péters | 7750954b2f | |
Frédéric Péters | 7b258dfdc6 | |
Frédéric Péters | 372b4ceece | |
Frédéric Péters | ea20e7bcac | |
Frédéric Péters | c77812450b | |
Frédéric Péters | d9c2fecb5d | |
Frédéric Péters | 2e14b82fe5 | |
Frédéric Péters | afc7e799f3 | |
Frédéric Péters | 84e7f29994 | |
Frédéric Péters | d8398e515b | |
Frédéric Péters | 8fc31b0d81 | |
Frédéric Péters | c0b20c8535 | |
Frédéric Péters | 57e4ed63df | |
Frédéric Péters | a1eb55d19e | |
Frédéric Péters | b1604787c3 | |
Frédéric Péters | d984478436 | |
Frédéric Péters | 1e38afbc6f | |
Valentin Deniaud | ad2e64880e | |
Thomas NOËL | 4ea852afe8 | |
Benjamin Dauvergne | 96bfaea4a7 | |
Frédéric Péters | a130f1d862 | |
Frédéric Péters | 1b976a36a7 | |
Frédéric Péters | 4ba3ebf6b4 | |
Frédéric Péters | f6e228b438 | |
Frédéric Péters | 977cdb0019 | |
Frédéric Péters | 99d1f4c21f | |
Frédéric Péters | 89f17153bf | |
Frédéric Péters | c72f9aed9d | |
Valentin Deniaud | 16e844a049 | |
Frédéric Péters | bf442ecf44 | |
Frédéric Péters | 658aff404e | |
Frédéric Péters | 06d6487bb3 | |
Frédéric Péters | eade8f8dda | |
Frédéric Péters | c24bac7837 | |
Frédéric Péters | de292cc399 | |
Frédéric Péters | 23e66ec078 | |
Frédéric Péters | 36e1f16a31 | |
Serghei Mihai | 1476d21ce1 | |
Frédéric Péters | 59d8b91f8d | |
Frédéric Péters | 8c26581924 | |
Valentin Deniaud | 3d42d456d9 | |
Valentin Deniaud | 0c0807a0d4 | |
Valentin Deniaud | 58df7bf7bb | |
Frédéric Péters | 728afe97b6 | |
Frédéric Péters | c03d216a70 | |
Thomas NOËL | 00584bff12 | |
Frédéric Péters | ab38410f34 | |
Frédéric Péters | 2eb29089a1 | |
Valentin Deniaud | f418c515f5 | |
Valentin Deniaud | d784acba07 | |
Valentin Deniaud | d9267a79ca | |
Frédéric Péters | 9cf2aae477 | |
Frédéric Péters | d10392f0fe | |
Valentin Deniaud | 7dfc90a1e5 | |
Frédéric Péters | da6469bde3 | |
Frédéric Péters | 49b2d0d2e4 | |
Frédéric Péters | 71d3b01834 | |
Frédéric Péters | 4e349f0dc5 | |
Frédéric Péters | f296d3dadd | |
Frédéric Péters | f1bead67ee | |
Frédéric Péters | 8b66e281b8 | |
Frédéric Péters | d02b92c4f2 | |
Frédéric Péters | b48214feac | |
Corentin Sechet | 8a7c779d91 | |
Frédéric Péters | 3e6eeff81c | |
Frédéric Péters | 555ae506e5 | |
Frédéric Péters | 445dac2e9b | |
Frédéric Péters | 16d1e680d0 | |
Frédéric Péters | f4e9e7d3ac | |
Frédéric Péters | 3cb981d8e4 | |
Frédéric Péters | 8f5adc758f | |
Nicolas Roche | eaf83221fb | |
Valentin Deniaud | 09d83b2ba6 | |
Valentin Deniaud | b64d76ba83 | |
Valentin Deniaud | 6da43ddcb9 | |
Valentin Deniaud | 8da31255ed | |
Frédéric Péters | 822010b131 | |
Frédéric Péters | f355b9ca02 | |
Frédéric Péters | 389a9bd165 | |
Valentin Deniaud | 104c1c903a |
|
@ -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,
|
||||
|
@ -43,7 +45,8 @@ Depends: graphviz,
|
|||
uwsgi-plugin-python3,
|
||||
${misc:Depends},
|
||||
${python3:Depends},
|
||||
Recommends: libreoffice-writer-nogui | libreoffice-writer,
|
||||
Recommends: graphicsmagick,
|
||||
libreoffice-writer-nogui | libreoffice-writer,
|
||||
poppler-utils,
|
||||
python3-docutils,
|
||||
python3-langdetect,
|
||||
|
|
|
@ -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" : [
|
||||
|
|
|
@ -361,6 +361,16 @@ n’est pas nécessaire de préciser l’identifiant d’un utilisateur.
|
|||
<input>GET https://www.example.net/api/forms/inscriptions/10/?anonymise</input>
|
||||
</screen>
|
||||
|
||||
<p>
|
||||
Par ailleurs, l’API « Liste de formulaires » accepte un paramètre
|
||||
<code>include-anonymised</code> permettant d’inclure (<code>on</code>) ou non
|
||||
(<code>off</code>) les demandes anonymisées dans la liste :
|
||||
</p>
|
||||
|
||||
<screen>
|
||||
<input>GET https://www.example.net/api/forms/inscriptions/list?include-anonymised=on</input>
|
||||
</screen>
|
||||
|
||||
</section>
|
||||
|
||||
<section id="global-data">
|
||||
|
|
2
setup.py
2
setup.py
|
@ -204,6 +204,8 @@ setup(
|
|||
'setproctitle',
|
||||
'phonenumbers',
|
||||
'emoji',
|
||||
'psutil',
|
||||
'freezegun',
|
||||
],
|
||||
package_dir={'wcs': 'wcs'},
|
||||
packages=find_packages(),
|
||||
|
|
|
@ -2,6 +2,7 @@ import os
|
|||
|
||||
import pytest
|
||||
|
||||
from wcs.formdef import FormDef
|
||||
from wcs.qommon.http_request import HTTPRequest
|
||||
from wcs.qommon.ident.password_accounts import PasswordAccount
|
||||
|
||||
|
@ -30,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')
|
||||
|
@ -52,6 +54,7 @@ def teardown_module(module):
|
|||
|
||||
|
||||
def test_empty_site(pub):
|
||||
pub.user_class.wipe()
|
||||
resp = get_app(pub).get('/backoffice/users/')
|
||||
resp = resp.click('New User')
|
||||
resp = get_app(pub).get('/backoffice/settings/')
|
||||
|
@ -86,6 +89,10 @@ def test_admin_for_all(pub):
|
|||
user = create_superuser(pub)
|
||||
role = create_role(pub)
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test'
|
||||
formdef.store()
|
||||
|
||||
try:
|
||||
with open(os.path.join(pub.app_dir, 'ADMIN_FOR_ALL'), 'w'):
|
||||
pass # create empty file
|
||||
|
|
|
@ -220,6 +220,25 @@ def test_block_export_import(pub):
|
|||
assert 'Invalid File (Unknown referenced objects)' in resp
|
||||
assert '<ul><li>Unknown datasources: foobar</li></ul>' in resp
|
||||
|
||||
# python expression
|
||||
if not pub.site_options.has_section('options'):
|
||||
pub.site_options.add_section('options')
|
||||
pub.site_options.set('options', 'forbid-new-python-expressions', 'true')
|
||||
with open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w') as fd:
|
||||
pub.site_options.write(fd)
|
||||
block.fields = [
|
||||
fields.StringField(id='2', label='python_prefill', prefill={'type': 'formula', 'value': '1 + 2'}),
|
||||
]
|
||||
block.store()
|
||||
resp = app.get('/backoffice/forms/blocks/%s/' % block.id)
|
||||
resp = resp.click(href=re.compile('^export$'))
|
||||
xml_export = resp.text
|
||||
resp = app.get('/backoffice/forms/blocks/')
|
||||
resp = resp.click(href='import')
|
||||
resp.form['file'] = Upload('block', xml_export.encode('utf-8'))
|
||||
resp = resp.form.submit()
|
||||
assert 'Python expression detected' in resp
|
||||
|
||||
|
||||
def test_block_delete(pub):
|
||||
create_superuser(pub)
|
||||
|
@ -257,6 +276,55 @@ def test_block_delete(pub):
|
|||
assert 'This block is still used' in resp
|
||||
|
||||
|
||||
def test_block_export_overwrite(pub):
|
||||
create_superuser(pub)
|
||||
BlockDef.wipe()
|
||||
block = BlockDef()
|
||||
block.name = 'foobar'
|
||||
block.fields = [fields.StringField(id='123', required=True, label='Test')]
|
||||
block.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/forms/blocks/%s/' % block.id)
|
||||
resp = resp.click(href=re.compile('^export$'))
|
||||
xml_export = resp.text
|
||||
|
||||
block.slug = 'new-slug'
|
||||
block.name = 'New foobar'
|
||||
block.fields = [fields.StringField(id='123', required=True, label='Test bebore overwrite')]
|
||||
block.store()
|
||||
|
||||
resp = app.get('/backoffice/forms/blocks/%s/' % block.id)
|
||||
resp = resp.click('Overwrite')
|
||||
resp = resp.form.submit('cancel').follow()
|
||||
resp = resp.click('Overwrite')
|
||||
resp = resp.form.submit()
|
||||
assert 'There were errors processing your form.' in resp
|
||||
|
||||
resp.form['file'] = Upload('block', xml_export.encode('utf-8'))
|
||||
resp = resp.form.submit()
|
||||
resp = resp.follow()
|
||||
assert BlockDef.count() == 1
|
||||
|
||||
block.refresh_from_storage()
|
||||
assert block.fields[0].label == 'Test'
|
||||
assert block.name == 'foobar'
|
||||
assert block.slug == 'new-slug' # not overwritten
|
||||
|
||||
# unknown reference
|
||||
block.fields = [fields.StringField(id='1', data_source={'type': 'foobar'})]
|
||||
block.store()
|
||||
resp = app.get('/backoffice/forms/blocks/%s/' % block.id)
|
||||
resp = resp.click(href=re.compile('^export$'))
|
||||
xml_export = resp.text
|
||||
resp = app.get('/backoffice/forms/blocks/%s/' % block.id)
|
||||
resp = resp.click('Overwrite')
|
||||
resp.form['file'] = Upload('block', xml_export.encode('utf-8'))
|
||||
resp = resp.form.submit()
|
||||
assert 'Invalid File (Unknown referenced objects)' in resp
|
||||
assert '<ul><li>Unknown datasources: foobar</li></ul>' in resp
|
||||
|
||||
|
||||
def test_block_edit_duplicate_delete_field(pub):
|
||||
create_superuser(pub)
|
||||
BlockDef.wipe()
|
||||
|
@ -319,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)
|
||||
|
@ -427,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):
|
||||
|
@ -453,14 +528,14 @@ def test_block_edit_field_warnings(pub):
|
|||
blockdef.store()
|
||||
resp = app.get('/backoffice/forms/blocks/%s/' % blockdef.id)
|
||||
assert 'more than 30 fields' not in resp.text
|
||||
assert '<div id="new-field"><h3>New Field</h3>' in resp.text
|
||||
assert resp.pyquery('#new-field')
|
||||
assert resp.pyquery('#fields-list a[title="Duplicate"]').length
|
||||
|
||||
blockdef.fields.extend([fields.StringField(id='%d' % i, label='field %d' % i) for i in range(21, 51)])
|
||||
blockdef.store()
|
||||
resp = app.get('/backoffice/forms/blocks/%s/' % blockdef.id)
|
||||
assert 'This block of fields contains 60 fields.' in resp.text
|
||||
assert '<div id="new-field"><h3>New Field</h3>' not in resp.text
|
||||
assert not resp.pyquery('#new-field')
|
||||
assert not resp.pyquery('#fields-list a[title="Duplicate"]').length
|
||||
|
||||
|
||||
|
@ -574,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'
|
||||
|
|
|
@ -404,16 +404,6 @@ def test_card_id_template(pub):
|
|||
carddata.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/cards/1/')
|
||||
resp = resp.click('Templates')
|
||||
assert 'id_template' not in resp.text
|
||||
|
||||
if not pub.site_options.has_section('options'):
|
||||
pub.site_options.add_section('options')
|
||||
pub.site_options.set('options', 'enable-card-identifier-template', 'true')
|
||||
with open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w') as fd:
|
||||
pub.site_options.write(fd)
|
||||
|
||||
resp = app.get('/backoffice/cards/1/')
|
||||
resp = resp.click('Templates')
|
||||
assert 'Identifier cannot be modified if there are existing cards.' in resp.text
|
||||
|
@ -437,6 +427,20 @@ def test_card_id_template(pub):
|
|||
carddata.store()
|
||||
assert carddata.id_display == 'XbarY'
|
||||
|
||||
# check option is not advertised if disabled
|
||||
if not pub.site_options.has_section('options'):
|
||||
pub.site_options.add_section('options')
|
||||
pub.site_options.set('options', 'enable-card-identifier-template', 'false')
|
||||
with open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w') as fd:
|
||||
pub.site_options.write(fd)
|
||||
resp = app.get('/backoffice/cards/1/')
|
||||
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)
|
||||
|
@ -1035,7 +1039,7 @@ def test_card_edit_field_warnings(pub):
|
|||
resp = app.get('/backoffice/cards/%s/fields/' % carddef.id)
|
||||
assert 'more than 200 fields' not in resp.text
|
||||
assert 'first field should be of type "page"' in resp.text
|
||||
assert '<div id="new-field"><h3>New Field</h3>' in resp.text
|
||||
assert resp.pyquery('#new-field')
|
||||
|
||||
carddef.fields.extend([fields.StringField(id='%d' % i, label='field %d' % i) for i in range(10, 210)])
|
||||
carddef.store()
|
||||
|
@ -1049,7 +1053,7 @@ def test_card_edit_field_warnings(pub):
|
|||
resp = app.get('/backoffice/cards/%s/fields/' % carddef.id)
|
||||
assert 'This card model contains 410 fields.' in resp.text
|
||||
assert 'first field should be of type "page"' in resp.text
|
||||
assert '<div id="new-field"><h3>New Field</h3>' not in resp.text
|
||||
assert not resp.pyquery('#new-field')
|
||||
assert '>Duplicate<' not in resp.text
|
||||
|
||||
|
||||
|
@ -1118,3 +1122,61 @@ 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'
|
||||
|
|
|
@ -1039,6 +1039,23 @@ def test_data_sources_import(pub):
|
|||
resp = resp.form.submit()
|
||||
assert 'Invalid File' in resp.text
|
||||
|
||||
# python expression
|
||||
if not pub.site_options.has_section('options'):
|
||||
pub.site_options.add_section('options')
|
||||
pub.site_options.set('options', 'forbid-new-python-expressions', 'true')
|
||||
with open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w') as fd:
|
||||
pub.site_options.write(fd)
|
||||
data_source.data_source = {'type': 'formula', 'value': repr([('1', 'un'), ('2', 'deux')])}
|
||||
data_source.store()
|
||||
resp = app.get('/backoffice/settings/data-sources/%s/' % data_source.id)
|
||||
resp = resp.click(href=re.compile('^export$'))
|
||||
xml_export = resp.text
|
||||
resp = app.get('/backoffice/settings/data-sources/')
|
||||
resp = resp.click(href='import')
|
||||
resp.form['file'] = Upload('ds', xml_export.encode('utf-8'))
|
||||
resp = resp.form.submit()
|
||||
assert 'Python expression detected' in resp
|
||||
|
||||
|
||||
def test_data_sources_edit_slug(pub):
|
||||
create_superuser(pub)
|
||||
|
|
|
@ -2,16 +2,17 @@ import io
|
|||
import json
|
||||
import os
|
||||
import zipfile
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
from quixote.http_request import Upload as QuixoteUpload
|
||||
|
||||
from wcs import fields
|
||||
from wcs.backoffice.deprecations import DeprecationsScanAfterJob
|
||||
from wcs.blocks import BlockDef
|
||||
from wcs.backoffice.deprecations import DeprecatedElementsDetected, DeprecationsScan
|
||||
from wcs.blocks import BlockDef, BlockdefImportError
|
||||
from wcs.carddef import CardDef
|
||||
from wcs.data_sources import NamedDataSource
|
||||
from wcs.formdef import FormDef
|
||||
from wcs.data_sources import NamedDataSource, NamedDataSourceImportError
|
||||
from wcs.formdef import FormDef, FormdefImportError
|
||||
from wcs.mail_templates import MailTemplate
|
||||
from wcs.qommon.form import UploadedFile
|
||||
from wcs.qommon.http_request import HTTPRequest
|
||||
|
@ -23,8 +24,13 @@ 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
|
||||
from wcs.wscalls import NamedWsCall
|
||||
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
|
||||
from .test_all import create_superuser
|
||||
|
@ -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')
|
||||
|
@ -183,6 +193,9 @@ def test_deprecations(pub):
|
|||
data_source = NamedDataSource(name='ds_jsonp')
|
||||
data_source.data_source = {'type': 'jsonp', 'value': 'xxx'}
|
||||
data_source.store()
|
||||
data_source = NamedDataSource(name='ds_csv')
|
||||
data_source.data_source = {'type': 'json', 'value': 'http://example.net/csvdatasource/plop/test'}
|
||||
data_source.store()
|
||||
|
||||
NamedWsCall.wipe()
|
||||
wscall = NamedWsCall()
|
||||
|
@ -190,6 +203,16 @@ def test_deprecations(pub):
|
|||
wscall.request = {'url': 'http://example.net', 'qs_data': {'a': '=1+2'}}
|
||||
wscall.store()
|
||||
|
||||
wscall = NamedWsCall()
|
||||
wscall.name = 'Hello CSV'
|
||||
wscall.request = {'url': 'http://example.net/csvdatasource/plop/test'}
|
||||
wscall.store()
|
||||
|
||||
wscall = NamedWsCall()
|
||||
wscall.name = 'Hello json data store'
|
||||
wscall.request = {'url': 'http://example.net/jsondatastore/plop'}
|
||||
wscall.store()
|
||||
|
||||
MailTemplate.wipe()
|
||||
mail_template1 = MailTemplate()
|
||||
mail_template1.name = 'Hello1'
|
||||
|
@ -253,11 +276,19 @@ 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')] == [
|
||||
'test / Daily Summary Email',
|
||||
]
|
||||
assert [x.text for x in resp.pyquery('.section--csv-connector li a')] == [
|
||||
'Data source "ds_csv"',
|
||||
'Webservice "Hello CSV"',
|
||||
]
|
||||
assert [x.text for x in resp.pyquery('.section--json-data-store li a')] == [
|
||||
'Webservice "Hello json data store"',
|
||||
]
|
||||
# check all links are ok
|
||||
for link in resp.pyquery('.section li a'):
|
||||
resp.click(href=link.attrib['href'], index=0)
|
||||
|
@ -273,7 +304,7 @@ def test_deprecations_choice_label(pub):
|
|||
accept = st0.add_action('choice', id='_choice')
|
||||
accept.label = '[test] action'
|
||||
|
||||
job = DeprecationsScanAfterJob()
|
||||
job = DeprecationsScan()
|
||||
job.execute()
|
||||
assert not job.report_lines
|
||||
|
||||
|
@ -285,7 +316,7 @@ def test_deprecations_skip_invalid_ezt(pub):
|
|||
display = st0.add_action('displaymsg')
|
||||
display.message = 'message with invalid [if-any] ezt'
|
||||
|
||||
job = DeprecationsScanAfterJob()
|
||||
job = DeprecationsScan()
|
||||
job.execute()
|
||||
assert not job.report_lines
|
||||
|
||||
|
@ -296,19 +327,19 @@ def test_deprecations_ignore_ezt_looking_tag(pub):
|
|||
sendmail = st0.add_action('sendmail')
|
||||
sendmail.subject = '[REMINDER] your appointment'
|
||||
workflow.store()
|
||||
job = DeprecationsScanAfterJob()
|
||||
job = DeprecationsScan()
|
||||
job.execute()
|
||||
assert not job.report_lines
|
||||
|
||||
sendmail.subject = '[reminder]'
|
||||
workflow.store()
|
||||
job = DeprecationsScanAfterJob()
|
||||
job = DeprecationsScan()
|
||||
job.execute()
|
||||
assert job.report_lines
|
||||
|
||||
sendmail.subject = '[if-any plop]test[end]'
|
||||
workflow.store()
|
||||
job = DeprecationsScanAfterJob()
|
||||
job = DeprecationsScan()
|
||||
job.execute()
|
||||
assert job.report_lines
|
||||
|
||||
|
@ -377,7 +408,7 @@ def test_deprecations_document_models(pub):
|
|||
export_to2.by = ['_submitter']
|
||||
workflow.store()
|
||||
|
||||
job = DeprecationsScanAfterJob()
|
||||
job = DeprecationsScan()
|
||||
job.execute()
|
||||
assert job.report_lines == [
|
||||
{
|
||||
|
@ -426,7 +457,7 @@ def test_deprecations_inspect_pages(pub):
|
|||
display.message = 'message with [ezt] info'
|
||||
workflow.store()
|
||||
|
||||
job = DeprecationsScanAfterJob()
|
||||
job = DeprecationsScan()
|
||||
job.execute()
|
||||
|
||||
create_superuser(pub)
|
||||
|
@ -465,7 +496,7 @@ def test_deprecations_inspect_pages(pub):
|
|||
display.message = 'message with {{django}} info'
|
||||
workflow.store()
|
||||
|
||||
job = DeprecationsScanAfterJob()
|
||||
job = DeprecationsScan()
|
||||
job.execute()
|
||||
|
||||
resp = app.get(formdef.get_admin_url() + 'inspect')
|
||||
|
@ -486,7 +517,7 @@ def test_deprecations_inspect_pages_old_format(pub):
|
|||
]
|
||||
formdef.store()
|
||||
|
||||
job = DeprecationsScanAfterJob()
|
||||
job = DeprecationsScan()
|
||||
job.execute()
|
||||
|
||||
with open(os.path.join(pub.app_dir, 'deprecations.json')) as f:
|
||||
|
@ -505,3 +536,131 @@ def test_deprecations_inspect_pages_old_format(pub):
|
|||
|
||||
resp = app.get('/backoffice/studio/deprecations/')
|
||||
assert resp.pyquery('.section--python-condition li a')
|
||||
|
||||
|
||||
def test_deprecations_on_import(pub):
|
||||
formdef = FormDef()
|
||||
formdef.name = 'foobar'
|
||||
formdef.fields = [
|
||||
fields.PageField(id='1', label='page1', condition={'type': 'python', 'value': 'True'}),
|
||||
]
|
||||
formdef.store()
|
||||
|
||||
blockdef = BlockDef()
|
||||
blockdef.name = 'foobar'
|
||||
blockdef.fields = [
|
||||
fields.StringField(id='2', label='python_prefill', prefill={'type': 'formula', 'value': '1 + 2'}),
|
||||
]
|
||||
blockdef.store()
|
||||
|
||||
workflow = Workflow(name='test')
|
||||
st0 = workflow.add_status('Status0', 'st0')
|
||||
sendsms = st0.add_action('sendsms', id='_sendsms')
|
||||
sendsms.to = 'xxx'
|
||||
sendsms.condition = {'type': 'python', 'value': 'True'}
|
||||
sendsms.parent = st0
|
||||
st0.items.append(sendsms)
|
||||
workflow.store()
|
||||
|
||||
data_source = NamedDataSource(name='ds_python')
|
||||
data_source.data_source = {'type': 'formula', 'value': repr([('1', 'un'), ('2', 'deux')])}
|
||||
data_source.store()
|
||||
|
||||
wscall = NamedWsCall()
|
||||
wscall.name = 'Hello'
|
||||
wscall.request = {'url': 'http://example.net', 'qs_data': {'a': '=1+2'}}
|
||||
wscall.store()
|
||||
|
||||
mail_template = MailTemplate() # no python expression in mail templates
|
||||
mail_template.name = 'Hello2'
|
||||
mail_template.subject = 'plop'
|
||||
mail_template.body = 'plop [ezt] plop'
|
||||
mail_template.store()
|
||||
|
||||
job = DeprecationsScan()
|
||||
job.check_deprecated_elements_in_object(formdef)
|
||||
formdef_xml = formdef.export_to_xml()
|
||||
FormDef.import_from_xml_tree(formdef_xml)
|
||||
|
||||
job = DeprecationsScan()
|
||||
job.check_deprecated_elements_in_object(blockdef)
|
||||
blockdef_xml = blockdef.export_to_xml()
|
||||
BlockDef.import_from_xml_tree(blockdef_xml)
|
||||
|
||||
job = DeprecationsScan()
|
||||
job.check_deprecated_elements_in_object(workflow)
|
||||
workflow_xml = workflow.export_to_xml()
|
||||
Workflow.import_from_xml_tree(workflow_xml)
|
||||
|
||||
job = DeprecationsScan()
|
||||
job.check_deprecated_elements_in_object(data_source)
|
||||
data_source_xml = data_source.export_to_xml()
|
||||
NamedDataSource.import_from_xml_tree(data_source_xml)
|
||||
|
||||
job = DeprecationsScan()
|
||||
job.check_deprecated_elements_in_object(wscall)
|
||||
wscall_xml = wscall.export_to_xml()
|
||||
NamedWsCall.import_from_xml_tree(wscall_xml)
|
||||
|
||||
job = DeprecationsScan()
|
||||
job.check_deprecated_elements_in_object(mail_template)
|
||||
mail_template_xml = mail_template.export_to_xml()
|
||||
MailTemplate.import_from_xml_tree(mail_template_xml)
|
||||
|
||||
if not pub.site_options.has_section('options'):
|
||||
pub.site_options.add_section('options')
|
||||
pub.site_options.set('options', 'forbid-new-python-expressions', 'true')
|
||||
with open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w') as fd:
|
||||
pub.site_options.write(fd)
|
||||
|
||||
job = DeprecationsScan()
|
||||
with pytest.raises(DeprecatedElementsDetected) as excinfo:
|
||||
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, check_deprecated=True)
|
||||
assert str(excinfo.value) == 'Python expression detected'
|
||||
|
||||
job = DeprecationsScan()
|
||||
with pytest.raises(DeprecatedElementsDetected) as excinfo:
|
||||
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, check_deprecated=True)
|
||||
assert str(excinfo.value) == 'Python expression detected'
|
||||
|
||||
job = DeprecationsScan()
|
||||
with pytest.raises(DeprecatedElementsDetected) as excinfo:
|
||||
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, check_deprecated=True)
|
||||
assert str(excinfo.value) == 'Python expression detected'
|
||||
|
||||
job = DeprecationsScan()
|
||||
with pytest.raises(DeprecatedElementsDetected) as excinfo:
|
||||
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, check_deprecated=True)
|
||||
assert str(excinfo.value) == 'Python expression detected'
|
||||
|
||||
job = DeprecationsScan()
|
||||
with pytest.raises(DeprecatedElementsDetected) as excinfo:
|
||||
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, 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
|
||||
|
||||
|
@ -242,13 +243,34 @@ def test_forms_edit_management(pub, formdef):
|
|||
# Misc management
|
||||
assert_option_display(resp, 'Management', 'Default')
|
||||
resp = resp.click('Management', href='options/management')
|
||||
assert resp.forms[0]['include_download_all_button'].checked is False
|
||||
resp.forms[0]['include_download_all_button'].checked = True
|
||||
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()
|
||||
assert resp.location == 'http://example.net/backoffice/forms/1/'
|
||||
resp = resp.follow()
|
||||
assert_option_display(resp, 'Management', 'Custom')
|
||||
assert FormDef.get(1).include_download_all_button is True
|
||||
assert 'general' in FormDef.get(1).management_sidebar_items
|
||||
assert 'download-files' in FormDef.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 FormDef.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
|
||||
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):
|
||||
|
@ -271,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')
|
||||
|
@ -284,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'),
|
||||
|
@ -510,6 +547,34 @@ def test_forms_edit_publication_date(pub):
|
|||
assert 'invalid value' in resp
|
||||
|
||||
|
||||
def test_forms_list_publication_date(pub):
|
||||
create_superuser(pub)
|
||||
create_role(pub)
|
||||
|
||||
FormDef.wipe()
|
||||
formdef = FormDef()
|
||||
formdef.name = 'form title'
|
||||
formdef.fields = []
|
||||
formdef.publication_date = '2024-03-06 00:00'
|
||||
formdef.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/forms/')
|
||||
assert resp.pyquery('.publication-dates').text() == 'Published from 2024-03-06 00:00'
|
||||
|
||||
formdef.expiration_date = '2024-03-10 00:00'
|
||||
formdef.store()
|
||||
resp = app.get('/backoffice/forms/')
|
||||
assert (
|
||||
resp.pyquery('.publication-dates').text() == 'Published from 2024-03-06 00:00 until 2024-03-10 00:00'
|
||||
)
|
||||
|
||||
formdef.publication_date = None
|
||||
formdef.store()
|
||||
resp = app.get('/backoffice/forms/')
|
||||
assert resp.pyquery('.publication-dates').text() == 'Published until 2024-03-10 00:00'
|
||||
|
||||
|
||||
def test_form_category(pub):
|
||||
create_superuser(pub)
|
||||
create_role(pub)
|
||||
|
@ -765,7 +830,7 @@ def test_form_workflow_link(pub):
|
|||
|
||||
def test_form_workflow_remapping(pub):
|
||||
AfterJob.wipe()
|
||||
create_superuser(pub)
|
||||
user = create_superuser(pub)
|
||||
create_role(pub)
|
||||
|
||||
FormDef.wipe()
|
||||
|
@ -872,6 +937,8 @@ def test_form_workflow_remapping(pub):
|
|||
resp = resp.follow() # -> to job processing page
|
||||
resp = resp.click('Back')
|
||||
assert resp.pyquery('[href="workflow"] .offset').text() == 'Workflow Three'
|
||||
assert pub.snapshot_class.select_object_history(formdef)[0].comment == 'Workflow change'
|
||||
assert pub.snapshot_class.select_object_history(formdef)[0].user_id == str(user.id)
|
||||
|
||||
# run a SQL SELECT and we known all columns are defined.
|
||||
FormDef.get(formdef.id).data_class().select()
|
||||
|
@ -1075,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)
|
||||
|
@ -1921,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):
|
||||
|
@ -3600,7 +3661,7 @@ def test_form_edit_field_warnings(pub):
|
|||
resp = app.get('/backoffice/forms/%s/fields/' % formdef.id)
|
||||
assert 'more than 200 fields' not in resp.text
|
||||
assert 'first field should be of type "page"' in resp.text
|
||||
assert '<div id="new-field"><h3>New Field</h3>' in resp.text
|
||||
assert resp.pyquery('#new-field')
|
||||
|
||||
formdef.fields.extend([fields.StringField(id='%d' % i, label='field %d' % i) for i in range(10, 210)])
|
||||
formdef.store()
|
||||
|
@ -3615,7 +3676,7 @@ def test_form_edit_field_warnings(pub):
|
|||
assert 'This form contains 410 fields.' in resp.text
|
||||
assert 'no new fields can be added.' in resp.text
|
||||
assert 'first field should be of type "page"' in resp.text
|
||||
assert '<div id="new-field"><h3>New Field</h3>' not in resp.text
|
||||
assert not resp.pyquery('#new-field')
|
||||
assert '>Duplicate<' not in resp.text
|
||||
assert resp.pyquery('aside .errornotice')
|
||||
assert not resp.pyquery('aside form[action=new]')
|
||||
|
@ -3628,11 +3689,46 @@ def test_form_edit_field_warnings(pub):
|
|||
|
||||
resp = app.get('/backoffice/forms/%s/fields/' % formdef.id)
|
||||
assert 'no new fields should be added.' in resp.text
|
||||
assert '<div id="new-field"><h3>New Field</h3>' in resp.text
|
||||
assert resp.pyquery('#new-field')
|
||||
assert '>Duplicate<' in resp.text
|
||||
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()
|
||||
|
||||
|
||||
|
@ -4034,6 +4130,39 @@ def test_form_overwrite(pub):
|
|||
assert resp.pyquery('.error').text() == 'Invalid File'
|
||||
|
||||
|
||||
def test_form_export_import_export(pub):
|
||||
create_superuser(pub)
|
||||
create_role(pub)
|
||||
|
||||
FormDef.wipe()
|
||||
formdef = FormDef()
|
||||
formdef.name = 'form title'
|
||||
formdef.table_name = 'xxx'
|
||||
formdef.fields = []
|
||||
formdef.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
|
||||
# python expression
|
||||
if not pub.site_options.has_section('options'):
|
||||
pub.site_options.add_section('options')
|
||||
pub.site_options.set('options', 'forbid-new-python-expressions', 'true')
|
||||
with open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w') as fd:
|
||||
pub.site_options.write(fd)
|
||||
formdef.fields = [
|
||||
fields.StringField(id='2', label='python_prefill', prefill={'type': 'formula', 'value': '1 + 2'}),
|
||||
]
|
||||
formdef.store()
|
||||
resp = app.get('/backoffice/forms/%s/' % formdef.id)
|
||||
resp = resp.click(href=re.compile('^export$'))
|
||||
xml_export = resp.text
|
||||
resp = app.get('/backoffice/forms/')
|
||||
resp = resp.click(href='import')
|
||||
resp.form['file'] = Upload('formdef', xml_export.encode('utf-8'))
|
||||
resp = resp.form.submit()
|
||||
assert 'Python expression detected' in resp
|
||||
|
||||
|
||||
def test_form_export_import_export_overwrite(pub):
|
||||
create_superuser(pub)
|
||||
create_role(pub)
|
||||
|
@ -4091,6 +4220,23 @@ def test_form_export_import_export_overwrite(pub):
|
|||
field_ow = formdef_overwrited.fields[i]
|
||||
assert (field.id, field.label, field.key) == (field_ow.id, field_ow.label, field_ow.key)
|
||||
|
||||
# python expression
|
||||
if not pub.site_options.has_section('options'):
|
||||
pub.site_options.add_section('options')
|
||||
pub.site_options.set('options', 'forbid-new-python-expressions', 'true')
|
||||
with open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w') as fd:
|
||||
pub.site_options.write(fd)
|
||||
formdef2.fields = [
|
||||
fields.StringField(id='2', label='python_prefill', prefill={'type': 'formula', 'value': '1 + 2'}),
|
||||
]
|
||||
formdef2.store()
|
||||
formdef2_xml = ET.tostring(formdef2.export_to_xml(include_id=True))
|
||||
resp = app.get('/backoffice/forms/%s/' % formdef.id)
|
||||
resp = resp.click(href='overwrite')
|
||||
resp.forms[0]['file'] = Upload('formdef.wcs', formdef2_xml)
|
||||
resp = resp.forms[0].submit()
|
||||
assert 'Python expression detected' in resp
|
||||
|
||||
|
||||
def test_form_overwrite_from_url(pub):
|
||||
create_superuser(pub)
|
||||
|
@ -4659,6 +4805,21 @@ def test_admin_form_inspect(pub):
|
|||
# check field links targets per-page URL
|
||||
assert '/pages/' not in resp.pyquery('.inspect-field h4 a')[0].attrib['href']
|
||||
|
||||
# check drafts lifespan value
|
||||
assert [
|
||||
PyQuery(x).parent().text()
|
||||
for x in resp.pyquery('.parameter')
|
||||
if x.text == 'Lifespan of drafts (in days):'
|
||||
] == ['Lifespan of drafts (in days): 100']
|
||||
formdef.drafts_lifespan = '40'
|
||||
formdef.store()
|
||||
resp = app.get('/backoffice/forms/%s/inspect' % formdef.id)
|
||||
assert [
|
||||
PyQuery(x).parent().text()
|
||||
for x in resp.pyquery('.parameter')
|
||||
if x.text == 'Lifespan of drafts (in days):'
|
||||
] == ['Lifespan of drafts (in days): 40']
|
||||
|
||||
|
||||
def test_admin_form_inspect_validation(pub):
|
||||
create_superuser(pub)
|
||||
|
@ -4693,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)
|
||||
|
@ -4868,3 +5158,24 @@ 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.'
|
||||
|
|
|
@ -7,7 +7,7 @@ from webtest import Upload
|
|||
from wcs.blocks import BlockDef
|
||||
from wcs.carddef import CardDef
|
||||
from wcs.categories import Category
|
||||
from wcs.fields import ItemField, StringField
|
||||
from wcs.fields import ItemField, PageField, StringField
|
||||
from wcs.formdef import FormDef
|
||||
from wcs.i18n import TranslatableMessage
|
||||
from wcs.mail_templates import MailTemplate
|
||||
|
@ -81,6 +81,13 @@ def test_i18n_page(pub):
|
|||
formdef = FormDef()
|
||||
formdef.name = 'test title'
|
||||
formdef.fields = [
|
||||
PageField(
|
||||
id='0',
|
||||
label='page field',
|
||||
post_conditions=[
|
||||
{'condition': {'type': 'django', 'value': 'blah'}, 'error_message': 'page error message'},
|
||||
],
|
||||
),
|
||||
StringField(id='1', label='text field'),
|
||||
StringField(
|
||||
id='2',
|
||||
|
@ -125,6 +132,9 @@ def test_i18n_page(pub):
|
|||
# check 'text field' only appears one
|
||||
assert TranslatableMessage.count([Equal('string', 'text field')]) == 1
|
||||
|
||||
# check page post condition
|
||||
assert TranslatableMessage.count([Equal('string', 'page error message')]) == 1
|
||||
|
||||
# check global action name appears only if there's a manual trigger
|
||||
assert TranslatableMessage.count([Equal('string', 'Global Manual')]) == 1
|
||||
assert TranslatableMessage.count([Equal('string', 'Global No Trigger')]) == 0
|
||||
|
@ -153,7 +163,7 @@ def test_i18n_page(pub):
|
|||
# check filtering on a formdef/carddef outputs related workflow strings
|
||||
resp.form['formdef'] = 'forms/1'
|
||||
resp = resp.form.submit()
|
||||
assert resp.pyquery('tr').length == 12
|
||||
assert resp.pyquery('tr').length == 14
|
||||
assert 'test title' in {x.text for x in resp.pyquery('tr td:first-child')}
|
||||
assert 'Global Manual' in {x.text for x in resp.pyquery('tr td:first-child')}
|
||||
assert 'second workflow' not in {x.text for x in resp.pyquery('tr td:first-child')}
|
||||
|
|
|
@ -390,3 +390,66 @@ def test_logged_error_cleanup(pub):
|
|||
'types$elementcarddef',
|
||||
'types$elementothers',
|
||||
]
|
||||
|
||||
|
||||
def test_logged_error_cleanup_from_filtered_page(pub):
|
||||
create_superuser(pub)
|
||||
|
||||
FormDef.wipe()
|
||||
CardDef.wipe()
|
||||
Workflow.wipe()
|
||||
pub.loggederror_class.wipe()
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'foo'
|
||||
formdef.store()
|
||||
carddef = CardDef()
|
||||
carddef.name = 'bar'
|
||||
carddef.store()
|
||||
workflow = Workflow()
|
||||
workflow.name = 'blah'
|
||||
workflow.store()
|
||||
|
||||
# FormDef error
|
||||
error1 = pub.loggederror_class()
|
||||
error1.summary = 'LoggedError'
|
||||
error1.formdef_class = 'FormDef'
|
||||
error1.formdef_id = formdef.id
|
||||
error1.first_occurence_timestamp = error1.latest_occurence_timestamp = datetime.datetime.now()
|
||||
error1.store()
|
||||
|
||||
# CardDef error
|
||||
error2 = pub.loggederror_class()
|
||||
error2.summary = 'LoggedError'
|
||||
error2.formdef_class = 'CardDef'
|
||||
error2.formdef_id = carddef.id
|
||||
error2.first_occurence_timestamp = error2.latest_occurence_timestamp = datetime.datetime.now()
|
||||
error2.store()
|
||||
|
||||
# workflow-only error
|
||||
error3 = pub.loggederror_class()
|
||||
error3.summary = 'LoggedError'
|
||||
error3.workflow_id = workflow.id
|
||||
error3.first_occurence_timestamp = error3.latest_occurence_timestamp = datetime.datetime.now()
|
||||
error3.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get(formdef.get_admin_url() + 'logged-errors/')
|
||||
resp = resp.click('Cleanup')
|
||||
resp.form['latest_occurence'] = (datetime.datetime.now() + datetime.timedelta(days=1)).strftime(
|
||||
'%Y-%m-%d'
|
||||
)
|
||||
resp = resp.form.submit('submit')
|
||||
assert not pub.loggederror_class.has_key(error1.id)
|
||||
assert pub.loggederror_class.has_key(error2.id)
|
||||
assert pub.loggederror_class.has_key(error3.id)
|
||||
|
||||
resp = app.get(workflow.get_admin_url() + 'logged-errors/')
|
||||
resp = resp.click('Cleanup')
|
||||
resp.form['latest_occurence'] = (datetime.datetime.now() + datetime.timedelta(days=1)).strftime(
|
||||
'%Y-%m-%d'
|
||||
)
|
||||
resp = resp.form.submit('submit')
|
||||
assert not pub.loggederror_class.has_key(error1.id)
|
||||
assert pub.loggederror_class.has_key(error2.id)
|
||||
assert not pub.loggederror_class.has_key(error3.id)
|
||||
|
|
|
@ -399,6 +399,93 @@ def test_settings_export_import(pub):
|
|||
resp.form['file'] = Upload('export.wcs', zip_content.getvalue())
|
||||
resp = resp.form.submit('submit').follow()
|
||||
assert 'Unknown referenced objects [Unknown datasources: foobar]' in resp
|
||||
BlockDef.wipe()
|
||||
|
||||
# python expressions
|
||||
if not pub.site_options.has_section('options'):
|
||||
pub.site_options.add_section('options')
|
||||
pub.site_options.set('options', 'forbid-new-python-expressions', 'true')
|
||||
with open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w') as fd:
|
||||
pub.site_options.write(fd)
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'foobar'
|
||||
formdef.fields = [
|
||||
fields.PageField(id='1', label='page1', condition={'type': 'python', 'value': 'True'}),
|
||||
]
|
||||
formdef.store()
|
||||
resp = app.get('/backoffice/settings/export')
|
||||
resp = resp.form.submit('submit').follow()
|
||||
resp = resp.click('Download Export')
|
||||
zip_content = io.BytesIO(resp.body)
|
||||
resp = app.get('/backoffice/settings/import')
|
||||
resp.form['file'] = Upload('export.wcs', zip_content.getvalue())
|
||||
resp.form['confirm'].checked = True
|
||||
resp = resp.form.submit('submit').follow()
|
||||
assert 'Python expression detected' in resp
|
||||
FormDef.wipe()
|
||||
|
||||
blockdef = BlockDef()
|
||||
blockdef.name = 'foobar'
|
||||
blockdef.fields = [
|
||||
fields.StringField(id='2', label='python_prefill', prefill={'type': 'formula', 'value': '1 + 2'}),
|
||||
]
|
||||
blockdef.store()
|
||||
resp = app.get('/backoffice/settings/export')
|
||||
resp = resp.form.submit('submit').follow()
|
||||
resp = resp.click('Download Export')
|
||||
zip_content = io.BytesIO(resp.body)
|
||||
resp = app.get('/backoffice/settings/import')
|
||||
resp.form['file'] = Upload('export.wcs', zip_content.getvalue())
|
||||
resp = resp.form.submit('submit').follow()
|
||||
assert 'Python expression detected' in resp
|
||||
BlockDef.wipe()
|
||||
|
||||
workflow = Workflow(name='test')
|
||||
st0 = workflow.add_status('Status0', 'st0')
|
||||
sendsms = st0.add_action('sendsms', id='_sendsms')
|
||||
sendsms.to = 'xxx'
|
||||
sendsms.condition = {'type': 'python', 'value': 'True'}
|
||||
sendsms.parent = st0
|
||||
st0.items.append(sendsms)
|
||||
workflow.store()
|
||||
resp = app.get('/backoffice/settings/export')
|
||||
resp = resp.form.submit('submit').follow()
|
||||
resp = resp.click('Download Export')
|
||||
zip_content = io.BytesIO(resp.body)
|
||||
resp = app.get('/backoffice/settings/import')
|
||||
resp.form['file'] = Upload('export.wcs', zip_content.getvalue())
|
||||
resp.form['confirm'].checked = True
|
||||
resp = resp.form.submit('submit').follow()
|
||||
assert 'Python expression detected' in resp
|
||||
Workflow.wipe()
|
||||
|
||||
data_source = NamedDataSource(name='ds_python')
|
||||
data_source.data_source = {'type': 'formula', 'value': repr([('1', 'un'), ('2', 'deux')])}
|
||||
data_source.store()
|
||||
resp = app.get('/backoffice/settings/export')
|
||||
resp = resp.form.submit('submit').follow()
|
||||
resp = resp.click('Download Export')
|
||||
zip_content = io.BytesIO(resp.body)
|
||||
resp = app.get('/backoffice/settings/import')
|
||||
resp.form['file'] = Upload('export.wcs', zip_content.getvalue())
|
||||
resp = resp.form.submit('submit').follow()
|
||||
assert 'Python expression detected' in resp
|
||||
NamedDataSource.wipe()
|
||||
|
||||
wscall = NamedWsCall()
|
||||
wscall.name = 'Hello'
|
||||
wscall.request = {'url': 'http://example.net', 'qs_data': {'a': '=1+2'}}
|
||||
wscall.store()
|
||||
resp = app.get('/backoffice/settings/export')
|
||||
resp = resp.form.submit('submit').follow()
|
||||
resp = resp.click('Download Export')
|
||||
zip_content = io.BytesIO(resp.body)
|
||||
resp = app.get('/backoffice/settings/import')
|
||||
resp.form['file'] = Upload('export.wcs', zip_content.getvalue())
|
||||
resp = resp.form.submit('submit').follow()
|
||||
assert 'Python expression detected' in resp
|
||||
NamedWsCall.wipe()
|
||||
|
||||
# check a backup of settings has been created
|
||||
assert [x for x in os.listdir(pub.app_dir) if x.startswith('config.pck.backup-')]
|
||||
|
@ -1084,17 +1171,31 @@ def test_i18n(pub):
|
|||
def test_submission_channels(pub):
|
||||
create_superuser(pub)
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/settings/submission-channels')
|
||||
resp = app.get('/backoffice/settings/backoffice-submission')
|
||||
resp.form['include-in-global-listing'].checked = True
|
||||
resp = resp.form.submit('submit')
|
||||
|
||||
pub.reload_cfg()
|
||||
assert pub.cfg['submission-channels']['include-in-global-listing'] is True
|
||||
|
||||
resp = app.get('/backoffice/settings/submission-channels')
|
||||
resp = app.get('/backoffice/settings/backoffice-submission')
|
||||
assert resp.form['include-in-global-listing'].checked
|
||||
|
||||
|
||||
def test_backoffice_submission(pub):
|
||||
create_superuser(pub)
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/settings/backoffice-submission')
|
||||
resp.form['redirect'] = 'https://example.net'
|
||||
resp = resp.form.submit('submit')
|
||||
|
||||
pub.reload_cfg()
|
||||
assert pub.cfg['backoffice-submission']['redirect'] == 'https://example.net'
|
||||
|
||||
resp = app.get('/backoffice/settings/backoffice-submission')
|
||||
assert resp.form['redirect'].value == 'https://example.net'
|
||||
|
||||
|
||||
def test_hobo_locked_settings(pub):
|
||||
create_superuser(pub)
|
||||
app = login(get_app(pub))
|
||||
|
|
|
@ -7,11 +7,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 +32,7 @@ def pub():
|
|||
pub.cfg['identification'] = {'methods': ['password']}
|
||||
pub.write_cfg()
|
||||
|
||||
pub.user_class.wipe()
|
||||
FormDef.wipe()
|
||||
TestDef.wipe()
|
||||
TestResult.wipe()
|
||||
|
@ -43,7 +46,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 +64,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
|
||||
|
@ -145,9 +152,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 +177,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
|
||||
|
||||
|
||||
|
@ -370,6 +381,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 +407,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 +419,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 +714,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 +736,13 @@ 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_manual_run(pub):
|
||||
|
@ -814,6 +836,10 @@ def test_tests_manual_run(pub):
|
|||
assert 'You should enter digits only, for example: 123.' in resp.text
|
||||
assert 'disabled' not in resp.text
|
||||
|
||||
resp = resp.click('Run tests again')
|
||||
resp = app.get('/backoffice/forms/1/tests/results/')
|
||||
assert len(resp.pyquery('tr')) == 4
|
||||
|
||||
TestDef.remove_object(testdef.id)
|
||||
resp = app.get('/backoffice/forms/1/tests/results/%s/' % result.id)
|
||||
assert 'disabled' in resp.text
|
||||
|
@ -950,7 +976,8 @@ def test_tests_result_sent_requests(pub, http_requests):
|
|||
assert 'Sent requests:' in resp.text
|
||||
assert 'POST http://remote.example.net/json' in resp.text
|
||||
assert 'Request was blocked since it is not a GET request.' in resp.text
|
||||
assert 'Recorded errors:' not in resp.text
|
||||
assert 'Recorded errors:' in resp.text
|
||||
assert 'error in HTTP request to remote.example.net (method must be GET)' in resp.text
|
||||
|
||||
resp = resp.click('You can create corresponding webservice response here.')
|
||||
assert 'Webservice responses' in resp.text
|
||||
|
@ -1001,12 +1028,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)
|
||||
|
@ -1023,6 +1054,11 @@ def test_tests_result_inspect(pub):
|
|||
jump.status = new_status.id
|
||||
jump.by = [role.id]
|
||||
|
||||
wscall = new_status.add_action('webservice_call')
|
||||
wscall.url = 'http://example.com/json'
|
||||
wscall.varname = 'test_webservice'
|
||||
wscall.qs_data = {'a': 'b'}
|
||||
|
||||
workflow.store()
|
||||
|
||||
formdef = FormDef()
|
||||
|
@ -1039,12 +1075,20 @@ 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'),
|
||||
]
|
||||
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()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/forms/1/tests/results/run')
|
||||
result_url = resp.location
|
||||
|
@ -1053,12 +1097,15 @@ def test_tests_result_inspect(pub):
|
|||
|
||||
assert 'form_var_text' in resp.text
|
||||
assert 'form_var_text_bo' in resp.text
|
||||
assert 'form_workflow_data_test_webservice_response_foo' in resp.text
|
||||
|
||||
assert [x.text_content() for x in resp.pyquery('div#inspect-timeline a')] == [
|
||||
'New status',
|
||||
'Backoffice Data',
|
||||
'Webservice',
|
||||
'Action button - Manual Jump Loop on status',
|
||||
'Backoffice Data',
|
||||
'Webservice',
|
||||
]
|
||||
|
||||
resp.form['django-condition'] = 'form_var_text == "hello"'
|
||||
|
@ -1071,6 +1118,11 @@ def test_tests_result_inspect(pub):
|
|||
assert 'Condition result' in resp.text
|
||||
assert 'result-true' in resp.text
|
||||
|
||||
resp.form['django-condition'] = 'form_submission_backoffice'
|
||||
resp = resp.form.submit()
|
||||
assert 'Condition result' in resp.text
|
||||
assert 'result-true' in resp.text
|
||||
|
||||
# check inspect is not accessible for old results
|
||||
light_test_result = TestResult.select()[-1]
|
||||
test_result = TestResult.get(light_test_result.id)
|
||||
|
@ -1141,6 +1193,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
|
||||
|
@ -1170,6 +1227,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')
|
||||
|
@ -1353,6 +1412,7 @@ def test_tests_webservice_response(pub):
|
|||
resp = resp.click('Test response')
|
||||
resp.form['url'] = 'http://example.com/'
|
||||
resp.form['payload'] = '{"a": "b"}'
|
||||
resp.form['status_code'] = '400'
|
||||
resp.form['qs_data$element0key'] = 'foo'
|
||||
resp.form['method'] = 'POST (JSON)'
|
||||
resp.form['post_data$element0key'] = 'bar'
|
||||
|
@ -1365,6 +1425,7 @@ def test_tests_webservice_response(pub):
|
|||
assert response.name == 'Test response'
|
||||
assert response.url == 'http://example.com/'
|
||||
assert response.payload == '{"a": "b"}'
|
||||
assert response.status_code == 400
|
||||
assert response.qs_data == {'foo': ''}
|
||||
assert response.method == 'POST'
|
||||
assert response.post_data == {'bar': ''}
|
||||
|
@ -1395,3 +1456,94 @@ def test_tests_webservice_response(pub):
|
|||
resp = resp.form.submit()
|
||||
|
||||
assert 'must start with http://' in resp.text
|
||||
|
||||
|
||||
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.form_data = {
|
||||
'1': 'Jane',
|
||||
'2': 'Doe',
|
||||
}
|
||||
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.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
|
||||
|
||||
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
|
||||
|
|
|
@ -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}/'
|
||||
|
||||
|
@ -990,6 +994,28 @@ def test_workflows_export_import(pub):
|
|||
assert 'Invalid File' in resp.text
|
||||
assert Workflow.count() == 2
|
||||
|
||||
# python expression
|
||||
if not pub.site_options.has_section('options'):
|
||||
pub.site_options.add_section('options')
|
||||
pub.site_options.set('options', 'forbid-new-python-expressions', 'true')
|
||||
with open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w') as fd:
|
||||
pub.site_options.write(fd)
|
||||
st0 = workflow.add_status('Status0', 'st0')
|
||||
sendsms = st0.add_action('sendsms', id='_sendsms')
|
||||
sendsms.to = 'xxx'
|
||||
sendsms.condition = {'type': 'python', 'value': 'True'}
|
||||
sendsms.parent = st0
|
||||
st0.items.append(sendsms)
|
||||
workflow.store()
|
||||
resp = app.get('/backoffice/workflows/%s/' % workflow.id)
|
||||
resp = resp.click(href=re.compile('^export$'))
|
||||
xml_export = resp.text
|
||||
resp = app.get('/backoffice/workflows/')
|
||||
resp = resp.click('Import')
|
||||
resp.form['file'] = Upload('wf.wcs', xml_export.encode('utf-8'))
|
||||
resp = resp.form.submit('submit')
|
||||
assert 'Python expression detected' in resp
|
||||
|
||||
|
||||
def test_workflows_import_from_url(pub):
|
||||
create_superuser(pub)
|
||||
|
@ -2081,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')
|
||||
|
@ -2090,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):
|
||||
|
@ -2161,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)
|
||||
|
@ -2212,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)
|
||||
|
||||
|
@ -2353,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'
|
||||
|
@ -2391,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()
|
||||
|
@ -2820,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
|
||||
|
@ -3300,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)'),
|
||||
]
|
||||
|
||||
|
@ -3789,8 +3751,16 @@ def test_workflows_wscall_options(pub, value):
|
|||
baz_status.add_action('webservice_call')
|
||||
workflow.store()
|
||||
|
||||
pub.cfg['debug'] = {}
|
||||
pub.write_cfg()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/workflows/%s/status/%s/items/1/' % (workflow.id, baz_status.id))
|
||||
assert 'notify_on_errors' not in resp.form.fields
|
||||
|
||||
pub.cfg['debug'] = {'error_email': 'test@localhost'}
|
||||
pub.write_cfg()
|
||||
resp = app.get('/backoffice/workflows/%s/status/%s/items/1/' % (workflow.id, baz_status.id))
|
||||
assert resp.form['notify_on_errors'].value is None
|
||||
assert resp.form['record_on_errors'].value == 'yes'
|
||||
resp.form['notify_on_errors'] = value
|
||||
|
@ -4404,3 +4374,58 @@ 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"'
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
import datetime
|
||||
import os
|
||||
|
||||
import pytest
|
||||
from django.utils.html import escape
|
||||
from django.utils.timezone import make_aware
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
@ -28,11 +30,17 @@ def pub():
|
|||
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 teardown_module(module):
|
||||
clean_temporary_pub()
|
||||
|
||||
|
||||
def test_workflow_tests_link_feature_flag(pub):
|
||||
create_superuser(pub)
|
||||
|
||||
|
@ -65,6 +73,7 @@ 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()
|
||||
|
@ -84,44 +93,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'
|
||||
|
@ -133,7 +113,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))
|
||||
|
@ -144,6 +123,15 @@ def test_workflow_tests_edit_actions(pub):
|
|||
assert 'There are no workflow test actions yet.' in resp.text
|
||||
assert len(resp.pyquery('.biglist li')) == 0
|
||||
|
||||
option_labels = [x[2] for x in resp.form['type'].options]
|
||||
assert (
|
||||
option_labels.index('Assert email is sent')
|
||||
< option_labels.index('Assert form status')
|
||||
< option_labels.index('—')
|
||||
< option_labels.index('Move forward in time')
|
||||
< option_labels.index('Simulate click on action button')
|
||||
)
|
||||
|
||||
# add workflow test action through sidebar form
|
||||
resp.form['type'] = 'button-click'
|
||||
resp = resp.form.submit().follow()
|
||||
|
@ -158,22 +146,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)
|
||||
|
@ -181,26 +185,19 @@ 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')
|
||||
new_status = workflow.add_status(name='New status')
|
||||
|
||||
jump = new_status.add_action('choice')
|
||||
jump.label = 'Button 1'
|
||||
jump.status = new_status.id
|
||||
|
||||
jump = new_status.add_action('choice')
|
||||
jump.label = 'Button 2'
|
||||
jump.status = new_status.id
|
||||
|
||||
jump = new_status.add_action('choice')
|
||||
jump.label = 'Button no target status'
|
||||
|
||||
workflow.store()
|
||||
|
||||
formdef = FormDef()
|
||||
|
@ -218,14 +215,70 @@ def test_workflow_tests_action_button_click(pub):
|
|||
testdef.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/forms/1/tests/%s/workflow/1/' % testdef.id)
|
||||
resp = app.get('/backoffice/forms/1/tests/%s/workflow/' % testdef.id)
|
||||
assert escape('Click on "Button 4" by backoffice user') in resp.text
|
||||
|
||||
resp = app.get('/backoffice/forms/1/tests/%s/workflow/1/' % testdef.id)
|
||||
assert 'Workflow has no action that displays a button.' in resp.text
|
||||
|
||||
jump = new_status.add_action('choice')
|
||||
jump.label = 'Button 1'
|
||||
jump.status = new_status.id
|
||||
|
||||
jump = new_status.add_action('choice')
|
||||
jump.label = 'Button 2'
|
||||
jump.status = new_status.id
|
||||
|
||||
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)'),
|
||||
]
|
||||
|
||||
resp.form['button_name'] = 'Button 1'
|
||||
resp.form['who'] = 'submitter'
|
||||
resp = resp.form.submit().follow()
|
||||
|
||||
assert escape('Click on "Button 1" by submitter') in resp.text
|
||||
|
||||
resp = app.get('/backoffice/forms/1/tests/%s/workflow/1/' % testdef.id)
|
||||
resp.form['who'] = 'other'
|
||||
resp.form['who_id'] = user.test_uuid
|
||||
resp = resp.form.submit().follow()
|
||||
|
||||
assert escape('Click on "Button 1" by test user') in resp.text
|
||||
|
||||
user.remove_self()
|
||||
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)
|
||||
|
@ -307,6 +360,7 @@ def test_workflow_tests_action_assert_email(pub):
|
|||
|
||||
resp = app.get('/backoffice/forms/1/tests/%s/workflow/' % testdef.id)
|
||||
assert 'not configured' not in resp.text
|
||||
assert 'Email to' not in resp.text
|
||||
|
||||
# empty configuration is allowed
|
||||
resp = resp.click('Edit')
|
||||
|
@ -317,10 +371,240 @@ def test_workflow_tests_action_assert_email(pub):
|
|||
resp.form['body_strings$element0'] = 'def'
|
||||
resp = resp.form.submit().follow()
|
||||
|
||||
assert 'Email to' not in resp.text
|
||||
|
||||
assert_email = TestDef.get(testdef.id).workflow_tests.actions[0]
|
||||
assert assert_email.subject_strings == ['abc']
|
||||
assert assert_email.body_strings == ['def']
|
||||
|
||||
resp = resp.click('Edit')
|
||||
resp.form['addresses$element0'] = 'test@entrouvert.com'
|
||||
resp = resp.form.submit().follow()
|
||||
|
||||
assert escape('Email to "test@entrouvert.com"') in resp.text
|
||||
|
||||
assert_email.addresses = ['a@entrouvert.com', 'b@entrouvert.com', 'c@entrouvert.com']
|
||||
assert_email.parent.store()
|
||||
|
||||
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)
|
||||
|
||||
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.AssertSMS(id='1'),
|
||||
]
|
||||
testdef.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
|
||||
resp = app.get('/backoffice/forms/1/tests/%s/workflow/' % testdef.id)
|
||||
assert 'not configured' not in resp.text
|
||||
assert 'SMS to' not in resp.text
|
||||
|
||||
# empty configuration is allowed
|
||||
resp = resp.click('Edit')
|
||||
resp = resp.form.submit().follow()
|
||||
|
||||
resp = resp.click('Edit')
|
||||
resp.form['phone_numbers$element0'] = '0123456789'
|
||||
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 your form has been submitted'
|
||||
|
||||
assert_sms.phone_numbers = ['0123456789', '0123456781', '0123456782']
|
||||
assert_sms.parent.store()
|
||||
|
||||
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)
|
||||
|
@ -388,6 +672,14 @@ def test_workflow_tests_action_assert_webservice_call(pub):
|
|||
]
|
||||
testdef.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
|
||||
resp = app.get('/backoffice/forms/1/tests/%s/workflow/1/' % testdef.id)
|
||||
assert 'you must define corresponding webservice response' in resp.text
|
||||
|
||||
resp = resp.click('Add webservice response')
|
||||
assert 'There are no webservice responses yet.' in resp.text
|
||||
|
||||
response = WebserviceResponse()
|
||||
response.testdef_id = testdef.id
|
||||
response.name = 'Fake response'
|
||||
|
@ -403,16 +695,14 @@ def test_workflow_tests_action_assert_webservice_call(pub):
|
|||
response3.name = 'Other response'
|
||||
response3.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
|
||||
resp = app.get('/backoffice/forms/1/tests/%s/workflow/1/' % testdef.id)
|
||||
assert resp.form['webservice_response_id'].options == [
|
||||
('1', False, 'Fake response'),
|
||||
('2', 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()
|
||||
|
||||
|
@ -420,7 +710,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()
|
||||
|
@ -481,12 +771,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')
|
||||
|
@ -512,7 +806,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'),
|
||||
]
|
||||
|
@ -564,3 +858,41 @@ def test_workflow_tests_run(pub):
|
|||
assert 'Form status when error occured: New status' in resp.text
|
||||
assert 'Email body: \nabc' in resp.text
|
||||
assert resp.pyquery('li#test-action').text() == 'Test action: Assert email is sent'
|
||||
|
||||
|
||||
def test_workfow_tests_creation_from_formdata(pub):
|
||||
create_superuser(pub)
|
||||
|
||||
workflow = Workflow(name='Workflow One')
|
||||
new_status = workflow.add_status(name='New status')
|
||||
end_status = workflow.add_status(name='End status')
|
||||
|
||||
jump = new_status.add_action('jump')
|
||||
jump.status = end_status.id
|
||||
|
||||
workflow.store()
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.workflow_id = workflow.id
|
||||
formdef.name = 'test title'
|
||||
formdef.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
|
||||
formdata = formdef.data_class()()
|
||||
formdata.just_created()
|
||||
formdata.receipt_time = make_aware(datetime.datetime(2022, 1, 1, 0, 0))
|
||||
formdata.store()
|
||||
formdata.perform_workflow()
|
||||
formdata.store()
|
||||
|
||||
resp = app.get('/backoffice/forms/%s/tests/new' % formdef.id)
|
||||
resp.form['name'] = 'First test'
|
||||
resp.form['creation_mode'] = 'formdata-wf'
|
||||
resp.form['formdata'].select(text='1-1 - Unknown User - 2022-01-01 00:00')
|
||||
resp = resp.form.submit().follow()
|
||||
|
||||
testdef = TestDef.select()[0]
|
||||
assert len(testdef.workflow_tests.actions) == 1
|
||||
assert testdef.workflow_tests.actions[0].key == 'assert-status'
|
||||
assert testdef.workflow_tests.actions[0].status_name == 'End status'
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import io
|
||||
import os
|
||||
import re
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
import pytest
|
||||
|
@ -207,6 +209,23 @@ def test_wscalls_import(pub, wscall):
|
|||
resp = resp.form.submit()
|
||||
assert 'Invalid File' in resp.text
|
||||
|
||||
# python expression
|
||||
if not pub.site_options.has_section('options'):
|
||||
pub.site_options.add_section('options')
|
||||
pub.site_options.set('options', 'forbid-new-python-expressions', 'true')
|
||||
with open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w') as fd:
|
||||
pub.site_options.write(fd)
|
||||
wscall.request = {'url': 'http://example.net', 'qs_data': {'a': '=1+2'}}
|
||||
wscall.store()
|
||||
resp = app.get('/backoffice/settings/wscalls/%s/' % wscall.id)
|
||||
resp = resp.click(href=re.compile('^export$'))
|
||||
xml_export = resp.text
|
||||
resp = app.get('/backoffice/settings/wscalls/')
|
||||
resp = resp.click(href='import')
|
||||
resp.form['file'] = Upload('wscall', xml_export.encode('utf-8'))
|
||||
resp = resp.form.submit()
|
||||
assert 'Python expression detected' in resp
|
||||
|
||||
|
||||
def test_wscalls_empty_param_values(pub):
|
||||
create_superuser(pub)
|
||||
|
|
|
@ -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:
|
||||
|
@ -177,6 +197,16 @@ def test_reverse_geocoding(pub):
|
|||
== 'https://nominatim.entrouvert.org/reverse?zoom=16&key=KEY&format=json&addressdetails=1&lat=0&lon=0&accept-language=en'
|
||||
)
|
||||
|
||||
pub.site_options.set('options', 'nominatim_contact_email', 'test@example.net')
|
||||
with open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w') as fd:
|
||||
pub.site_options.write(fd)
|
||||
resp = get_app(pub).get('/api/reverse-geocoding?lat=0&lon=0')
|
||||
assert (
|
||||
rsps.calls[-1].request.url
|
||||
== 'https://nominatim.entrouvert.org/reverse?zoom=16&key=KEY&email=test%40example.net&'
|
||||
'format=json&addressdetails=1&lat=0&lon=0&accept-language=en'
|
||||
)
|
||||
|
||||
pub.site_options.set(
|
||||
'options', 'reverse_geocoding_service_url', 'http://reverse.example.net/?param=value'
|
||||
)
|
||||
|
@ -209,7 +239,7 @@ def test_geocoding(pub):
|
|||
pub.site_options.write(fd)
|
||||
resp = get_app(pub).get('/api/geocoding?q=test')
|
||||
assert rsps.calls[-1].request.url == (
|
||||
'https://nominatim.entrouvert.org/search?viewbox=2.34,1.23,3.45,2.34&bounded=1&'
|
||||
'https://nominatim.entrouvert.org/search?viewbox=2.34%2C1.23%2C3.45%2C2.34&bounded=1&'
|
||||
'format=json&q=test&accept-language=en'
|
||||
)
|
||||
|
||||
|
@ -230,7 +260,7 @@ def test_geocoding(pub):
|
|||
pub.site_options.write(fd)
|
||||
resp = get_app(pub).get('/api/geocoding?q=test')
|
||||
assert rsps.calls[-1].request.url == (
|
||||
'https://nominatim.entrouvert.org/search?key=KEY&viewbox=2.34,1.23,3.45,2.34&bounded=1&'
|
||||
'https://nominatim.entrouvert.org/search?key=KEY&viewbox=2.34%2C1.23%2C3.45%2C2.34&bounded=1&'
|
||||
'format=json&q=test&accept-language=en'
|
||||
)
|
||||
|
||||
|
@ -277,13 +307,13 @@ def test_afterjobs_base_directory(pub):
|
|||
get_app(pub).get(sign_url('/api/jobs/?orig=coucou', '1234'), status=404)
|
||||
|
||||
|
||||
def test_preview_json_payload(pub, admin_user):
|
||||
get_app(pub).get('/api/preview-json-payload', status=403)
|
||||
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-json-payload')
|
||||
resp = app.get('/api/preview-payload-structure')
|
||||
|
||||
assert resp.pyquery('div.payload-preview').length == 1
|
||||
assert '<h2>Payload preview</h2>' in resp.text
|
||||
assert '<h2>Payload structure preview</h2>' in resp.text
|
||||
assert resp.pyquery('div.payload-preview').text() == '{}'
|
||||
params = {
|
||||
'request$post_data$added_elements': 1,
|
||||
|
@ -295,7 +325,7 @@ def test_preview_json_payload(pub, admin_user):
|
|||
'request$post_data$element2value$value_python': '',
|
||||
'request$post_data$element3key': 'user/0',
|
||||
}
|
||||
resp = app.get('/api/preview-json-payload', params=params)
|
||||
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(
|
||||
|
@ -304,10 +334,10 @@ def test_preview_json_payload(pub, admin_user):
|
|||
'request$post_data$element3value$value_python': '',
|
||||
}
|
||||
)
|
||||
resp = app.get('/api/preview-json-payload', params=params)
|
||||
resp = app.get('/api/preview-payload-structure', params=params)
|
||||
|
||||
assert resp.pyquery('div.payload-preview div.errornotice').length == 1
|
||||
assert 'Unable to generate payload' in resp.pyquery('div.payload-preview div.errornotice').text()
|
||||
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()
|
||||
|
@ -323,5 +353,5 @@ def test_preview_json_payload(pub, admin_user):
|
|||
'post_data$element3key': '1/0',
|
||||
'post_data$element3value$value_template': '',
|
||||
}
|
||||
resp = app.get('/api/preview-json-payload', params=params)
|
||||
resp = app.get('/api/preview-payload-structure', params=params)
|
||||
assert resp.pyquery('div.payload-preview').text() == '[["Foo",{{ form_name }}],[""]]'
|
||||
|
|
|
@ -7,8 +7,9 @@ import xml.etree.ElementTree as ET
|
|||
|
||||
import pytest
|
||||
|
||||
from wcs.api_export_import import klass_to_slug
|
||||
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'
|
||||
|
@ -940,6 +953,58 @@ def test_export_import_bundle_import(pub):
|
|||
assert formdef.disabled is True
|
||||
assert formdef.workflow_roles == {'_receiver': extra_role.id}
|
||||
|
||||
# bad file format
|
||||
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'
|
||||
job = BundleImportJob.get(afterjob_url.split('/')[-2])
|
||||
assert job.status == 'failed'
|
||||
assert job.failure_label == 'Error: Invalid tar file.'
|
||||
|
||||
# missing manifest
|
||||
tar_io = io.BytesIO()
|
||||
with tarfile.open(mode='w', fileobj=tar_io) as tar:
|
||||
foo_fd = io.BytesIO(json.dumps({'foo': 'bar'}, indent=2).encode())
|
||||
tarinfo = tarfile.TarInfo('foo.json')
|
||||
tarinfo.size = len(foo_fd.getvalue())
|
||||
tar.addfile(tarinfo, fileobj=foo_fd)
|
||||
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'
|
||||
job = BundleImportJob.get(afterjob_url.split('/')[-2])
|
||||
assert job.status == 'failed'
|
||||
assert job.failure_label == 'Error: Invalid tar file, missing manifest.'
|
||||
|
||||
# missing component
|
||||
tar_io = io.BytesIO()
|
||||
with tarfile.open(mode='w', fileobj=tar_io) as tar:
|
||||
manifest_json = {
|
||||
'application': 'Test',
|
||||
'slug': 'test',
|
||||
'elements': [{'type': 'forms', 'slug': 'foo', 'name': 'foo'}],
|
||||
}
|
||||
manifest_fd = io.BytesIO(json.dumps(manifest_json, indent=2).encode())
|
||||
tarinfo = tarfile.TarInfo('manifest.json')
|
||||
tarinfo.size = len(manifest_fd.getvalue())
|
||||
tar.addfile(tarinfo, fileobj=manifest_fd)
|
||||
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'
|
||||
job = BundleImportJob.get(afterjob_url.split('/')[-2])
|
||||
assert job.status == 'failed'
|
||||
assert job.failure_label == 'Error: Invalid tar file, missing component forms/foo.'
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'category_class',
|
||||
|
@ -986,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'
|
||||
|
@ -1011,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'
|
||||
|
@ -1036,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'
|
||||
|
@ -1061,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'
|
||||
|
@ -1089,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'
|
||||
|
@ -1117,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'
|
||||
|
@ -1151,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'
|
||||
|
@ -1251,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'
|
||||
|
@ -1284,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'
|
||||
|
@ -1321,6 +1404,35 @@ def test_export_import_bundle_declare(pub):
|
|||
== []
|
||||
)
|
||||
|
||||
# bad file format
|
||||
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'
|
||||
job = BundleDeclareJob.get(afterjob_url.split('/')[-2])
|
||||
assert job.status == 'failed'
|
||||
assert job.failure_label == 'Error: Invalid tar file.'
|
||||
|
||||
# missing manifest
|
||||
tar_io = io.BytesIO()
|
||||
with tarfile.open(mode='w', fileobj=tar_io) as tar:
|
||||
foo_fd = io.BytesIO(json.dumps({'foo': 'bar'}, indent=2).encode())
|
||||
tarinfo = tarfile.TarInfo('foo.json')
|
||||
tarinfo.size = len(foo_fd.getvalue())
|
||||
tar.addfile(tarinfo, fileobj=foo_fd)
|
||||
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'
|
||||
job = BundleDeclareJob.get(afterjob_url.split('/')[-2])
|
||||
assert job.status == 'failed'
|
||||
assert job.failure_label == 'Error: Invalid tar file, missing manifest.'
|
||||
|
||||
|
||||
def test_export_import_bundle_unlink(pub):
|
||||
application = Application()
|
||||
|
@ -1563,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': [],
|
||||
|
@ -1596,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'
|
||||
|
@ -1607,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': [],
|
||||
|
@ -1709,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'
|
||||
|
@ -1718,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': [],
|
||||
|
@ -1746,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': [
|
||||
|
@ -1848,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': [],
|
||||
|
@ -1870,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': [],
|
||||
|
@ -1896,6 +2032,46 @@ def test_export_import_bundle_check(pub):
|
|||
},
|
||||
}
|
||||
|
||||
# bad file format
|
||||
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
|
||||
tar_io = io.BytesIO()
|
||||
with tarfile.open(mode='w', fileobj=tar_io) as tar:
|
||||
foo_fd = io.BytesIO(json.dumps({'foo': 'bar'}, indent=2).encode())
|
||||
tarinfo = tarfile.TarInfo('foo.json')
|
||||
tarinfo.size = len(foo_fd.getvalue())
|
||||
tar.addfile(tarinfo, fileobj=foo_fd)
|
||||
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'
|
||||
|
||||
# missing component
|
||||
tar_io = io.BytesIO()
|
||||
with tarfile.open(mode='w', fileobj=tar_io) as tar:
|
||||
manifest_json = {
|
||||
'application': 'Test',
|
||||
'slug': 'test',
|
||||
'version_number': '42',
|
||||
'elements': [{'type': 'forms', 'slug': 'foo', 'name': 'foo'}],
|
||||
}
|
||||
manifest_fd = io.BytesIO(json.dumps(manifest_json, indent=2).encode())
|
||||
tarinfo = tarfile.TarInfo('manifest.json')
|
||||
tarinfo.size = len(manifest_fd.getvalue())
|
||||
tar.addfile(tarinfo, fileobj=manifest_fd)
|
||||
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'
|
||||
|
||||
|
||||
def test_export_import_workflow_options(pub):
|
||||
FormDef.wipe()
|
||||
|
@ -1925,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'
|
||||
|
@ -1938,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'
|
||||
|
@ -1946,6 +2126,111 @@ def test_export_import_workflow_options(pub):
|
|||
assert formdef.workflow_options == {'foo': 'bar2'}
|
||||
|
||||
|
||||
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')
|
||||
pub.site_options.set('options', 'forbid-new-python-expressions', 'true')
|
||||
with open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w') as fd:
|
||||
pub.site_options.write(fd)
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'foo'
|
||||
formdef.fields = [
|
||||
PageField(id='1', label='page1', condition={'type': 'python', 'value': 'True'}),
|
||||
]
|
||||
formdef.store()
|
||||
bundle = create_bundle(
|
||||
[
|
||||
{'type': 'forms', 'slug': 'foo', 'name': 'foo'},
|
||||
],
|
||||
('forms/foo', formdef),
|
||||
)
|
||||
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'
|
||||
blockdef.fields = [
|
||||
StringField(id='2', label='python_prefill', prefill={'type': 'formula', 'value': '1 + 2'}),
|
||||
]
|
||||
blockdef.store()
|
||||
bundle = create_bundle(
|
||||
[
|
||||
{'type': 'blocks', 'slug': 'foo', 'name': 'foo'},
|
||||
],
|
||||
('blocks/foo', blockdef),
|
||||
)
|
||||
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'
|
||||
|
||||
workflow = Workflow(name='foo')
|
||||
st0 = workflow.add_status('Status0', 'st0')
|
||||
sendsms = st0.add_action('sendsms', id='_sendsms')
|
||||
sendsms.to = 'xxx'
|
||||
sendsms.condition = {'type': 'python', 'value': 'True'}
|
||||
sendsms.parent = st0
|
||||
st0.items.append(sendsms)
|
||||
workflow.store()
|
||||
bundle = create_bundle(
|
||||
[
|
||||
{'type': 'workflows', 'slug': 'foo', 'name': 'foo'},
|
||||
],
|
||||
('workflows/foo', workflow),
|
||||
)
|
||||
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'
|
||||
|
||||
data_source = NamedDataSource(name='foo')
|
||||
data_source.data_source = {'type': 'formula', 'value': repr([('1', 'un'), ('2', 'deux')])}
|
||||
data_source.store()
|
||||
bundle = create_bundle(
|
||||
[
|
||||
{'type': 'data-sources', 'slug': 'foo', 'name': 'foo'},
|
||||
],
|
||||
('data-sources/foo', data_source),
|
||||
)
|
||||
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'
|
||||
|
||||
wscall = NamedWsCall()
|
||||
wscall.name = 'foo'
|
||||
wscall.request = {'url': 'http://example.net', 'qs_data': {'a': '=1+2'}}
|
||||
wscall.store()
|
||||
bundle = create_bundle(
|
||||
[
|
||||
{'type': 'wscalls', 'slug': 'foo', 'name': 'foo'},
|
||||
],
|
||||
('wscalls/foo', wscall),
|
||||
)
|
||||
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'
|
||||
|
||||
|
||||
def test_api_export_import_invalid_slug(pub):
|
||||
pub.role_class.wipe()
|
||||
role1 = pub.role_class(name='Test role 1')
|
||||
|
|
|
@ -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
|
||||
|
@ -49,6 +50,12 @@ def pub(emails):
|
|||
'''\
|
||||
[api-secrets]
|
||||
coucou = 1234
|
||||
|
||||
[variables]
|
||||
idp_api_url = https://authentic.example.invalid/api/'
|
||||
|
||||
[wscall-secrets]
|
||||
authentic.example.invalid = 4460cf12e156d841c116fbebd52d7ebe41282c63ac2605740068ba5fd89b7316
|
||||
'''
|
||||
)
|
||||
|
||||
|
@ -163,6 +170,7 @@ def test_formdata(pub, local_user, user, auth):
|
|||
block = BlockDef()
|
||||
block.name = 'foobar'
|
||||
block.fields = [
|
||||
fields.TitleField(id='dsd', label='Title'),
|
||||
fields.StringField(id='abc', label='Foo', varname='foo'),
|
||||
fields.ItemField(id='xyz', label='Test', data_source={'type': 'foobar'}, varname='bar'),
|
||||
]
|
||||
|
@ -1365,6 +1373,9 @@ def test_api_list_formdata_string_filter(pub, local_user):
|
|||
('existing', 'on', 3),
|
||||
('between', 'FOO 1|FOO 2', 1),
|
||||
('between', 'FOO 2|FOO 1', 1),
|
||||
('icontains', 'FOO', 3),
|
||||
('icontains', 'foo', 3),
|
||||
('icontains', '2', 1),
|
||||
]
|
||||
for operator, value, result in params:
|
||||
resp = get_app(pub).get(
|
||||
|
@ -1541,6 +1552,9 @@ def test_api_list_formdata_text_filter(pub, local_user):
|
|||
('existing', 'on', 3),
|
||||
('between', 'FOO 1|FOO 2', 1),
|
||||
('between', 'FOO 2|FOO 1', 1),
|
||||
('icontains', 'FOO', 3),
|
||||
('icontains', 'foo', 3),
|
||||
('icontains', '2', 1),
|
||||
]
|
||||
for operator, value, result in params:
|
||||
resp = get_app(pub).get(
|
||||
|
@ -2010,6 +2024,8 @@ def test_api_list_formdata_email_filter(pub, local_user):
|
|||
('not_in', 'a@localhost|b@localhost', 1),
|
||||
('absent', 'on', 2),
|
||||
('existing', 'on', 3),
|
||||
('icontains', 'A@LOCAL', 1),
|
||||
('icontains', 'C@LOCAL', 0),
|
||||
]
|
||||
for operator, value, result in params:
|
||||
resp = get_app(pub).get(
|
||||
|
@ -2306,6 +2322,8 @@ def test_api_list_formdata_block_field_filter(pub, local_user):
|
|||
('existing', 'on', 12),
|
||||
('between', 'plop1|plop5', 7),
|
||||
('between', 'plop5|plop1', 7),
|
||||
('icontains', 'PLOP', 12),
|
||||
('icontains', 'LOP1', 4), # plop1 (twice), plop10, plop11
|
||||
]
|
||||
for operator, value, result in params:
|
||||
resp = get_app(pub).get(
|
||||
|
@ -2974,9 +2992,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()
|
||||
|
@ -2996,7 +3017,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'
|
||||
|
@ -3014,6 +3034,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')
|
||||
|
@ -3042,6 +3085,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()
|
||||
|
@ -3070,6 +3128,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()
|
||||
|
|
|
@ -2184,7 +2184,8 @@ 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.include_download_all_button = True
|
||||
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)
|
||||
resp = resp.click('Download all files as .zip')
|
||||
|
@ -2264,6 +2265,48 @@ def test_backoffice_geolocation_info(pub):
|
|||
assert 'data-init-lat="48.83' in resp.text
|
||||
|
||||
|
||||
def test_backoffice_sidebar_elements(pub):
|
||||
user = create_user(pub)
|
||||
|
||||
FormDef.wipe()
|
||||
formdef = FormDef()
|
||||
formdef.name = 'form title'
|
||||
formdef.geolocations = {'base': 'Geolocation'}
|
||||
formdef.store()
|
||||
|
||||
formdata = formdef.data_class()()
|
||||
formdata.geolocations = {'base': {'lat': 48.83, 'lon': 2.32}}
|
||||
formdata.user_id = user.id
|
||||
formdata.just_created()
|
||||
formdata.store()
|
||||
|
||||
other_formdata = formdef.data_class()()
|
||||
other_formdata.just_created()
|
||||
other_formdata.just_created()
|
||||
other_formdata.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get(formdata.get_backoffice_url())
|
||||
assert [x.text for x in resp.pyquery('#sidebar .extra-context h3')] == [
|
||||
'General Information',
|
||||
'Associated User',
|
||||
'Geolocation',
|
||||
]
|
||||
assert len(resp.pyquery('[data-async-url$="/user-pending-forms"]')) == 1
|
||||
|
||||
formdef.management_sidebar_items = ['general', 'pending-forms']
|
||||
formdef.store()
|
||||
resp = app.get(formdata.get_backoffice_url())
|
||||
assert [x.text for x in resp.pyquery('#sidebar .extra-context h3')] == ['General Information']
|
||||
assert len(resp.pyquery('[data-async-url$="/user-pending-forms"]')) == 1
|
||||
|
||||
formdef.management_sidebar_items = ['geolocation']
|
||||
formdef.store()
|
||||
resp = app.get(formdata.get_backoffice_url())
|
||||
assert [x.text for x in resp.pyquery('#sidebar .extra-context h3')] == ['Geolocation']
|
||||
assert len(resp.pyquery('[data-async-url$="/user-pending-forms"]')) == 0
|
||||
|
||||
|
||||
def test_backoffice_info_text(pub):
|
||||
create_user(pub)
|
||||
create_environment(pub)
|
||||
|
@ -3847,6 +3890,13 @@ def test_formdata_lookup(pub):
|
|||
resp = resp.follow()
|
||||
assert 'No such tracking code or identifier.' in resp.text
|
||||
|
||||
# check it's not possible to replace back value with anything else
|
||||
for invalid_value in ('http://example.invalid/', 'xxx'):
|
||||
resp = app.get('/backoffice/management/listing')
|
||||
resp.forms[0]['back'] = invalid_value
|
||||
resp = resp.forms[0].submit()
|
||||
assert resp.location == 'http://example.net/backoffice/management/'
|
||||
|
||||
|
||||
def test_backoffice_sidebar_user_context(pub):
|
||||
user = create_user(pub)
|
||||
|
@ -5081,7 +5131,8 @@ def test_backoffice_logged_errors(pub):
|
|||
assert 'ZeroDivisionError' in resp2.text
|
||||
resp = resp2.click('Failed to evaluate condition')
|
||||
assert 'ZeroDivisionError: integer division or modulo by zero' in resp.text
|
||||
assert 'Python Expression: <code>1//0</code>' in resp.text
|
||||
assert 'Condition: <code>1//0</code>' in resp.text
|
||||
assert 'Condition type: <code>python</code>' in resp.text
|
||||
resp = resp.click('Delete').follow()
|
||||
assert pub.loggederror_class.count() == 0
|
||||
|
||||
|
@ -5459,322 +5510,6 @@ def test_lazy_eval_with_conditional_workflow_form(pub):
|
|||
assert context['form_var_foo_bar'] == 'go'
|
||||
|
||||
|
||||
@pytest.fixture(params=[{'attach_to_history': True}, {}])
|
||||
def create_formdata(request, pub):
|
||||
admin = create_user(pub, is_admin=True)
|
||||
|
||||
FormDef.wipe()
|
||||
|
||||
source_formdef = FormDef()
|
||||
source_formdef.name = 'source form'
|
||||
source_formdef.workflow_roles = {'_receiver': 1}
|
||||
source_formdef.fields = [
|
||||
fields.StringField(id='0', label='string', varname='toto_string'),
|
||||
fields.FileField(id='1', label='file', varname='toto_file'),
|
||||
]
|
||||
source_formdef.store()
|
||||
|
||||
target_formdef = FormDef()
|
||||
target_formdef.name = 'target form'
|
||||
target_formdef.workflow_roles = {'_receiver': 1}
|
||||
target_formdef.backoffice_submission_roles = admin.roles[:]
|
||||
target_formdef.fields = [
|
||||
fields.StringField(id='0', label='string', varname='foo_string'),
|
||||
fields.FileField(id='1', label='file', varname='foo_file'),
|
||||
]
|
||||
target_formdef.store()
|
||||
wf = Workflow(name='create-formdata')
|
||||
|
||||
st1 = wf.add_status('New')
|
||||
st2 = wf.add_status('Resubmit')
|
||||
|
||||
jump = st1.add_action('choice', id='_resubmit')
|
||||
jump.label = 'Resubmit'
|
||||
jump.by = ['_receiver']
|
||||
jump.status = st2.id
|
||||
|
||||
create_formdata = st2.add_action('create_formdata', id='_create_formdata')
|
||||
create_formdata.varname = 'resubmitted'
|
||||
create_formdata.draft = True
|
||||
create_formdata.formdef_slug = target_formdef.url_name
|
||||
create_formdata.user_association_mode = 'keep-user'
|
||||
create_formdata.backoffice_submission = True
|
||||
create_formdata.attach_to_history = request.param.get('attach_to_history', False)
|
||||
create_formdata.mappings = [
|
||||
Mapping(field_id='0', expression='=form_var_toto_string'),
|
||||
Mapping(field_id='1', expression='=form_var_toto_file_raw'),
|
||||
]
|
||||
|
||||
redirect = st2.add_action('redirect_to_url', id='_redirect')
|
||||
redirect.url = '{{ form_links_resubmitted.form_backoffice_url }}'
|
||||
|
||||
jump = st2.add_action('jumponsubmit', id='_jump')
|
||||
jump.status = st1.id
|
||||
|
||||
wf.store()
|
||||
source_formdef.workflow_id = wf.id
|
||||
source_formdef.store()
|
||||
source_formdef.data_class().wipe()
|
||||
target_formdef.data_class().wipe()
|
||||
return locals()
|
||||
|
||||
|
||||
def test_backoffice_create_formdata_backoffice_submission(pub, create_formdata):
|
||||
# create submitting user
|
||||
user = create_formdata['pub'].user_class()
|
||||
user.name = 'Jean Darmette'
|
||||
user.email = 'jean.darmette@triffouilis.fr'
|
||||
user.store()
|
||||
|
||||
# create source formdata
|
||||
formdata = create_formdata['source_formdef'].data_class()()
|
||||
upload = PicklableUpload('/foo/bar', content_type='text/plain')
|
||||
upload.receive([b'hello world'])
|
||||
formdata.data = {
|
||||
'0': 'coucou',
|
||||
'1': upload,
|
||||
}
|
||||
formdata.user = user
|
||||
formdata.just_created()
|
||||
formdata.store()
|
||||
formdata.perform_workflow()
|
||||
|
||||
# agent login and go to backoffice management pages
|
||||
app = get_app(create_formdata['pub'])
|
||||
app = login(app)
|
||||
resp = app.get(create_formdata['source_formdef'].get_url(backoffice=True))
|
||||
|
||||
# click on first available formdata
|
||||
resp = resp.click('%s-%s' % (create_formdata['source_formdef'].id, formdata.id))
|
||||
target_data_class = create_formdata['target_formdef'].data_class()
|
||||
assert target_data_class.count() == 0
|
||||
# resubmit it through backoffice submission
|
||||
resp = resp.form.submit(name='button_resubmit')
|
||||
assert pub.loggederror_class.count() == 0
|
||||
assert target_data_class.count() == 1
|
||||
target_formdata = target_data_class.select()[0]
|
||||
|
||||
assert target_formdata.submission_context == {
|
||||
'orig_object_type': 'formdef',
|
||||
'orig_formdata_id': '1',
|
||||
'orig_formdef_id': '1',
|
||||
}
|
||||
assert target_formdata.submission_agent_id == str(create_formdata['admin'].id)
|
||||
assert target_formdata.user.id == user.id
|
||||
assert target_formdata.status == 'draft'
|
||||
assert target_formdata.receipt_time
|
||||
assert resp.location == 'http://example.net/backoffice/management/target-form/%s/' % target_formdata.id
|
||||
resp = resp.follow()
|
||||
assert resp.location == 'http://example.net/backoffice/submission/target-form/%s/' % target_formdata.id
|
||||
resp = resp.follow()
|
||||
# second redirect with magic-token
|
||||
resp = resp.follow()
|
||||
resp = resp.form.submit(name='submit') # -> validation
|
||||
resp = resp.form.submit(name='submit') # -> submission
|
||||
target_formdata = target_data_class.get(id=target_formdata.id)
|
||||
assert target_formdata.user.id == user.id
|
||||
assert target_formdata.status == 'wf-new'
|
||||
resp = resp.follow()
|
||||
pq = resp.pyquery.remove_namespaces()
|
||||
assert pq('.field-type-string .value').text() == 'coucou'
|
||||
assert pq('.field-type-file .value').text() == 'bar'
|
||||
|
||||
|
||||
def test_linked_forms_variables(pub, create_formdata):
|
||||
# create source formdata
|
||||
formdata = create_formdata['source_formdef'].data_class()()
|
||||
upload = PicklableUpload('/foo/bar', content_type='text/plain')
|
||||
upload.receive([b'hello world'])
|
||||
formdata.data = {
|
||||
'0': 'coucou',
|
||||
'1': upload,
|
||||
}
|
||||
formdata.just_created()
|
||||
formdata.store()
|
||||
formdata.perform_workflow()
|
||||
formdata.store()
|
||||
formdata.jump_status('2')
|
||||
formdata.perform_workflow()
|
||||
formdata.store()
|
||||
|
||||
pub.substitutions.reset()
|
||||
pub.substitutions.feed(formdata)
|
||||
substvars = pub.substitutions.get_context_variables(mode='lazy')
|
||||
assert str(substvars['form_links_resubmitted_form_var_foo_string']) == 'coucou'
|
||||
assert 'form_links_resubmitted_form_var_foo_string' in substvars.get_flat_keys()
|
||||
|
||||
source_formdata = create_formdata['source_formdef'].data_class().select()[0]
|
||||
|
||||
app = get_app(create_formdata['pub'])
|
||||
app = login(app)
|
||||
resp = app.get(source_formdata.get_url(backoffice=True) + 'inspect')
|
||||
assert '?expand=form_links_resubmitted' in resp
|
||||
resp = app.get(source_formdata.get_url(backoffice=True) + 'inspect?expand=form_links_resubmitted')
|
||||
assert 'form_links_resubmitted_form_var_foo_string' in resp
|
||||
|
||||
# delete target formdata
|
||||
create_formdata['target_formdef'].data_class().wipe()
|
||||
resp = app.get(source_formdata.get_url(backoffice=True) + 'inspect')
|
||||
assert '?expand=form_links_resubmitted' not in resp
|
||||
assert 'form_links_resubmitted_form_var_foo_string' not in resp
|
||||
|
||||
# delete target formdef
|
||||
create_formdata['target_formdef'].remove_self()
|
||||
resp = app.get(source_formdata.get_url(backoffice=True) + 'inspect')
|
||||
|
||||
|
||||
def test_backoffice_create_formdata_map_fields_by_varname(pub, create_formdata):
|
||||
create_formdata['create_formdata'].map_fields_by_varname = True
|
||||
create_formdata['create_formdata'].mappings = []
|
||||
create_formdata['wf'].store()
|
||||
create_formdata['source_formdef'].fields = [
|
||||
fields.StringField(id='0', label='string', varname='string0'),
|
||||
fields.FileField(id='1', label='file', varname='file1'),
|
||||
fields.StringField(id='2', label='string', varname='string2', required=False),
|
||||
fields.FileField(id='3', label='file', varname='file3', required=False),
|
||||
]
|
||||
create_formdata['source_formdef'].store()
|
||||
create_formdata['target_formdef'].fields = [
|
||||
fields.StringField(id='0', label='string', varname='string0'),
|
||||
fields.FileField(id='1', label='file', varname='file1'),
|
||||
fields.StringField(id='2', label='string', varname='string2', required=False),
|
||||
fields.FileField(id='3', label='file', varname='file3', required=False),
|
||||
]
|
||||
create_formdata['target_formdef'].store()
|
||||
|
||||
# create submitting user
|
||||
user = create_formdata['pub'].user_class()
|
||||
user.name = 'Jean Darmette'
|
||||
user.email = 'jean.darmette@triffouilis.fr'
|
||||
user.store()
|
||||
|
||||
# create source formdata
|
||||
create_formdata['source_formdef'].digest_templates = {'default': 'blah'}
|
||||
create_formdata['source_formdef'].store()
|
||||
formdata = create_formdata['source_formdef'].data_class()()
|
||||
create_formdata['formdata'] = formdata
|
||||
upload = PicklableUpload('/foo/bar', content_type='text/plain')
|
||||
upload.receive([b'hello world'])
|
||||
formdata.data = {
|
||||
'0': 'coucou',
|
||||
'1': upload,
|
||||
}
|
||||
formdata.user = user
|
||||
formdata.just_created()
|
||||
formdata.store()
|
||||
formdata.perform_workflow()
|
||||
|
||||
# agent login and go to backoffice management pages
|
||||
app = get_app(create_formdata['pub'])
|
||||
app = login(app)
|
||||
resp = app.get(create_formdata['source_formdef'].get_url(backoffice=True))
|
||||
|
||||
# click on first available formdata
|
||||
resp = resp.click('%s-%s' % (create_formdata['source_formdef'].id, formdata.id))
|
||||
target_data_class = create_formdata['target_formdef'].data_class()
|
||||
assert target_data_class.count() == 0
|
||||
# resubmit it through backoffice submission
|
||||
resp = resp.form.submit(name='button_resubmit')
|
||||
assert pub.loggederror_class.count() == 0
|
||||
assert target_data_class.count() == 1
|
||||
target_formdata = target_data_class.select()[0]
|
||||
|
||||
assert target_formdata.submission_context == {
|
||||
'orig_object_type': 'formdef',
|
||||
'orig_formdata_id': '1',
|
||||
'orig_formdef_id': '1',
|
||||
}
|
||||
assert target_formdata.submission_agent_id == str(create_formdata['admin'].id)
|
||||
assert target_formdata.user.id == user.id
|
||||
assert target_formdata.status == 'draft'
|
||||
assert resp.location == 'http://example.net/backoffice/management/target-form/%s/' % target_formdata.id
|
||||
resp = resp.follow()
|
||||
assert resp.location == 'http://example.net/backoffice/submission/target-form/%s/' % target_formdata.id
|
||||
resp = resp.follow()
|
||||
# second redirect with magic-token
|
||||
resp = resp.follow()
|
||||
# check parent form is displayed in sidebar
|
||||
assert resp.pyquery('.extra-context--orig-data').attr.href == formdata.get_backoffice_url()
|
||||
assert resp.pyquery('.extra-context--orig-data').text() == 'source form #1-1 (blah)'
|
||||
resp = resp.form.submit(name='submit') # -> validation
|
||||
resp = resp.form.submit(name='submit') # -> submission
|
||||
target_formdata = target_data_class.get(id=target_formdata.id)
|
||||
assert target_formdata.user.id == user.id
|
||||
assert target_formdata.status == 'wf-new'
|
||||
resp = resp.follow()
|
||||
pq = resp.pyquery.remove_namespaces()
|
||||
assert pq('.field-type-string .value').text() == 'coucou'
|
||||
assert pq('.field-type-file .value').text() == 'bar'
|
||||
|
||||
resp = app.get(create_formdata['formdata'].get_url(backoffice=True))
|
||||
pq = resp.pyquery.remove_namespaces()
|
||||
assert pq('.field-type-string .value').text() == 'coucou'
|
||||
if create_formdata['create_formdata'].attach_to_history:
|
||||
assert pq('.wf-links')
|
||||
else:
|
||||
assert not pq('.wf-links')
|
||||
|
||||
|
||||
def test_backoffice_create_formdata_map_fields_by_varname_plus_empty(pub, create_formdata):
|
||||
create_formdata['create_formdata'].map_fields_by_varname = True
|
||||
create_formdata['create_formdata'].mappings = [
|
||||
Mapping(field_id='0', expression=None),
|
||||
]
|
||||
create_formdata['wf'].store()
|
||||
create_formdata['source_formdef'].fields = [
|
||||
fields.StringField(id='0', label='string', varname='string0'),
|
||||
fields.StringField(id='2', label='string', varname='string2', required=False),
|
||||
]
|
||||
create_formdata['source_formdef'].store()
|
||||
create_formdata['target_formdef'].fields = [
|
||||
fields.StringField(id='0', label='string', varname='string0'),
|
||||
fields.StringField(id='2', label='string', varname='string2', required=False),
|
||||
]
|
||||
create_formdata['target_formdef'].store()
|
||||
|
||||
# create submitting user
|
||||
user = create_formdata['pub'].user_class()
|
||||
user.name = 'Jean Darmette'
|
||||
user.email = 'jean.darmette@triffouilis.fr'
|
||||
user.store()
|
||||
|
||||
# create source formdata
|
||||
formdata = create_formdata['source_formdef'].data_class()()
|
||||
create_formdata['formdata'] = formdata
|
||||
formdata.data = {
|
||||
'0': 'foo',
|
||||
'2': 'bar',
|
||||
}
|
||||
formdata.user = user
|
||||
formdata.just_created()
|
||||
formdata.store()
|
||||
formdata.perform_workflow()
|
||||
|
||||
# agent login and go to backoffice management pages
|
||||
app = get_app(create_formdata['pub'])
|
||||
app = login(app)
|
||||
resp = app.get(create_formdata['source_formdef'].get_url(backoffice=True))
|
||||
|
||||
# click on first available formdata
|
||||
resp = resp.click('%s-%s' % (create_formdata['source_formdef'].id, formdata.id))
|
||||
target_data_class = create_formdata['target_formdef'].data_class()
|
||||
assert target_data_class.count() == 0
|
||||
# resubmit it through backoffice submission
|
||||
resp = resp.form.submit(name='button_resubmit')
|
||||
assert target_data_class.count() == 1
|
||||
target_formdata = target_data_class.select()[0]
|
||||
|
||||
assert target_formdata.submission_context == {
|
||||
'orig_object_type': 'formdef',
|
||||
'orig_formdata_id': '1',
|
||||
'orig_formdef_id': '1',
|
||||
}
|
||||
assert target_formdata.submission_agent_id == str(create_formdata['admin'].id)
|
||||
assert target_formdata.user.id == user.id
|
||||
assert target_formdata.status == 'draft'
|
||||
assert target_formdata.data == {'0': None, '2': 'bar'}
|
||||
|
||||
|
||||
def test_backoffice_create_carddata_from_formdata(pub):
|
||||
CardDef.wipe()
|
||||
FormDef.wipe()
|
||||
|
@ -6458,3 +6193,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
|
||||
|
||||
|
|
|
@ -12,7 +12,8 @@ from wcs.carddef import CardDef
|
|||
from wcs.data_sources import NamedDataSource
|
||||
from wcs.formdef import FormDef
|
||||
from wcs.qommon.http_request import HTTPRequest
|
||||
from wcs.workflows import Workflow, WorkflowBackofficeFieldsFormDef
|
||||
from wcs.wf.criticality import MODE_INC
|
||||
from wcs.workflows import Workflow, WorkflowBackofficeFieldsFormDef, WorkflowCriticalityLevel
|
||||
|
||||
from ..utilities import clean_temporary_pub, create_temporary_pub, get_app, login
|
||||
from .test_all import create_environment, create_superuser
|
||||
|
@ -1048,6 +1049,7 @@ def test_backoffice_string_filter(pub):
|
|||
'not_in',
|
||||
'absent',
|
||||
'existing',
|
||||
'icontains',
|
||||
]
|
||||
|
||||
resp.forms['listing-settings']['filter-4-value'].value = 'a'
|
||||
|
@ -1167,6 +1169,7 @@ def test_backoffice_text_filter(pub):
|
|||
'not_in',
|
||||
'absent',
|
||||
'existing',
|
||||
'icontains',
|
||||
]
|
||||
|
||||
resp.forms['listing-settings']['filter-4-value'].value = 'a'
|
||||
|
@ -1227,6 +1230,7 @@ def test_backoffice_email_filter(pub):
|
|||
'not_in',
|
||||
'absent',
|
||||
'existing',
|
||||
'icontains',
|
||||
]
|
||||
|
||||
resp.forms['listing-settings']['filter-4-value'].value = 'a@localhost'
|
||||
|
@ -1245,6 +1249,12 @@ def test_backoffice_email_filter(pub):
|
|||
assert resp.text.count('>a@localhost</') > 0
|
||||
assert resp.text.count('>b@localhost</') == 0
|
||||
|
||||
resp.forms['listing-settings']['filter-4-value'].value = 'a@local'
|
||||
resp.forms['listing-settings']['filter-4-operator'].value = 'icontains'
|
||||
resp = resp.forms['listing-settings'].submit()
|
||||
assert resp.text.count('>a@localhost</') > 0
|
||||
assert resp.text.count('>b@localhost</') == 0
|
||||
|
||||
|
||||
def test_backoffice_date_filter(pub):
|
||||
pub.user_class.wipe()
|
||||
|
@ -1416,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):
|
||||
|
@ -1761,6 +1821,7 @@ def test_backoffice_block_field_filter(pub):
|
|||
'not_in',
|
||||
'absent',
|
||||
'existing',
|
||||
'icontains',
|
||||
]
|
||||
resp.forms['listing-settings']['filter-0-1-value'].value = 'plop0'
|
||||
resp = resp.forms['listing-settings'].submit()
|
||||
|
@ -1879,6 +1940,7 @@ def test_backoffice_block_field_filter(pub):
|
|||
'not_in',
|
||||
'absent',
|
||||
'existing',
|
||||
'icontains',
|
||||
]
|
||||
resp.forms['listing-settings']['filter-0-5-value'].value = 'a@localhost'
|
||||
resp = resp.forms['listing-settings'].submit()
|
||||
|
@ -1893,6 +1955,10 @@ def test_backoffice_block_field_filter(pub):
|
|||
resp.forms['listing-settings']['filter-0-5-operator'].value = 'ne'
|
||||
resp = resp.forms['listing-settings'].submit()
|
||||
assert resp.text.count('<tr') == 1 + 4
|
||||
resp.forms['listing-settings']['filter-0-5-value'].value = '@localhost'
|
||||
resp.forms['listing-settings']['filter-0-5-operator'].value = 'icontains'
|
||||
resp = resp.forms['listing-settings'].submit()
|
||||
assert resp.text.count('<tr') == 1 + 10
|
||||
|
||||
# mix
|
||||
resp = app.get('/backoffice/management/form-title/')
|
||||
|
@ -1975,3 +2041,76 @@ def test_backoffice_numeric_filter(pub):
|
|||
resp = resp.forms['listing-settings'].submit()
|
||||
assert resp.text.count('<td>123.4</td>') == 0
|
||||
assert resp.text.count('<td>315</td>') > 0
|
||||
|
||||
|
||||
def test_backoffice_criticality_filter(pub):
|
||||
pub.user_class.wipe()
|
||||
create_superuser(pub)
|
||||
pub.role_class.wipe()
|
||||
role = pub.role_class(name='test')
|
||||
role.store()
|
||||
|
||||
Workflow.wipe()
|
||||
workflow = Workflow(name='test')
|
||||
workflow.criticality_levels = [
|
||||
WorkflowCriticalityLevel(name='green'),
|
||||
WorkflowCriticalityLevel(name='yellow'),
|
||||
WorkflowCriticalityLevel(name='red'),
|
||||
WorkflowCriticalityLevel(name='black'),
|
||||
]
|
||||
workflow.add_status('st1')
|
||||
st2 = workflow.add_status('st2')
|
||||
action = st2.add_action('modify_criticality')
|
||||
action.mode = MODE_INC
|
||||
st3 = workflow.add_status('st3')
|
||||
action = st3.add_action('modify_criticality')
|
||||
action.mode = MODE_INC
|
||||
action = st3.add_action('modify_criticality')
|
||||
action.mode = MODE_INC
|
||||
workflow.store()
|
||||
|
||||
FormDef.wipe()
|
||||
formdef = FormDef()
|
||||
formdef.name = 'form-title'
|
||||
formdef.fields = [
|
||||
fields.StringField(id='1', label='Test', type='string', display_locations=['listings']),
|
||||
]
|
||||
formdef.workflow_roles = {'_receiver': role.id}
|
||||
formdef.workflow = workflow
|
||||
formdef.store()
|
||||
|
||||
data_class = formdef.data_class()
|
||||
data_class.wipe()
|
||||
|
||||
for i in range(3):
|
||||
formdata = data_class()
|
||||
formdata.data = {'1': f'baz{i}'}
|
||||
formdata.just_created()
|
||||
formdata.store()
|
||||
if i == 0:
|
||||
formdata.jump_status(st2.id)
|
||||
else:
|
||||
formdata.jump_status(st3.id)
|
||||
formdata.perform_workflow()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/management/form-title/')
|
||||
resp.forms['listing-settings']['filter'] = 'all'
|
||||
resp.forms['listing-settings']['filter-criticality-level'].checked = True
|
||||
resp = resp.forms['listing-settings'].submit()
|
||||
assert resp.text.count('<td>baz') == 3
|
||||
resp.forms['listing-settings']['filter-criticality-level-value'] = '0'
|
||||
resp = resp.forms['listing-settings'].submit()
|
||||
assert resp.text.count('<td>baz') == 0
|
||||
resp.forms['listing-settings']['filter-criticality-level-value'] = '1'
|
||||
resp = resp.forms['listing-settings'].submit()
|
||||
assert resp.text.count('<td>baz') == 1
|
||||
resp.forms['listing-settings']['filter-criticality-level-value'] = '2'
|
||||
resp = resp.forms['listing-settings'].submit()
|
||||
assert resp.text.count('<td>baz') == 2
|
||||
resp.forms['listing-settings']['filter-criticality-level-value'] = '3'
|
||||
resp = resp.forms['listing-settings'].submit()
|
||||
assert resp.text.count('<td>baz') == 0
|
||||
resp.forms['listing-settings']['filter-criticality-level-value'] = ''
|
||||
resp = resp.forms['listing-settings'].submit()
|
||||
assert resp.text.count('<td>baz') == 3
|
||||
|
|
|
@ -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)'
|
||||
)
|
||||
|
|
|
@ -148,6 +148,12 @@ def test_backoffice_statistics_status_select(pub):
|
|||
assert 'Total number of records: 26' in resp.text
|
||||
assert resp.pyquery('ul.resolution-times.status-wf-new li')[0].text == 'Count: 9'
|
||||
|
||||
resp.forms['listing-settings']['filter-%s-value' % field1.id].value = 'baz'
|
||||
resp.forms['listing-settings']['filter-%s-operator' % field1.id].value = 'existing'
|
||||
resp = resp.forms['listing-settings'].submit()
|
||||
assert 'Total number of records: 50' in resp.text
|
||||
assert resp.pyquery('ul.resolution-times.status-wf-new li')[0].text == 'Count: 17'
|
||||
|
||||
resp.forms['listing-settings']['filter-%s-value' % field1.id].value = 'foo'
|
||||
resp.forms['listing-settings']['filter-%s-operator' % field1.id].value = 'eq'
|
||||
resp = resp.forms['listing-settings'].submit()
|
||||
|
|
|
@ -10,6 +10,7 @@ from wcs import fields
|
|||
from wcs.carddef import CardDef
|
||||
from wcs.formdef import FormDef
|
||||
from wcs.qommon.http_request import HTTPRequest
|
||||
from wcs.qommon.ident.password_accounts import PasswordAccount
|
||||
from wcs.workflows import Workflow, WorkflowBackofficeFieldsFormDef
|
||||
from wcs.wscalls import NamedWsCall
|
||||
|
||||
|
@ -161,6 +162,52 @@ def test_backoffice_submission(pub):
|
|||
assert resp.location == 'http://www.example.org/'
|
||||
|
||||
|
||||
def test_backoffice_submission_menu_entry(pub):
|
||||
user = create_user(pub)
|
||||
|
||||
FormDef.wipe()
|
||||
formdef = FormDef()
|
||||
formdef.name = 'form title'
|
||||
formdef.fields = []
|
||||
formdef.workflow_roles = {'_receiver': 1}
|
||||
formdef.backoffice_submission_roles = user.roles[:]
|
||||
formdef.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/management/forms')
|
||||
assert resp.pyquery('#sidepage-menu .icon-submission')
|
||||
|
||||
pub.cfg['backoffice-submission'] = {}
|
||||
pub.cfg['backoffice-submission']['sidebar_menu_entry'] = 'visible'
|
||||
pub.write_cfg()
|
||||
resp = app.get('/backoffice/submission/', status=200)
|
||||
assert resp.pyquery('#sidepage-menu .icon-submission')
|
||||
|
||||
pub.cfg['backoffice-submission']['sidebar_menu_entry'] = 'redirect'
|
||||
pub.cfg['backoffice-submission']['redirect'] = 'https://example.net/'
|
||||
pub.write_cfg()
|
||||
resp = app.get('/backoffice/management/forms')
|
||||
assert resp.pyquery('#sidepage-menu .icon-submission')
|
||||
resp = app.get('/backoffice/submission/', status=302)
|
||||
assert resp.location == 'https://example.net/'
|
||||
|
||||
pub.cfg['backoffice-submission']['sidebar_menu_entry'] = 'hidden'
|
||||
pub.write_cfg()
|
||||
resp = app.get('/backoffice/management/forms')
|
||||
assert not resp.pyquery('#sidepage-menu .icon-submission')
|
||||
resp = app.get('/backoffice/submission/', status=302)
|
||||
assert resp.location == 'https://example.net/'
|
||||
|
||||
pub.cfg['backoffice-submission'][
|
||||
'redirect'
|
||||
] = '{% if session_user_email == "admin@localhost" %}https://example.net/{% endif %}'
|
||||
pub.write_cfg()
|
||||
app.get('/backoffice/submission/', status=302) # redirection
|
||||
user.email = 'admin2@localhost'
|
||||
user.store()
|
||||
app.get('/backoffice/submission/', status=200) # native screen
|
||||
|
||||
|
||||
def test_backoffice_submission_with_tracking_code(pub):
|
||||
user = create_user(pub)
|
||||
|
||||
|
@ -389,8 +436,8 @@ def test_backoffice_parallel_submission(pub, autosave):
|
|||
formdata.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/submission/')
|
||||
assert 'Submission to complete' in resp.text
|
||||
resp = app.get('/backoffice/submission/pending')
|
||||
assert resp.pyquery('tbody tr')
|
||||
resp1 = app.get('/backoffice/submission/form-title/%s/' % formdata.id)
|
||||
resp1 = resp1.follow()
|
||||
resp2 = app.get('/backoffice/submission/form-title/%s/' % formdata.id)
|
||||
|
@ -600,13 +647,16 @@ def test_backoffice_submission_drafts(pub):
|
|||
tracking_code = data_class.select()[0].tracking_code
|
||||
|
||||
# stop here, go back to index
|
||||
pub.cfg['submission-channels'] = {'include-in-global-listing': True}
|
||||
pub.write_cfg()
|
||||
resp = app.get('/backoffice/submission/')
|
||||
assert '%s/%s' % (formdef.url_name, formdata_no) in resp.text
|
||||
assert '>#%s' % formdata_no in resp.text
|
||||
resp = resp.click('Pending submissions')
|
||||
assert resp.pyquery('tbody tr a').text() == formdata.get_display_name()
|
||||
assert resp.pyquery('tbody tr a')[0].attrib['href'] == f'{formdef.url_name}/{formdata_no}/'
|
||||
formdata.submission_channel = 'mail'
|
||||
formdata.store()
|
||||
resp = app.get('/backoffice/submission/')
|
||||
assert '>Mail #%s' % formdata_no in resp.text
|
||||
resp = app.get('/backoffice/submission/pending')
|
||||
assert resp.pyquery('tbody td:nth-child(1)').text() == 'Mail'
|
||||
|
||||
# check it can also be accessed using its final URL
|
||||
resp2 = app.get('/backoffice/management/%s/%s/' % (formdef.url_name, formdata_no))
|
||||
|
@ -628,6 +678,33 @@ def test_backoffice_submission_drafts(pub):
|
|||
assert resp.location == 'http://example.net/backoffice/management/form-title/%s/' % formdata_no
|
||||
|
||||
|
||||
def test_backoffice_draft_with_digest(pub):
|
||||
user = create_user(pub)
|
||||
|
||||
FormDef.wipe()
|
||||
formdef = FormDef()
|
||||
formdef.name = 'form title'
|
||||
formdef.fields = [
|
||||
fields.StringField(id='1', label='1st field', varname='foo'),
|
||||
]
|
||||
formdef.backoffice_submission_roles = user.roles[:]
|
||||
formdef.digest_templates = {'default': 'digest: {{ form_var_foo }}'}
|
||||
formdef.workflow_roles = {'_receiver': 1}
|
||||
formdef.store()
|
||||
|
||||
formdef.data_class().wipe()
|
||||
formdata = formdef.data_class()()
|
||||
formdata.data = {'1': 'bar'}
|
||||
formdata.status = 'draft'
|
||||
formdata.backoffice_submission = True
|
||||
formdata.submission_agent_id = str(user.id)
|
||||
formdata.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/submission/pending')
|
||||
assert resp.pyquery('tbody td:nth-child(1)').text() == 'form title #1-1 digest: bar'
|
||||
|
||||
|
||||
def test_backoffice_submission_remove_drafts(pub):
|
||||
user = create_user(pub)
|
||||
|
||||
|
@ -661,7 +738,7 @@ def test_backoffice_submission_remove_drafts(pub):
|
|||
formdata_no = formdata.id
|
||||
|
||||
# stop here, go back to the index
|
||||
resp = app.get('/backoffice/submission/')
|
||||
resp = app.get('/backoffice/submission/pending')
|
||||
resp = resp.click('#%s' % formdata_no)
|
||||
resp = resp.follow()
|
||||
|
||||
|
@ -673,7 +750,7 @@ def test_backoffice_submission_remove_drafts(pub):
|
|||
assert pub.tracking_code_class().count() == 1
|
||||
|
||||
# and this time for real
|
||||
resp = app.get('/backoffice/submission/')
|
||||
resp = app.get('/backoffice/submission/pending')
|
||||
resp = resp.click('#%s' % formdata_no)
|
||||
resp = resp.follow()
|
||||
resp = resp.click('Discard this form')
|
||||
|
@ -951,48 +1028,6 @@ def test_backoffice_submission_conditional_jump_based_on_bo_field(pub):
|
|||
assert formdef.data_class().select()[0].status == 'wf-st1'
|
||||
|
||||
|
||||
def test_backoffice_submission_sections(pub):
|
||||
user = create_user(pub)
|
||||
|
||||
FormDef.wipe()
|
||||
formdef = FormDef()
|
||||
formdef.name = 'form title'
|
||||
formdef.fields = [
|
||||
fields.StringField(
|
||||
id='1', label='1st field', display_locations=['validation', 'summary', 'listings']
|
||||
),
|
||||
]
|
||||
formdef.backoffice_submission_roles = user.roles[:]
|
||||
formdef.store()
|
||||
|
||||
data_class = formdef.data_class()
|
||||
data_class.wipe()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/submission/')
|
||||
assert 'Submission to complete' not in resp.text
|
||||
assert 'Running submission' not in resp.text
|
||||
|
||||
formdata = data_class()
|
||||
formdata.data = {}
|
||||
formdata.status = 'draft'
|
||||
formdata.backoffice_submission = True
|
||||
formdata.receipt_time = make_aware(datetime.datetime(2015, 1, 1))
|
||||
formdata.store()
|
||||
|
||||
resp = app.get('/backoffice/submission/')
|
||||
assert 'Submission to complete' in resp.text
|
||||
assert 'Running submission' not in resp.text
|
||||
assert '>#%s' % formdata.id in resp.text
|
||||
|
||||
formdata.data = {'1': 'xxx'}
|
||||
formdata.store()
|
||||
resp = app.get('/backoffice/submission/')
|
||||
assert 'Submission to complete' not in resp.text
|
||||
assert 'Running submission' in resp.text
|
||||
assert '>#%s' % formdata.id in resp.text
|
||||
|
||||
|
||||
def test_backoffice_submission_drafts_order(pub):
|
||||
user = create_user(pub)
|
||||
|
||||
|
@ -1011,28 +1046,39 @@ def test_backoffice_submission_drafts_order(pub):
|
|||
data_class.wipe()
|
||||
|
||||
formdata_ids = []
|
||||
for i in range(10):
|
||||
for i in range(25):
|
||||
formdata = data_class()
|
||||
formdata.data = {}
|
||||
formdata.status = 'draft'
|
||||
formdata.backoffice_submission = True
|
||||
formdata.receipt_time = make_aware(datetime.datetime(2023, 11, 20 - i))
|
||||
formdata.receipt_time = make_aware(datetime.datetime(2023, 11, 30 - i))
|
||||
formdata.store()
|
||||
formdata_ids.append(formdata.id)
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/submission/')
|
||||
assert [x.attrib['href'] for x in resp.pyquery('.biglist.empty a:not(.fake)')] == [
|
||||
f'form-title/{x}/' for x in reversed(formdata_ids)
|
||||
resp = app.get('/backoffice/submission/pending')
|
||||
assert [x.attrib['data-link'] for x in resp.pyquery('tbody tr')] == [
|
||||
f'form-title/{x}/' for x in formdata_ids[:20]
|
||||
]
|
||||
|
||||
formdata.receipt_time = None # check a missing receipt_time is ok
|
||||
formdata.store()
|
||||
resp = app.get('/backoffice/submission/')
|
||||
assert [x.attrib['href'] for x in resp.pyquery('.biglist.empty a:not(.fake)')] == [
|
||||
f'form-title/{x}/' for x in reversed(formdata_ids)
|
||||
new_order = [formdata.id] + [x for x in formdata_ids if x != formdata.id]
|
||||
resp = app.get('/backoffice/submission/pending')
|
||||
assert [x.attrib['data-link'] for x in resp.pyquery('tbody tr')] == [
|
||||
f'form-title/{x}/' for x in new_order[:20]
|
||||
]
|
||||
assert 'unknown date' in resp.pyquery('li.smallitem:first').text()
|
||||
|
||||
resp = resp.click('<!--Next Page-->')
|
||||
assert [x.attrib['data-link'] for x in resp.pyquery('tbody tr')] == [
|
||||
f'form-title/{x}/' for x in new_order[20:]
|
||||
]
|
||||
|
||||
# check ajax call result
|
||||
resp = app.get('/backoffice/submission/pending?ajax=true')
|
||||
assert 'appbar' not in resp.text
|
||||
assert '<table' in resp.text
|
||||
assert 'page-links' in resp.text
|
||||
|
||||
|
||||
def test_backoffice_submission_prefill_user(pub):
|
||||
|
@ -1267,7 +1313,7 @@ def test_backoffice_submission_multiple_page_restore_on_validation(pub):
|
|||
assert formdef.data_class().count() == 1
|
||||
formdata = formdef.data_class().select()[0]
|
||||
# restore draft
|
||||
resp = app.get('/backoffice/submission/')
|
||||
resp = app.get('/backoffice/submission/pending')
|
||||
resp = resp.click(href='form-title/%s' % formdata.id)
|
||||
resp = resp.follow()
|
||||
assert 'Check values then click submit.' in resp.text
|
||||
|
@ -2200,3 +2246,50 @@ def test_backoffice_submission_no_roles(pub):
|
|||
assert formdef.data_class().count() == 1
|
||||
formdata = formdef.data_class().select()[0]
|
||||
assert formdata.data == {'1': 'xxx'}
|
||||
|
||||
|
||||
def test_backoffice_submission_then_front(pub):
|
||||
user = create_user(pub)
|
||||
|
||||
front_user = pub.user_class()
|
||||
front_user.name = 'front user'
|
||||
front_user.email = 'test@invalid'
|
||||
front_user.store()
|
||||
account = PasswordAccount(id='front')
|
||||
account.set_password('front')
|
||||
account.user_id = front_user.id
|
||||
account.store()
|
||||
|
||||
FormDef.wipe()
|
||||
formdef = FormDef()
|
||||
formdef.name = 'form title'
|
||||
formdef.fields = [
|
||||
fields.PageField(id='0', label='1st page'),
|
||||
fields.StringField(id='1', label='Field on 1st page'),
|
||||
fields.PageField(id='2', label='2nd page'),
|
||||
fields.StringField(id='3', label='Field on 2nd page'),
|
||||
]
|
||||
formdef.backoffice_submission_roles = user.roles[:]
|
||||
formdef.workflow_roles = {'_receiver': 1}
|
||||
formdef.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/submission/')
|
||||
resp = resp.click(formdef.name)
|
||||
|
||||
resp.form['user_id'] = str(front_user.id) # happens via javascript
|
||||
resp.form['submission_channel'] = 'phone'
|
||||
resp.form['f1'] = 'test submission'
|
||||
resp = resp.form.submit('submit') # -> 2nd page
|
||||
resp.form['f3'] = 'baz'
|
||||
resp = resp.form.submit('submit') # -> validation page
|
||||
resp = resp.form.submit('submit') # final submit
|
||||
|
||||
formdata = formdef.data_class().get(resp.location.split('/')[-2])
|
||||
resp = login(get_app(pub), username='front', password='front').get(formdata.get_url())
|
||||
assert (
|
||||
resp.pyquery('.text-form-recorded').text()
|
||||
== f'The form has been recorded on {formdata.receipt_time.strftime("%Y-%m-%d %H:%M")} '
|
||||
f'with the number {formdata.get_display_id()}. It has been submitted for you by '
|
||||
f'admin after a phone call.'
|
||||
)
|
||||
|
|
|
@ -118,7 +118,8 @@ def test_workflow_inspect_page(pub):
|
|||
|
||||
resp = app.get('/backoffice/workflows/%s/inspect' % workflow.id)
|
||||
assert (
|
||||
'<span class="parameter">Model:</span> '
|
||||
'<span class="parameter">Model:</span> File</li>'
|
||||
'<li class="parameter-model_file">'
|
||||
'<a href="status/st3/items/_export_to/?file=model_file">test.odt</a></li>'
|
||||
) in resp.text
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import time
|
|||
import urllib.parse
|
||||
import xml.etree.ElementTree as ET
|
||||
import zipfile
|
||||
import zoneinfo
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
@ -515,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'
|
||||
|
||||
|
@ -1282,7 +1283,7 @@ def test_form_multi_page_page_name_as_title(pub):
|
|||
next_page = next_page.forms[0].submit('submit')
|
||||
assert_current_page(next_page, 'Validating')
|
||||
assert 'Check values then click submit.' in next_page.text
|
||||
assert next_page.text.count('1st page') == 2 # in steps and in main body
|
||||
assert next_page.text.count('1st page') == 3 # in steps (twice) and in main body
|
||||
|
||||
# add a comment that will not be displayed and should therefore not be
|
||||
# considered.
|
||||
|
@ -1305,7 +1306,48 @@ def test_form_multi_page_page_name_as_title(pub):
|
|||
next_page = next_page.forms[0].submit('submit')
|
||||
assert_current_page(next_page, 'Validating')
|
||||
assert 'Check values then click submit.' in next_page.text
|
||||
assert next_page.text.count('1st page') == 2 # in steps and in main body
|
||||
assert next_page.text.count('1st page') == 3 # in steps (twice) and in main body
|
||||
|
||||
|
||||
def test_form_multi_page_go_back(pub):
|
||||
formdef = create_formdef()
|
||||
formdef.fields = [
|
||||
fields.PageField(id='0', label='1st page'),
|
||||
fields.StringField(id='1', label='string'),
|
||||
fields.PageField(id='2', label='2nd page'),
|
||||
fields.PageField(id='3', label='3rd page'),
|
||||
fields.StringField(id='4', label='string 2'),
|
||||
]
|
||||
formdef.store()
|
||||
formdef.data_class().wipe()
|
||||
|
||||
resp = get_app(pub).get('/test/')
|
||||
resp.forms[0]['f1'] = 'foo'
|
||||
resp = resp.forms[0].submit('submit') # -> 2nd page
|
||||
assert_current_page(resp, '2nd page')
|
||||
resp = resp.forms[0].submit('submit') # -> 3rd page
|
||||
assert_current_page(resp, '3rd page')
|
||||
resp.forms[0]['f4'] = 'foo'
|
||||
resp = resp.forms[0].submit('submit') # -> validation page
|
||||
assert_current_page(resp, 'Validating')
|
||||
|
||||
# go back to second page (javascript would set this)
|
||||
resp.forms[0]['previous-page-id'] = '2'
|
||||
resp = resp.forms[0].submit('previous')
|
||||
assert_current_page(resp, '2nd page')
|
||||
resp = resp.forms[0].submit('submit') # -> 3rd page
|
||||
|
||||
# go back to first page (javascript would set this)
|
||||
resp.forms[0]['previous-page-id'] = '0'
|
||||
resp = resp.forms[0].submit('previous')
|
||||
assert_current_page(resp, '1st page')
|
||||
resp = resp.forms[0].submit('submit') # -> 2nd page
|
||||
resp = resp.forms[0].submit('submit') # -> 3rd page
|
||||
|
||||
# go back to invalid page (javascript would not set this)
|
||||
resp.forms[0]['previous-page-id'] = '10'
|
||||
resp = resp.forms[0].submit('previous')
|
||||
assert_current_page(resp, '1st page') # fallback to first page
|
||||
|
||||
|
||||
def test_form_submit_with_user(pub, emails):
|
||||
|
@ -1320,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']
|
||||
|
@ -3231,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)
|
||||
|
@ -3366,7 +3450,16 @@ def test_logged_errors(pub):
|
|||
)
|
||||
)[0]
|
||||
assert error.occurences_count == 2
|
||||
assert error.expression == '2//0'
|
||||
assert error.context == {
|
||||
'stack': [
|
||||
{
|
||||
'condition': '2//0',
|
||||
'condition_type': 'python',
|
||||
'source_label': 'Automatic Jump',
|
||||
'source_url': 'http://example.net/backoffice/workflows/12/status/just_submitted/items/_jump/',
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
assert pub.loggederror_class.count([Equal('formdef_id', '34')]) == 1
|
||||
assert pub.loggederror_class.count([Equal('formdef_id', 'X')]) == 0
|
||||
|
@ -3898,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
|
||||
|
@ -5088,10 +5197,33 @@ 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.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)
|
||||
|
||||
|
@ -6011,3 +6143,129 @@ def test_form_errors_summary(pub):
|
|||
resp = resp.forms[0].submit('submit')
|
||||
assert 'The following field has an error: testblock' in resp.pyquery('.errornotice').text()
|
||||
assert resp.pyquery('.error').text() == 'required field required field '
|
||||
|
||||
|
||||
def test_form_submit_no_csrf(pub):
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test'
|
||||
formdef.fields = [fields.StringField(id='0', label='string')]
|
||||
formdef.confirmation = False
|
||||
formdef.store()
|
||||
formdef.data_class().wipe()
|
||||
|
||||
create_user(pub)
|
||||
app = get_app(pub)
|
||||
login(app, username='foo', password='foo')
|
||||
resp = app.get(formdef.get_url())
|
||||
resp.form['f0'] = 'hello'
|
||||
# get expected data
|
||||
form_data = {x: y for x, y in resp.form.submit_fields('submit')}
|
||||
# remove token values
|
||||
form_data['_form_id'] = 'xxx'
|
||||
form_data['_ajax_form_token'] = 'xxx'
|
||||
form_data['magictoken'] = 'xxx'
|
||||
# simulate call from remote/attacker site (form token prevents this)
|
||||
resp = app.post(formdef.get_url(), params=form_data)
|
||||
assert 'The form you have submitted is invalid.' in resp.text
|
||||
|
||||
# with confirmation page
|
||||
formdef.confirmation = True
|
||||
formdef.store()
|
||||
resp = app.get(formdef.get_url())
|
||||
resp.form['f0'] = 'hello'
|
||||
resp = resp.form.submit('submit')
|
||||
# get expected data
|
||||
form_data = {x: y for x, y in resp.form.submit_fields('submit')}
|
||||
# remove token values
|
||||
form_data['_form_id'] = 'xxx'
|
||||
form_data['_ajax_form_token'] = 'xxx'
|
||||
form_data['magictoken'] = 'xxx'
|
||||
# simulate call from remote/attacker site (magictoken prevents this)
|
||||
resp = app.post(formdef.get_url(), params=form_data, status=302)
|
||||
assert resp.location == formdef.get_url()
|
||||
|
||||
# with multiple pages
|
||||
formdef.confirmation = False
|
||||
formdef.fields = [
|
||||
fields.PageField(id='1', label='page1'),
|
||||
fields.PageField(id='2', label='page2'),
|
||||
fields.StringField(id='3', label='string'),
|
||||
]
|
||||
formdef.store()
|
||||
resp = app.get(formdef.get_url())
|
||||
resp = resp.form.submit('submit')
|
||||
resp.form['f3'] = 'hello'
|
||||
# get expected data
|
||||
form_data = {x: y for x, y in resp.form.submit_fields('submit')}
|
||||
# remove token values
|
||||
form_data['_form_id'] = 'xxx'
|
||||
form_data['_ajax_form_token'] = 'xxx'
|
||||
form_data['magictoken'] = 'xxx'
|
||||
|
||||
# simulate call from remote/attacker site (magictokens prevents this)
|
||||
resp = app.post(formdef.get_url(), params=form_data, status=302)
|
||||
assert resp.location == formdef.get_url()
|
||||
|
||||
|
||||
def test_form_submit_no_csrf_suddenly_single_page(pub):
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test'
|
||||
formdef.fields = [
|
||||
fields.PageField(id='1', label='page1'),
|
||||
fields.ComputedField(id='2', label='computed', varname='plop', value_template='{{ "plop" }}'),
|
||||
fields.PageField(
|
||||
id='3', label='page2', condition={'type': 'django', 'value': 'form_var_plop != "plop"'}
|
||||
),
|
||||
]
|
||||
formdef.confirmation = False
|
||||
formdef.store()
|
||||
formdef.data_class().wipe()
|
||||
|
||||
create_user(pub)
|
||||
app = get_app(pub)
|
||||
login(app, username='foo', password='foo')
|
||||
resp = app.get(formdef.get_url())
|
||||
resp = resp.form.submit('submit').follow()
|
||||
assert formdef.data_class().select()[0].status == 'wf-new'
|
||||
|
||||
|
||||
def test_form_submit_timezone(pub):
|
||||
pub.load_site_options()
|
||||
if not pub.site_options.has_section('options'):
|
||||
pub.site_options.add_section('options')
|
||||
pub.site_options.set('options', 'timezone', 'Brazil/East')
|
||||
with open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w') as fd:
|
||||
pub.site_options.write(fd)
|
||||
|
||||
formdef = create_formdef()
|
||||
formdef.data_class().wipe()
|
||||
app = get_app(pub)
|
||||
resp = app.get('/test/')
|
||||
resp = resp.forms[0].submit('submit') # -> validation
|
||||
resp = resp.forms[0].submit('submit').follow() # -> done
|
||||
formdata = formdef.data_class().select()[0]
|
||||
assert formdata.receipt_time.astimezone(zoneinfo.ZoneInfo('Brazil/East')).strftime('%H:%M') in resp.text
|
||||
assert (
|
||||
formdata.receipt_time.astimezone(zoneinfo.ZoneInfo('Europe/Paris')).strftime('%H:%M') not in resp.text
|
||||
)
|
||||
|
||||
pub.site_options.set('options', 'timezone', 'Europe/Paris')
|
||||
with open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w') as fd:
|
||||
pub.site_options.write(fd)
|
||||
|
||||
resp = app.get(formdata.get_url())
|
||||
assert (
|
||||
formdata.receipt_time.astimezone(zoneinfo.ZoneInfo('Brazil/East')).strftime('%H:%M') not in resp.text
|
||||
)
|
||||
assert formdata.receipt_time.astimezone(zoneinfo.ZoneInfo('Europe/Paris')).strftime('%H:%M') in resp.text
|
||||
|
||||
# do not crash on invalid timezone
|
||||
pub.site_options.set('options', 'timezone', 'invalid')
|
||||
with open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w') as fd:
|
||||
pub.site_options.write(fd)
|
||||
|
||||
resp = app.get(formdata.get_url())
|
||||
assert (
|
||||
formdata.receipt_time.astimezone(zoneinfo.ZoneInfo('Brazil/East')).strftime('%H:%M') not in resp.text
|
||||
)
|
||||
assert formdata.receipt_time.astimezone(zoneinfo.ZoneInfo('Europe/Paris')).strftime('%H:%M') in resp.text
|
||||
|
|
|
@ -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()
|
||||
|
@ -2652,6 +2691,92 @@ def test_block_prefill_full_block_email(pub):
|
|||
}
|
||||
|
||||
|
||||
def test_block_prefill_full_block_card_item(pub):
|
||||
FormDef.wipe()
|
||||
BlockDef.wipe()
|
||||
CardDef.wipe()
|
||||
create_user(pub)
|
||||
|
||||
carddef = CardDef()
|
||||
carddef.name = 'Test'
|
||||
carddef.fields = [
|
||||
fields.StringField(id='0', label='blah', varname='blah'),
|
||||
]
|
||||
carddef.digest_templates = {'default': '{{ form_var_blah|upper }}'}
|
||||
carddef.store()
|
||||
carddef.data_class().wipe()
|
||||
|
||||
carddata1 = carddef.data_class()()
|
||||
carddata1.data = {'0': 'bar'}
|
||||
carddata1.just_created()
|
||||
carddata1.store()
|
||||
|
||||
block = BlockDef()
|
||||
block.name = 'foobar'
|
||||
block.fields = [
|
||||
fields.ItemField(
|
||||
id='123',
|
||||
required=False,
|
||||
hint='-----',
|
||||
label='Test',
|
||||
varname='plop',
|
||||
data_source={'type': 'carddef:test'},
|
||||
),
|
||||
]
|
||||
block.digest_template = '{{block_var_plop}}'
|
||||
block.store()
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'form title'
|
||||
formdef.fields = [
|
||||
fields.PageField(id='0', label='1st page'),
|
||||
fields.PageField(id='2', label='2nd page'),
|
||||
fields.BlockField(
|
||||
id='1',
|
||||
label='test',
|
||||
block_slug='foobar',
|
||||
max_items=5,
|
||||
prefill={
|
||||
'type': 'string',
|
||||
'value': '{% block_value plop="1" %}',
|
||||
},
|
||||
),
|
||||
]
|
||||
formdef.store()
|
||||
formdef.data_class().wipe()
|
||||
|
||||
app = get_app(pub)
|
||||
login(app, username='foo', password='foo')
|
||||
resp = app.get(formdef.get_url())
|
||||
resp = resp.form.submit('submit') # -> page 2
|
||||
assert resp.form['f1$element0$f123'].value == '1'
|
||||
resp = resp.form.submit('submit') # validation
|
||||
resp = resp.form.submit('submit') # done
|
||||
assert formdef.data_class().select()[0].data == {
|
||||
'1': {
|
||||
'data': [
|
||||
{'123': '1', '123_display': 'BAR', '123_structured': {'blah': 'bar', 'id': 1, 'text': 'BAR'}}
|
||||
],
|
||||
'schema': {'123': 'item'},
|
||||
},
|
||||
'1_display': 'BAR',
|
||||
}
|
||||
|
||||
# prefill with unknown value
|
||||
pub.loggederror_class.wipe()
|
||||
formdef.fields[2].prefill['value'] = '{% block_value plop="123" %}'
|
||||
formdef.store()
|
||||
formdef.data_class().wipe()
|
||||
resp = app.get(formdef.get_url())
|
||||
resp = resp.form.submit('submit') # -> page 2
|
||||
assert not resp.form['f1$element0$f123'].value
|
||||
assert pub.loggederror_class.count() == 1
|
||||
assert (
|
||||
pub.loggederror_class.select()[0].summary
|
||||
== 'invalid value when creating block: unknown card value (\'123\')'
|
||||
)
|
||||
|
||||
|
||||
def test_block_titles_and_empty_block_on_summary_page(pub, emails):
|
||||
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()
|
||||
|
@ -813,3 +857,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
|
||||
|
@ -238,6 +239,42 @@ def test_form_file_field_image_submit(pub):
|
|||
assert '<img alt="" src="tempfile?' not in resp.text
|
||||
|
||||
|
||||
def test_form_file_field_html_submit(pub):
|
||||
FormDef.wipe()
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test'
|
||||
formdef.fields = [fields.FileField(id='0', label='file')]
|
||||
formdef.store()
|
||||
formdef.data_class().wipe()
|
||||
|
||||
html_content = b'<html><body>hello</body></html>'
|
||||
upload = Upload('test.html', html_content, 'text/html')
|
||||
|
||||
app = get_app(pub)
|
||||
resp = app.get('/test/')
|
||||
resp.forms[0]['f0$file'] = upload
|
||||
resp = resp.forms[0].submit('submit')
|
||||
assert 'Check values then click submit.' in resp.text
|
||||
tempfile_id = resp.pyquery('.fileinfo .filename a').attr.href.split('=')[1]
|
||||
|
||||
resp_tempfile = app.get('/test/tempfile?t=%s' % tempfile_id)
|
||||
assert resp_tempfile.body == html_content
|
||||
|
||||
resp = resp.form.submit('submit').follow()
|
||||
assert resp.click('test.html').follow().content_type == 'text/html'
|
||||
assert resp.click('test.html').follow().body == html_content
|
||||
|
||||
# check it's also served raw from backoffice
|
||||
user = create_user(pub)
|
||||
user.is_admin = True
|
||||
user.store()
|
||||
app = get_app(pub)
|
||||
login(app, username='foo', password='foo')
|
||||
resp = app.get(formdef.data_class().select()[0].get_backoffice_url())
|
||||
assert resp.click('test.html').follow().content_type == 'text/html'
|
||||
assert resp.click('test.html').follow().body == html_content
|
||||
|
||||
|
||||
def test_form_file_field_submit_document_type(pub):
|
||||
FormDef.wipe()
|
||||
formdef = FormDef()
|
||||
|
@ -427,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):
|
||||
|
@ -455,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>'''
|
||||
|
@ -537,3 +640,42 @@ def test_form_file_field_in_block_aria_description(pub):
|
|||
resp.pyquery.find('#' + resp.pyquery('[aria-describedby]').attr['aria-describedby']).text()
|
||||
== 'field label'
|
||||
)
|
||||
|
||||
|
||||
def test_file_download_url_on_wrong_field(pub):
|
||||
FormDef.wipe()
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test'
|
||||
formdef.fields = [fields.StringField(id='1', label='str1')]
|
||||
formdef.store()
|
||||
formdef.data_class().wipe()
|
||||
|
||||
create_user(pub)
|
||||
app = get_app(pub)
|
||||
login(app, username='foo', password='foo')
|
||||
resp = app.get(formdef.get_url())
|
||||
resp.form['f1'] = 'test'
|
||||
resp = resp.form.submit('submit') # -> validation
|
||||
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
|
||||
|
|
|
@ -18,10 +18,14 @@ from wcs.data_sources import NamedDataSource
|
|||
from wcs.formdef import FormDef
|
||||
from wcs.qommon.form import UploadedFile
|
||||
from wcs.qommon.misc import ConnectionError
|
||||
from wcs.wf.create_formdata import Mapping
|
||||
from wcs.wf.export_to_model import transform_to_pdf
|
||||
from wcs.wf.form import WorkflowFormFieldsFormDef
|
||||
from wcs.workflows import ContentSnapshotPart, Workflow, WorkflowBackofficeFieldsFormDef
|
||||
from wcs.workflows import (
|
||||
AttachmentEvolutionPart,
|
||||
ContentSnapshotPart,
|
||||
Workflow,
|
||||
WorkflowBackofficeFieldsFormDef,
|
||||
)
|
||||
from wcs.wscalls import NamedWsCall
|
||||
|
||||
from ..utilities import clean_temporary_pub, create_temporary_pub, get_app, login
|
||||
|
@ -258,7 +262,7 @@ def test_formdata_attachment_download_to_backoffice_file_field_only(pub):
|
|||
attach = st1.add_action('addattachment', id='_attach')
|
||||
attach.by = ['_submitter']
|
||||
attach.backoffice_filefield_id = 'bo1'
|
||||
attach.attach_to_history = False # store only in backoffice field
|
||||
attach.attach_to_history = False # do not display in history
|
||||
wf.store()
|
||||
|
||||
assert attach.get_backoffice_filefield_options() == [('bo1', 'bo field 1', 'bo1')]
|
||||
|
@ -291,11 +295,56 @@ def test_formdata_attachment_download_to_backoffice_file_field_only(pub):
|
|||
assert bo1.content_type == 'text/plain'
|
||||
assert bo1.get_content() == b'foobar'
|
||||
|
||||
# but nothing in history
|
||||
# nothing displayed in history
|
||||
resp = resp.follow()
|
||||
assert 'resp.text' not in resp.text
|
||||
assert len(formdata.evolution) == 2
|
||||
assert len(formdata.evolution[0].parts) == 1
|
||||
assert isinstance(formdata.evolution[0].parts[0], ContentSnapshotPart)
|
||||
assert formdata.evolution[1].parts is None
|
||||
|
||||
# but attachment stored
|
||||
assert isinstance(formdata.evolution[1].parts[0], AttachmentEvolutionPart)
|
||||
|
||||
|
||||
def test_formdata_attachment_stored(pub):
|
||||
create_user(pub)
|
||||
wf = Workflow(name='status')
|
||||
st1 = wf.add_status('Status1', 'st1')
|
||||
attach = st1.add_action('addattachment', id='_attach')
|
||||
attach.by = ['_submitter']
|
||||
attach.backoffice_filefield_id = None # do not store as backoffice field
|
||||
attach.attach_to_history = False # do not display in history
|
||||
wf.store()
|
||||
|
||||
FormDef.wipe()
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test'
|
||||
formdef.workflow_id = wf.id
|
||||
formdef.fields = []
|
||||
formdef.store()
|
||||
formdef.data_class().wipe()
|
||||
|
||||
resp = login(get_app(pub), username='foo', password='foo').get('/test/')
|
||||
resp = resp.forms[0].submit('submit')
|
||||
assert 'Check values then click submit.' in resp.text
|
||||
resp = resp.forms[0].submit('submit')
|
||||
assert resp.status_int == 302
|
||||
resp = resp.follow()
|
||||
assert 'The form has been recorded' in resp.text
|
||||
|
||||
resp.forms[0]['attachment_attach$file'] = Upload('test.txt', b'foobar', 'text/plain')
|
||||
resp = resp.forms[0].submit('button_attach')
|
||||
|
||||
# nothing displayed in history
|
||||
resp = resp.follow()
|
||||
assert 'resp.text' not in resp.text
|
||||
formdata = formdef.data_class().select()[0]
|
||||
assert len(formdata.evolution) == 2
|
||||
assert len(formdata.evolution[0].parts) == 1
|
||||
assert isinstance(formdata.evolution[0].parts[0], ContentSnapshotPart)
|
||||
|
||||
# but attachment stored
|
||||
assert isinstance(formdata.evolution[1].parts[0], AttachmentEvolutionPart)
|
||||
|
||||
|
||||
def test_formdata_attachment_file_options(pub):
|
||||
|
@ -1540,6 +1589,57 @@ def test_formdata_named_wscall_in_conditions(http_requests, pub):
|
|||
assert http_requests.count() == 1
|
||||
|
||||
|
||||
def test_formdata_error_with_wscall_in_conditions(http_requests, pub):
|
||||
create_user(pub)
|
||||
NamedWsCall.wipe()
|
||||
|
||||
wscall = NamedWsCall()
|
||||
wscall.name = 'Hello world'
|
||||
wscall.request = {'url': 'http://remote.example.net/404', 'method': 'GET'}
|
||||
wscall.record_on_errors = True
|
||||
wscall.store()
|
||||
assert wscall.slug == 'hello_world'
|
||||
|
||||
FormDef.wipe()
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test'
|
||||
formdef.fields = [
|
||||
fields.PageField(id='0', label='1st page'),
|
||||
fields.PageField(
|
||||
id='1',
|
||||
label='2nd page',
|
||||
condition={'type': 'python', 'value': 'webservice.hello_world["foo"] == "bar"'},
|
||||
),
|
||||
]
|
||||
formdef.store()
|
||||
formdef.data_class().wipe()
|
||||
|
||||
pub.loggederror_class.wipe()
|
||||
resp = login(get_app(pub), username='foo', password='foo').get('/test/')
|
||||
assert '>1st page<' in resp.text
|
||||
assert '>2nd page<' in resp.text
|
||||
|
||||
# condition error and wscall error
|
||||
assert pub.loggederror_class.count() == 2
|
||||
wscall_error, condition_error = pub.loggederror_class.select(order_by='id')
|
||||
assert (
|
||||
wscall_error.context
|
||||
== condition_error.context
|
||||
== {
|
||||
'stack': [
|
||||
{
|
||||
'condition': 'webservice.hello_world["foo"] == "bar"',
|
||||
'condition_type': 'python',
|
||||
'source_label': 'Field: 2nd page',
|
||||
'source_url': 'http://example.net/backoffice/forms/1/fields/1/',
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
assert wscall_error.summary == '[WSCALL] 404 Not Found'
|
||||
assert condition_error.summary == 'Failed to evaluate condition'
|
||||
|
||||
|
||||
def test_formdata_named_wscall_in_comment(pub):
|
||||
create_user(pub)
|
||||
NamedWsCall.wipe()
|
||||
|
@ -1730,131 +1830,6 @@ def test_formdata_evolution_register_comment_to_with_attachment(pub):
|
|||
]
|
||||
|
||||
|
||||
def test_create_formdata_show_link_in_history(pub):
|
||||
FormDef.wipe()
|
||||
pub.tracking_code_class.wipe()
|
||||
|
||||
target_formdef = FormDef()
|
||||
target_formdef.name = 'target-form'
|
||||
target_formdef.fields = [
|
||||
fields.StringField(id='0', label='string', varname='foo_string'),
|
||||
]
|
||||
target_formdef.store()
|
||||
|
||||
wf = Workflow(name='create-formdata')
|
||||
wf.possible_status = Workflow.get_default_workflow().possible_status[:]
|
||||
create = wf.possible_status[1].add_action('create_formdata', id='_create', prepend=True)
|
||||
create.label = 'create a new linked form'
|
||||
create.varname = 'resubmitted'
|
||||
create.mappings = [
|
||||
Mapping(field_id='0', expression='="coincoin"'),
|
||||
]
|
||||
wf.store()
|
||||
|
||||
source_formdef = FormDef()
|
||||
source_formdef.name = 'source-form'
|
||||
source_formdef.fields = []
|
||||
source_formdef.workflow_id = wf.id
|
||||
source_formdef.enable_tracking_codes = True
|
||||
source_formdef.store()
|
||||
|
||||
create.formdef_slug = target_formdef.url_name
|
||||
create.attach_to_history = True
|
||||
wf.store()
|
||||
|
||||
source_formdef.data_class().wipe()
|
||||
target_formdef.data_class().wipe()
|
||||
|
||||
create_user(pub)
|
||||
app = login(get_app(pub), username='foo', password='foo')
|
||||
resp = app.get('/source-form/')
|
||||
resp = resp.forms[0].submit('submit')
|
||||
assert 'Check values then click submit.' in resp.text
|
||||
resp = resp.forms[0].submit('submit')
|
||||
assert resp.status_int == 302
|
||||
resp = resp.follow()
|
||||
assert 'The form has been recorded' in resp.text
|
||||
|
||||
formdata = source_formdef.data_class().select()[0]
|
||||
|
||||
# logged access: show link to created formdata
|
||||
resp = app.get('/source-form/%s/' % formdata.id)
|
||||
assert 'The form has been recorded on' in resp.text
|
||||
assert 'New form "target-form" created' in resp.text
|
||||
assert resp.pyquery('.wf-links a')
|
||||
|
||||
# anonymous access via tracking code: no link
|
||||
app = get_app(pub)
|
||||
resp = app.get('/code/%s/load' % formdata.tracking_code)
|
||||
resp = resp.follow()
|
||||
assert 'The form has been recorded on' in resp.text
|
||||
assert 'New form "target-form" created' not in resp.text
|
||||
assert not resp.pyquery('.wf-links a')
|
||||
|
||||
|
||||
def test_create_formdata_multiple(pub):
|
||||
FormDef.wipe()
|
||||
pub.tracking_code_class.wipe()
|
||||
|
||||
target_formdef = FormDef()
|
||||
target_formdef.name = 'target-form'
|
||||
target_formdef.fields = [
|
||||
fields.StringField(id='0', label='string', varname='foo_string'),
|
||||
]
|
||||
target_formdef.store()
|
||||
|
||||
wf = Workflow(name='create-formdata')
|
||||
wf.possible_status = Workflow.get_default_workflow().possible_status[:]
|
||||
global_action = wf.add_global_action('create formdata')
|
||||
trigger = global_action.triggers[0]
|
||||
trigger.roles = ['_submitter']
|
||||
create = global_action.add_action('create_formdata')
|
||||
create.label = 'create a new linked form'
|
||||
create.varname = 'resubmitted'
|
||||
create.mappings = [Mapping(field_id='0', expression='plop')]
|
||||
wf.store()
|
||||
|
||||
source_formdef = FormDef()
|
||||
source_formdef.name = 'source-form'
|
||||
source_formdef.fields = []
|
||||
source_formdef.workflow_id = wf.id
|
||||
source_formdef.enable_tracking_codes = True
|
||||
source_formdef.store()
|
||||
|
||||
create.formdef_slug = target_formdef.url_name
|
||||
wf.store()
|
||||
|
||||
source_formdef.data_class().wipe()
|
||||
target_formdef.data_class().wipe()
|
||||
|
||||
user = create_user(pub)
|
||||
|
||||
formdata = source_formdef.data_class()()
|
||||
formdata.user_id = user.id
|
||||
formdata.just_created()
|
||||
formdata.store()
|
||||
|
||||
formdata2 = source_formdef.data_class()()
|
||||
formdata2.user_id = user.id
|
||||
formdata2.just_created()
|
||||
formdata2.store()
|
||||
|
||||
app = login(get_app(pub), username='foo', password='foo')
|
||||
resp = app.get(formdata.get_url())
|
||||
|
||||
resp = resp.form.submit('button-action-1')
|
||||
assert target_formdef.data_class().count() == 1
|
||||
|
||||
resp = app.get(formdata.get_url())
|
||||
resp = resp.form.submit('button-action-1')
|
||||
assert target_formdef.data_class().count() == 2
|
||||
|
||||
# do it from another formdata (should not trigger recursive call detection)
|
||||
resp = app.get(formdata2.get_url())
|
||||
resp = resp.form.submit('button-action-1')
|
||||
assert target_formdef.data_class().count() == 3
|
||||
|
||||
|
||||
def test_include_authors_in_form_history(pub):
|
||||
user, admin = create_user_and_admin(pub)
|
||||
pub.role_class.wipe()
|
||||
|
|
|
@ -86,9 +86,18 @@ def test_i18n_form(pub, user, emails, http_requests):
|
|||
formdef = FormDef()
|
||||
formdef.name = 'test form'
|
||||
formdef.fields = [
|
||||
PageField(id='0', label='page field'),
|
||||
PageField(
|
||||
id='0',
|
||||
label='page field',
|
||||
post_conditions=[
|
||||
{
|
||||
'condition': {'type': 'django', 'value': 'form_var_text == "test"'},
|
||||
'error_message': 'page error message',
|
||||
},
|
||||
],
|
||||
),
|
||||
# label has a trailing white space to check for strip()
|
||||
StringField(id='1', label='text field ', hint='an hint text'),
|
||||
StringField(id='1', label='text field ', hint='an hint text', varname='text'),
|
||||
ItemField(
|
||||
id='2',
|
||||
label='list field',
|
||||
|
@ -126,6 +135,7 @@ def test_i18n_form(pub, user, emails, http_requests):
|
|||
('Notification Body', 'Contenu de notification'),
|
||||
('an hint text', 'un texte d’aide'),
|
||||
('a second hint text', 'un deuxième texte d’aide'),
|
||||
('page error message', 'message d’erreur de page'),
|
||||
):
|
||||
msg = TranslatableMessage()
|
||||
msg.string = en
|
||||
|
@ -152,6 +162,10 @@ def test_i18n_form(pub, user, emails, http_requests):
|
|||
assert resp.pyquery('#form_label_f1').text() == 'champ texte*'
|
||||
assert resp.pyquery('option:nth-child(3)').text() == 'deuxième'
|
||||
|
||||
resp.form['f1'] = 'xxx'
|
||||
resp = resp.form.submit('submit', headers={'Accept-Language': 'fr'})
|
||||
assert 'message d’erreur de page' in resp.pyquery('.global-errors').text()
|
||||
|
||||
resp.form['f1'] = 'test'
|
||||
resp.form['f2'] = 'second'
|
||||
resp.form['f3$element0'] = True
|
||||
|
|
|
@ -604,10 +604,10 @@ def test_item_field_from_custom_view_on_cards(pub):
|
|||
resp = app.get(formdef.data_class().select()[0].get_url())
|
||||
assert resp.pyquery('.field-type-item .value').text() == 'Yattr%sZ' % baz_id
|
||||
|
||||
# remove card (back to value stored at first)
|
||||
# remove card, the value is still displayed
|
||||
carddef.data_class().wipe()
|
||||
resp = app.get(formdef.data_class().select()[0].get_url())
|
||||
assert resp.pyquery('.field-type-item .value').text() == 'Xattr%sY' % baz_id
|
||||
assert resp.pyquery('.field-type-item .value').text() == 'Yattr%sZ' % baz_id
|
||||
|
||||
|
||||
def test_item_field_from_custom_view_on_cards_filter_status(pub):
|
||||
|
|
|
@ -777,6 +777,7 @@ def test_field_live_select_autocomplete_jsonvalue_prefill(pub, http_requests):
|
|||
assert resp.pyquery('[data-field-id="1"][data-live-source]')
|
||||
|
||||
|
||||
@pytest.mark.skipif('JOB_NAME' in os.environ, reason='jenkins python segfault')
|
||||
def test_field_live_select(pub, http_requests):
|
||||
FormDef.wipe()
|
||||
formdef = FormDef()
|
||||
|
@ -1777,6 +1778,102 @@ def test_dynamic_item_field_from_custom_view_on_cards(pub, field_type):
|
|||
assert logged_error.summary == '[DATASOURCE] Unknown custom view "as-data-source" for CardDef "items"'
|
||||
|
||||
|
||||
def test_dynamic_date_field_from_custom_view_on_cards(pub):
|
||||
pub.role_class.wipe()
|
||||
pub.custom_view_class.wipe()
|
||||
|
||||
user = create_user(pub)
|
||||
role = pub.role_class(name='xxx')
|
||||
role.store()
|
||||
user.roles = [role.id]
|
||||
user.is_admin = True
|
||||
user.store()
|
||||
|
||||
FormDef.wipe()
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test'
|
||||
formdef.fields = []
|
||||
formdef.store()
|
||||
formdef.data_class().wipe()
|
||||
|
||||
CardDef.wipe()
|
||||
carddef = CardDef()
|
||||
carddef.name = 'items'
|
||||
carddef.digest_templates = {'default': '{{form_var_attr}}'}
|
||||
carddef.workflow_roles = {'_editor': user.roles[0]}
|
||||
carddef.fields = [
|
||||
fields.StringField(id='1', label='string', varname='attr'),
|
||||
fields.DateField(id='2', label='date'),
|
||||
]
|
||||
carddef.store()
|
||||
carddef.data_class().wipe()
|
||||
for i, value in enumerate(['foo', 'bar', 'baz']):
|
||||
carddata = carddef.data_class()()
|
||||
carddata.data = {
|
||||
'1': value,
|
||||
'2': datetime.date(2024, 1, 1 + i).timetuple(),
|
||||
}
|
||||
carddata.just_created()
|
||||
carddata.store()
|
||||
|
||||
# create custom view
|
||||
app = login(get_app(pub), username='foo', password='foo')
|
||||
|
||||
resp = app.get('/backoffice/data/items/')
|
||||
resp.forms['listing-settings']['filter-2'].checked = True
|
||||
resp.forms['listing-settings']['filter-status'].checked = True
|
||||
resp = resp.forms['listing-settings'].submit()
|
||||
|
||||
resp.forms['listing-settings']['filter'].value = 'recorded'
|
||||
resp = resp.forms['listing-settings'].submit()
|
||||
|
||||
resp.forms['save-custom-view']['title'] = 'as data source'
|
||||
resp.forms['save-custom-view']['visibility'] = 'datasource'
|
||||
resp = resp.forms['save-custom-view'].submit().follow()
|
||||
|
||||
# make sure <input type=date> is not used, so a template can be entered
|
||||
assert resp.pyquery('[name="filter-2-value"]')[0].attrib['type'] == 'text'
|
||||
resp.forms['listing-settings']['filter-2-value'] = '{{ form_var_blah }}'
|
||||
resp.forms['listing-settings']['filter-2-operator'].value = 'gte'
|
||||
|
||||
resp = resp.forms['listing-settings'].submit()
|
||||
assert resp.forms['listing-settings']['filter-2-value'].value == '{{ form_var_blah }}'
|
||||
assert resp.text.count('<tr') == 1 # thead only
|
||||
|
||||
# save custom view with filter
|
||||
resp = resp.forms['save-custom-view'].submit().follow()
|
||||
|
||||
custom_view = pub.custom_view_class.select()[0]
|
||||
|
||||
# use custom view as source
|
||||
ds = {'type': 'carddef:%s:%s' % (carddef.url_name, custom_view.slug)}
|
||||
formdef.fields = [
|
||||
fields.PageField(id='2', label='1st page'),
|
||||
fields.ItemField(
|
||||
id='0', label='item', varname='blah', items=['2023-01-02', '2024-01-02', '2025-01-02']
|
||||
),
|
||||
fields.ItemField(id='1', label='string', data_source=ds, display_disabled_items=True),
|
||||
]
|
||||
formdef.store()
|
||||
|
||||
resp = get_app(pub).get('/test/')
|
||||
assert resp.form['f1'].options == [('', False, '---')]
|
||||
resp.form['f0'] = '2024-01-02'
|
||||
live_resp = app.post('/test/live?modified_field_id[]=0', params=resp.form.submit_fields())
|
||||
assert live_resp.json['result']['1']['items'] == [
|
||||
{'attr': 'bar', 'id': 2, 'text': 'bar'},
|
||||
{'attr': 'baz', 'id': 3, 'text': 'baz'},
|
||||
]
|
||||
|
||||
resp.form['f0'] = '2023-01-02'
|
||||
live_resp = app.post('/test/live?modified_field_id[]=0', params=resp.form.submit_fields())
|
||||
assert len(live_resp.json['result']['1']['items']) == 3
|
||||
|
||||
resp.form['f0'] = '2025-01-02'
|
||||
live_resp = app.post('/test/live?modified_field_id[]=0', params=resp.form.submit_fields())
|
||||
assert len(live_resp.json['result']['1']['items']) == 0
|
||||
|
||||
|
||||
def test_dynamic_item_fields_from_custom_view_on_cards(pub):
|
||||
pub.role_class.wipe()
|
||||
pub.custom_view_class.wipe()
|
||||
|
|
|
@ -233,6 +233,7 @@ def test_form_map_field_default_position(pub):
|
|||
resp.form['f1'] = '169 rue du chateau, paris'
|
||||
resp = resp.form.submit('submit')
|
||||
assert resp.pyquery('.qommon-map').attr('data-def-lat') == '13'
|
||||
assert resp.pyquery('.qommon-map').attr('data-def-template')
|
||||
|
||||
formdef.fields[3].initial_position = 'template'
|
||||
formdef.fields[3].position_template = '{{ form_var_address }}'
|
||||
|
@ -244,3 +245,4 @@ def test_form_map_field_default_position(pub):
|
|||
rsps.get('https://nominatim.entrouvert.org/search', json=[{'lat': '48.8337085', 'lon': '2.3233693'}])
|
||||
resp = resp.form.submit('submit')
|
||||
assert resp.pyquery('.qommon-map').attr('data-def-lat') == '48.83370850'
|
||||
assert resp.pyquery('.qommon-map').attr('data-def-template')
|
||||
|
|
|
@ -13,6 +13,7 @@ from wcs.blocks import BlockDef
|
|||
from wcs.carddef import CardDef
|
||||
from wcs.categories import Category
|
||||
from wcs.data_sources import NamedDataSource
|
||||
from wcs.formdef import FormDef
|
||||
from wcs.wf.create_formdata import Mapping
|
||||
from wcs.workflows import Workflow
|
||||
|
||||
|
@ -631,6 +632,109 @@ def test_form_page_item_with_variable_data_source_prefill(pub):
|
|||
assert not resp.pyquery('#form_error_f2').text()
|
||||
|
||||
|
||||
def test_form_page_item_with_card_with_custom_id_prefill(pub):
|
||||
create_user(pub)
|
||||
CardDef.wipe()
|
||||
FormDef.wipe()
|
||||
|
||||
carddef = CardDef()
|
||||
carddef.name = 'Test'
|
||||
carddef.fields = [
|
||||
fields.StringField(id='0', label='blah', varname='blah'),
|
||||
]
|
||||
carddef.digest_templates = {'default': '{{ form_var_blah|upper }}'}
|
||||
carddef.id_template = '{{ form_var_blah }}'
|
||||
carddef.store()
|
||||
carddef.data_class().wipe()
|
||||
|
||||
carddata1 = carddef.data_class()()
|
||||
carddata1.data = {'0': 'bar'}
|
||||
carddata1.just_created()
|
||||
carddata1.store()
|
||||
|
||||
carddata2 = carddef.data_class()()
|
||||
carddata2.data = {'0': 'foo'}
|
||||
carddata2.just_created()
|
||||
carddata2.store()
|
||||
|
||||
formdef = create_formdef()
|
||||
formdef.data_class().wipe()
|
||||
formdef.fields = [
|
||||
fields.ItemField(
|
||||
id='2',
|
||||
label='item',
|
||||
varname='item',
|
||||
required=False,
|
||||
data_source={'type': 'carddef:test'},
|
||||
prefill={'type': 'string', 'value': 'foo'},
|
||||
),
|
||||
]
|
||||
formdef.store()
|
||||
|
||||
resp = get_app(pub).get('/test/')
|
||||
assert [x.attrib['value'] for x in resp.pyquery('#form_f2 option')] == ['bar', 'foo']
|
||||
assert resp.form['f2'].value == 'foo'
|
||||
assert not resp.pyquery('#form_error_f2').text()
|
||||
|
||||
|
||||
def test_form_page_block_with_item_with_card_with_custom_id_prefill(pub):
|
||||
create_user(pub)
|
||||
CardDef.wipe()
|
||||
FormDef.wipe()
|
||||
|
||||
carddef = CardDef()
|
||||
carddef.name = 'Test'
|
||||
carddef.fields = [
|
||||
fields.StringField(id='0', label='blah', varname='blah'),
|
||||
]
|
||||
carddef.digest_templates = {'default': 'card {{ form_var_blah }}'}
|
||||
carddef.id_template = '{{ form_var_blah }}'
|
||||
carddef.store()
|
||||
carddef.data_class().wipe()
|
||||
|
||||
carddata1 = carddef.data_class()()
|
||||
carddata1.data = {'0': 'bar'}
|
||||
carddata1.just_created()
|
||||
carddata1.store()
|
||||
|
||||
carddata2 = carddef.data_class()()
|
||||
carddata2.data = {'0': 'foo'}
|
||||
carddata2.just_created()
|
||||
carddata2.store()
|
||||
|
||||
BlockDef.wipe()
|
||||
block = BlockDef()
|
||||
block.name = 'foobar'
|
||||
block.fields = [
|
||||
fields.ItemField(
|
||||
id='123',
|
||||
label='item',
|
||||
varname='item',
|
||||
required=False,
|
||||
data_source={'type': 'carddef:test'},
|
||||
),
|
||||
]
|
||||
block.store()
|
||||
|
||||
formdef = create_formdef()
|
||||
formdef.data_class().wipe()
|
||||
formdef.fields = [
|
||||
fields.BlockField(
|
||||
id='2',
|
||||
label='test',
|
||||
block_slug='foobar',
|
||||
varname='foobar',
|
||||
prefill={'type': 'string', 'value': '{% block_value item="foo" %}'},
|
||||
),
|
||||
]
|
||||
formdef.store()
|
||||
|
||||
resp = get_app(pub).get('/test/')
|
||||
assert [x.attrib['value'] for x in resp.pyquery('#form_f2__element0__f123 option')] == ['bar', 'foo']
|
||||
assert resp.form['f2$element0$f123'].value == 'foo'
|
||||
assert not resp.pyquery('.widget-with-error')
|
||||
|
||||
|
||||
def test_form_page_item_with_computed_field_variable_data_source_prefill(pub):
|
||||
create_user(pub)
|
||||
formdef = create_formdef()
|
||||
|
@ -1579,3 +1683,37 @@ 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
|
||||
|
|
Binary file not shown.
|
@ -5,6 +5,7 @@ import xml.etree.ElementTree as ET
|
|||
import pytest
|
||||
|
||||
from wcs.blocks import BlockDef
|
||||
from wcs.carddata import UpdateRelationsAfterJob
|
||||
from wcs.carddef import CardDef
|
||||
from wcs.categories import CardDefCategory
|
||||
from wcs.data_sources import NamedDataSource
|
||||
|
@ -623,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()
|
||||
|
@ -760,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')] == [
|
||||
|
@ -1328,3 +1331,400 @@ 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'
|
||||
|
||||
|
||||
def test_card_update_related(pub):
|
||||
BlockDef.wipe()
|
||||
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()
|
||||
|
||||
# check update against item field
|
||||
formdef = FormDef()
|
||||
formdef.name = 'foo'
|
||||
formdef.fields = [
|
||||
ItemField(id='1', label='Test', data_source={'type': 'carddef:foo'}),
|
||||
]
|
||||
formdef.store()
|
||||
|
||||
formdata = formdef.data_class()()
|
||||
formdata.data = {'1': '1'}
|
||||
formdata.data['1_display'] = formdef.fields[0].store_display_value(formdata.data, formdef.fields[0].id)
|
||||
assert formdata.data['1_display'] == 'card1'
|
||||
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'
|
||||
|
||||
# check update against items field
|
||||
formdef = FormDef()
|
||||
formdef.name = 'foo'
|
||||
formdef.fields = [
|
||||
ItemsField(id='1', label='Test', data_source={'type': 'carddef:foo'}),
|
||||
]
|
||||
formdef.store()
|
||||
pub.reset_caches()
|
||||
|
||||
formdata = formdef.data_class()()
|
||||
formdata.data = {'1': ['1', '2']}
|
||||
formdata.data['1_display'] = formdef.fields[0].store_display_value(formdata.data, formdef.fields[0].id)
|
||||
assert formdata.data['1_display'] == 'card1-change1, card2'
|
||||
formdata.just_created()
|
||||
formdata.store()
|
||||
|
||||
pub.cleanup()
|
||||
carddef = carddef.get(carddef.id)
|
||||
carddata1 = carddef.data_class().get(carddata1.id)
|
||||
carddata1.data = {'1': 'card1-change2'}
|
||||
carddata1.store()
|
||||
|
||||
formdata.refresh_from_storage()
|
||||
assert formdata.data['1_display'] == 'card1-change2, card2'
|
||||
|
||||
# check update against block field
|
||||
blockdef = BlockDef()
|
||||
blockdef.name = 'foo'
|
||||
blockdef.fields = [
|
||||
ItemField(id='1', label='Test', varname='bar', data_source={'type': 'carddef:foo'}),
|
||||
]
|
||||
blockdef.digest_template = 'bloc:{{ block_var_bar }}'
|
||||
blockdef.store()
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'foo2'
|
||||
formdef.fields = [
|
||||
BlockField(id='1', label='Test', block_slug=blockdef.slug),
|
||||
BlockField(id='2', label='Test2', block_slug=blockdef.slug), # left empty
|
||||
]
|
||||
formdef.store()
|
||||
pub.reset_caches()
|
||||
|
||||
formdata = formdef.data_class()()
|
||||
formdata.data = {
|
||||
'1': {
|
||||
'data': [
|
||||
{
|
||||
'1': '1',
|
||||
'1_display': 'card1-change2',
|
||||
},
|
||||
{
|
||||
'1': '2',
|
||||
'1_display': 'card2',
|
||||
},
|
||||
],
|
||||
'schema': {},
|
||||
}
|
||||
}
|
||||
|
||||
formdata.data['1_display'] = formdef.fields[0].store_display_value(formdata.data, formdef.fields[0].id)
|
||||
assert formdata.data['1_display'] == 'bloc:card1-change2, bloc:card2'
|
||||
formdata.just_created()
|
||||
formdata.store()
|
||||
|
||||
pub.cleanup()
|
||||
carddef = carddef.get(carddef.id)
|
||||
carddata1 = carddef.data_class().get(carddata1.id)
|
||||
carddata1.data = {'1': 'card1-change3'}
|
||||
carddata1.store()
|
||||
|
||||
formdata.refresh_from_storage()
|
||||
assert formdata.data['1']['data'][0]['1'] == '1'
|
||||
assert formdata.data['1']['data'][0]['1_display'] == 'card1-change3'
|
||||
assert formdata.data['1']['data'][1]['1'] == '2'
|
||||
assert formdata.data['1']['data'][1]['1_display'] == 'card2'
|
||||
assert formdata.data['1_display'] == 'bloc:card1-change3, bloc:card2'
|
||||
|
||||
|
||||
def test_card_update_related_with_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'),
|
||||
]
|
||||
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'}
|
||||
carddata1.just_created()
|
||||
carddata1.store()
|
||||
|
||||
carddata2 = carddef.data_class()()
|
||||
carddata2.data = {'1': 'card2'}
|
||||
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.visibility = 'datasource'
|
||||
custom_view.store()
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'foo'
|
||||
formdef.fields = [
|
||||
ItemField(id='1', label='Test', data_source={'type': 'carddef:foo:view'}),
|
||||
]
|
||||
formdef.store()
|
||||
|
||||
formdata = formdef.data_class()()
|
||||
formdata.data = {'1': '1'}
|
||||
formdata.data['1_display'] = formdef.fields[0].store_display_value(formdata.data, formdef.fields[0].id)
|
||||
assert formdata.data['1_display'] == 'view-card1'
|
||||
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'] == 'view-card1-change1'
|
||||
|
||||
|
||||
def test_card_update_related_cascading(pub):
|
||||
BlockDef.wipe()
|
||||
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()
|
||||
|
||||
carddef2 = CardDef()
|
||||
carddef2.name = 'bar'
|
||||
carddef2.fields = [
|
||||
ItemField(id='1', label='Test', varname='foo', data_source={'type': 'carddef:foo'}),
|
||||
]
|
||||
carddef2.digest_templates = {'default': 'bar-{{ form_var_foo }}'}
|
||||
carddef2.store()
|
||||
carddef2.data_class().wipe()
|
||||
|
||||
carddata2 = carddef2.data_class()()
|
||||
carddata2.data = {'1': '1'}
|
||||
carddata2.data['1_display'] = carddef2.fields[0].store_display_value(
|
||||
carddata2.data, carddef2.fields[0].id
|
||||
)
|
||||
carddata2.just_created()
|
||||
carddata2.store()
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'foo'
|
||||
formdef.fields = [
|
||||
ItemField(id='1', label='Test', data_source={'type': 'carddef:bar'}),
|
||||
]
|
||||
formdef.store()
|
||||
|
||||
formdata = formdef.data_class()()
|
||||
formdata.data = {'1': '1'}
|
||||
formdata.data['1_display'] = formdef.fields[0].store_display_value(formdata.data, formdef.fields[0].id)
|
||||
assert formdata.data['1_display'] == 'bar-card1'
|
||||
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'] == 'bar-card1-change1'
|
||||
|
||||
|
||||
def test_card_update_related_cascading_loop(pub):
|
||||
BlockDef.wipe()
|
||||
CardDef.wipe()
|
||||
FormDef.wipe()
|
||||
|
||||
carddef = CardDef()
|
||||
carddef.name = 'foo'
|
||||
carddef.fields = [
|
||||
StringField(id='1', label='Test', varname='foo'),
|
||||
ItemField(id='2', label='Test', varname='x', data_source={'type': 'carddef:bar'}),
|
||||
]
|
||||
carddef.digest_templates = {'default': '{{ form_var_foo }} {{ form_var_x }}'}
|
||||
carddef.store()
|
||||
carddef.data_class().wipe()
|
||||
|
||||
carddef2 = CardDef()
|
||||
carddef2.name = 'bar'
|
||||
carddef2.fields = [
|
||||
StringField(id='1', label='Test', varname='foo'),
|
||||
ItemField(id='2', label='Test', varname='x', data_source={'type': 'carddef:foo'}),
|
||||
]
|
||||
carddef2.digest_templates = {'default': '{{ form_var_foo }} {{ form_var_x }}'}
|
||||
carddef2.store()
|
||||
carddef2.data_class().wipe()
|
||||
|
||||
carddata1 = carddef.data_class()()
|
||||
carddata1.data = {'1': 'card1'}
|
||||
carddata1.just_created()
|
||||
carddata1.store()
|
||||
|
||||
carddata2 = carddef2.data_class()()
|
||||
carddata2.data = {'1': 'card2', '2': '1'}
|
||||
carddata2.data['2_display'] = carddef2.fields[1].store_display_value(
|
||||
carddata2.data, carddef2.fields[1].id
|
||||
)
|
||||
assert carddata2.data['2_display'] == 'card1 None'
|
||||
carddata2.just_created()
|
||||
carddata2.store()
|
||||
|
||||
pub.cleanup()
|
||||
carddef = carddef.get(carddef.id)
|
||||
carddata1 = carddef.data_class().get(carddata1.id)
|
||||
carddata1.data['2'] = str(carddata2.id)
|
||||
carddata1.data['2_display'] = carddef.fields[1].store_display_value(carddata1.data, carddef.fields[1].id)
|
||||
carddata1.store()
|
||||
|
||||
# check it will have stopped once getting back to carddata2
|
||||
carddata2.refresh_from_storage()
|
||||
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()
|
||||
FormDef.wipe()
|
||||
|
||||
carddef = CardDef()
|
||||
carddef.name = 'foo'
|
||||
carddef.fields = [
|
||||
StringField(id='1', label='Test', varname='foo'),
|
||||
]
|
||||
carddef.digest_templates = {'default': 'card-{{ form_var_foo }}'}
|
||||
carddef.store()
|
||||
carddef.data_class().wipe()
|
||||
|
||||
carddata1 = carddef.data_class()()
|
||||
carddata1.data = {'1': 'card1'}
|
||||
carddata1.just_created()
|
||||
carddata1.store()
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'foo'
|
||||
formdef.fields = [
|
||||
ItemField(id='1', label='Test', data_source={'type': 'carddef:foo'}),
|
||||
]
|
||||
formdef.store()
|
||||
|
||||
formdata = formdef.data_class()()
|
||||
formdata.data = {'1': '1'}
|
||||
formdata.data['1_display'] = formdef.fields[0].store_display_value(formdata.data, formdef.fields[0].id)
|
||||
assert formdata.data['1_display'] == 'card-card1'
|
||||
formdata.just_created()
|
||||
formdata.store()
|
||||
formdef.remove_self()
|
||||
|
||||
pub.cleanup()
|
||||
carddef = carddef.get(carddef.id)
|
||||
carddata1 = carddef.data_class().get(carddata1.id)
|
||||
carddata1.data = {'1': 'card1-change1'}
|
||||
carddata1.store() # do not crash looking for related formdef that has been deleted
|
||||
|
||||
# check the job doesn't fail if the carddef or carddata have been removed
|
||||
job = UpdateRelationsAfterJob(carddata=carddata1)
|
||||
carddata1.remove_self()
|
||||
job.execute()
|
||||
|
||||
carddef.remove_self()
|
||||
job.execute()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import xml.etree.ElementTree as ET
|
||||
|
||||
import pytest
|
||||
|
||||
from wcs import data_sources
|
||||
|
@ -57,11 +59,11 @@ def test_datasource_users(pub):
|
|||
|
||||
assert data_sources.get_items({'type': datasource.slug}) == [
|
||||
(
|
||||
'1',
|
||||
str(users[0].id),
|
||||
'John Doe 0',
|
||||
'1',
|
||||
str(users[0].id),
|
||||
{
|
||||
'id': 1,
|
||||
'id': users[0].id,
|
||||
'text': 'John Doe 0',
|
||||
'user_name_identifier_0': 'abc0',
|
||||
'user_nameid': 'abc0',
|
||||
|
@ -72,11 +74,11 @@ def test_datasource_users(pub):
|
|||
},
|
||||
),
|
||||
(
|
||||
'2',
|
||||
str(users[1].id),
|
||||
'John Doe 1',
|
||||
'2',
|
||||
str(users[1].id),
|
||||
{
|
||||
'id': 2,
|
||||
'id': users[1].id,
|
||||
'text': 'John Doe 1',
|
||||
'user_name_identifier_0': 'abc1',
|
||||
'user_nameid': 'abc1',
|
||||
|
@ -89,11 +91,11 @@ def test_datasource_users(pub):
|
|||
]
|
||||
assert data_sources.get_items(datasource.extended_data_source) == [
|
||||
(
|
||||
'1',
|
||||
str(users[0].id),
|
||||
'John Doe 0',
|
||||
'1',
|
||||
str(users[0].id),
|
||||
{
|
||||
'id': 1,
|
||||
'id': users[0].id,
|
||||
'text': 'John Doe 0',
|
||||
'user_name_identifier_0': 'abc0',
|
||||
'user_nameid': 'abc0',
|
||||
|
@ -104,11 +106,11 @@ def test_datasource_users(pub):
|
|||
},
|
||||
),
|
||||
(
|
||||
'2',
|
||||
str(users[1].id),
|
||||
'John Doe 1',
|
||||
'2',
|
||||
str(users[1].id),
|
||||
{
|
||||
'id': 2,
|
||||
'id': users[1].id,
|
||||
'text': 'John Doe 1',
|
||||
'user_name_identifier_0': 'abc1',
|
||||
'user_nameid': 'abc1',
|
||||
|
@ -121,7 +123,7 @@ def test_datasource_users(pub):
|
|||
]
|
||||
assert data_sources.get_structured_items({'type': datasource.slug}) == [
|
||||
{
|
||||
'id': 1,
|
||||
'id': users[0].id,
|
||||
'text': 'John Doe 0',
|
||||
'user_name_identifier_0': 'abc0',
|
||||
'user_nameid': 'abc0',
|
||||
|
@ -131,7 +133,7 @@ def test_datasource_users(pub):
|
|||
'user_email': None,
|
||||
},
|
||||
{
|
||||
'id': 2,
|
||||
'id': users[1].id,
|
||||
'text': 'John Doe 1',
|
||||
'user_name_identifier_0': 'abc1',
|
||||
'user_nameid': 'abc1',
|
||||
|
@ -143,7 +145,7 @@ def test_datasource_users(pub):
|
|||
]
|
||||
assert data_sources.get_structured_items(datasource.extended_data_source) == [
|
||||
{
|
||||
'id': 1,
|
||||
'id': users[0].id,
|
||||
'text': 'John Doe 0',
|
||||
'user_name_identifier_0': 'abc0',
|
||||
'user_nameid': 'abc0',
|
||||
|
@ -153,7 +155,7 @@ def test_datasource_users(pub):
|
|||
'user_email': None,
|
||||
},
|
||||
{
|
||||
'id': 2,
|
||||
'id': users[1].id,
|
||||
'text': 'John Doe 1',
|
||||
'user_name_identifier_0': 'abc1',
|
||||
'user_nameid': 'abc1',
|
||||
|
@ -169,7 +171,7 @@ def test_datasource_users(pub):
|
|||
datasource.store()
|
||||
assert data_sources.get_structured_items({'type': datasource.slug}) == [
|
||||
{
|
||||
'id': 1,
|
||||
'id': users[0].id,
|
||||
'text': 'John Doe 0',
|
||||
'user_name_identifier_0': 'abc0',
|
||||
'user_nameid': 'abc0',
|
||||
|
@ -179,7 +181,7 @@ def test_datasource_users(pub):
|
|||
'user_email': None,
|
||||
},
|
||||
{
|
||||
'id': 2,
|
||||
'id': users[1].id,
|
||||
'text': 'John Doe 1',
|
||||
'user_name_identifier_0': 'abc1',
|
||||
'user_nameid': 'abc1',
|
||||
|
@ -191,7 +193,7 @@ def test_datasource_users(pub):
|
|||
]
|
||||
assert data_sources.get_structured_items(datasource.extended_data_source) == [
|
||||
{
|
||||
'id': 1,
|
||||
'id': users[0].id,
|
||||
'text': 'John Doe 0',
|
||||
'user_name_identifier_0': 'abc0',
|
||||
'user_nameid': 'abc0',
|
||||
|
@ -201,7 +203,7 @@ def test_datasource_users(pub):
|
|||
'user_email': None,
|
||||
},
|
||||
{
|
||||
'id': 2,
|
||||
'id': users[1].id,
|
||||
'text': 'John Doe 1',
|
||||
'user_name_identifier_0': 'abc1',
|
||||
'user_nameid': 'abc1',
|
||||
|
@ -223,7 +225,7 @@ def test_datasource_users(pub):
|
|||
users[0].store()
|
||||
assert data_sources.get_structured_items({'type': datasource.slug}) == [
|
||||
{
|
||||
'id': 1,
|
||||
'id': users[0].id,
|
||||
'text': 'John Doe 0',
|
||||
'user_name_identifier_0': 'abc0',
|
||||
'user_nameid': 'abc0',
|
||||
|
@ -235,7 +237,7 @@ def test_datasource_users(pub):
|
|||
]
|
||||
assert data_sources.get_structured_items(datasource.extended_data_source) == [
|
||||
{
|
||||
'id': 1,
|
||||
'id': users[0].id,
|
||||
'text': 'John Doe 0',
|
||||
'user_name_identifier_0': 'abc0',
|
||||
'user_nameid': 'abc0',
|
||||
|
@ -254,7 +256,7 @@ def test_datasource_users(pub):
|
|||
datasource.store()
|
||||
assert data_sources.get_structured_items({'type': datasource.slug}) == [
|
||||
{
|
||||
'id': 1,
|
||||
'id': users[0].id,
|
||||
'text': 'John Doe 0',
|
||||
'user_name_identifier_0': 'abc0',
|
||||
'user_nameid': 'abc0',
|
||||
|
@ -266,7 +268,7 @@ def test_datasource_users(pub):
|
|||
]
|
||||
assert data_sources.get_structured_items(datasource.extended_data_source) == [
|
||||
{
|
||||
'id': 1,
|
||||
'id': users[0].id,
|
||||
'text': 'John Doe 0',
|
||||
'user_name_identifier_0': 'abc0',
|
||||
'user_nameid': 'abc0',
|
||||
|
@ -291,7 +293,7 @@ def test_datasource_users(pub):
|
|||
assert not datasource.include_disabled_users
|
||||
assert data_sources.get_structured_items({'type': datasource.slug}) == [
|
||||
{
|
||||
'id': 1,
|
||||
'id': users[0].id,
|
||||
'text': 'John Doe 0',
|
||||
'user_name_identifier_0': 'abc0',
|
||||
'user_nameid': 'abc0',
|
||||
|
@ -303,7 +305,7 @@ def test_datasource_users(pub):
|
|||
]
|
||||
assert data_sources.get_structured_items(datasource.extended_data_source) == [
|
||||
{
|
||||
'id': 1,
|
||||
'id': users[0].id,
|
||||
'text': 'John Doe 0',
|
||||
'user_name_identifier_0': 'abc0',
|
||||
'user_nameid': 'abc0',
|
||||
|
@ -319,7 +321,7 @@ def test_datasource_users(pub):
|
|||
datasource.store()
|
||||
assert data_sources.get_structured_items({'type': datasource.slug}) == [
|
||||
{
|
||||
'id': 1,
|
||||
'id': users[0].id,
|
||||
'text': 'John Doe 0',
|
||||
'user_name_identifier_0': 'abc0',
|
||||
'user_nameid': 'abc0',
|
||||
|
@ -329,7 +331,7 @@ def test_datasource_users(pub):
|
|||
'user_email': None,
|
||||
},
|
||||
{
|
||||
'id': 2,
|
||||
'id': users[1].id,
|
||||
'text': 'John Doe 1',
|
||||
'user_name_identifier_0': 'abc1',
|
||||
'user_nameid': 'abc1',
|
||||
|
@ -341,7 +343,7 @@ def test_datasource_users(pub):
|
|||
]
|
||||
assert data_sources.get_structured_items(datasource.extended_data_source) == [
|
||||
{
|
||||
'id': 1,
|
||||
'id': users[0].id,
|
||||
'text': 'John Doe 0',
|
||||
'user_name_identifier_0': 'abc0',
|
||||
'user_nameid': 'abc0',
|
||||
|
@ -351,7 +353,7 @@ def test_datasource_users(pub):
|
|||
'user_email': None,
|
||||
},
|
||||
{
|
||||
'id': 2,
|
||||
'id': users[1].id,
|
||||
'text': 'John Doe 1',
|
||||
'user_name_identifier_0': 'abc1',
|
||||
'user_nameid': 'abc1',
|
||||
|
@ -365,7 +367,7 @@ def test_datasource_users(pub):
|
|||
|
||||
# by uuid
|
||||
assert datasource.get_structured_value('abc0') == {
|
||||
'id': 1,
|
||||
'id': users[0].id,
|
||||
'text': 'John Doe 0',
|
||||
'user_name_identifier_0': 'abc0',
|
||||
'user_nameid': 'abc0',
|
||||
|
@ -376,7 +378,7 @@ def test_datasource_users(pub):
|
|||
}
|
||||
assert datasource.get_display_value('abc0') == 'John Doe 0'
|
||||
assert datasource.get_structured_value('abc1') == {
|
||||
'id': 2,
|
||||
'id': users[1].id,
|
||||
'text': 'John Doe 1',
|
||||
'user_name_identifier_0': 'abc1',
|
||||
'user_nameid': 'abc1',
|
||||
|
@ -388,8 +390,8 @@ def test_datasource_users(pub):
|
|||
assert datasource.get_display_value('abc1') == 'John Doe 1'
|
||||
|
||||
# by id
|
||||
assert datasource.get_structured_value('1') == {
|
||||
'id': 1,
|
||||
assert datasource.get_structured_value(str(users[0].id)) == {
|
||||
'id': users[0].id,
|
||||
'text': 'John Doe 0',
|
||||
'user_name_identifier_0': 'abc0',
|
||||
'user_nameid': 'abc0',
|
||||
|
@ -398,9 +400,9 @@ def test_datasource_users(pub):
|
|||
'user_display_name': 'John Doe 0',
|
||||
'user_email': None,
|
||||
}
|
||||
assert datasource.get_display_value('1') == 'John Doe 0'
|
||||
assert datasource.get_structured_value('2') == {
|
||||
'id': 2,
|
||||
assert datasource.get_display_value(str(users[0].id)) == 'John Doe 0'
|
||||
assert datasource.get_structured_value(str(users[1].id)) == {
|
||||
'id': users[1].id,
|
||||
'text': 'John Doe 1',
|
||||
'user_name_identifier_0': 'abc1',
|
||||
'user_nameid': 'abc1',
|
||||
|
@ -409,11 +411,11 @@ def test_datasource_users(pub):
|
|||
'user_display_name': 'John Doe 1',
|
||||
'user_email': None,
|
||||
}
|
||||
assert datasource.get_display_value('2') == 'John Doe 1'
|
||||
assert datasource.get_display_value(str(users[1].id)) == 'John Doe 1'
|
||||
|
||||
# by numeric id
|
||||
assert datasource.get_structured_value(1) == {
|
||||
'id': 1,
|
||||
assert datasource.get_structured_value(users[0].id) == {
|
||||
'id': users[0].id,
|
||||
'text': 'John Doe 0',
|
||||
'user_name_identifier_0': 'abc0',
|
||||
'user_nameid': 'abc0',
|
||||
|
@ -422,9 +424,9 @@ def test_datasource_users(pub):
|
|||
'user_display_name': 'John Doe 0',
|
||||
'user_email': None,
|
||||
}
|
||||
assert datasource.get_display_value(1) == 'John Doe 0'
|
||||
assert datasource.get_structured_value(2) == {
|
||||
'id': 2,
|
||||
assert datasource.get_display_value(users[0].id) == 'John Doe 0'
|
||||
assert datasource.get_structured_value(users[1].id) == {
|
||||
'id': users[1].id,
|
||||
'text': 'John Doe 1',
|
||||
'user_name_identifier_0': 'abc1',
|
||||
'user_nameid': 'abc1',
|
||||
|
@ -433,7 +435,7 @@ def test_datasource_users(pub):
|
|||
'user_display_name': 'John Doe 1',
|
||||
'user_email': None,
|
||||
}
|
||||
assert datasource.get_display_value(2) == 'John Doe 1'
|
||||
assert datasource.get_display_value(users[1].id) == 'John Doe 1'
|
||||
|
||||
datasource.users_included_roles = [role1.id]
|
||||
datasource.users_excluded_roles = [role2.id]
|
||||
|
@ -445,7 +447,7 @@ def test_datasource_users(pub):
|
|||
assert datasource.get_structured_value('abc0') is None
|
||||
assert datasource.get_display_value('abc0') is None
|
||||
assert datasource.get_structured_value('abc1') == {
|
||||
'id': 2,
|
||||
'id': users[1].id,
|
||||
'text': 'John Doe 1',
|
||||
'user_name_identifier_0': 'abc1',
|
||||
'user_nameid': 'abc1',
|
||||
|
@ -457,10 +459,10 @@ def test_datasource_users(pub):
|
|||
assert datasource.get_display_value('abc1') == 'John Doe 1'
|
||||
|
||||
# by id
|
||||
assert datasource.get_structured_value('1') is None
|
||||
assert datasource.get_display_value('1') is None
|
||||
assert datasource.get_structured_value('2') == {
|
||||
'id': 2,
|
||||
assert datasource.get_structured_value(str(users[0].id)) is None
|
||||
assert datasource.get_display_value(str(users[0].id)) is None
|
||||
assert datasource.get_structured_value(str(users[1].id)) == {
|
||||
'id': users[1].id,
|
||||
'text': 'John Doe 1',
|
||||
'user_name_identifier_0': 'abc1',
|
||||
'user_nameid': 'abc1',
|
||||
|
@ -469,7 +471,7 @@ def test_datasource_users(pub):
|
|||
'user_display_name': 'John Doe 1',
|
||||
'user_email': None,
|
||||
}
|
||||
assert datasource.get_display_value('2') == 'John Doe 1'
|
||||
assert datasource.get_display_value(str(users[1].id)) == 'John Doe 1'
|
||||
|
||||
datasource.include_disabled_users = False
|
||||
datasource.store()
|
||||
|
@ -483,10 +485,10 @@ def test_datasource_users(pub):
|
|||
assert datasource.get_display_value('abc1') is None
|
||||
|
||||
# by id
|
||||
assert datasource.get_structured_value('1') is None
|
||||
assert datasource.get_display_value('1') is None
|
||||
assert datasource.get_structured_value('2') is None
|
||||
assert datasource.get_display_value('2') is None
|
||||
assert datasource.get_structured_value(str(users[0].id)) is None
|
||||
assert datasource.get_display_value(str(users[0].id)) is None
|
||||
assert datasource.get_structured_value(str(users[1].id)) is None
|
||||
assert datasource.get_display_value(str(users[1].id)) is None
|
||||
|
||||
|
||||
def test_datasource_users_user_formdef(pub):
|
||||
|
@ -509,9 +511,9 @@ def test_datasource_users_user_formdef(pub):
|
|||
|
||||
assert data_sources.get_items({'type': datasource.slug}) == [
|
||||
(
|
||||
'3',
|
||||
str(user.id),
|
||||
'John Doe',
|
||||
'3',
|
||||
str(user.id),
|
||||
{
|
||||
'user_display_name': 'John Doe',
|
||||
'user_email': None,
|
||||
|
@ -520,8 +522,67 @@ def test_datasource_users_user_formdef(pub):
|
|||
'user_var_plop': 'Bar',
|
||||
'user_admin_access': False,
|
||||
'user_backoffice_access': False,
|
||||
'id': 3,
|
||||
'id': user.id,
|
||||
'text': 'John Doe',
|
||||
},
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def test_legacy_format_import(pub):
|
||||
data_source_xml = """<datasource id="255">
|
||||
<name>Agents de la ville</name>
|
||||
<slug>agents_de_la_ville</slug>
|
||||
<data_source>
|
||||
<type>wcs:users</type>
|
||||
<value />
|
||||
</data_source><users_included_roles>
|
||||
<item>8201764fc2c24b92bd691fd231a4cf76</item>
|
||||
</users_included_roles>
|
||||
</datasource>"""
|
||||
ds = NamedDataSource.import_from_xml_tree(ET.fromstring(data_source_xml))
|
||||
assert ds.users_included_roles == ['8201764fc2c24b92bd691fd231a4cf76']
|
||||
|
||||
|
||||
def test_new_format_import(pub):
|
||||
data_source_xml = """<datasource id="255">
|
||||
<name>Agents de la ville</name>
|
||||
<slug>agents_de_la_ville</slug>
|
||||
<data_source>
|
||||
<type>wcs:users</type>
|
||||
<value />
|
||||
</data_source><users_included_roles>
|
||||
<role role-id="8201764fc2c24b92bd691fd231a4cf76" role-slug="agent">Agents</role>
|
||||
</users_included_roles>
|
||||
</datasource>"""
|
||||
ds = NamedDataSource.import_from_xml_tree(ET.fromstring(data_source_xml))
|
||||
assert ds.users_included_roles == [] # role doesn't exist
|
||||
|
||||
# import with id match
|
||||
pub.role_class.wipe()
|
||||
role1 = pub.role_class(name='role')
|
||||
role1.id = '8201764fc2c24b92bd691fd231a4cf76'
|
||||
role1.store()
|
||||
|
||||
ds = NamedDataSource.import_from_xml_tree(ET.fromstring(data_source_xml), include_id=True)
|
||||
assert ds.users_included_roles == [role1.id]
|
||||
|
||||
# import with slug match
|
||||
pub.role_class.wipe()
|
||||
role1 = pub.role_class(name='Agents')
|
||||
role1.slug = 'agent'
|
||||
role1.store()
|
||||
|
||||
ds = NamedDataSource.import_from_xml_tree(ET.fromstring(data_source_xml), include_id=False)
|
||||
assert ds.users_included_roles == [role1.id]
|
||||
|
||||
# import with name match
|
||||
pub.role_class.wipe()
|
||||
role1 = pub.role_class(name='Agents')
|
||||
role1.slug = 'agent'
|
||||
role1.store()
|
||||
|
||||
ds = NamedDataSource.import_from_xml_tree(
|
||||
ET.fromstring(data_source_xml.replace('role-slug="agent"', '')), include_id=False
|
||||
)
|
||||
assert ds.users_included_roles == [role1.id]
|
||||
|
|
|
@ -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:
|
||||
|
@ -153,11 +153,11 @@ def test_text(pub):
|
|||
|
||||
form = Form(use_tokens=False)
|
||||
fields.TextField(display_mode='rich').add_to_form(form)
|
||||
assert 'data-godo-schema="full"' in str(form.render())
|
||||
assert PyQuery(str(form.render()))('godo-editor[schema=full]')
|
||||
|
||||
form = Form(use_tokens=False)
|
||||
fields.TextField(display_mode='basic-rich').add_to_form(form)
|
||||
assert 'data-godo-schema="basic"' in str(form.render())
|
||||
assert PyQuery(str(form.render()))('godo-editor[schema=basic]')
|
||||
|
||||
|
||||
def test_text_anonymise(pub):
|
||||
|
@ -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'})
|
||||
|
@ -915,6 +915,17 @@ def test_file_convert_from_anything():
|
|||
assert value.get_file_pointer().read() == b'hello'
|
||||
|
||||
|
||||
def test_file_from_json_value(pub):
|
||||
value = fields.FileField().from_json_value({'content': 'aGVsbG8=', 'filename': 'test.txt'})
|
||||
assert value.base_filename == 'test.txt'
|
||||
assert value.get_file_pointer().read() == b'hello'
|
||||
|
||||
value = fields.FileField().from_json_value(
|
||||
{'content': 'aGVsbG8', 'filename': 'test.txt'} # invalid padding
|
||||
)
|
||||
assert value is None
|
||||
|
||||
|
||||
def test_new_field_type_options(pub):
|
||||
pub.load_site_options()
|
||||
if not pub.site_options.has_section('options'):
|
||||
|
|
|
@ -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
|
||||
|
@ -3742,6 +3782,16 @@ def test_formdata_user_field(pub, variable_test_data):
|
|||
assert condition.evaluate() is False
|
||||
|
||||
|
||||
def test_formdata_user_has_deleted_account(pub, variable_test_data):
|
||||
condition = Condition({'type': 'django', 'value': 'form_user_has_deleted_account'})
|
||||
assert condition.evaluate() is False
|
||||
|
||||
local_user = variable_test_data._formdata.user
|
||||
local_user.set_deleted()
|
||||
condition = Condition({'type': 'django', 'value': 'form_user_has_deleted_account'})
|
||||
assert condition.evaluate() is True
|
||||
|
||||
|
||||
def test_string_filters(pub, variable_test_data):
|
||||
tmpl = Template('{% with form_var_foo_foo|split:"a" as x %}{{x.0}}{% endwith %}', raises=True)
|
||||
for mode in (None, 'lazy'):
|
||||
|
@ -4236,6 +4286,8 @@ def test_formdata_filtering_on_fields(pub):
|
|||
('between', 'plop5|plop1', '4'),
|
||||
('between', ['plop1', 'plop5'], '4'),
|
||||
('between', ['plop5', 'plop1'], '4'),
|
||||
('icontains', 'plop', '10'),
|
||||
('icontains', 'PLOP', '10'),
|
||||
]
|
||||
for operator, value, result in params:
|
||||
context['value'] = None
|
||||
|
@ -4494,6 +4546,8 @@ def test_formdata_filtering_on_fields(pub):
|
|||
('not_in', 'a@localhost|b@localhost', '1'),
|
||||
('absent', '', '2'),
|
||||
('existing', '', '10'),
|
||||
('icontains', 'A@local', '5'),
|
||||
('icontains', '@LOCAL', '10'),
|
||||
]
|
||||
for operator, value, result in params:
|
||||
if value:
|
||||
|
@ -4543,6 +4597,8 @@ def test_formdata_filtering_on_fields(pub):
|
|||
('between', 'plop5|plop1', '4'),
|
||||
('between', ['plop1', 'plop5'], '4'),
|
||||
('between', ['plop5', 'plop1'], '4'),
|
||||
('icontains', 'plop', '10'),
|
||||
('icontains', 'PLOP', '10'),
|
||||
]
|
||||
for operator, value, result in params:
|
||||
context['value'] = None
|
||||
|
@ -4664,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()
|
||||
|
||||
|
@ -4678,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 = {
|
||||
|
@ -5007,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()
|
||||
|
@ -5312,10 +5377,15 @@ def test_fts_phone(pub):
|
|||
formdata.just_created()
|
||||
formdata.store()
|
||||
|
||||
assert formdef.data_class().count([FtsMatch('01 23 45 67 89')]) == 1
|
||||
assert formdef.data_class().count([FtsMatch('0123456789')]) == 1
|
||||
assert formdef.data_class().count([FtsMatch('+33123456789')]) == 1
|
||||
assert formdef.data_class().count([FtsMatch('+33(0)123456789')]) == 1
|
||||
formdata = formdef.data_class()()
|
||||
formdata.data = {'1': None, '2': '0123456789'}
|
||||
formdata.just_created()
|
||||
formdata.store()
|
||||
|
||||
assert formdef.data_class().count([FtsMatch('01 23 45 67 89')]) == 2
|
||||
assert formdef.data_class().count([FtsMatch('0123456789')]) == 2
|
||||
assert formdef.data_class().count([FtsMatch('+33123456789')]) == 2
|
||||
assert formdef.data_class().count([FtsMatch('+33(0)123456789')]) == 2
|
||||
assert formdef.data_class().count([FtsMatch('+33(0)123456789 foo')]) == 1
|
||||
assert formdef.data_class().count([FtsMatch('+33(0)123456789 bar')]) == 0
|
||||
assert formdef.data_class().count([FtsMatch('foo +33(0)123456789')]) == 1
|
||||
|
@ -5714,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)
|
||||
|
@ -5728,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()
|
||||
|
|
|
@ -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=[]),
|
||||
]
|
||||
|
@ -1077,3 +1077,12 @@ def test_tracking_code_attributes(pub):
|
|||
assert f2.enable_tracking_codes == formdef.enable_tracking_codes
|
||||
assert f2.tracking_code_verify_fields == formdef.tracking_code_verify_fields
|
||||
assert f2.confirmation == formdef.confirmation
|
||||
|
||||
|
||||
def test_management_sidebar_items(pub):
|
||||
formdef = FormDef()
|
||||
formdef.name = 'Foo'
|
||||
formdef.url_name = 'foo'
|
||||
formdef.management_sidebar_items = {'general', 'pending-forms'}
|
||||
f2 = assert_xml_import_export_works(formdef)
|
||||
assert f2.management_sidebar_items == {'general', 'pending-forms'}
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -474,6 +474,7 @@ PROFILE = {
|
|||
|
||||
def test_process_notification_user_provision(pub):
|
||||
User = pub.user_class
|
||||
User.wipe()
|
||||
|
||||
# create some roles
|
||||
from wcs.ctl.management.commands.hobo_deploy import Command
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -2,6 +2,8 @@ import datetime
|
|||
import html
|
||||
import os
|
||||
import string
|
||||
import subprocess
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
from django.test import override_settings
|
||||
|
@ -34,6 +36,8 @@ def pub():
|
|||
pub.substitutions.feed(pub)
|
||||
req = HTTPRequest(None, {'SCRIPT_NAME': '/', 'SERVER_NAME': 'example.net'})
|
||||
pub.set_app_dir(req)
|
||||
pub._set_request(req)
|
||||
req.session = pub.session_manager.session_class(id='1')
|
||||
pub.site_options.set('options', 'working_day_calendar', '')
|
||||
with open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w') as fd:
|
||||
pub.site_options.write(fd)
|
||||
|
@ -1634,10 +1638,25 @@ def test_json_dumps(pub):
|
|||
)
|
||||
|
||||
|
||||
def test_empty_make_public_url(pub):
|
||||
def test_make_public_url(pub):
|
||||
# empty value
|
||||
context = {'value': None}
|
||||
assert Template('{% make_public_url url=value %}').render(context) == ''
|
||||
|
||||
# lazy value
|
||||
FormDef.wipe()
|
||||
formdef = FormDef()
|
||||
formdef.name = 'lazy'
|
||||
formdef.fields = [fields.StringField(id='0', label='string', varname='foo')]
|
||||
formdef.store()
|
||||
formdata = formdef.data_class()()
|
||||
formdata.data = {'0': 'https://example.net'}
|
||||
formdata.store()
|
||||
context = CompatibilityNamesDict({'form': LazyFormData(formdata)})
|
||||
assert (
|
||||
Template('{% make_public_url url=form_var_foo %}').render(context).startswith('/api/sign-url-token/')
|
||||
)
|
||||
|
||||
|
||||
def test_with_auth(pub):
|
||||
context = {'service_url': 'https://www.example.net/api/whatever?x=y'}
|
||||
|
@ -1696,3 +1715,126 @@ def test_details_format(pub):
|
|||
pub.loggederror_class.wipe()
|
||||
assert tmpl.render(context) == 'String:\n foo'
|
||||
assert pub.loggederror_class.count() == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize('image_format', ['jpeg', 'png', 'pdf'])
|
||||
def test_convert_image_format(pub, image_format):
|
||||
with pub.complex_data():
|
||||
img = Template('{{ url|qrcode|convert_image_format:"%s" }}' % image_format).render(
|
||||
{'url': 'http://example.com/', 'allow_complex': True}
|
||||
)
|
||||
assert pub.has_cached_complex_data(img)
|
||||
value = pub.get_cached_complex_data(img)
|
||||
assert value.orig_filename == 'qrcode.%s' % image_format
|
||||
assert value.content_type == {'jpeg': 'image/jpeg', 'png': 'image/png', 'pdf': 'application/pdf'}.get(
|
||||
image_format
|
||||
)
|
||||
with value.get_file_pointer() as fp:
|
||||
if image_format in ('jpeg', 'png'):
|
||||
img = PIL.Image.open(fp)
|
||||
assert img.format == image_format.upper()
|
||||
assert img.size == (330, 330)
|
||||
assert (
|
||||
zbar_decode_qrcode(img, symbols=[ZBarSymbol.QRCODE])[0].data.decode()
|
||||
== 'http://example.com/'
|
||||
)
|
||||
else:
|
||||
assert b'%PDF-' in fp.read()[:200]
|
||||
|
||||
|
||||
def test_convert_image_format_no_name(pub):
|
||||
with pub.complex_data():
|
||||
img = Template('{{ url|qrcode|rename_file:""|convert_image_format:"jpeg" }}').render(
|
||||
{'url': 'http://example.com/', 'allow_complex': True}
|
||||
)
|
||||
assert pub.has_cached_complex_data(img)
|
||||
value = pub.get_cached_complex_data(img)
|
||||
assert value.orig_filename == 'file.jpeg'
|
||||
|
||||
|
||||
def test_convert_image_format_errors(pub):
|
||||
pub.loggederror_class.wipe()
|
||||
with pub.complex_data():
|
||||
img = Template('{{ "xxx"|convert_image_format:"gif" }}').render({'allow_complex': True})
|
||||
assert pub.has_cached_complex_data(img)
|
||||
assert pub.get_cached_complex_data(img) is None
|
||||
assert pub.loggederror_class.count() == 1
|
||||
assert (
|
||||
pub.loggederror_class.select()[0].summary
|
||||
== '|convert_image_format: unknown format (must be one of jpeg, pdf, png)'
|
||||
)
|
||||
|
||||
pub.loggederror_class.wipe()
|
||||
with pub.complex_data():
|
||||
img = Template('{{ "xxx"|convert_image_format:"jpeg" }}').render({'allow_complex': True})
|
||||
assert pub.has_cached_complex_data(img)
|
||||
assert pub.get_cached_complex_data(img) is None
|
||||
assert pub.loggederror_class.count() == 1
|
||||
assert pub.loggederror_class.select()[0].summary == '|convert_image_format: missing input'
|
||||
|
||||
pub.loggederror_class.wipe()
|
||||
with mock.patch('subprocess.run', side_effect=FileNotFoundError()):
|
||||
with pub.complex_data():
|
||||
img = Template('{{ url|qrcode|convert_image_format:"jpeg" }}').render(
|
||||
{'url': 'http://example.com/', 'allow_complex': True}
|
||||
)
|
||||
assert pub.has_cached_complex_data(img)
|
||||
assert pub.get_cached_complex_data(img) is None
|
||||
assert pub.loggederror_class.count() == 1
|
||||
assert pub.loggederror_class.select()[0].summary == '|convert_image_format: not supported'
|
||||
|
||||
pub.loggederror_class.wipe()
|
||||
with mock.patch(
|
||||
'subprocess.run', side_effect=subprocess.CalledProcessError(returncode=-1, cmd='xx', stderr=b'xxx')
|
||||
):
|
||||
with pub.complex_data():
|
||||
img = Template('{{ url|qrcode|convert_image_format:"jpeg" }}').render(
|
||||
{'url': 'http://example.com/', 'allow_complex': True}
|
||||
)
|
||||
assert pub.has_cached_complex_data(img)
|
||||
assert pub.get_cached_complex_data(img) is None
|
||||
assert pub.loggederror_class.count() == 1
|
||||
assert pub.loggederror_class.select()[0].summary == '|convert_image_format: conversion error (xxx)'
|
||||
|
||||
|
||||
def test_temporary_access_url(pub):
|
||||
FormDef.wipe()
|
||||
formdef = FormDef()
|
||||
formdef.name = 'foobar'
|
||||
formdef.fields = [fields.StringField(id='1', label='Test', varname='foo')]
|
||||
formdef.store()
|
||||
|
||||
# no formdata
|
||||
context = pub.substitutions.get_context_variables(mode='lazy')
|
||||
assert Template('{% temporary_access_url %}').render(context) == ''
|
||||
|
||||
# formdata
|
||||
formdata = formdef.data_class()()
|
||||
formdata.data = {'1': 'Foo Bar'}
|
||||
formdata.store()
|
||||
pub.substitutions.feed(formdata)
|
||||
context = pub.substitutions.get_context_variables(mode='lazy')
|
||||
assert Template('{% temporary_access_url %}').render(context).startswith('http://example.net/code/')
|
||||
|
||||
# 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}) == ''
|
||||
|
|
|
@ -81,7 +81,7 @@ def test_testdef_export_to_xml(pub):
|
|||
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
|
||||
|
||||
|
@ -501,8 +501,16 @@ def test_validation_item_field(pub):
|
|||
testdef = TestDef.create_from_formdata(formdef, formdata)
|
||||
testdef.run(formdef)
|
||||
|
||||
# no check on invalid value
|
||||
formdata.data['1'] = 'xxx'
|
||||
testdef = TestDef.create_from_formdata(formdef, formdata)
|
||||
with pytest.raises(TestError) as excinfo:
|
||||
testdef.run(formdef)
|
||||
assert str(excinfo.value) == 'Invalid value "xxx" for field "Test": invalid value selected'
|
||||
|
||||
# no check on invalid value for field with data source
|
||||
formdef.fields[0].data_source = {'type': 'jsonvalue', 'value': json.dumps({})}
|
||||
formdef.store()
|
||||
|
||||
testdef = TestDef.create_from_formdata(formdef, formdata)
|
||||
testdef.run(formdef)
|
||||
|
||||
|
@ -533,8 +541,19 @@ def test_validation_item_field_inside_block(pub):
|
|||
testdef = TestDef.create_from_formdata(formdef, formdata)
|
||||
testdef.run(formdef)
|
||||
|
||||
# no check on invalid value
|
||||
formdata.data['1'] = {'data': [{'1': 'xxx'}]}
|
||||
testdef = TestDef.create_from_formdata(formdef, formdata)
|
||||
with pytest.raises(TestError) as excinfo:
|
||||
testdef.run(formdef)
|
||||
assert (
|
||||
str(excinfo.value) == 'Empty value for field "Test" (of field "Block Data"): invalid value selected'
|
||||
)
|
||||
|
||||
# no check on invalid value for field with data source
|
||||
block.fields[0].data_source = {'type': 'jsonvalue', 'value': json.dumps({})}
|
||||
block.store()
|
||||
formdef.refresh_from_storage()
|
||||
|
||||
testdef = TestDef.create_from_formdata(formdef, formdata)
|
||||
testdef.run(formdef)
|
||||
|
||||
|
@ -1252,6 +1271,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
|
||||
|
|
|
@ -595,7 +595,7 @@ def test_wysiwygwidget_img():
|
|||
def test_mini_rich_text_widget():
|
||||
widget = MiniRichTextWidget('test')
|
||||
form = MockHtmlForm(widget)
|
||||
assert 'data-godo-schema="basic"' in form.as_html
|
||||
assert PyQuery(form.as_html)('godo-editor[schema=basic]')
|
||||
|
||||
|
||||
def test_mini_rich_text_widget_maxlength():
|
||||
|
@ -613,7 +613,7 @@ def test_mini_rich_text_widget_maxlength():
|
|||
def test_rich_text_widget():
|
||||
widget = RichTextWidget('test')
|
||||
form = MockHtmlForm(widget)
|
||||
assert 'data-godo-schema="full"' in form.as_html
|
||||
assert PyQuery(form.as_html)('godo-editor[schema=full]')
|
||||
|
||||
|
||||
def test_select_hint_widget():
|
||||
|
|
|
@ -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):
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -241,12 +241,12 @@ def test_webservice_on_error(http_requests, emails, notify_on_errors, record_on_
|
|||
'500': '500 Internal Server Error',
|
||||
'json-err0': None,
|
||||
'json-err0int': None,
|
||||
'json-err1': 'err: 1',
|
||||
'json-err1int': 'err: 1',
|
||||
'json-err1-with-desc': 'err: 1, err_desc: :(',
|
||||
'json-errstr': 'err: bug',
|
||||
'json-errheader1': 'err: 1',
|
||||
'json-errheaderstr': 'err: bug',
|
||||
'json-err1': None,
|
||||
'json-err1int': None,
|
||||
'json-err1-with-desc': None,
|
||||
'json-errstr': None,
|
||||
'json-errheader1': None,
|
||||
'json-errheaderstr': None,
|
||||
}
|
||||
wscall.request = {'url': 'http://remote.example.net/%s' % url_part}
|
||||
wscall.store()
|
||||
|
|
|
@ -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'))
|
||||
|
|
|
@ -1964,6 +1964,7 @@ def test_redirect_to_url(pub):
|
|||
|
||||
|
||||
def test_workflow_action_condition(pub):
|
||||
Workflow.wipe()
|
||||
workflow = Workflow(name='jump condition migration')
|
||||
st1 = workflow.add_status('Status1', 'st1')
|
||||
workflow.store()
|
||||
|
@ -2046,8 +2047,16 @@ def test_workflow_action_condition(pub):
|
|||
assert logged_error.summary == 'Failed to evaluate condition'
|
||||
assert logged_error.exception_class == 'NameError'
|
||||
assert logged_error.exception_message == "name 'foobar' is not defined"
|
||||
assert logged_error.expression == 'foobar == barfoo'
|
||||
assert logged_error.expression_type == 'python'
|
||||
assert logged_error.context == {
|
||||
'stack': [
|
||||
{
|
||||
'condition': 'foobar == barfoo',
|
||||
'condition_type': 'python',
|
||||
'source_label': 'Manual Jump',
|
||||
'source_url': 'http://example.net/backoffice/workflows/1/status/st1/items/_x/',
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def test_workflow_field_migration(pub):
|
||||
|
@ -2344,3 +2353,15 @@ def test_visibility_migration(pub):
|
|||
assert workflow.possible_status[0].visibility == ['__restricted__']
|
||||
assert workflow.possible_status[1].visibility == ['__hidden__']
|
||||
assert not workflow.possible_status[2].visibility
|
||||
|
||||
|
||||
def test_variables_formdef_clean_prefill(pub):
|
||||
workflow = Workflow(name='variables')
|
||||
workflow.variables_formdef = WorkflowVariablesFieldsFormDef(workflow=workflow)
|
||||
workflow.variables_formdef.fields.append(
|
||||
StringField(label='Test', default_value='123', prefill={'type': 'string', 'value': 'plop'})
|
||||
)
|
||||
workflow.store()
|
||||
|
||||
workflow = Workflow.get(id=workflow.id)
|
||||
assert not workflow.variables_formdef.fields[0].prefill
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -1119,6 +1119,117 @@ def test_edit_carddata_targeting_itself(pub):
|
|||
assert carddata.status == 'wf-%s' % st2.id
|
||||
|
||||
|
||||
def test_edit_carddata_auto_targeting_custom_id(pub):
|
||||
CardDef.wipe()
|
||||
|
||||
carddef = CardDef()
|
||||
carddef.name = 'Foo Card'
|
||||
carddef.fields = [
|
||||
StringField(id='0', label='foo', varname='foo'),
|
||||
StringField(id='1', label='slug', varname='slug'),
|
||||
]
|
||||
carddef.id_template = 'card_{{form_var_slug}}'
|
||||
carddef.store()
|
||||
carddef.data_class().wipe()
|
||||
|
||||
carddata = carddef.data_class()()
|
||||
carddata.data = {'0': 'foo', '1': 'foo'}
|
||||
carddata.store()
|
||||
carddata.just_created()
|
||||
carddata.store()
|
||||
assert carddata.identifier == 'card_foo'
|
||||
|
||||
carddef2 = CardDef()
|
||||
carddef2.name = 'Bar Card'
|
||||
carddef2.fields = [
|
||||
ItemField(id='1', label='card', varname='card', data_source={'type': 'carddef:%s' % carddef.url_name})
|
||||
]
|
||||
carddef2.store()
|
||||
|
||||
card_wf = Workflow(name='Card workflow')
|
||||
st1 = card_wf.add_status('Status1')
|
||||
st2 = card_wf.add_status('Status2')
|
||||
|
||||
edit = st1.add_action('edit_carddata', id='_edit')
|
||||
edit.formdef_slug = carddef.url_name
|
||||
edit.target_mode = 'all'
|
||||
edit.mappings = [Mapping(field_id='0', expression='bar')]
|
||||
|
||||
jump = st1.add_action('jump', '_jump')
|
||||
jump.status = st2.id
|
||||
|
||||
card_wf.store()
|
||||
|
||||
carddef2.workflow = card_wf
|
||||
carddef2.store()
|
||||
|
||||
carddata2 = carddef2.data_class()()
|
||||
carddata2.data = {
|
||||
'1': 'card_foo',
|
||||
}
|
||||
carddata2.store()
|
||||
carddata2.just_created()
|
||||
carddata2.store()
|
||||
carddata2.perform_workflow()
|
||||
|
||||
carddata.refresh_from_storage()
|
||||
assert carddata.data['0'] == 'bar'
|
||||
|
||||
|
||||
def test_edit_carddata_manual_targeting_custom_id(pub):
|
||||
CardDef.wipe()
|
||||
|
||||
carddef = CardDef()
|
||||
carddef.name = 'Foo Card'
|
||||
carddef.fields = [
|
||||
StringField(id='0', label='foo', varname='foo'),
|
||||
StringField(id='1', label='slug', varname='slug'),
|
||||
]
|
||||
carddef.id_template = 'card_{{form_var_slug}}'
|
||||
carddef.store()
|
||||
carddef.data_class().wipe()
|
||||
|
||||
carddata = carddef.data_class()()
|
||||
carddata.data = {'0': 'foo', '1': 'foo'}
|
||||
carddata.store()
|
||||
carddata.just_created()
|
||||
carddata.store()
|
||||
assert carddata.identifier == 'card_foo'
|
||||
|
||||
carddef2 = CardDef()
|
||||
carddef2.name = 'Bar Card'
|
||||
carddef2.fields = []
|
||||
carddef2.store()
|
||||
|
||||
card_wf = Workflow(name='Card workflow')
|
||||
st1 = card_wf.add_status('Status1')
|
||||
st2 = card_wf.add_status('Status2')
|
||||
|
||||
edit = st1.add_action('edit_carddata', id='_edit')
|
||||
edit.formdef_slug = carddef.url_name
|
||||
edit.target_mode = 'manual'
|
||||
edit.target_id = 'card_foo'
|
||||
edit.mappings = [Mapping(field_id='0', expression='bar')]
|
||||
|
||||
jump = st1.add_action('jump', '_jump')
|
||||
jump.status = st2.id
|
||||
|
||||
card_wf.store()
|
||||
|
||||
carddef2.workflow = card_wf
|
||||
carddef2.store()
|
||||
|
||||
carddata2 = carddef2.data_class()()
|
||||
carddata2.data = {}
|
||||
carddata2.store()
|
||||
carddata2.just_created()
|
||||
carddata2.store()
|
||||
carddata2.perform_workflow()
|
||||
|
||||
carddata.refresh_from_storage()
|
||||
assert carddata.data['0'] == 'bar'
|
||||
|
||||
|
||||
def test_edit_carddata_from_created_object(pub):
|
||||
FormDef.wipe()
|
||||
CardDef.wipe()
|
||||
|
@ -1263,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)'),
|
||||
]
|
||||
|
|
|
@ -3,16 +3,20 @@ import datetime
|
|||
import pytest
|
||||
from quixote import cleanup, get_publisher, get_response
|
||||
|
||||
from wcs import sessions
|
||||
from wcs import fields, sessions
|
||||
from wcs.carddef import CardDef
|
||||
from wcs.fields import EmailField, ItemField, StringField
|
||||
from wcs.formdef import FormDef
|
||||
from wcs.qommon.http_request import HTTPRequest
|
||||
from wcs.qommon.upload_storage import PicklableUpload
|
||||
from wcs.wf.create_formdata import Mapping
|
||||
from wcs.workflow_traces import WorkflowTrace
|
||||
from wcs.workflows import Workflow
|
||||
|
||||
from ..utilities import clean_temporary_pub, create_temporary_pub
|
||||
from ..backoffice_pages.test_all import create_user as create_backoffice_user
|
||||
from ..backoffice_pages.test_all import login
|
||||
from ..form_pages.test_all import create_user
|
||||
from ..utilities import clean_temporary_pub, create_temporary_pub, get_app
|
||||
|
||||
|
||||
def setup_module(module):
|
||||
|
@ -27,6 +31,7 @@ def teardown_module(module):
|
|||
def pub(request):
|
||||
pub = create_temporary_pub()
|
||||
pub.cfg['language'] = {'language': 'en'}
|
||||
pub.cfg['identification'] = {'methods': ['password']}
|
||||
pub.write_cfg()
|
||||
req = HTTPRequest(None, {'SERVER_NAME': 'example.net', 'SCRIPT_NAME': ''})
|
||||
req.response.filter = {}
|
||||
|
@ -122,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
|
||||
|
@ -553,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 = {}
|
||||
|
@ -574,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[:]
|
||||
|
@ -586,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 = {}
|
||||
|
@ -647,3 +656,514 @@ def test_global_timeouts_create_formdata(pub):
|
|||
pub.apply_global_action_timeouts()
|
||||
|
||||
assert subformdef.data_class().count() == 1
|
||||
|
||||
|
||||
@pytest.fixture(params=[{'attach_to_history': True}, {}])
|
||||
def create_formdata(request, pub):
|
||||
admin = create_backoffice_user(pub, is_admin=True)
|
||||
|
||||
FormDef.wipe()
|
||||
|
||||
source_formdef = FormDef()
|
||||
source_formdef.name = 'source form'
|
||||
source_formdef.workflow_roles = {'_receiver': 1}
|
||||
source_formdef.fields = [
|
||||
fields.StringField(id='0', label='string', varname='toto_string'),
|
||||
fields.FileField(id='1', label='file', varname='toto_file'),
|
||||
]
|
||||
source_formdef.store()
|
||||
|
||||
target_formdef = FormDef()
|
||||
target_formdef.name = 'target form'
|
||||
target_formdef.workflow_roles = {'_receiver': 1}
|
||||
target_formdef.backoffice_submission_roles = admin.roles[:]
|
||||
target_formdef.fields = [
|
||||
fields.StringField(id='0', label='string', varname='foo_string'),
|
||||
fields.FileField(id='1', label='file', varname='foo_file'),
|
||||
]
|
||||
target_formdef.store()
|
||||
wf = Workflow(name='create-formdata')
|
||||
|
||||
st1 = wf.add_status('New')
|
||||
st2 = wf.add_status('Resubmit')
|
||||
|
||||
jump = st1.add_action('choice', id='_resubmit')
|
||||
jump.label = 'Resubmit'
|
||||
jump.by = ['_receiver']
|
||||
jump.status = st2.id
|
||||
|
||||
create_formdata = st2.add_action('create_formdata', id='_create_formdata')
|
||||
create_formdata.varname = 'resubmitted'
|
||||
create_formdata.draft = True
|
||||
create_formdata.formdef_slug = target_formdef.url_name
|
||||
create_formdata.user_association_mode = 'keep-user'
|
||||
create_formdata.backoffice_submission = True
|
||||
create_formdata.attach_to_history = request.param.get('attach_to_history', False)
|
||||
create_formdata.mappings = [
|
||||
Mapping(field_id='0', expression='=form_var_toto_string'),
|
||||
Mapping(field_id='1', expression='=form_var_toto_file_raw'),
|
||||
]
|
||||
|
||||
redirect = st2.add_action('redirect_to_url', id='_redirect')
|
||||
redirect.url = '{{ form_links_resubmitted.form_backoffice_url }}'
|
||||
|
||||
jump = st2.add_action('jumponsubmit', id='_jump')
|
||||
jump.status = st1.id
|
||||
|
||||
wf.store()
|
||||
source_formdef.workflow_id = wf.id
|
||||
source_formdef.store()
|
||||
source_formdef.data_class().wipe()
|
||||
target_formdef.data_class().wipe()
|
||||
return locals()
|
||||
|
||||
|
||||
def test_backoffice_create_formdata_backoffice_submission(pub, create_formdata):
|
||||
# create submitting user
|
||||
user = create_formdata['pub'].user_class()
|
||||
user.name = 'Jean Darmette'
|
||||
user.email = 'jean.darmette@triffouilis.fr'
|
||||
user.store()
|
||||
|
||||
# create source formdata
|
||||
formdata = create_formdata['source_formdef'].data_class()()
|
||||
upload = PicklableUpload('/foo/bar', content_type='text/plain')
|
||||
upload.receive([b'hello world'])
|
||||
formdata.data = {
|
||||
'0': 'coucou',
|
||||
'1': upload,
|
||||
}
|
||||
formdata.user = user
|
||||
formdata.just_created()
|
||||
formdata.store()
|
||||
formdata.perform_workflow()
|
||||
|
||||
# agent login and go to backoffice management pages
|
||||
app = get_app(create_formdata['pub'])
|
||||
app = login(app)
|
||||
resp = app.get(create_formdata['source_formdef'].get_url(backoffice=True))
|
||||
|
||||
# click on first available formdata
|
||||
resp = resp.click('%s-%s' % (create_formdata['source_formdef'].id, formdata.id))
|
||||
target_data_class = create_formdata['target_formdef'].data_class()
|
||||
assert target_data_class.count() == 0
|
||||
# resubmit it through backoffice submission
|
||||
resp = resp.form.submit(name='button_resubmit')
|
||||
assert pub.loggederror_class.count() == 0
|
||||
assert target_data_class.count() == 1
|
||||
target_formdata = target_data_class.select()[0]
|
||||
|
||||
assert target_formdata.submission_context == {
|
||||
'orig_object_type': 'formdef',
|
||||
'orig_formdata_id': '1',
|
||||
'orig_formdef_id': '1',
|
||||
}
|
||||
assert target_formdata.submission_agent_id == str(create_formdata['admin'].id)
|
||||
assert target_formdata.user.id == user.id
|
||||
assert target_formdata.status == 'draft'
|
||||
assert target_formdata.receipt_time
|
||||
assert resp.location == 'http://example.net/backoffice/management/target-form/%s/' % target_formdata.id
|
||||
resp = resp.follow()
|
||||
assert resp.location == 'http://example.net/backoffice/submission/target-form/%s/' % target_formdata.id
|
||||
resp = resp.follow()
|
||||
# second redirect with magic-token
|
||||
resp = resp.follow()
|
||||
resp = resp.form.submit(name='submit') # -> validation
|
||||
resp = resp.form.submit(name='submit') # -> submission
|
||||
target_formdata = target_data_class.get(id=target_formdata.id)
|
||||
assert target_formdata.user.id == user.id
|
||||
assert target_formdata.status == 'wf-new'
|
||||
resp = resp.follow()
|
||||
pq = resp.pyquery.remove_namespaces()
|
||||
assert pq('.field-type-string .value').text() == 'coucou'
|
||||
assert pq('.field-type-file .value').text() == 'bar'
|
||||
|
||||
|
||||
def test_linked_forms_variables(pub, create_formdata):
|
||||
# create source formdata
|
||||
formdata = create_formdata['source_formdef'].data_class()()
|
||||
upload = PicklableUpload('/foo/bar', content_type='text/plain')
|
||||
upload.receive([b'hello world'])
|
||||
formdata.data = {
|
||||
'0': 'coucou',
|
||||
'1': upload,
|
||||
}
|
||||
formdata.just_created()
|
||||
formdata.store()
|
||||
formdata.perform_workflow()
|
||||
formdata.store()
|
||||
formdata.jump_status('2')
|
||||
formdata.perform_workflow()
|
||||
formdata.store()
|
||||
|
||||
pub.substitutions.reset()
|
||||
pub.substitutions.feed(formdata)
|
||||
substvars = pub.substitutions.get_context_variables(mode='lazy')
|
||||
assert str(substvars['form_links_resubmitted_form_var_foo_string']) == 'coucou'
|
||||
assert 'form_links_resubmitted_form_var_foo_string' in substvars.get_flat_keys()
|
||||
|
||||
source_formdata = create_formdata['source_formdef'].data_class().select()[0]
|
||||
|
||||
app = get_app(create_formdata['pub'])
|
||||
app = login(app)
|
||||
resp = app.get(source_formdata.get_url(backoffice=True) + 'inspect')
|
||||
assert '?expand=form_links_resubmitted' in resp
|
||||
resp = app.get(source_formdata.get_url(backoffice=True) + 'inspect?expand=form_links_resubmitted')
|
||||
assert 'form_links_resubmitted_form_var_foo_string' in resp
|
||||
|
||||
# delete target formdata
|
||||
create_formdata['target_formdef'].data_class().wipe()
|
||||
resp = app.get(source_formdata.get_url(backoffice=True) + 'inspect')
|
||||
assert '?expand=form_links_resubmitted' not in resp
|
||||
assert 'form_links_resubmitted_form_var_foo_string' not in resp
|
||||
|
||||
# delete target formdef
|
||||
create_formdata['target_formdef'].remove_self()
|
||||
resp = app.get(source_formdata.get_url(backoffice=True) + 'inspect')
|
||||
|
||||
|
||||
def test_backoffice_create_formdata_map_fields_by_varname(pub, create_formdata):
|
||||
create_formdata['create_formdata'].map_fields_by_varname = True
|
||||
create_formdata['create_formdata'].mappings = []
|
||||
create_formdata['wf'].store()
|
||||
create_formdata['source_formdef'].fields = [
|
||||
fields.StringField(id='0', label='string', varname='string0'),
|
||||
fields.FileField(id='1', label='file', varname='file1'),
|
||||
fields.StringField(id='2', label='string', varname='string2', required=False),
|
||||
fields.FileField(id='3', label='file', varname='file3', required=False),
|
||||
]
|
||||
create_formdata['source_formdef'].store()
|
||||
create_formdata['target_formdef'].fields = [
|
||||
fields.StringField(id='0', label='string', varname='string0'),
|
||||
fields.FileField(id='1', label='file', varname='file1'),
|
||||
fields.StringField(id='2', label='string', varname='string2', required=False),
|
||||
fields.FileField(id='3', label='file', varname='file3', required=False),
|
||||
]
|
||||
create_formdata['target_formdef'].store()
|
||||
|
||||
# create submitting user
|
||||
user = create_formdata['pub'].user_class()
|
||||
user.name = 'Jean Darmette'
|
||||
user.email = 'jean.darmette@triffouilis.fr'
|
||||
user.store()
|
||||
|
||||
# create source formdata
|
||||
create_formdata['source_formdef'].digest_templates = {'default': 'blah'}
|
||||
create_formdata['source_formdef'].store()
|
||||
formdata = create_formdata['source_formdef'].data_class()()
|
||||
create_formdata['formdata'] = formdata
|
||||
upload = PicklableUpload('/foo/bar', content_type='text/plain')
|
||||
upload.receive([b'hello world'])
|
||||
formdata.data = {
|
||||
'0': 'coucou',
|
||||
'1': upload,
|
||||
}
|
||||
formdata.user = user
|
||||
formdata.just_created()
|
||||
formdata.store()
|
||||
formdata.perform_workflow()
|
||||
|
||||
# agent login and go to backoffice management pages
|
||||
app = get_app(create_formdata['pub'])
|
||||
app = login(app)
|
||||
resp = app.get(create_formdata['source_formdef'].get_url(backoffice=True))
|
||||
|
||||
# click on first available formdata
|
||||
resp = resp.click('%s-%s' % (create_formdata['source_formdef'].id, formdata.id))
|
||||
target_data_class = create_formdata['target_formdef'].data_class()
|
||||
assert target_data_class.count() == 0
|
||||
# resubmit it through backoffice submission
|
||||
resp = resp.form.submit(name='button_resubmit')
|
||||
assert pub.loggederror_class.count() == 0
|
||||
assert target_data_class.count() == 1
|
||||
target_formdata = target_data_class.select()[0]
|
||||
|
||||
assert target_formdata.submission_context == {
|
||||
'orig_object_type': 'formdef',
|
||||
'orig_formdata_id': '1',
|
||||
'orig_formdef_id': '1',
|
||||
}
|
||||
assert target_formdata.submission_agent_id == str(create_formdata['admin'].id)
|
||||
assert target_formdata.user.id == user.id
|
||||
assert target_formdata.status == 'draft'
|
||||
assert resp.location == 'http://example.net/backoffice/management/target-form/%s/' % target_formdata.id
|
||||
resp = resp.follow()
|
||||
assert resp.location == 'http://example.net/backoffice/submission/target-form/%s/' % target_formdata.id
|
||||
resp = resp.follow()
|
||||
# second redirect with magic-token
|
||||
resp = resp.follow()
|
||||
# check parent form is displayed in sidebar
|
||||
assert resp.pyquery('.extra-context--orig-data').attr.href == formdata.get_backoffice_url()
|
||||
assert resp.pyquery('.extra-context--orig-data').text() == 'source form #1-1 (blah)'
|
||||
resp = resp.form.submit(name='submit') # -> validation
|
||||
resp = resp.form.submit(name='submit') # -> submission
|
||||
target_formdata = target_data_class.get(id=target_formdata.id)
|
||||
assert target_formdata.user.id == user.id
|
||||
assert target_formdata.status == 'wf-new'
|
||||
resp = resp.follow()
|
||||
pq = resp.pyquery.remove_namespaces()
|
||||
assert pq('.field-type-string .value').text() == 'coucou'
|
||||
assert pq('.field-type-file .value').text() == 'bar'
|
||||
|
||||
resp = app.get(create_formdata['formdata'].get_url(backoffice=True))
|
||||
pq = resp.pyquery.remove_namespaces()
|
||||
assert pq('.field-type-string .value').text() == 'coucou'
|
||||
if create_formdata['create_formdata'].attach_to_history:
|
||||
assert pq('.wf-links')
|
||||
else:
|
||||
assert not pq('.wf-links')
|
||||
|
||||
|
||||
def test_backoffice_create_formdata_map_fields_by_varname_plus_empty(pub, create_formdata):
|
||||
create_formdata['create_formdata'].map_fields_by_varname = True
|
||||
create_formdata['create_formdata'].mappings = [
|
||||
Mapping(field_id='0', expression=None),
|
||||
]
|
||||
create_formdata['wf'].store()
|
||||
create_formdata['source_formdef'].fields = [
|
||||
fields.StringField(id='0', label='string', varname='string0'),
|
||||
fields.StringField(id='2', label='string', varname='string2', required=False),
|
||||
]
|
||||
create_formdata['source_formdef'].store()
|
||||
create_formdata['target_formdef'].fields = [
|
||||
fields.StringField(id='0', label='string', varname='string0'),
|
||||
fields.StringField(id='2', label='string', varname='string2', required=False),
|
||||
]
|
||||
create_formdata['target_formdef'].store()
|
||||
|
||||
# create submitting user
|
||||
user = create_formdata['pub'].user_class()
|
||||
user.name = 'Jean Darmette'
|
||||
user.email = 'jean.darmette@triffouilis.fr'
|
||||
user.store()
|
||||
|
||||
# create source formdata
|
||||
formdata = create_formdata['source_formdef'].data_class()()
|
||||
create_formdata['formdata'] = formdata
|
||||
formdata.data = {
|
||||
'0': 'foo',
|
||||
'2': 'bar',
|
||||
}
|
||||
formdata.user = user
|
||||
formdata.just_created()
|
||||
formdata.store()
|
||||
formdata.perform_workflow()
|
||||
|
||||
# agent login and go to backoffice management pages
|
||||
app = get_app(create_formdata['pub'])
|
||||
app = login(app)
|
||||
resp = app.get(create_formdata['source_formdef'].get_url(backoffice=True))
|
||||
|
||||
# click on first available formdata
|
||||
resp = resp.click('%s-%s' % (create_formdata['source_formdef'].id, formdata.id))
|
||||
target_data_class = create_formdata['target_formdef'].data_class()
|
||||
assert target_data_class.count() == 0
|
||||
# resubmit it through backoffice submission
|
||||
resp = resp.form.submit(name='button_resubmit')
|
||||
assert target_data_class.count() == 1
|
||||
target_formdata = target_data_class.select()[0]
|
||||
|
||||
assert target_formdata.submission_context == {
|
||||
'orig_object_type': 'formdef',
|
||||
'orig_formdata_id': '1',
|
||||
'orig_formdef_id': '1',
|
||||
}
|
||||
assert target_formdata.submission_agent_id == str(create_formdata['admin'].id)
|
||||
assert target_formdata.user.id == user.id
|
||||
assert target_formdata.status == 'draft'
|
||||
assert target_formdata.data == {'0': None, '2': 'bar'}
|
||||
|
||||
|
||||
def test_create_formdata_show_link_in_history(pub):
|
||||
FormDef.wipe()
|
||||
pub.tracking_code_class.wipe()
|
||||
|
||||
target_formdef = FormDef()
|
||||
target_formdef.name = 'target-form'
|
||||
target_formdef.fields = [
|
||||
fields.StringField(id='0', label='string', varname='foo_string'),
|
||||
]
|
||||
target_formdef.store()
|
||||
|
||||
wf = Workflow(name='create-formdata')
|
||||
wf.possible_status = Workflow.get_default_workflow().possible_status[:]
|
||||
create = wf.possible_status[1].add_action('create_formdata', id='_create', prepend=True)
|
||||
create.label = 'create a new linked form'
|
||||
create.varname = 'resubmitted'
|
||||
create.mappings = [
|
||||
Mapping(field_id='0', expression='="coincoin"'),
|
||||
]
|
||||
wf.store()
|
||||
|
||||
source_formdef = FormDef()
|
||||
source_formdef.name = 'source-form'
|
||||
source_formdef.fields = []
|
||||
source_formdef.workflow_id = wf.id
|
||||
source_formdef.enable_tracking_codes = True
|
||||
source_formdef.store()
|
||||
|
||||
create.formdef_slug = target_formdef.url_name
|
||||
create.attach_to_history = True
|
||||
wf.store()
|
||||
|
||||
source_formdef.data_class().wipe()
|
||||
target_formdef.data_class().wipe()
|
||||
|
||||
create_user(pub)
|
||||
app = login(get_app(pub), username='foo', password='foo')
|
||||
resp = app.get('/source-form/')
|
||||
resp = resp.forms[0].submit('submit')
|
||||
assert 'Check values then click submit.' in resp.text
|
||||
resp = resp.forms[0].submit('submit')
|
||||
assert resp.status_int == 302
|
||||
resp = resp.follow()
|
||||
assert 'The form has been recorded' in resp.text
|
||||
|
||||
formdata = source_formdef.data_class().select()[0]
|
||||
|
||||
# logged access: show link to created formdata
|
||||
resp = app.get('/source-form/%s/' % formdata.id)
|
||||
assert 'The form has been recorded on' in resp.text
|
||||
assert 'New form "target-form" created' in resp.text
|
||||
assert resp.pyquery('.wf-links a')
|
||||
|
||||
# anonymous access via tracking code: no link
|
||||
app = get_app(pub)
|
||||
resp = app.get('/code/%s/load' % formdata.tracking_code)
|
||||
resp = resp.follow()
|
||||
assert 'The form has been recorded on' in resp.text
|
||||
assert 'New form "target-form" created' not in resp.text
|
||||
assert not resp.pyquery('.wf-links a')
|
||||
|
||||
|
||||
def test_create_formdata_multiple(pub):
|
||||
FormDef.wipe()
|
||||
pub.tracking_code_class.wipe()
|
||||
|
||||
target_formdef = FormDef()
|
||||
target_formdef.name = 'target-form'
|
||||
target_formdef.fields = [
|
||||
fields.StringField(id='0', label='string', varname='foo_string'),
|
||||
]
|
||||
target_formdef.store()
|
||||
|
||||
wf = Workflow(name='create-formdata')
|
||||
wf.possible_status = Workflow.get_default_workflow().possible_status[:]
|
||||
global_action = wf.add_global_action('create formdata')
|
||||
trigger = global_action.triggers[0]
|
||||
trigger.roles = ['_submitter']
|
||||
create = global_action.add_action('create_formdata')
|
||||
create.label = 'create a new linked form'
|
||||
create.varname = 'resubmitted'
|
||||
create.mappings = [Mapping(field_id='0', expression='plop')]
|
||||
wf.store()
|
||||
|
||||
source_formdef = FormDef()
|
||||
source_formdef.name = 'source-form'
|
||||
source_formdef.fields = []
|
||||
source_formdef.workflow_id = wf.id
|
||||
source_formdef.enable_tracking_codes = True
|
||||
source_formdef.store()
|
||||
|
||||
create.formdef_slug = target_formdef.url_name
|
||||
wf.store()
|
||||
|
||||
source_formdef.data_class().wipe()
|
||||
target_formdef.data_class().wipe()
|
||||
|
||||
user = create_user(pub)
|
||||
|
||||
formdata = source_formdef.data_class()()
|
||||
formdata.user_id = user.id
|
||||
formdata.just_created()
|
||||
formdata.store()
|
||||
|
||||
formdata2 = source_formdef.data_class()()
|
||||
formdata2.user_id = user.id
|
||||
formdata2.just_created()
|
||||
formdata2.store()
|
||||
|
||||
app = login(get_app(pub), username='foo', password='foo')
|
||||
resp = app.get(formdata.get_url())
|
||||
|
||||
resp = resp.form.submit('button-action-1')
|
||||
assert target_formdef.data_class().count() == 1
|
||||
|
||||
resp = app.get(formdata.get_url())
|
||||
resp = resp.form.submit('button-action-1')
|
||||
assert target_formdef.data_class().count() == 2
|
||||
|
||||
# do it from another formdata (should not trigger recursive call detection)
|
||||
resp = app.get(formdata2.get_url())
|
||||
resp = resp.form.submit('button-action-1')
|
||||
assert target_formdef.data_class().count() == 3
|
||||
|
||||
|
||||
@pytest.mark.parametrize('mode', ['single', 'partial'])
|
||||
def test_create_formdata_edit_single_or_partial_pages(pub, mode):
|
||||
FormDef.wipe()
|
||||
pub.tracking_code_class.wipe()
|
||||
|
||||
target_formdef = FormDef()
|
||||
target_formdef.name = 'target-form'
|
||||
target_formdef.fields = [
|
||||
fields.PageField(id='1', label='page1'),
|
||||
fields.StringField(id='2', label='string', varname='foo_string'),
|
||||
fields.PageField(id='3', label='page2', varname='page2'),
|
||||
fields.StringField(id='4', label='string2', varname='bar_string'),
|
||||
fields.PageField(id='4', label='page3'),
|
||||
]
|
||||
target_formdef.store()
|
||||
|
||||
wf = Workflow(name='create-formdata')
|
||||
wf.possible_status = Workflow.get_default_workflow().possible_status[:]
|
||||
create = wf.possible_status[1].add_action('create_formdata', id='_create', prepend=True)
|
||||
create.label = 'create a new linked form'
|
||||
create.varname = 'resubmitted'
|
||||
create.draft = True
|
||||
create.formdef_slug = target_formdef.url_name
|
||||
create.attach_to_history = True
|
||||
create.draft_edit_operation_mode = mode
|
||||
create.page_identifier = 'page2'
|
||||
create.mappings = [
|
||||
Mapping(field_id='2', expression='blah1'),
|
||||
Mapping(field_id='4', expression='blah2'),
|
||||
]
|
||||
wf.store()
|
||||
|
||||
source_formdef = FormDef()
|
||||
source_formdef.name = 'source-form'
|
||||
source_formdef.fields = []
|
||||
source_formdef.workflow_id = wf.id
|
||||
source_formdef.enable_tracking_codes = True
|
||||
source_formdef.store()
|
||||
|
||||
source_formdef.data_class().wipe()
|
||||
target_formdef.data_class().wipe()
|
||||
|
||||
create_user(pub)
|
||||
app = login(get_app(pub), username='foo', password='foo')
|
||||
resp = app.get('/source-form/')
|
||||
resp = resp.forms[0].submit('submit') # -> validation
|
||||
resp = resp.forms[0].submit('submit').follow() # -> submit
|
||||
assert 'The form has been recorded' in resp.text
|
||||
|
||||
created_url = resp.pyquery('.wf-links a')[0].attrib['href']
|
||||
resp = app.get(created_url).follow()
|
||||
|
||||
if mode == 'single':
|
||||
assert resp.pyquery('.wcs-step').length == 2
|
||||
else:
|
||||
assert resp.pyquery('.wcs-step').length == 3
|
||||
assert resp.pyquery('.wcs-step.current .label').text() == 'page2 (current step)'
|
||||
assert resp.forms[1]['f4'].value == 'blah2'
|
||||
|
||||
if mode == 'partial':
|
||||
resp = resp.forms[1].submit('submit') # -> page 3
|
||||
assert resp.pyquery('.wcs-step.current .label').text() == 'page3 (current step)'
|
||||
|
||||
resp = resp.forms[1].submit('submit') # -> validation
|
||||
resp = resp.forms[1].submit('submit') # -> submit
|
||||
assert target_formdef.data_class().count() == 1
|
||||
formdata = target_formdef.data_class().select()[0]
|
||||
assert formdata.data == {'2': 'blah1', '4': 'blah2'}
|
|
@ -58,7 +58,7 @@ def test_display_message_rich_text(pub):
|
|||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get(display_message.get_admin_url())
|
||||
assert resp.pyquery('textarea[data-godo-schema]') # godo
|
||||
assert resp.pyquery('godo-editor') # godo
|
||||
|
||||
display_message.message = '<table><tr><td>hello world</td></tr></table>'
|
||||
workflow.store()
|
||||
|
@ -76,13 +76,13 @@ def test_display_message_rich_text(pub):
|
|||
display_message.message = '<ul>{% for item in lists %}<li>{{ item }}</li>{% endfor %}</ul>'
|
||||
workflow.store()
|
||||
resp = app.get(display_message.get_admin_url())
|
||||
assert resp.pyquery('textarea:not([data-config]):not([data-godo-schema])') # plain textarea
|
||||
assert resp.pyquery('textarea:not([data-config])') # plain textarea
|
||||
|
||||
pub.site_options.set('options', 'rich-text-wf-displaymsg', 'auto-textarea')
|
||||
with open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w') as fd:
|
||||
pub.site_options.write(fd)
|
||||
resp = app.get(display_message.get_admin_url())
|
||||
assert resp.pyquery('textarea:not([data-config]):not([data-godo-schema])') # plain textarea
|
||||
assert resp.pyquery('textarea:not([data-config])') # plain textarea
|
||||
|
||||
pub.site_options.set('options', 'rich-text-wf-displaymsg', 'auto-ckeditor')
|
||||
with open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w') as fd:
|
||||
|
@ -102,10 +102,10 @@ def test_display_message_rich_text(pub):
|
|||
with open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w') as fd:
|
||||
pub.site_options.write(fd)
|
||||
resp = app.get(display_message.get_admin_url())
|
||||
assert resp.pyquery('textarea[data-godo-schema]') # godo
|
||||
assert resp.pyquery('godo-editor') # godo
|
||||
|
||||
pub.site_options.set('options', 'rich-text-wf-displaymsg', 'textarea')
|
||||
with open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w') as fd:
|
||||
pub.site_options.write(fd)
|
||||
resp = app.get(display_message.get_admin_url())
|
||||
assert resp.pyquery('textarea:not([data-config]):not([data-godo-schema])') # plain textarea
|
||||
assert resp.pyquery('textarea:not([data-config])') # plain textarea
|
||||
|
|
|
@ -18,7 +18,8 @@ from wcs.wf.backoffice_fields import SetBackofficeFieldsWorkflowStatusItem
|
|||
from wcs.wf.sendmail import SendmailWorkflowStatusItem
|
||||
from wcs.workflows import Workflow, WorkflowBackofficeFieldsFormDef
|
||||
|
||||
from ..utilities import MockSubstitutionVariables, clean_temporary_pub, create_temporary_pub
|
||||
from ..admin_pages.test_all import create_superuser
|
||||
from ..utilities import MockSubstitutionVariables, clean_temporary_pub, create_temporary_pub, get_app, login
|
||||
|
||||
|
||||
def setup_module(module):
|
||||
|
@ -33,6 +34,7 @@ def teardown_module(module):
|
|||
def pub(request):
|
||||
pub = create_temporary_pub()
|
||||
pub.cfg['language'] = {'language': 'en'}
|
||||
pub.cfg['identification'] = {'methods': ['password']}
|
||||
pub.write_cfg()
|
||||
req = HTTPRequest(None, {'SERVER_NAME': 'example.net', 'SCRIPT_NAME': ''})
|
||||
req.response.filter = {}
|
||||
|
@ -748,3 +750,26 @@ def test_email_invalid_recipients(pub, req):
|
|||
if req:
|
||||
get_response().process_after_jobs()
|
||||
assert send_email_job.call_count == 0
|
||||
|
||||
|
||||
def test_workflows_edit_sendmail_action(pub):
|
||||
create_superuser(pub)
|
||||
Workflow.wipe()
|
||||
workflow = Workflow(name='foo')
|
||||
st1 = workflow.add_status(name='baz')
|
||||
workflow.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get(st1.get_admin_url())
|
||||
|
||||
resp.forms[0]['action-interaction'] = 'Email'
|
||||
resp = resp.forms[0].submit()
|
||||
resp = resp.follow()
|
||||
|
||||
resp = resp.click('Email')
|
||||
resp.form['to$element0$choice'] = '__other'
|
||||
resp.form['to$element0$other$value_template'] = '{{ test }}'
|
||||
resp.form.submit('submit')
|
||||
|
||||
workflow.refresh_from_storage()
|
||||
assert workflow.possible_status[0].items[0].to == ['{{ test }}']
|
||||
|
|
|
@ -14,6 +14,7 @@ from webtest import Radio, Upload
|
|||
|
||||
from wcs import sessions
|
||||
from wcs.blocks import BlockDef
|
||||
from wcs.carddef import CardDef
|
||||
from wcs.fields import (
|
||||
BlockField,
|
||||
BoolField,
|
||||
|
@ -35,7 +36,7 @@ from wcs.qommon import force_str
|
|||
from wcs.qommon.form import UploadedFile
|
||||
from wcs.qommon.http_request import HTTPRequest
|
||||
from wcs.qommon.upload_storage import PicklableUpload
|
||||
from wcs.wf.export_to_model import ExportToModel, UploadValidationError, transform_to_pdf
|
||||
from wcs.wf.export_to_model import ExportToModel, transform_to_pdf
|
||||
from wcs.workflows import Workflow, WorkflowBackofficeFieldsFormDef
|
||||
|
||||
from ..admin_pages.test_all import create_superuser
|
||||
|
@ -319,6 +320,8 @@ def test_export_to_model_xml(pub):
|
|||
item.attach_to_history = True
|
||||
|
||||
def run(template, filename='/foo/template.xml', content_type='application/xml'):
|
||||
formdata.evolution[-1].parts = None
|
||||
formdata.store()
|
||||
pub.loggederror_class.wipe()
|
||||
upload = QuixoteUpload(filename, content_type=content_type)
|
||||
upload.fp = io.BytesIO()
|
||||
|
@ -329,8 +332,9 @@ def test_export_to_model_xml(pub):
|
|||
pub.substitutions.reset()
|
||||
pub.substitutions.feed(formdata)
|
||||
item.perform(formdata)
|
||||
with open(formdata.evolution[0].parts[-1].get_file_path()) as fd:
|
||||
return fd.read()
|
||||
if formdata.evolution[0].parts:
|
||||
with open(formdata.evolution[0].parts[-1].get_file_path()) as fd:
|
||||
return fd.read()
|
||||
|
||||
# good XML
|
||||
assert run(template='<a>{{ form_var_string }}</a>') == '<a>écho</a>'
|
||||
|
@ -340,23 +344,53 @@ def test_export_to_model_xml(pub):
|
|||
assert run(template='<a>{{ form_var_string }}</a>', filename='/foo/template.svg') == '<a>écho</a>'
|
||||
|
||||
# unknown file format
|
||||
with pytest.raises(UploadValidationError) as e:
|
||||
run(
|
||||
template='<a>{{ form_var_string }}</a>',
|
||||
filename='/foo/template.txt',
|
||||
content_type='application/octet-stream',
|
||||
)
|
||||
assert str(e.value) == 'Only OpenDocument and XML files can be used.'
|
||||
assert not run(
|
||||
template='<a>{{ form_var_string }}</a>',
|
||||
filename='/foo/template.txt',
|
||||
content_type='application/octet-stream',
|
||||
)
|
||||
assert pub.loggederror_class.count() == 1
|
||||
assert pub.loggederror_class.select()[0].summary == 'Only OpenDocument and XML files can be used.'
|
||||
|
||||
# invalid UTF-8
|
||||
with pytest.raises(UploadValidationError) as e:
|
||||
assert run(template=b'<name>test \xE0 {{form_var_string}}</name>') == ''
|
||||
assert str(e.value) == 'XML model files must be UTF-8.'
|
||||
assert not run(template=b'<name>test \xE0 {{form_var_string}}</name>')
|
||||
assert pub.loggederror_class.count() == 1
|
||||
assert pub.loggederror_class.select()[0].summary == 'XML model files must be UTF-8.'
|
||||
|
||||
# malformed XML
|
||||
assert run(template='<a>{{ form_var_string }}<a>') == '<a>écho<a>'
|
||||
# on error in the XML correctness no exception is raised but an error is logged
|
||||
assert pub.loggederror_class.count() == 1
|
||||
assert pub.loggederror_class.select()[0].summary == 'The rendered template is not a valid XML document.'
|
||||
|
||||
|
||||
def test_export_to_model_disabled_rtf(pub):
|
||||
formdef = FormDef()
|
||||
formdef.name = 'foo-export-to-template-with-django'
|
||||
formdef.fields = [
|
||||
StringField(id='1', label='String', varname='string'),
|
||||
]
|
||||
formdef.store()
|
||||
formdata = formdef.data_class()()
|
||||
formdata.just_created()
|
||||
formdata.store()
|
||||
|
||||
item = ExportToModel()
|
||||
item.method = 'non-interactive'
|
||||
item.attach_to_history = True
|
||||
upload = QuixoteUpload('test.rtf', content_type='application/rtf')
|
||||
upload.fp = io.BytesIO()
|
||||
upload.fp.write(b'{\\rtf...')
|
||||
upload.fp.seek(0)
|
||||
item.model_file = UploadedFile(pub.app_dir, None, upload)
|
||||
item.convert_to_pdf = False
|
||||
pub.substitutions.reset()
|
||||
pub.substitutions.feed(formdata)
|
||||
|
||||
pub.loggederror_class.wipe()
|
||||
item.perform(formdata)
|
||||
assert pub.loggederror_class.count() == 1
|
||||
assert pub.loggederror_class.select()[0].summary == 'Only OpenDocument and XML files can be used.'
|
||||
|
||||
|
||||
@pytest.mark.parametrize('filename', ['template-form-details.odt', 'template-form-details-no-styles.odt'])
|
||||
|
@ -754,3 +788,80 @@ def test_workflows_edit_export_to_model_action_check_template(pub):
|
|||
model_content = zip_out_fp.getvalue()
|
||||
resp.form['model_file'] = Upload('test.odt', model_content)
|
||||
resp.form.submit('submit').follow() # success
|
||||
|
||||
|
||||
def test_export_to_model_from_template(pub):
|
||||
CardDef.wipe()
|
||||
carddef = CardDef()
|
||||
carddef.name = 'card'
|
||||
carddef.fields = [
|
||||
FileField(id='1', label='File', varname='file'),
|
||||
StringField(id='2', label='String', varname='string'),
|
||||
]
|
||||
carddef.store()
|
||||
|
||||
template_filename = os.path.join(os.path.dirname(__file__), '..', 'template.odt')
|
||||
with open(template_filename, 'rb') as fd:
|
||||
template = fd.read()
|
||||
upload = QuixoteUpload('/foo/template.odt', content_type='application/octet-stream')
|
||||
upload.fp = io.BytesIO()
|
||||
upload.fp.write(template)
|
||||
upload.fp.seek(0)
|
||||
|
||||
carddata = carddef.data_class()()
|
||||
carddata.data = {'1': upload, '2': 'blah'}
|
||||
carddata.just_created()
|
||||
carddata.store()
|
||||
|
||||
wf = Workflow(name='test_export_to_model_from_template')
|
||||
wf.backoffice_fields_formdef = WorkflowBackofficeFieldsFormDef(wf)
|
||||
wf.backoffice_fields_formdef.fields = [
|
||||
FileField(id='bo1', label='bo field 1', varname='backoffice_file1'),
|
||||
]
|
||||
st1 = wf.add_status('Status1')
|
||||
wf.store()
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'foo-export'
|
||||
formdef.fields = [
|
||||
StringField(id='1', label='String', varname='string'),
|
||||
]
|
||||
formdef.workflow_id = wf.id
|
||||
formdef.store()
|
||||
formdata = formdef.data_class()()
|
||||
formdata.data = {}
|
||||
formdata.just_created()
|
||||
formdata.store()
|
||||
pub.substitutions.feed(formdata)
|
||||
|
||||
item = ExportToModel()
|
||||
item.method = 'non-interactive'
|
||||
item.convert_to_pdf = False
|
||||
item.model_file_mode = 'template'
|
||||
item.model_file_template = '{{cards|objects:"card"|first|get:"form_var_file" }}'
|
||||
item.parent = st1
|
||||
item.backoffice_filefield_id = 'bo1'
|
||||
item.perform(formdata)
|
||||
|
||||
assert 'bo1' in formdata.data
|
||||
fbo1 = formdata.data['bo1']
|
||||
assert fbo1.base_filename == 'template.odt'
|
||||
assert fbo1.content_type == 'application/octet-stream'
|
||||
with zipfile.ZipFile(fbo1.get_file()) as zfile:
|
||||
assert b'foo-export' in zfile.read('content.xml')
|
||||
|
||||
pub.loggederror_class.wipe()
|
||||
item.model_file_template = '{{cards|objects:"card"|first|get:"form_var_string" }}'
|
||||
formdata.data = {}
|
||||
item.perform(formdata)
|
||||
assert 'bo1' not in formdata.data
|
||||
assert pub.loggederror_class.count() == 1
|
||||
assert pub.loggederror_class.select()[0].summary == 'Invalid value obtained for model file (\'blah\')'
|
||||
|
||||
pub.loggederror_class.wipe()
|
||||
item.model_file_template = '{% if foo %}{{ foo }}{% end %}' # invalid template
|
||||
formdata.data = {}
|
||||
item.perform(formdata)
|
||||
assert 'bo1' not in formdata.data
|
||||
assert pub.loggederror_class.count() == 1
|
||||
assert pub.loggederror_class.select()[0].summary == 'Failed to evaluate template for action'
|
||||
|
|
|
@ -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
|
||||
|
||||
|
@ -158,8 +161,16 @@ def test_jump_bad_python_condition(pub):
|
|||
assert logged_error.summary == 'Failed to evaluate condition'
|
||||
assert logged_error.exception_class == 'NameError'
|
||||
assert logged_error.exception_message == "name 'form_var_foobar' is not defined"
|
||||
assert logged_error.expression == 'form_var_foobar == 0'
|
||||
assert logged_error.expression_type == 'python'
|
||||
assert logged_error.context == {
|
||||
'stack': [
|
||||
{
|
||||
'condition': 'form_var_foobar == 0',
|
||||
'condition_type': 'python',
|
||||
'source_label': 'Automatic Jump',
|
||||
'source_url': '',
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
pub.loggederror_class.wipe()
|
||||
item.condition = {'type': 'python', 'value': '~ invalid ~'}
|
||||
|
@ -169,8 +180,16 @@ def test_jump_bad_python_condition(pub):
|
|||
assert logged_error.summary == 'Failed to evaluate condition'
|
||||
assert logged_error.exception_class == 'SyntaxError'
|
||||
assert logged_error.exception_message == 'invalid syntax (<string>, line 1)'
|
||||
assert logged_error.expression == '~ invalid ~'
|
||||
assert logged_error.expression_type == 'python'
|
||||
assert logged_error.context == {
|
||||
'stack': [
|
||||
{
|
||||
'condition': '~ invalid ~',
|
||||
'source_url': '',
|
||||
'source_label': 'Automatic Jump',
|
||||
'condition_type': 'python',
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def test_jump_django_conditions(pub):
|
||||
|
@ -207,8 +226,16 @@ def test_jump_django_conditions(pub):
|
|||
assert logged_error.summary == 'Failed to evaluate condition'
|
||||
assert logged_error.exception_class == 'TemplateSyntaxError'
|
||||
assert logged_error.exception_message == "Could not parse the remainder: '~' from '~'"
|
||||
assert logged_error.expression == '~ invalid ~'
|
||||
assert logged_error.expression_type == 'django'
|
||||
assert logged_error.context == {
|
||||
'stack': [
|
||||
{
|
||||
'condition': '~ invalid ~',
|
||||
'source_url': '',
|
||||
'source_label': 'Automatic Jump',
|
||||
'condition_type': 'django',
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def test_timeout(pub):
|
||||
|
@ -605,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
|
||||
|
|
|
@ -7,7 +7,8 @@ import pytest
|
|||
import responses
|
||||
from quixote import cleanup, get_publisher
|
||||
|
||||
from wcs.fields import BoolField, FileField, ItemField, ItemsField, StringField
|
||||
from wcs.blocks import BlockDef
|
||||
from wcs.fields import BlockField, BoolField, FileField, ItemField, ItemsField, StringField
|
||||
from wcs.formdef import FormDef
|
||||
from wcs.qommon.http_request import HTTPRequest
|
||||
from wcs.wf.wscall import JournalWsCallErrorPart, WebserviceCallStatusItem
|
||||
|
@ -1141,6 +1142,12 @@ def test_webservice_with_complex_data_in_query_string(http_requests, pub):
|
|||
),
|
||||
}
|
||||
|
||||
BlockDef.wipe()
|
||||
block = BlockDef()
|
||||
block.name = 'foobar'
|
||||
block.fields = [StringField(id='1', label='String', varname='string')]
|
||||
block.store()
|
||||
|
||||
FormDef.wipe()
|
||||
formdef = FormDef()
|
||||
formdef.name = 'baz'
|
||||
|
@ -1151,6 +1158,7 @@ def test_webservice_with_complex_data_in_query_string(http_requests, pub):
|
|||
StringField(id='4', label='4th field', varname='empty_str'),
|
||||
StringField(id='5', label='5th field', varname='none'),
|
||||
BoolField(id='6', label='6th field', varname='bool'),
|
||||
BlockField(id='7', label='7th field', varname='block', block_slug=block.slug, max_items=3),
|
||||
]
|
||||
formdef.workflow_id = wf.id
|
||||
formdef.store()
|
||||
|
@ -1167,6 +1175,17 @@ def test_webservice_with_complex_data_in_query_string(http_requests, pub):
|
|||
formdata.data['4'] = 'empty_str'
|
||||
formdata.data['5'] = None
|
||||
formdata.data['6'] = False
|
||||
formdata.data['7'] = {
|
||||
'data': [
|
||||
{
|
||||
'1': 'plop',
|
||||
},
|
||||
{
|
||||
'1': 'poulpe',
|
||||
},
|
||||
],
|
||||
'schema': {},
|
||||
}
|
||||
formdata.just_created()
|
||||
formdata.store()
|
||||
|
||||
|
@ -1191,11 +1210,12 @@ def test_webservice_with_complex_data_in_query_string(http_requests, pub):
|
|||
'none': '{{ form_var_none }}',
|
||||
'bool': '{{ form_var_bool_raw }}',
|
||||
'time': '{{ "13:12"|time }}',
|
||||
'block_template': '{% for b in form_var_block %}{{ b.string }}{% endfor %}',
|
||||
}
|
||||
pub.substitutions.feed(formdata)
|
||||
with get_publisher().complex_data():
|
||||
item.perform(formdata)
|
||||
item.perform(formdata)
|
||||
assert sorted(urllib.parse.parse_qsl(urllib.parse.urlparse(http_requests.get_last('url')).query)) == [
|
||||
('block_template', 'ploppoulpe'),
|
||||
('bool', 'False'),
|
||||
('decimal', '1E+3'),
|
||||
('decimal2', '1000.1'),
|
||||
|
|
2
tox.ini
2
tox.ini
|
@ -12,6 +12,7 @@ setenv =
|
|||
LC_ALL=C
|
||||
LC_TIME=C
|
||||
LANG=C
|
||||
JOB_NAME={env:JOB_NAME:}
|
||||
coverage: COVERAGE=--cov-report xml --cov-report html --cov=wcs/ --cov-config .coveragerc -v
|
||||
passenv =
|
||||
USER
|
||||
|
@ -76,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,
|
||||
},
|
||||
)
|
||||
|
|
|
@ -54,6 +54,7 @@ class BlockDirectory(FieldsDirectory):
|
|||
'inspect',
|
||||
'duplicate',
|
||||
('history', 'snapshots_dir'),
|
||||
'overwrite',
|
||||
]
|
||||
field_def_page_class = BlockFieldDefPage
|
||||
blacklisted_types = ['page', 'table', 'table-select', 'tablerows', 'ranked-items', 'blocks', 'computed']
|
||||
|
@ -106,6 +107,14 @@ class BlockDirectory(FieldsDirectory):
|
|||
r = TemplateIO(html=True)
|
||||
r += htmltext('<div id="appbar">')
|
||||
r += htmltext('<h2>%s</h2>') % self.objectdef.name
|
||||
r += htmltext('<span class="actions">')
|
||||
r += htmltext('<a class="extra-actions-menu-opener"></a>')
|
||||
r += htmltext('<ul class="extra-actions-menu">')
|
||||
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 += htmltext('<a href="settings" rel="popup" role="button">%s</a>') % _('Settings')
|
||||
r += htmltext('</span>')
|
||||
r += htmltext('</div>')
|
||||
r += utils.last_modification_block(obj=self.objectdef)
|
||||
r += get_session().display_message()
|
||||
|
@ -132,17 +141,25 @@ class BlockDirectory(FieldsDirectory):
|
|||
|
||||
def get_new_field_form_sidebar(self, page_id):
|
||||
r = TemplateIO(html=True)
|
||||
r += htmltext('<ul id="sidebar-actions">')
|
||||
r += htmltext('<li><a href="delete" rel="popup">%s</a></li>') % _('Delete')
|
||||
r += htmltext('<li><a href="duplicate" rel="popup">%s</a></li>') % _('Duplicate')
|
||||
r += htmltext('<li><a href="export">%s</a></li>') % _('Export')
|
||||
if get_publisher().snapshot_class:
|
||||
r += htmltext('<li><a rel="popup" href="history/save">%s</a></li>') % _('Save snapshot')
|
||||
r += htmltext('<li><a href="history/">%s</a></li>') % _('History')
|
||||
r += htmltext('<li><a href="inspect">%s</a></li>') % _('Inspector')
|
||||
r += htmltext('<li><a href="settings" rel="popup">%s</a></li>') % _('Settings')
|
||||
r += htmltext('</ul>')
|
||||
r += super().get_new_field_form_sidebar(page_id=page_id)
|
||||
r += htmltext('<h3>%s</h3>') % _('Actions')
|
||||
r += htmltext('<ul class="sidebar--buttons">')
|
||||
r += htmltext('<li><a class="button button-paragraph" href="duplicate" rel="popup">%s</a>') % _(
|
||||
'Duplicate'
|
||||
)
|
||||
if get_publisher().snapshot_class:
|
||||
r += htmltext('<li><a class="button button-paragraph" href="history/save">%s</a>') % _(
|
||||
'Save snapshot'
|
||||
)
|
||||
r += htmltext('<li><a class="button button-paragraph" rel="popup" href="overwrite">%s</a>') % _(
|
||||
'Overwrite with new import'
|
||||
)
|
||||
r += htmltext('</ul>')
|
||||
r += htmltext('<h3>%s</h3>') % _('Navigation')
|
||||
r += htmltext('<ul class="sidebar--buttons">')
|
||||
r += htmltext('<li><a class="button button-paragraph" href="history/">%s</a></li>') % _('History')
|
||||
r += htmltext('<li><a class="button button-paragraph" href="inspect">%s</a></li>') % _('Inspector')
|
||||
r += htmltext('</ul>')
|
||||
return r.getvalue()
|
||||
|
||||
def delete(self):
|
||||
|
@ -209,6 +226,41 @@ class BlockDirectory(FieldsDirectory):
|
|||
content_type='application/x-wcs-form',
|
||||
)
|
||||
|
||||
def overwrite(self):
|
||||
form = Form(enctype='multipart/form-data')
|
||||
form.widgets.append(
|
||||
HtmlWidget(
|
||||
'<div class="warningnotice"><p>%s</p></div>'
|
||||
% _('Field data will be lost if overwriting with an incompatible block.')
|
||||
)
|
||||
)
|
||||
form.add(FileWidget, 'file', title=_('File'), required=True)
|
||||
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():
|
||||
try:
|
||||
return self.overwrite_submit(form)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
get_response().breadcrumb.append(('overwrite', _('Overwrite')))
|
||||
get_response().set_title(title=_('Overwrite'))
|
||||
r = TemplateIO(html=True)
|
||||
r += htmltext('<h2>%s</h2>') % _('Overwrite')
|
||||
r += form.render()
|
||||
return r.getvalue()
|
||||
|
||||
def overwrite_submit(self, form):
|
||||
blockdef = BlocksDirectory.import_blockdef(form)
|
||||
self.objectdef.name = blockdef.name
|
||||
self.objectdef.digest_template = blockdef.digest_template
|
||||
self.objectdef.fields = blockdef.fields
|
||||
self.objectdef.store(comment=_('Overwritten'))
|
||||
return redirect('.')
|
||||
|
||||
def settings(self):
|
||||
get_response().breadcrumb.append(('settings', _('Settings')))
|
||||
form = Form()
|
||||
|
@ -409,12 +461,13 @@ class BlocksDirectory(Directory):
|
|||
r += form.render()
|
||||
return r.getvalue()
|
||||
|
||||
def import_submit(self, form):
|
||||
@classmethod
|
||||
def import_blockdef(cls, form):
|
||||
fp = form.get_widget('file').parse().fp
|
||||
|
||||
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
|
||||
|
@ -433,6 +486,10 @@ class BlocksDirectory(Directory):
|
|||
form.set_error('file', msg)
|
||||
raise ValueError()
|
||||
|
||||
return blockdef
|
||||
|
||||
def import_submit(self, form):
|
||||
blockdef = self.import_blockdef(form)
|
||||
initial_blockdef_name = blockdef.name
|
||||
blockdef_names = [x.name for x in BlockDef.select()]
|
||||
copy_no = 1
|
||||
|
|
|
@ -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'))
|
||||
|
|
|
@ -150,7 +150,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
|
||||
|
|
|
@ -27,6 +27,7 @@ from wcs.categories import CardDefCategory, DataSourceCategory
|
|||
from wcs.data_sources import (
|
||||
DataSourceSelectionWidget,
|
||||
NamedDataSource,
|
||||
NamedDataSourceImportError,
|
||||
RefreshAgendas,
|
||||
get_structured_items,
|
||||
has_chrono,
|
||||
|
@ -388,7 +389,7 @@ class NamedDataSourcePage(Directory):
|
|||
def preview_block(self):
|
||||
get_request().disable_error_notifications = True
|
||||
get_request().ignore_session = True
|
||||
get_response().filter = {'raw': True}
|
||||
get_response().raw = True
|
||||
data_source = self.datasource.extended_data_source
|
||||
try:
|
||||
items = get_structured_items(data_source)
|
||||
|
@ -667,15 +668,22 @@ class NamedDataSourcesDirectory(Directory):
|
|||
def import_submit(self, form):
|
||||
fp = form.get_widget('file').parse().fp
|
||||
|
||||
error = False
|
||||
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
|
||||
reason = str(e)
|
||||
except ValueError:
|
||||
error = True
|
||||
|
||||
if error:
|
||||
form.set_error('file', _('Invalid File'))
|
||||
if reason:
|
||||
msg = _('Invalid File (%s)') % reason
|
||||
else:
|
||||
msg = _('Invalid File')
|
||||
form.set_error('file', msg)
|
||||
raise ValueError()
|
||||
|
||||
try:
|
||||
|
|
|
@ -98,11 +98,16 @@ class FieldDefPage(Directory):
|
|||
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,
|
||||
)
|
||||
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 +160,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 +187,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 +201,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():
|
||||
|
@ -374,6 +392,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':
|
||||
|
@ -527,8 +557,8 @@ class FieldsDirectory(Directory):
|
|||
)
|
||||
return r.getvalue()
|
||||
|
||||
r += htmltext('<div id="new-field">')
|
||||
r += htmltext('<h3>%s</h3>') % _('New Field')
|
||||
r += htmltext('<div id="new-field">')
|
||||
get_request().form = None # ignore the eventual ?page=x
|
||||
form = self.get_new_field_form(page_id)
|
||||
r += form.render()
|
||||
|
|
|
@ -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,7 +62,7 @@ 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
|
||||
|
@ -211,7 +214,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 +293,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,
|
||||
|
@ -332,10 +352,12 @@ class OptionsDirectory(Directory):
|
|||
def management(self):
|
||||
form = Form(enctype='multipart/form-data')
|
||||
form.add(
|
||||
CheckboxWidget,
|
||||
'include_download_all_button',
|
||||
title=_('Include button to download all files'),
|
||||
value=self.formdef.include_download_all_button,
|
||||
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(
|
||||
CheckboxWidget,
|
||||
|
@ -493,7 +515,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)
|
||||
|
@ -503,6 +528,10 @@ class OptionsDirectory(Directory):
|
|||
if has_error:
|
||||
continue
|
||||
new_value = widget.parse()
|
||||
if attr == '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:
|
||||
self.changed = True
|
||||
|
@ -604,7 +633,6 @@ class FormDefPage(Directory, TempfileDirectoryMixin):
|
|||
'enable',
|
||||
'workflow',
|
||||
'role',
|
||||
('workflow-options', 'workflow_options'),
|
||||
('workflow-variables', 'workflow_variables'),
|
||||
('workflow-status-remapping', 'workflow_status_remapping'),
|
||||
'roles',
|
||||
|
@ -777,7 +805,11 @@ class FormDefPage(Directory, TempfileDirectoryMixin):
|
|||
'options/management',
|
||||
_('Management'),
|
||||
_('Custom')
|
||||
if (self.formdef.skip_from_360_view or self.formdef.include_download_all_button)
|
||||
if (
|
||||
self.formdef.skip_from_360_view
|
||||
or self.formdef.management_sidebar_items
|
||||
not in ({'__default__'}, self.formdef.get_default_management_sidebar_items())
|
||||
)
|
||||
else _('Default'),
|
||||
),
|
||||
'tracking_code': self.add_option_line(
|
||||
|
@ -826,17 +858,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:
|
||||
|
@ -1140,7 +1163,12 @@ class FormDefPage(Directory, TempfileDirectoryMixin):
|
|||
# there are existing formdata, status will have to be mapped
|
||||
return redirect('workflow-status-remapping?new=%s' % workflow_id)
|
||||
|
||||
job = WorkflowChangeJob(formdef=self.formdef, new_workflow_id=workflow_id, status_mapping={})
|
||||
job = WorkflowChangeJob(
|
||||
formdef=self.formdef,
|
||||
new_workflow_id=workflow_id,
|
||||
status_mapping={},
|
||||
user_id=get_session().user,
|
||||
)
|
||||
job.store()
|
||||
get_response().add_after_job(job)
|
||||
return redirect(job.get_processing_url())
|
||||
|
@ -1230,7 +1258,10 @@ class FormDefPage(Directory, TempfileDirectoryMixin):
|
|||
return self.workflow_status_remapping()
|
||||
|
||||
job = WorkflowChangeJob(
|
||||
formdef=self.formdef, new_workflow_id=new_workflow.id, status_mapping=status_mapping
|
||||
formdef=self.formdef,
|
||||
new_workflow_id=new_workflow.id,
|
||||
status_mapping=status_mapping,
|
||||
user_id=get_session().user,
|
||||
)
|
||||
job.store()
|
||||
get_response().add_after_job(job)
|
||||
|
@ -1417,7 +1448,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
|
||||
|
@ -1664,55 +1695,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')))
|
||||
|
@ -1775,6 +1757,56 @@ class FormDefPage(Directory, TempfileDirectoryMixin):
|
|||
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
|
||||
context['is_carddef'] = isinstance(self.formdef, CardDef)
|
||||
|
||||
return template.QommonTemplateResponse(
|
||||
templates=[self.inspect_template_name],
|
||||
context=context,
|
||||
|
@ -1802,6 +1834,7 @@ class FormsDirectory(AccessControlled, Directory):
|
|||
'categories',
|
||||
('data-sources', 'data_sources'),
|
||||
('application', 'applications_dir'),
|
||||
('test-users', 'test_users'),
|
||||
]
|
||||
|
||||
category_class = Category
|
||||
|
@ -1834,6 +1867,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)
|
||||
|
@ -1987,7 +2026,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)
|
||||
|
@ -2042,19 +2081,20 @@ class FormsDirectory(AccessControlled, Directory):
|
|||
|
||||
|
||||
class WorkflowChangeJob(AfterJob):
|
||||
def __init__(self, formdef, new_workflow_id, status_mapping):
|
||||
def __init__(self, formdef, new_workflow_id, status_mapping, user_id):
|
||||
super().__init__(
|
||||
label=_('Updating data for new workflow'),
|
||||
formdef_class=formdef.__class__,
|
||||
formdef_id=formdef.id,
|
||||
new_workflow_id=new_workflow_id,
|
||||
status_mapping=status_mapping,
|
||||
user_id=user_id,
|
||||
)
|
||||
|
||||
def execute(self):
|
||||
formdef = self.kwargs['formdef_class'].get(self.kwargs['formdef_id'])
|
||||
workflow = Workflow.get(self.kwargs['new_workflow_id'])
|
||||
formdef.change_workflow(workflow, self.kwargs['status_mapping'])
|
||||
formdef.change_workflow(workflow, self.kwargs['status_mapping'], user_id=self.kwargs.get('user_id'))
|
||||
|
||||
def done_action_url(self):
|
||||
formdef = self.kwargs['formdef_class'].get(self.kwargs['formdef_id'])
|
||||
|
|
|
@ -28,6 +28,28 @@ from wcs.qommon.form import CheckboxesWidget, DateWidget, Form
|
|||
from wcs.sql_criterias import Equal, Less, NotEqual, NotNull, Null, Or
|
||||
|
||||
|
||||
class ErrorFrame:
|
||||
def __init__(self, context):
|
||||
self.context = context or {}
|
||||
|
||||
def source(self):
|
||||
if self.context.get('source_url'):
|
||||
return {
|
||||
'url': self.context.get('source_url'),
|
||||
'label': self.context.get('source_label'),
|
||||
}
|
||||
return None
|
||||
|
||||
def get_frame_lines(self):
|
||||
for key, value in self.context.items():
|
||||
key_label = {
|
||||
'condition': _('Condition'),
|
||||
'condition_type': _('Condition type'),
|
||||
}.get(key)
|
||||
if key_label:
|
||||
yield {'label': key_label, 'value': value}
|
||||
|
||||
|
||||
class LoggedErrorDirectory(Directory):
|
||||
_q_exports = ['', 'delete', 'ack']
|
||||
do_not_call_in_templates = True
|
||||
|
@ -63,6 +85,10 @@ class LoggedErrorDirectory(Directory):
|
|||
'text': _('Text'),
|
||||
}.get(self.error.expression_type, _('Unknown'))
|
||||
|
||||
def get_context_frames(self):
|
||||
for frame_context in reversed(self.error.context.get('stack') or []):
|
||||
yield ErrorFrame(frame_context)
|
||||
|
||||
def get_tabs(self):
|
||||
r = TemplateIO(html=True)
|
||||
parts = (
|
||||
|
@ -253,14 +279,15 @@ class LoggedErrorsDirectory(AccessControlled, Directory):
|
|||
options.append(('carddef', _('Card Models'), 'carddef'))
|
||||
if backoffice_root.is_accessible('workflows'):
|
||||
options.append(('others', _('Others'), 'others'))
|
||||
form.add(
|
||||
CheckboxesWidget,
|
||||
'types',
|
||||
title=_('Error types'),
|
||||
value=[x[0] for x in options], # check all by default
|
||||
options=options,
|
||||
required=True,
|
||||
)
|
||||
if not (self.formdef_id or self.workflow_id):
|
||||
form.add(
|
||||
CheckboxesWidget,
|
||||
'types',
|
||||
title=_('Error types'),
|
||||
value=[x[0] for x in options], # check all by default
|
||||
options=options,
|
||||
required=True,
|
||||
)
|
||||
form.add(
|
||||
DateWidget,
|
||||
'latest_occurence',
|
||||
|
@ -275,17 +302,27 @@ class LoggedErrorsDirectory(AccessControlled, Directory):
|
|||
return redirect('.')
|
||||
|
||||
if form.get_submit() == 'submit' and not form.has_errors():
|
||||
type_criterias = []
|
||||
if 'formdef' in form.get_widget('types').parse():
|
||||
type_criterias.append(Equal('formdef_class', 'FormDef'))
|
||||
if 'carddef' in form.get_widget('types').parse():
|
||||
type_criterias.append(Equal('formdef_class', 'CardDef'))
|
||||
if 'others' in form.get_widget('types').parse():
|
||||
type_criterias.append(Null('formdef_class'))
|
||||
criterias = [
|
||||
Less('latest_occurence_timestamp', form.get_widget('latest_occurence').parse()),
|
||||
Or(type_criterias),
|
||||
]
|
||||
criterias = []
|
||||
|
||||
if self.formdef_id and self.formdef_class:
|
||||
criterias.append(Equal('formdef_id', self.formdef_id))
|
||||
criterias.append(Equal('formdef_class', self.formdef_class.__name__))
|
||||
elif self.workflow_id:
|
||||
criterias.append(Equal('workflow_id', self.workflow_id))
|
||||
else:
|
||||
if 'formdef' in form.get_widget('types').parse():
|
||||
criterias.append(Equal('formdef_class', 'FormDef'))
|
||||
if 'carddef' in form.get_widget('types').parse():
|
||||
criterias.append(Equal('formdef_class', 'CardDef'))
|
||||
if 'others' in form.get_widget('types').parse():
|
||||
criterias.append(Null('formdef_class'))
|
||||
criterias = [Or(criterias)]
|
||||
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('.')
|
||||
|
||||
|
|
|
@ -150,7 +150,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
|
||||
|
|
|
@ -36,10 +36,10 @@ from quixote.html import TemplateIO, htmltext
|
|||
from wcs.api_access import ApiAccess
|
||||
from wcs.blocks import BlockDef, BlockdefImportError
|
||||
from wcs.carddef import CardDef
|
||||
from wcs.data_sources import NamedDataSource
|
||||
from wcs.data_sources import NamedDataSource, NamedDataSourceImportError
|
||||
from wcs.fields.map import MapOptionsMixin
|
||||
from wcs.formdef import FormDef, FormdefImportError, get_formdefs_of_all_kinds
|
||||
from wcs.qommon import _, audit, errors, get_cfg, ident, misc, template
|
||||
from wcs.qommon import _, audit, errors, get_cfg, ident, misc, pgettext_lazy, template
|
||||
from wcs.qommon.admin.cfg import cfg_submit, hobo_kwargs
|
||||
from wcs.qommon.admin.emails import EmailsDirectory
|
||||
from wcs.qommon.admin.texts import TextsDirectory
|
||||
|
@ -61,6 +61,7 @@ from wcs.qommon.form import (
|
|||
TextWidget,
|
||||
)
|
||||
from wcs.workflows import Workflow, WorkflowImportError
|
||||
from wcs.wscalls import NamedWsCallImportError
|
||||
|
||||
from .api_access import ApiAccessDirectory
|
||||
from .data_sources import NamedDataSourcesDirectory
|
||||
|
@ -487,7 +488,7 @@ class SettingsDirectory(AccessControlled, Directory):
|
|||
('data-sources', 'data_sources'),
|
||||
'wscalls',
|
||||
('api-access', 'api_access'),
|
||||
('submission-channels', 'submission_channels'),
|
||||
('backoffice-submission', 'backoffice_submission'),
|
||||
]
|
||||
|
||||
emails = EmailsDirectory()
|
||||
|
@ -636,10 +637,10 @@ class SettingsDirectory(AccessControlled, Directory):
|
|||
_('Geolocation'),
|
||||
_('Configure geolocation and geocoding'),
|
||||
)
|
||||
if enabled('submission-channels'):
|
||||
r += htmltext('<dt><a href="submission-channels">%s</a></dt> <dd>%s</dd>') % (
|
||||
_('Submission channels'),
|
||||
_('Configure submission channels related options'),
|
||||
if enabled('backoffice-submission'):
|
||||
r += htmltext('<dt><a href="backoffice-submission">%s</a></dt> <dd>%s</dd>') % (
|
||||
_('Backoffice Submission'),
|
||||
_('Configure backoffice submission related options'),
|
||||
)
|
||||
if enabled('users'):
|
||||
r += htmltext('<dt><a href="users/">%s</a></dt> <dd>%s</dd>') % (_('Users'), _('Configure users'))
|
||||
|
@ -1272,9 +1273,29 @@ $('#form_default-zoom-level').on('change', function() {
|
|||
)
|
||||
return redirect('.')
|
||||
|
||||
def submission_channels(self):
|
||||
def backoffice_submission(self):
|
||||
form = Form(enctype='multipart/form-data')
|
||||
submission_channels_cfg = get_cfg('submission-channels', {})
|
||||
backoffice_submission_cfg = get_cfg('backoffice-submission', {})
|
||||
form.add(
|
||||
RadiobuttonsWidget,
|
||||
'sidebar_menu_entry',
|
||||
title=_('Sidebar menu entry'),
|
||||
value=backoffice_submission_cfg.get('sidebar_menu_entry', 'visible'),
|
||||
options=[
|
||||
('visible', pgettext_lazy('sidebar_menu_entry', 'Visible'), 'visible'),
|
||||
('hidden', pgettext_lazy('sidebar_menu_entry', 'Hidden'), 'hidden'),
|
||||
],
|
||||
extra_css_class='widget-inline-radio',
|
||||
)
|
||||
form.add(
|
||||
StringWidget,
|
||||
'redirect',
|
||||
title=_('URL for backoffice submission'),
|
||||
hint=_('Leave empty to use native screen.'),
|
||||
value=backoffice_submission_cfg.get('redirect', ''),
|
||||
size=80,
|
||||
)
|
||||
form.add(
|
||||
CheckboxWidget,
|
||||
'include-in-global-listing',
|
||||
|
@ -1288,18 +1309,15 @@ $('#form_default-zoom-level').on('change', function() {
|
|||
return redirect('.')
|
||||
|
||||
if not form.is_submitted() or form.has_errors():
|
||||
get_response().breadcrumb.append(('submission-channels', _('Submission channels')))
|
||||
get_response().set_title(_('Submission channels'))
|
||||
get_response().breadcrumb.append(('backoffice-submission', _('Backoffice Submission')))
|
||||
get_response().set_title(_('Backoffice submission settings'))
|
||||
r = TemplateIO(html=True)
|
||||
r += htmltext('<h2>%s</h2>') % _('Submission channels')
|
||||
r += htmltext('<h2>%s</h2>') % _('Backoffice submission settings')
|
||||
r += form.render()
|
||||
return r.getvalue()
|
||||
else:
|
||||
cfg_submit(
|
||||
form,
|
||||
'submission-channels',
|
||||
('include-in-global-listing',),
|
||||
)
|
||||
cfg_submit(form, 'submission-channels', ('include-in-global-listing',))
|
||||
cfg_submit(form, 'backoffice-submission', ('sidebar_menu_entry', 'redirect'))
|
||||
return redirect('.')
|
||||
|
||||
|
||||
|
@ -1495,7 +1513,10 @@ class SiteImportAfterJob(AfterJob):
|
|||
msg = _(e.msg) % e.msg_args
|
||||
if e.details:
|
||||
msg += ' [%s]' % e.details
|
||||
error = _('Failed to import a workflow (%s); site import did not complete.') % msg
|
||||
error = _('Failed to import objects (%s); site import did not complete.') % msg
|
||||
except (NamedDataSourceImportError, NamedWsCallImportError) as e:
|
||||
results = None
|
||||
error = _('Failed to import objects (%s); site import did not complete.') % str(e)
|
||||
|
||||
self.results = results
|
||||
if error:
|
||||
|
|
|
@ -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
|
||||
|
@ -35,14 +36,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 +71,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):
|
||||
|
@ -155,6 +155,7 @@ class TestPage(FormBackOfficeStatusPage):
|
|||
self.testdef = TestDef.get(component)
|
||||
except KeyError:
|
||||
raise TraversalError()
|
||||
self.testdef.formdef = objectdef
|
||||
|
||||
filled = self.testdef.build_formdata(objectdef, include_fields=True)
|
||||
super().__init__(objectdef, filled)
|
||||
|
@ -227,13 +228,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,13 +253,7 @@ class TestPage(FormBackOfficeStatusPage):
|
|||
return r.getvalue()
|
||||
else:
|
||||
self.testdef.name = form.get_widget('name').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.user_uuid = form.get_widget('user').parse()
|
||||
|
||||
self.testdef.store()
|
||||
return redirect('.')
|
||||
|
@ -344,13 +340,18 @@ class TestsDirectory(Directory):
|
|||
]
|
||||
|
||||
if formdata_options:
|
||||
creation_options = [
|
||||
('empty', _('Fill data manually'), 'empty'),
|
||||
('formdata', _('Import data from form'), 'formdata'),
|
||||
]
|
||||
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',
|
||||
options=[
|
||||
('empty', _('Fill data manually'), 'empty'),
|
||||
('formdata', _('Import data from form'), 'formdata'),
|
||||
],
|
||||
options=creation_options,
|
||||
value='empty',
|
||||
attrs={'data-dynamic-display-parent': 'true'},
|
||||
)
|
||||
|
@ -362,7 +363,7 @@ class TestsDirectory(Directory):
|
|||
hint=_('Form is only used for initial data alimentation, no link is kept with created test.'),
|
||||
attrs={
|
||||
'data-dynamic-display-child-of': 'creation_mode',
|
||||
'data-dynamic-display-value-in': 'formdata',
|
||||
'data-dynamic-display-value-in': 'formdata|formdata-wf',
|
||||
},
|
||||
**{'data-autocomplete': 'true'},
|
||||
)
|
||||
|
@ -381,20 +382,27 @@ 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.agent_id = test_agent_user.test_uuid
|
||||
testdef.store()
|
||||
return redirect(testdef.get_admin_url() + 'edit-data/')
|
||||
else:
|
||||
formdata_id = form.get_widget('formdata').parse()
|
||||
formdata = self.objectdef.data_class().get(formdata_id)
|
||||
|
||||
testdef = TestDef.create_from_formdata(self.objectdef, formdata)
|
||||
testdef = TestDef.create_from_formdata(
|
||||
self.objectdef,
|
||||
formdata,
|
||||
add_workflow_tests=bool(creation_mode_widget.parse() == 'formdata-wf'),
|
||||
)
|
||||
testdef.name = form.get_widget('name').parse()
|
||||
testdef.agent_id = get_session().user
|
||||
testdef.agent_id = test_agent_user.test_uuid
|
||||
testdef.store()
|
||||
return redirect(testdef.get_admin_url())
|
||||
|
||||
|
@ -535,6 +543,7 @@ class TestResultDetailPage(Directory):
|
|||
formdata.geolocations = formdata_json.get('geolocations')
|
||||
formdata.criticality_level = formdata_json['criticality_level']
|
||||
formdata.anonymised = formdata_json['anonymised']
|
||||
formdata.workflow_data = formdata_json.get('workflow', {}).get('data', {})
|
||||
formdata.set_auto_fields()
|
||||
|
||||
# load fields
|
||||
|
@ -640,12 +649,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,
|
||||
)
|
||||
|
||||
|
@ -656,7 +666,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'])
|
||||
|
@ -664,11 +674,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)
|
||||
|
@ -753,6 +766,16 @@ class WebserviceResponsePage(Directory):
|
|||
validation_function=validate_json,
|
||||
)
|
||||
|
||||
form.add(
|
||||
RadiobuttonsWidget,
|
||||
'status_code',
|
||||
title=_('Response status code'),
|
||||
required=True,
|
||||
options=[200, 204, 400, 401, 403, 404, 500, 502, 503],
|
||||
value=self.webservice_response.status_code,
|
||||
extra_css_class='widget-inline-radio',
|
||||
)
|
||||
|
||||
form.add(
|
||||
WidgetDict,
|
||||
'qs_data',
|
||||
|
@ -819,6 +842,7 @@ class WebserviceResponsePage(Directory):
|
|||
self.webservice_response.name = form.get_widget('name').parse()
|
||||
self.webservice_response.payload = form.get_widget('payload').parse()
|
||||
self.webservice_response.url = form.get_widget('url').parse()
|
||||
self.webservice_response.status_code = form.get_widget('status_code').parse()
|
||||
self.webservice_response.qs_data = form.get_widget('qs_data').parse()
|
||||
self.webservice_response.method = form.get_widget('method').parse()
|
||||
self.webservice_response.post_data = form.get_widget('post_data').parse()
|
||||
|
@ -901,3 +925,164 @@ class WebserviceResponseDirectory(Directory):
|
|||
webservice_response.store()
|
||||
|
||||
return redirect(self.testdef.get_admin_url() + 'webservice-responses/%s/' % webservice_response.id)
|
||||
|
||||
|
||||
class TestUserPage(Directory):
|
||||
_q_exports = ['', 'delete']
|
||||
|
||||
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('<h2>%s</h2>') % (_('Edit test user'))
|
||||
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('..')
|
||||
|
||||
|
||||
class TestUsersDirectory(Directory):
|
||||
_q_exports = ['', 'new']
|
||||
|
||||
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()
|
||||
|
|
|
@ -401,7 +401,7 @@ class UsersDirectory(Directory):
|
|||
r += htmltext('</div>')
|
||||
|
||||
if get_request().form.get('ajax') == 'true':
|
||||
get_response().filter = {'raw': True}
|
||||
get_response().raw = True
|
||||
return r.getvalue()
|
||||
|
||||
ident_methods = get_cfg('identification', {}).get('methods', [])
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
@ -43,7 +44,10 @@ class WorkflowTestActionPage(Directory):
|
|||
|
||||
self.action.fill_admin_form(form, self.formdef)
|
||||
|
||||
form.add_submit('submit', _('Submit'))
|
||||
if not form.widgets:
|
||||
form.add_global_errors([htmltext(self.action.empty_form_error)])
|
||||
else:
|
||||
form.add_submit('submit', _('Submit'))
|
||||
form.add_submit('cancel', _('Cancel'))
|
||||
|
||||
if form.get_widget('cancel').parse():
|
||||
|
@ -92,7 +96,8 @@ class WorkflowTestActionPage(Directory):
|
|||
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)
|
||||
action_position = self.testdef.workflow_tests.actions.index(self.action)
|
||||
self.testdef.workflow_tests.actions.insert(action_position + 1, new_action)
|
||||
self.testdef.store()
|
||||
return redirect('..')
|
||||
|
||||
|
@ -144,7 +149,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,
|
||||
|
|
|
@ -40,18 +40,16 @@ from wcs.qommon.afterjobs import AfterJob
|
|||
from wcs.qommon.form import (
|
||||
CheckboxWidget,
|
||||
ColourWidget,
|
||||
CompositeWidget,
|
||||
ComputedExpressionWidget,
|
||||
FileWidget,
|
||||
Form,
|
||||
HtmlWidget,
|
||||
RadiobuttonsWidget,
|
||||
RichTextWidget,
|
||||
SingleSelectWidget,
|
||||
SlugWidget,
|
||||
StringWidget,
|
||||
UrlWidget,
|
||||
VarnameWidget,
|
||||
WysiwygTextWidget,
|
||||
)
|
||||
from wcs.sql_criterias import Equal
|
||||
from wcs.workflows import (
|
||||
|
@ -443,7 +441,7 @@ class WorkflowUI:
|
|||
|
||||
|
||||
class WorkflowItemPage(Directory):
|
||||
_q_exports = ['', 'delete', 'copy', ('payload-eval', 'payload_eval')]
|
||||
_q_exports = ['', 'delete', 'copy']
|
||||
|
||||
def __init__(self, workflow, parent, component):
|
||||
try:
|
||||
|
@ -992,7 +990,7 @@ class WorkflowStatusPage(Directory):
|
|||
value=(self.status.forced_endpoint is True),
|
||||
)
|
||||
form.add(
|
||||
WysiwygTextWidget,
|
||||
RichTextWidget,
|
||||
'backoffice_info_text',
|
||||
title=_('Information text for backoffice'),
|
||||
value=self.status.backoffice_info_text,
|
||||
|
@ -1084,80 +1082,15 @@ 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):
|
||||
section = 'workflows'
|
||||
blacklisted_attributes = ['condition', 'prefill', 'display_locations', 'anonymise']
|
||||
|
||||
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'),
|
||||
|
@ -1168,11 +1101,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):
|
||||
|
@ -1591,7 +1519,7 @@ class GlobalActionPage(WorkflowStatusPage):
|
|||
def options(self):
|
||||
form = Form(enctype='multipart/form-data')
|
||||
form.add(
|
||||
WysiwygTextWidget,
|
||||
RichTextWidget,
|
||||
'backoffice_info_text',
|
||||
title=_('Information text for backoffice'),
|
||||
value=self.action.backoffice_info_text,
|
||||
|
@ -2223,7 +2151,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
|
||||
|
|
|
@ -26,7 +26,7 @@ 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.utils import grep_strings
|
||||
from wcs.wscalls import NamedWsCall, WsCallRequestWidget
|
||||
from wcs.wscalls import NamedWsCall, NamedWsCallImportError, WsCallRequestWidget
|
||||
|
||||
|
||||
class NamedWsCallUI:
|
||||
|
@ -51,6 +51,17 @@ class NamedWsCallUI:
|
|||
)
|
||||
form.add(WsCallRequestWidget, 'request', value=self.wscall.request, title=_('Request'), required=True)
|
||||
|
||||
form.widgets.append(
|
||||
HtmlWidget(
|
||||
'<div class="infonotice"><p>%s</p></div>'
|
||||
% _(
|
||||
'This tab is about connection, payload, and HTTP errors. '
|
||||
'Application errors ("err" property in response different than zero) '
|
||||
'are always silent.'
|
||||
),
|
||||
tab=('error', _('Error Handling')),
|
||||
)
|
||||
)
|
||||
form.add(
|
||||
CheckboxWidget,
|
||||
'notify_on_errors',
|
||||
|
@ -63,6 +74,7 @@ class NamedWsCallUI:
|
|||
'record_on_errors',
|
||||
title=_('Record on errors'),
|
||||
value=self.wscall.record_on_errors if self.wscall.slug else True,
|
||||
default_value=True,
|
||||
tab=('error', _('Error Handling')),
|
||||
)
|
||||
if not self.wscall.is_readonly():
|
||||
|
@ -161,7 +173,7 @@ class NamedWsCallPage(Directory):
|
|||
def usage(self):
|
||||
get_request().disable_error_notifications = True
|
||||
get_request().ignore_session = True
|
||||
get_response().filter = {'raw': True}
|
||||
get_response().raw = True
|
||||
|
||||
usage = {}
|
||||
|
||||
|
@ -306,15 +318,22 @@ class NamedWsCallsDirectory(Directory):
|
|||
def import_submit(self, form):
|
||||
fp = form.get_widget('file').parse().fp
|
||||
|
||||
error = False
|
||||
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
|
||||
reason = str(e)
|
||||
except ValueError:
|
||||
error = True
|
||||
|
||||
if error:
|
||||
form.set_error('file', _('Invalid File'))
|
||||
if reason:
|
||||
msg = _('Invalid File (%s)') % reason
|
||||
else:
|
||||
msg = _('Invalid File')
|
||||
form.set_error('file', msg)
|
||||
raise ValueError()
|
||||
|
||||
try:
|
||||
|
|
19
wcs/api.py
19
wcs/api.py
|
@ -1409,7 +1409,7 @@ class ApiDirectory(Directory):
|
|||
'geojson',
|
||||
'jobs',
|
||||
('card-file-by-token', 'card_file_by_token'),
|
||||
('preview-json-payload', 'preview_json_payload'),
|
||||
('preview-payload-structure', 'preview_payload_structure'),
|
||||
('sign-url-token', 'sign_url_token'),
|
||||
]
|
||||
|
||||
|
@ -1437,7 +1437,7 @@ class ApiDirectory(Directory):
|
|||
get_response().set_content_type('application/json')
|
||||
return json.dumps({'err': 0, 'data': list_roles})
|
||||
|
||||
def preview_json_payload(self):
|
||||
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')
|
||||
|
||||
|
@ -1495,14 +1495,16 @@ class ApiDirectory(Directory):
|
|||
|
||||
payload = parse_payload()
|
||||
r = TemplateIO(html=True)
|
||||
r += htmltext('<h2>%s</h2>') % _('Payload preview')
|
||||
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 generate payload.'),
|
||||
_('Unable to preview payload.'),
|
||||
_('Following error occured: '),
|
||||
e,
|
||||
)
|
||||
|
@ -1552,6 +1554,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
|
||||
|
|
|
@ -26,7 +26,7 @@ from quixote import get_publisher, get_response
|
|||
|
||||
from wcs.api_utils import is_url_signed
|
||||
from wcs.applications import Application, ApplicationElement
|
||||
from wcs.blocks import BlockDef
|
||||
from wcs.blocks import BlockDef, BlockdefImportError
|
||||
from wcs.carddef import CardDef
|
||||
from wcs.categories import (
|
||||
BlockCategory,
|
||||
|
@ -38,12 +38,12 @@ from wcs.categories import (
|
|||
WorkflowCategory,
|
||||
)
|
||||
from wcs.comment_templates import CommentTemplate
|
||||
from wcs.data_sources import NamedDataSource
|
||||
from wcs.formdef import FormDef
|
||||
from wcs.data_sources import NamedDataSource, NamedDataSourceImportError
|
||||
from wcs.formdef import FormDef, FormdefImportError
|
||||
from wcs.mail_templates import MailTemplate
|
||||
from wcs.sql import Equal, Role
|
||||
from wcs.workflows import Workflow
|
||||
from wcs.wscalls import NamedWsCall
|
||||
from wcs.workflows import Workflow, WorkflowImportError
|
||||
from wcs.wscalls import NamedWsCall, NamedWsCallImportError
|
||||
|
||||
from .qommon import _
|
||||
from .qommon.afterjobs import AfterJob
|
||||
|
@ -269,102 +269,119 @@ def object_dependencies(request, objects, slug):
|
|||
|
||||
@signature_required
|
||||
def bundle_check(request):
|
||||
tar_io = io.BytesIO(request.body)
|
||||
with tarfile.open(fileobj=tar_io) as tar:
|
||||
manifest = json.loads(tar.extractfile('manifest.json').read().decode())
|
||||
application_slug = manifest.get('slug')
|
||||
application_version = manifest.get('version_number')
|
||||
if not application_slug or not application_version:
|
||||
return JsonResponse({'data': {}})
|
||||
|
||||
differences = []
|
||||
unknown_elements = []
|
||||
no_history_elements = []
|
||||
legacy_elements = []
|
||||
for element in manifest.get('elements'):
|
||||
if element['type'] not in klasses or element['type'] == 'roles':
|
||||
continue
|
||||
element_klass = klasses[element['type']]
|
||||
element_content = tar.extractfile('%s/%s' % (element['type'], element['slug'])).read()
|
||||
tree = ET.fromstring(element_content)
|
||||
if hasattr(element_klass, 'url_name'):
|
||||
slug = xml_node_text(tree.find('url_name'))
|
||||
else:
|
||||
slug = xml_node_text(tree.find('slug'))
|
||||
bundle = request.FILES['bundle']
|
||||
try:
|
||||
with tarfile.open(fileobj=bundle) as tar:
|
||||
try:
|
||||
obj = element_klass.get_by_slug(slug)
|
||||
if obj is None:
|
||||
raise KeyError
|
||||
manifest = json.loads(tar.extractfile('manifest.json').read().decode())
|
||||
except KeyError:
|
||||
# element not found, report this as unexisting
|
||||
unknown_elements.append(
|
||||
{
|
||||
'type': element['type'],
|
||||
'slug': element['slug'],
|
||||
}
|
||||
)
|
||||
continue
|
||||
applications = Application.select([Equal('slug', application_slug)])
|
||||
legacy = False
|
||||
if not applications:
|
||||
legacy = True
|
||||
else:
|
||||
application = applications[0]
|
||||
elements = ApplicationElement.select(
|
||||
return JsonResponse({'err': 1, 'err_desc': _('Invalid tar file, missing manifest')})
|
||||
application_slug = manifest.get('slug')
|
||||
application_version = manifest.get('version_number')
|
||||
if not application_slug or not application_version:
|
||||
return JsonResponse({'data': {}})
|
||||
|
||||
differences = []
|
||||
unknown_elements = []
|
||||
no_history_elements = []
|
||||
legacy_elements = []
|
||||
for element in manifest.get('elements'):
|
||||
if element['type'] not in klasses or element['type'] == 'roles':
|
||||
continue
|
||||
element_klass = klasses[element['type']]
|
||||
try:
|
||||
element_content = tar.extractfile('%s/%s' % (element['type'], element['slug'])).read()
|
||||
except KeyError:
|
||||
return JsonResponse(
|
||||
{
|
||||
'err': 1,
|
||||
'err_desc': _(
|
||||
'Invalid tar file, missing component %s/%s'
|
||||
% (element['type'], element['slug'])
|
||||
),
|
||||
}
|
||||
)
|
||||
tree = ET.fromstring(element_content)
|
||||
if hasattr(element_klass, 'url_name'):
|
||||
slug = xml_node_text(tree.find('url_name'))
|
||||
else:
|
||||
slug = xml_node_text(tree.find('slug'))
|
||||
try:
|
||||
obj = element_klass.get_by_slug(slug)
|
||||
if obj is None:
|
||||
raise KeyError
|
||||
except KeyError:
|
||||
# element not found, report this as unexisting
|
||||
unknown_elements.append(
|
||||
{
|
||||
'type': element['type'],
|
||||
'slug': element['slug'],
|
||||
}
|
||||
)
|
||||
continue
|
||||
applications = Application.select([Equal('slug', application_slug)])
|
||||
legacy = False
|
||||
if not applications:
|
||||
legacy = True
|
||||
else:
|
||||
application = applications[0]
|
||||
elements = ApplicationElement.select(
|
||||
[
|
||||
Equal('application_id', application.id),
|
||||
Equal('object_type', obj.xml_root_node),
|
||||
Equal('object_id', obj.id),
|
||||
]
|
||||
)
|
||||
if not elements:
|
||||
legacy = True
|
||||
if legacy:
|
||||
# object exists, but not linked to the application
|
||||
legacy_elements.append(
|
||||
{
|
||||
'type': element['type'],
|
||||
'slug': element['slug'],
|
||||
# information needed here, Relation objects may not exist yet in hobo
|
||||
'text': obj.name,
|
||||
'url': request.build_absolute_uri(
|
||||
reverse(
|
||||
'api-export-import-object-redirect',
|
||||
kwargs={'objects': element['type'], 'slug': element['slug']},
|
||||
)
|
||||
),
|
||||
}
|
||||
)
|
||||
continue
|
||||
snapshots_for_app = get_publisher().snapshot_class.select(
|
||||
[
|
||||
Equal('application_id', application.id),
|
||||
Equal('object_type', obj.xml_root_node),
|
||||
Equal('object_id', obj.id),
|
||||
]
|
||||
)
|
||||
if not elements:
|
||||
legacy = True
|
||||
if legacy:
|
||||
# object exists, but not linked to the application
|
||||
legacy_elements.append(
|
||||
{
|
||||
'type': element['type'],
|
||||
'slug': element['slug'],
|
||||
# information needed here, Relation objects may not exist yet in hobo
|
||||
'text': obj.name,
|
||||
'url': request.build_absolute_uri(
|
||||
reverse(
|
||||
'api-export-import-object-redirect',
|
||||
kwargs={'objects': element['type'], 'slug': element['slug']},
|
||||
)
|
||||
),
|
||||
}
|
||||
)
|
||||
continue
|
||||
snapshots_for_app = get_publisher().snapshot_class.select(
|
||||
[
|
||||
Equal('object_type', obj.xml_root_node),
|
||||
Equal('object_id', obj.id),
|
||||
Equal('application_slug', application_slug),
|
||||
Equal('application_version', application_version),
|
||||
],
|
||||
order_by='-timestamp',
|
||||
)
|
||||
if not snapshots_for_app:
|
||||
# legacy, no snapshot for this bundle
|
||||
no_history_elements.append(
|
||||
{
|
||||
'type': element['type'],
|
||||
'slug': element['slug'],
|
||||
}
|
||||
)
|
||||
continue
|
||||
snapshot_for_app = snapshots_for_app[0]
|
||||
last_snapshot = get_publisher().snapshot_class.select_object_history(obj)[0]
|
||||
if snapshot_for_app.id != last_snapshot.id:
|
||||
differences.append(
|
||||
{
|
||||
'type': element['type'],
|
||||
'slug': element['slug'],
|
||||
'url': '%shistory/compare?version1=%s&version2=%s'
|
||||
% (obj.get_admin_url(), snapshot_for_app.id, last_snapshot.id),
|
||||
}
|
||||
Equal('application_slug', application_slug),
|
||||
Equal('application_version', application_version),
|
||||
],
|
||||
order_by='-timestamp',
|
||||
)
|
||||
if not snapshots_for_app:
|
||||
# legacy, no snapshot for this bundle
|
||||
no_history_elements.append(
|
||||
{
|
||||
'type': element['type'],
|
||||
'slug': element['slug'],
|
||||
}
|
||||
)
|
||||
continue
|
||||
snapshot_for_app = snapshots_for_app[0]
|
||||
last_snapshot = get_publisher().snapshot_class.select_object_history(obj)[0]
|
||||
if snapshot_for_app.id != last_snapshot.id:
|
||||
differences.append(
|
||||
{
|
||||
'type': element['type'],
|
||||
'slug': element['slug'],
|
||||
'url': '%shistory/compare?version1=%s&version2=%s'
|
||||
% (obj.get_admin_url(), snapshot_for_app.id, last_snapshot.id),
|
||||
}
|
||||
)
|
||||
except tarfile.TarError:
|
||||
return JsonResponse({'err': 1, 'err_desc': _('Invalid tar file')})
|
||||
|
||||
return JsonResponse(
|
||||
{
|
||||
|
@ -378,6 +395,10 @@ def bundle_check(request):
|
|||
)
|
||||
|
||||
|
||||
class BundleKeyError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class BundleImportJob(AfterJob):
|
||||
def __init__(self, tar_content, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
@ -389,48 +410,77 @@ class BundleImportJob(AfterJob):
|
|||
object_types = sorted(object_types, key=lambda a: 'categories' in a, reverse=True)
|
||||
|
||||
tar_io = io.BytesIO(self.tar_content)
|
||||
with tarfile.open(fileobj=tar_io) as self.tar:
|
||||
manifest = json.loads(self.tar.extractfile('manifest.json').read().decode())
|
||||
self.application = Application.update_or_create_from_manifest(
|
||||
manifest, self.tar, editable=False, install=False
|
||||
)
|
||||
error = None
|
||||
try:
|
||||
with tarfile.open(fileobj=tar_io) as self.tar:
|
||||
try:
|
||||
manifest = json.loads(self.tar.extractfile('manifest.json').read().decode())
|
||||
except KeyError:
|
||||
raise BundleKeyError(_('Invalid tar file, missing manifest.'))
|
||||
self.application = Application.update_or_create_from_manifest(
|
||||
manifest, self.tar, editable=False, install=False
|
||||
)
|
||||
|
||||
# count number of actions
|
||||
self.total_count = 0
|
||||
self.total_count += len(
|
||||
[
|
||||
x
|
||||
for x in manifest.get('elements')
|
||||
if x.get('type') in ('forms', 'cards', 'blocks', 'workflows')
|
||||
]
|
||||
)
|
||||
self.total_count += (
|
||||
len([x for x in manifest.get('elements') if x.get('type') in object_types]) * 2
|
||||
)
|
||||
# count number of actions
|
||||
self.total_count = 0
|
||||
self.total_count += len(
|
||||
[
|
||||
x
|
||||
for x in manifest.get('elements')
|
||||
if x.get('type') in ('forms', 'cards', 'blocks', 'workflows')
|
||||
]
|
||||
)
|
||||
self.total_count += (
|
||||
len([x for x in manifest.get('elements') if x.get('type') in object_types]) * 2
|
||||
)
|
||||
|
||||
# init cache of application elements, from imported manifest
|
||||
self.application_elements = set()
|
||||
# init cache of application elements, from imported manifest
|
||||
self.application_elements = set()
|
||||
|
||||
# first pass on formdef/carddef/blockdef/workflows to create them empty
|
||||
# (name and slug); so they can be found for sure in import pass
|
||||
for _type in ('forms', 'cards', 'blocks', 'workflows'):
|
||||
self.pre_install([x for x in manifest.get('elements') if x.get('type') == _type])
|
||||
# first pass on formdef/carddef/blockdef/workflows to create them empty
|
||||
# (name and slug); so they can be found for sure in import pass
|
||||
for _type in ('forms', 'cards', 'blocks', 'workflows'):
|
||||
self.pre_install([x for x in manifest.get('elements') if x.get('type') == _type])
|
||||
|
||||
# real installation pass
|
||||
for _type in object_types:
|
||||
self.install([x for x in manifest.get('elements') if x.get('type') == _type])
|
||||
# real installation pass
|
||||
for _type in object_types:
|
||||
self.install([x for x in manifest.get('elements') if x.get('type') == _type])
|
||||
|
||||
# again, to remove [pre-install] in dependencies labels
|
||||
for _type in object_types:
|
||||
self.install([x for x in manifest.get('elements') if x.get('type') == _type], finalize=True)
|
||||
# again, to remove [pre-install] in dependencies labels
|
||||
for _type in object_types:
|
||||
self.install(
|
||||
[x for x in manifest.get('elements') if x.get('type') == _type], finalize=True
|
||||
)
|
||||
|
||||
# remove obsolete application elements
|
||||
self.unlink_obsolete_objects()
|
||||
# remove obsolete application elements
|
||||
self.unlink_obsolete_objects()
|
||||
|
||||
except (
|
||||
BlockdefImportError,
|
||||
FormdefImportError,
|
||||
WorkflowImportError,
|
||||
NamedDataSourceImportError,
|
||||
NamedWsCallImportError,
|
||||
) as e:
|
||||
error = str(e)
|
||||
except tarfile.TarError:
|
||||
error = _('Invalid tar file.')
|
||||
except BundleKeyError as e:
|
||||
error = str(e)
|
||||
|
||||
if error:
|
||||
self.status = 'failed'
|
||||
self.failure_label = str(_('Error: %s') % error)
|
||||
|
||||
def pre_install(self, elements):
|
||||
for element in elements:
|
||||
element_klass = klasses[element['type']]
|
||||
element_content = self.tar.extractfile('%s/%s' % (element['type'], element['slug'])).read()
|
||||
try:
|
||||
element_content = self.tar.extractfile('%s/%s' % (element['type'], element['slug'])).read()
|
||||
except KeyError:
|
||||
raise BundleKeyError(
|
||||
'Invalid tar file, missing component %s/%s.' % (element['type'], element['slug'])
|
||||
)
|
||||
tree = ET.fromstring(element_content)
|
||||
if hasattr(element_klass, 'url_name'):
|
||||
slug = xml_node_text(tree.find('url_name'))
|
||||
|
@ -472,9 +522,17 @@ class BundleImportJob(AfterJob):
|
|||
imported_positions = {}
|
||||
|
||||
for element in elements:
|
||||
element_content = self.tar.extractfile('%s/%s' % (element['type'], element['slug'])).read()
|
||||
try:
|
||||
element_content = self.tar.extractfile('%s/%s' % (element['type'], element['slug'])).read()
|
||||
except KeyError:
|
||||
raise BundleKeyError(
|
||||
'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
|
||||
|
@ -576,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()})
|
||||
|
@ -587,24 +645,40 @@ class BundleDeclareJob(BundleImportJob):
|
|||
object_types = [x for x in klasses if x != 'roles']
|
||||
|
||||
tar_io = io.BytesIO(self.tar_content)
|
||||
with tarfile.open(fileobj=tar_io) as self.tar:
|
||||
manifest = json.loads(self.tar.extractfile('manifest.json').read().decode())
|
||||
self.application = Application.update_or_create_from_manifest(
|
||||
manifest, self.tar, editable=True, install=True
|
||||
)
|
||||
error = None
|
||||
try:
|
||||
with tarfile.open(fileobj=tar_io) as self.tar:
|
||||
try:
|
||||
manifest = json.loads(self.tar.extractfile('manifest.json').read().decode())
|
||||
except KeyError:
|
||||
raise BundleKeyError(_('Invalid tar file, missing manifest.'))
|
||||
else:
|
||||
self.application = Application.update_or_create_from_manifest(
|
||||
manifest, self.tar, editable=True, install=True
|
||||
)
|
||||
|
||||
# count number of actions
|
||||
self.total_count = len([x for x in manifest.get('elements') if x.get('type') in object_types])
|
||||
# count number of actions
|
||||
self.total_count = len(
|
||||
[x for x in manifest.get('elements') if x.get('type') in object_types]
|
||||
)
|
||||
|
||||
# init cache of application elements, from manifest
|
||||
self.application_elements = set()
|
||||
# init cache of application elements, from manifest
|
||||
self.application_elements = set()
|
||||
|
||||
# declare elements
|
||||
for type in object_types:
|
||||
self.declare([x for x in manifest.get('elements') if x.get('type') == type])
|
||||
# declare elements
|
||||
for type in object_types:
|
||||
self.declare([x for x in manifest.get('elements') if x.get('type') == type])
|
||||
|
||||
# remove obsolete application elements
|
||||
self.unlink_obsolete_objects()
|
||||
# remove obsolete application elements
|
||||
self.unlink_obsolete_objects()
|
||||
except tarfile.TarError:
|
||||
error = _('Invalid tar file.')
|
||||
except BundleKeyError as e:
|
||||
error = str(e)
|
||||
|
||||
if error:
|
||||
self.status = 'failed'
|
||||
self.failure_label = str(_('Error: %s') % error)
|
||||
|
||||
def declare(self, elements):
|
||||
for element in elements:
|
||||
|
@ -618,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)
|
||||
|
|
|
@ -22,17 +22,24 @@ import re
|
|||
from quixote import get_publisher, get_request, get_response, redirect
|
||||
from quixote.directory import Directory
|
||||
|
||||
from wcs.blocks import BlockDef
|
||||
from wcs.carddef import CardDef
|
||||
from wcs.data_sources import NamedDataSource
|
||||
from wcs.formdef import get_formdefs_of_all_kinds
|
||||
from wcs.formdef import FormDef, get_formdefs_of_all_kinds
|
||||
from wcs.mail_templates import MailTemplate
|
||||
from wcs.qommon import _, ezt, template
|
||||
from wcs.qommon.afterjobs import AfterJob
|
||||
from wcs.qommon.template import Template
|
||||
from wcs.wf.export_to_model import UploadValidationError
|
||||
from wcs.wf.form import FormWorkflowStatusItem
|
||||
from wcs.workflows import Workflow
|
||||
from wcs.wscalls import NamedWsCall
|
||||
|
||||
|
||||
class DeprecatedElementsDetected(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class DeprecationsDirectory(Directory):
|
||||
do_not_call_in_templates = True
|
||||
_q_exports = ['', 'scan']
|
||||
|
@ -66,7 +73,7 @@ class DeprecationsDirectory(Directory):
|
|||
|
||||
def scan(self):
|
||||
job = get_response().add_after_job(
|
||||
DeprecationsScanAfterJob(
|
||||
DeprecationsScan(
|
||||
label=_('Scanning for deprecations'),
|
||||
user_id=get_request().user.id,
|
||||
return_url='/backoffice/studio/deprecations/',
|
||||
|
@ -88,6 +95,8 @@ class DeprecationsDirectory(Directory):
|
|||
'script': _('Filesystem Script'),
|
||||
'fields': _('Obsolete field types'),
|
||||
'actions': _('Obsolete action types'),
|
||||
'csv-connector': _('CSV connector'),
|
||||
'json-data-store': _('JSON Data Store connector'),
|
||||
}
|
||||
|
||||
@property
|
||||
|
@ -99,6 +108,8 @@ class DeprecationsDirectory(Directory):
|
|||
'python-expression': _('Use Django templates.'),
|
||||
'python-prefill': _('Use Django templates.'),
|
||||
'python-data-source': _('Use cards.'),
|
||||
'csv-connector': _('Use cards.'),
|
||||
'json-data-store': _('Use cards.'),
|
||||
'rtf': _('Use OpenDocument format.'),
|
||||
'script': _('Use a dedicated template tags application.'),
|
||||
'fields': _('Use block fields to replace tables and ranked order fields.'),
|
||||
|
@ -118,10 +129,12 @@ class DeprecationsDirectory(Directory):
|
|||
'script': 'https://doc-publik.entrouvert.com/admin-fonctionnel/elements-deprecies/',
|
||||
'fields': 'https://doc-publik.entrouvert.com/admin-fonctionnel/elements-deprecies/',
|
||||
'actions': 'https://doc-publik.entrouvert.com/admin-fonctionnel/elements-deprecies/',
|
||||
'csv-connector': 'https://doc-publik.entrouvert.com/admin-fonctionnel/elements-deprecies/',
|
||||
'json-data-store': 'https://doc-publik.entrouvert.com/admin-fonctionnel/elements-deprecies/',
|
||||
}
|
||||
|
||||
|
||||
class DeprecationsScanAfterJob(AfterJob):
|
||||
class DeprecationsScan(AfterJob):
|
||||
def done_action_url(self):
|
||||
return self.kwargs['return_url']
|
||||
|
||||
|
@ -149,173 +162,25 @@ class DeprecationsScanAfterJob(AfterJob):
|
|||
)
|
||||
self.store()
|
||||
|
||||
for formdef in formdefs:
|
||||
if formdef.id:
|
||||
source = f'{formdef.xml_root_node}:{formdef.id}' if formdef.id else ''
|
||||
elif formdef.get_workflow():
|
||||
source = f'workflow:{formdef.get_workflow().id}'
|
||||
else:
|
||||
source = '-'
|
||||
for field in formdef.fields or []:
|
||||
location_label = _('%(name)s / Field "%(label)s"') % {
|
||||
'name': formdef.name,
|
||||
'label': field.ellipsized_label,
|
||||
}
|
||||
url = formdef.get_field_admin_url(field)
|
||||
self.check_data_source(
|
||||
getattr(field, 'data_source', None),
|
||||
location_label=location_label,
|
||||
url=url,
|
||||
source=source,
|
||||
)
|
||||
prefill = getattr(field, 'prefill', None)
|
||||
if prefill:
|
||||
if prefill.get('type') == 'formula':
|
||||
self.add_report_line(
|
||||
location_label=location_label,
|
||||
url=url,
|
||||
category='python-prefill',
|
||||
source=source,
|
||||
)
|
||||
else:
|
||||
self.check_string(
|
||||
prefill.get('value'),
|
||||
location_label=location_label,
|
||||
url=url,
|
||||
python_check=False,
|
||||
source=source,
|
||||
)
|
||||
if field.key == 'page':
|
||||
for condition in field.get_conditions():
|
||||
if condition and condition.get('type') == 'python':
|
||||
self.add_report_line(
|
||||
location_label=location_label,
|
||||
url=url,
|
||||
category='python-condition',
|
||||
source=source,
|
||||
)
|
||||
break
|
||||
if field.key in ('title', 'subtitle', 'comment'):
|
||||
self.check_string(
|
||||
field.label,
|
||||
location_label=location_label,
|
||||
url=url,
|
||||
python_check=False,
|
||||
source=source,
|
||||
)
|
||||
if field.key in ('table', 'table-select', 'tablerows', 'ranked-items'):
|
||||
self.add_report_line(
|
||||
location_label=location_label,
|
||||
url=url,
|
||||
category='fields',
|
||||
source=source,
|
||||
)
|
||||
|
||||
self.increment_count()
|
||||
|
||||
for workflow in workflows:
|
||||
source = f'workflow:{workflow.id}'
|
||||
for action in workflow.get_all_items():
|
||||
location_label = '%s / %s' % (workflow.name, action.description)
|
||||
url = action.get_admin_url()
|
||||
for string in action.get_computed_strings():
|
||||
self.check_string(string, location_label=location_label, url=url, source=source)
|
||||
if getattr(action, 'condition', None):
|
||||
if action.condition.get('type') == 'python':
|
||||
self.add_report_line(
|
||||
location_label=location_label,
|
||||
url=url,
|
||||
category='python-condition',
|
||||
css_class='important' if (action.key == 'jump' and action.timeout) else '',
|
||||
source=source,
|
||||
)
|
||||
if action.key == 'export_to_model':
|
||||
try:
|
||||
kind = action.model_file_validation(action.model_file, allow_rtf=True)
|
||||
except UploadValidationError:
|
||||
pass
|
||||
else:
|
||||
if kind == 'rtf':
|
||||
self.add_report_line(
|
||||
location_label=location_label, url=url, category='rtf', source=source
|
||||
)
|
||||
if action.key in ('aggregationemail', 'resubmit'):
|
||||
self.add_report_line(
|
||||
location_label=location_label, url=url, category='actions', source=source
|
||||
)
|
||||
if action.key in ('register-comment', 'sendmail'):
|
||||
for attachment in getattr(action, 'attachments', None) or []:
|
||||
if attachment and not ('{%' in attachment or '{{' in attachment):
|
||||
self.add_report_line(
|
||||
location_label=location_label,
|
||||
url=url,
|
||||
category='python-expression',
|
||||
source=source,
|
||||
)
|
||||
break
|
||||
|
||||
for global_action in workflow.global_actions or []:
|
||||
location_label = '%s / %s' % (workflow.name, _('trigger in %s') % global_action.name)
|
||||
for trigger in global_action.triggers or []:
|
||||
url = '%striggers/%s/' % (global_action.get_admin_url(), trigger.id)
|
||||
if trigger.key == 'timeout' and trigger.anchor == 'python':
|
||||
self.add_report_line(
|
||||
location_label=location_label,
|
||||
url=url,
|
||||
category='python-expression',
|
||||
source=source,
|
||||
)
|
||||
break
|
||||
|
||||
self.increment_count()
|
||||
|
||||
for named_data_source in named_data_sources:
|
||||
source = f'datasource:{named_data_source.id}'
|
||||
location_label = _('%(title)s "%(name)s"') % {
|
||||
'title': _('Data source'),
|
||||
'name': named_data_source.name,
|
||||
}
|
||||
url = named_data_source.get_admin_url()
|
||||
|
||||
self.check_data_source(
|
||||
getattr(named_data_source, 'data_source', None),
|
||||
location_label=location_label,
|
||||
url=url,
|
||||
source=source,
|
||||
)
|
||||
self.increment_count()
|
||||
|
||||
for named_ws_call in named_ws_calls:
|
||||
source = f'wscall:{named_ws_call.id}'
|
||||
location_label = _('%(title)s "%(name)s"') % {
|
||||
'title': _('Webservice'),
|
||||
'name': named_ws_call.name,
|
||||
}
|
||||
url = named_ws_call.get_admin_url()
|
||||
for string in named_ws_call.get_computed_strings():
|
||||
self.check_string(string, location_label=location_label, url=url, source=source)
|
||||
self.increment_count()
|
||||
|
||||
for mail_template in mail_templates:
|
||||
source = f'mail_template:{mail_template.id}'
|
||||
location_label = _('%(title)s "%(name)s"') % {
|
||||
'title': _('Mail Template'),
|
||||
'name': mail_template.name,
|
||||
}
|
||||
url = mail_template.get_admin_url()
|
||||
for string in mail_template.get_computed_strings():
|
||||
self.check_string(string, location_label=location_label, url=url, source=source)
|
||||
for string in mail_template.attachments or []:
|
||||
# legacy was to have straight python expressions (not prefixed by "=").
|
||||
if not Template.is_template_string(string):
|
||||
self.add_report_line(
|
||||
location_label=location_label, url=url, category='python-expression', source=source
|
||||
)
|
||||
self.increment_count()
|
||||
self.check_objects(formdefs + workflows + named_data_sources + named_ws_calls + mail_templates)
|
||||
|
||||
self.build_report_file()
|
||||
self.increment_count()
|
||||
|
||||
def check_objects(self, objects):
|
||||
for obj in objects:
|
||||
if isinstance(obj, (FormDef, CardDef, BlockDef)):
|
||||
self.check_formdef(obj)
|
||||
elif isinstance(obj, Workflow):
|
||||
self.check_workflow(obj)
|
||||
elif isinstance(obj, NamedDataSource):
|
||||
self.check_named_data_source(obj)
|
||||
elif isinstance(obj, NamedWsCall):
|
||||
self.check_named_ws_call(obj)
|
||||
elif isinstance(obj, MailTemplate):
|
||||
self.check_mail_template(obj)
|
||||
self.increment_count()
|
||||
|
||||
def check_data_source(self, data_source, location_label, url, source):
|
||||
if not data_source:
|
||||
return
|
||||
|
@ -337,6 +202,7 @@ class DeprecationsScanAfterJob(AfterJob):
|
|||
self.check_string(
|
||||
data_source.get('value'), location_label, url, python_check=False, source=source
|
||||
)
|
||||
self.check_remote_call_url(data_source.get('value'), location_label, url, source=source)
|
||||
|
||||
def check_string(self, string, location_label, url, source, python_check=True):
|
||||
if not isinstance(string, str):
|
||||
|
@ -361,6 +227,179 @@ class DeprecationsScanAfterJob(AfterJob):
|
|||
if re.findall(r'\Wscript\.\w', string):
|
||||
self.add_report_line(location_label=location_label, url=url, category='script', source=source)
|
||||
|
||||
def check_remote_call_url(self, wscall_url, location_label, url, source):
|
||||
if 'csvdatasource/' in (wscall_url or ''):
|
||||
self.add_report_line(
|
||||
location_label=location_label, url=url, category='csv-connector', source=source
|
||||
)
|
||||
if 'jsondatastore/' in (wscall_url or ''):
|
||||
self.add_report_line(
|
||||
location_label=location_label, url=url, category='json-data-store', source=source
|
||||
)
|
||||
|
||||
def check_formdef(self, formdef):
|
||||
if formdef.id:
|
||||
source = f'{formdef.xml_root_node}:{formdef.id}' if formdef.id else ''
|
||||
elif hasattr(formdef, 'get_workflow') and formdef.get_workflow():
|
||||
source = f'workflow:{formdef.get_workflow().id}'
|
||||
else:
|
||||
source = '-'
|
||||
for field in formdef.fields or []:
|
||||
location_label = _('%(name)s / Field "%(label)s"') % {
|
||||
'name': formdef.name,
|
||||
'label': field.ellipsized_label,
|
||||
}
|
||||
url = formdef.get_field_admin_url(field)
|
||||
self.check_data_source(
|
||||
getattr(field, 'data_source', None),
|
||||
location_label=location_label,
|
||||
url=url,
|
||||
source=source,
|
||||
)
|
||||
prefill = getattr(field, 'prefill', None)
|
||||
if prefill:
|
||||
if prefill.get('type') == 'formula':
|
||||
self.add_report_line(
|
||||
location_label=location_label,
|
||||
url=url,
|
||||
category='python-prefill',
|
||||
source=source,
|
||||
)
|
||||
else:
|
||||
self.check_string(
|
||||
prefill.get('value'),
|
||||
location_label=location_label,
|
||||
url=url,
|
||||
python_check=False,
|
||||
source=source,
|
||||
)
|
||||
if field.key == 'page':
|
||||
for condition in field.get_conditions():
|
||||
if condition and condition.get('type') == 'python':
|
||||
self.add_report_line(
|
||||
location_label=location_label,
|
||||
url=url,
|
||||
category='python-condition',
|
||||
source=source,
|
||||
)
|
||||
break
|
||||
if field.key in ('title', 'subtitle', 'comment'):
|
||||
self.check_string(
|
||||
field.label,
|
||||
location_label=location_label,
|
||||
url=url,
|
||||
python_check=False,
|
||||
source=source,
|
||||
)
|
||||
if field.key in ('table', 'table-select', 'tablerows', 'ranked-items'):
|
||||
self.add_report_line(
|
||||
location_label=location_label,
|
||||
url=url,
|
||||
category='fields',
|
||||
source=source,
|
||||
)
|
||||
|
||||
def check_workflow(self, workflow):
|
||||
source = f'workflow:{workflow.id}'
|
||||
for action in workflow.get_all_items():
|
||||
location_label = '%s / %s' % (workflow.name, action.description)
|
||||
url = action.get_admin_url()
|
||||
for string in action.get_computed_strings():
|
||||
self.check_string(string, location_label=location_label, url=url, source=source)
|
||||
if getattr(action, 'condition', None):
|
||||
if action.condition.get('type') == 'python':
|
||||
self.add_report_line(
|
||||
location_label=location_label,
|
||||
url=url,
|
||||
category='python-condition',
|
||||
css_class='important' if (action.key == 'jump' and action.timeout) else '',
|
||||
source=source,
|
||||
)
|
||||
if action.key == 'export_to_model':
|
||||
try:
|
||||
kind = action.model_file_validation(action.model_file, allow_rtf=True)
|
||||
except UploadValidationError:
|
||||
pass
|
||||
else:
|
||||
if kind == 'rtf':
|
||||
self.add_report_line(
|
||||
location_label=location_label, url=url, category='rtf', source=source
|
||||
)
|
||||
if action.key in ('aggregationemail', 'resubmit'):
|
||||
self.add_report_line(
|
||||
location_label=location_label, url=url, category='actions', source=source
|
||||
)
|
||||
if action.key in ('register-comment', 'sendmail'):
|
||||
for attachment in getattr(action, 'attachments', None) or []:
|
||||
if attachment and not ('{%' in attachment or '{{' in attachment):
|
||||
self.add_report_line(
|
||||
location_label=location_label,
|
||||
url=url,
|
||||
category='python-expression',
|
||||
source=source,
|
||||
)
|
||||
break
|
||||
if action.key == 'webservice_call':
|
||||
self.check_remote_call_url(action.url, location_label=location_label, url=url, source=source)
|
||||
|
||||
for global_action in workflow.global_actions or []:
|
||||
location_label = '%s / %s' % (workflow.name, _('trigger in %s') % global_action.name)
|
||||
for trigger in global_action.triggers or []:
|
||||
url = '%striggers/%s/' % (global_action.get_admin_url(), trigger.id)
|
||||
if trigger.key == 'timeout' and trigger.anchor == 'python':
|
||||
self.add_report_line(
|
||||
location_label=location_label,
|
||||
url=url,
|
||||
category='python-expression',
|
||||
source=source,
|
||||
)
|
||||
break
|
||||
|
||||
def check_named_data_source(self, named_data_source):
|
||||
source = f'datasource:{named_data_source.id}'
|
||||
location_label = _('%(title)s "%(name)s"') % {
|
||||
'title': _('Data source'),
|
||||
'name': named_data_source.name,
|
||||
}
|
||||
url = named_data_source.get_admin_url()
|
||||
|
||||
self.check_data_source(
|
||||
getattr(named_data_source, 'data_source', None),
|
||||
location_label=location_label,
|
||||
url=url,
|
||||
source=source,
|
||||
)
|
||||
|
||||
def check_named_ws_call(self, named_ws_call):
|
||||
source = f'wscall:{named_ws_call.id}'
|
||||
location_label = _('%(title)s "%(name)s"') % {
|
||||
'title': _('Webservice'),
|
||||
'name': named_ws_call.name,
|
||||
}
|
||||
url = named_ws_call.get_admin_url()
|
||||
for string in named_ws_call.get_computed_strings():
|
||||
self.check_string(string, location_label=location_label, url=url, source=source)
|
||||
if named_ws_call.request and named_ws_call.request.get('url'):
|
||||
self.check_remote_call_url(
|
||||
named_ws_call.request['url'], location_label=location_label, url=url, source=source
|
||||
)
|
||||
|
||||
def check_mail_template(self, mail_template):
|
||||
source = f'mail_template:{mail_template.id}'
|
||||
location_label = _('%(title)s "%(name)s"') % {
|
||||
'title': _('Mail Template'),
|
||||
'name': mail_template.name,
|
||||
}
|
||||
url = mail_template.get_admin_url()
|
||||
for string in mail_template.get_computed_strings():
|
||||
self.check_string(string, location_label=location_label, url=url, source=source)
|
||||
for string in mail_template.attachments or []:
|
||||
# legacy was to have straight python expressions (not prefixed by "=").
|
||||
if not Template.is_template_string(string):
|
||||
self.add_report_line(
|
||||
location_label=location_label, url=url, category='python-expression', source=source
|
||||
)
|
||||
|
||||
def add_report_line(self, **kwargs):
|
||||
if kwargs not in self.report_lines:
|
||||
self.report_lines.append(kwargs)
|
||||
|
@ -375,3 +414,33 @@ class DeprecationsScanAfterJob(AfterJob):
|
|||
fd,
|
||||
indent=2,
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
self.report_lines = []
|
||||
objects = [obj]
|
||||
if isinstance(obj, Workflow):
|
||||
for status in obj.possible_status:
|
||||
for item in status.items:
|
||||
if isinstance(item, FormWorkflowStatusItem) and item.formdef:
|
||||
objects.append(item.formdef)
|
||||
if obj.variables_formdef:
|
||||
objects.append(obj.variables_formdef)
|
||||
if obj.backoffice_fields_formdef:
|
||||
objects.append(obj.backoffice_fields_formdef)
|
||||
|
||||
self.check_objects(objects)
|
||||
|
||||
for report_line in self.report_lines:
|
||||
if 'python' in report_line['category'] and get_publisher().has_site_option(
|
||||
'forbid-new-python-expressions'
|
||||
):
|
||||
raise DeprecatedElementsDetected(_('Python expression detected'))
|
||||
|
||||
|
||||
class DeprecationsScanAfterJob(DeprecationsScan):
|
||||
pass # legacy name, to load old pickle files
|
||||
|
|
|
@ -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
|
||||
|
@ -298,7 +299,10 @@ class ManagementDirectory(Directory):
|
|||
get_session().message = None
|
||||
return redirect(formdata.get_url(backoffice=True))
|
||||
|
||||
return redirect(get_request().form.get('back') or '.')
|
||||
back_place = get_request().form.get('back')
|
||||
if back_place not in ('listing', 'forms'):
|
||||
back_place = '.' # auto
|
||||
return redirect(back_place)
|
||||
|
||||
def get_lookup_sidebox(self, back_place=''):
|
||||
r = TemplateIO(html=True)
|
||||
|
@ -717,7 +721,7 @@ class ManagementDirectory(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()
|
||||
|
||||
get_response().filter['sidebar'] = self.get_global_listing_sidebar(
|
||||
|
@ -758,8 +762,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)
|
||||
|
@ -1129,27 +1133,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',
|
||||
]
|
||||
return types
|
||||
|
||||
def get_filter_sidebar(
|
||||
self,
|
||||
selected_filter=None,
|
||||
|
@ -1160,25 +1143,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,
|
||||
)
|
||||
]
|
||||
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)
|
||||
|
@ -1236,30 +1220,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())
|
||||
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)
|
||||
|
||||
|
@ -1269,109 +1244,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)]
|
||||
r += SingleSelectWidget(
|
||||
filter_field_key,
|
||||
title=filter_field.label,
|
||||
options=options,
|
||||
value=filter_field_value,
|
||||
render_br=False,
|
||||
).render()
|
||||
|
||||
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()
|
||||
]
|
||||
r += SingleSelectWidget(
|
||||
filter_field_key,
|
||||
title=filter_field.label,
|
||||
options=options,
|
||||
value=filter_field_value,
|
||||
render_br=False,
|
||||
).render()
|
||||
|
||||
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)
|
||||
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
|
||||
|
@ -1454,24 +1328,17 @@ class FormPage(Directory, TempfileDirectoryMixin):
|
|||
)
|
||||
r += render_widget(widget, operators)
|
||||
|
||||
elif filter_field.key in ('string', 'text', 'email', 'numeric'):
|
||||
elif filter_field.key in ('string', 'text', 'email', 'numeric', 'date'):
|
||||
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 == 'date':
|
||||
widget = DateWidget(
|
||||
filter_field_key, title=filter_field.label, value=filter_field_value, render_br=False
|
||||
)
|
||||
r += render_widget(widget, operators)
|
||||
|
||||
# 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:
|
||||
|
@ -1575,7 +1442,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'
|
||||
|
@ -1858,24 +1725,24 @@ class FormPage(Directory, TempfileDirectoryMixin):
|
|||
return redirect('..')
|
||||
|
||||
def get_formdef_fields(self, include_block_items_fields=False):
|
||||
yield FakeField('id', 'id', _('Identifier') if self.formdef.id_template else _('Number'))
|
||||
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):
|
||||
if getattr(field, 'block_field', None):
|
||||
|
@ -1901,17 +1768,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:
|
||||
|
@ -1988,17 +1850,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')),
|
||||
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 = []
|
||||
|
||||
|
@ -2035,7 +1901,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):
|
||||
|
@ -2063,6 +1929,10 @@ class FormPage(Directory, TempfileDirectoryMixin):
|
|||
if filter_field.key == 'distance' and filters_dict.get('filter-distance'):
|
||||
filters_dict['filter-distance-value'] = filters_dict['filter-distance']
|
||||
|
||||
if filter_field.key == 'criticality-level' and filters_dict.get('filter-criticality-level'):
|
||||
if filters_dict['filter-criticality-level'] != 'on':
|
||||
filters_dict['filter-criticality-level-value'] = filters_dict['filter-criticality-level']
|
||||
|
||||
if filter_field.key == 'user-id' and not filters_dict.get('filter-user-function'):
|
||||
# convert uuid based filter into local id filter.
|
||||
# do not apply if there's filter-user-function as it indicates the filtering
|
||||
|
@ -2087,7 +1957,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:
|
||||
|
@ -2215,7 +2085,7 @@ class FormPage(Directory, TempfileDirectoryMixin):
|
|||
filter_field_value = False
|
||||
else:
|
||||
raise RequestError('Invalid value "%s" for "%s"' % (filter_field_value, filter_field_key))
|
||||
elif filter_field.key in ('item', 'items', 'string', 'email', 'numeric'):
|
||||
elif filter_field.key in ('item', 'items', 'string', 'email', 'numeric', 'date'):
|
||||
if Template.is_template_string(filter_field_value, ezt_support=False):
|
||||
if keep_templates:
|
||||
# use Equal criteria here, the only use is in CardDef.get_data_source_referenced_varnames
|
||||
|
@ -2263,7 +2133,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'):
|
||||
|
@ -2278,10 +2148,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:
|
||||
|
@ -2303,6 +2173,9 @@ class FormPage(Directory, TempfileDirectoryMixin):
|
|||
raise RequestError('Distance filter missing a center')
|
||||
center = misc.normalize_geolocation({'lat': center_lat, 'lon': center_lon})
|
||||
criterias.append(Distance(center, float(filter_field_value)))
|
||||
elif filter_field.key == 'criticality-level':
|
||||
level = 100 + int(filter_field_value)
|
||||
criterias.append(Equal('criticality_level', level))
|
||||
elif filter_field.key in ('item', 'items', 'bool', 'string', 'text', 'email', 'date', 'numeric'):
|
||||
criterias.append(
|
||||
lazy_manager.get_criteria_from_operator(
|
||||
|
@ -2442,7 +2315,7 @@ class FormPage(Directory, TempfileDirectoryMixin):
|
|||
|
||||
if get_request().form.get('ajax') == 'true':
|
||||
get_request().ignore_session = True
|
||||
get_response().filter = {'raw': True}
|
||||
get_response().raw = True
|
||||
r = TemplateIO(html=True)
|
||||
r += multi_form.render()
|
||||
r += get_session().display_message()
|
||||
|
@ -3129,7 +3002,7 @@ class FormPage(Directory, TempfileDirectoryMixin):
|
|||
|
||||
if get_request().form.get('ajax') == 'true':
|
||||
get_request().ignore_session = True
|
||||
get_response().filter = {'raw': True}
|
||||
get_response().raw = True
|
||||
return r.getvalue()
|
||||
|
||||
page = TemplateIO(html=True)
|
||||
|
@ -3368,13 +3241,13 @@ class FormBackOfficeStatusPage(FormStatusPage):
|
|||
|
||||
def lateral_block(self):
|
||||
self.check_receiver()
|
||||
get_response().filter = {'raw': True}
|
||||
get_response().raw = True
|
||||
response = self.get_lateral_block()
|
||||
return response
|
||||
|
||||
def user_pending_forms(self):
|
||||
self.check_receiver()
|
||||
get_response().filter = {'raw': True}
|
||||
get_response().raw = True
|
||||
response = self.get_user_pending_forms()
|
||||
|
||||
# preemptive locking of forms
|
||||
|
@ -3416,6 +3289,7 @@ class FormBackOfficeStatusPage(FormStatusPage):
|
|||
|
||||
def get_extra_context_bar(self, parent=None):
|
||||
formdata = self.filled
|
||||
management_sidebar_items = self.formdef.get_management_sidebar_items()
|
||||
|
||||
r = TemplateIO(html=True)
|
||||
|
||||
|
@ -3423,7 +3297,6 @@ class FormBackOfficeStatusPage(FormStatusPage):
|
|||
r += htmltext('<p><a class="button" id="back-to-listing" href="..">%s</a></p>') % _(
|
||||
'Back to Listing'
|
||||
)
|
||||
r += htmltext('<div class="extra-context">')
|
||||
if (
|
||||
formdata.backoffice_submission
|
||||
and formdata.submission_agent_id == str(get_request().user.id)
|
||||
|
@ -3432,9 +3305,13 @@ class FormBackOfficeStatusPage(FormStatusPage):
|
|||
):
|
||||
# keep displaying tracking code to submission agent for 30
|
||||
# minutes after submission
|
||||
r += htmltext('<div class="extra-context">')
|
||||
r += htmltext('<h3>%s</h3>') % _('Tracking Code')
|
||||
r += htmltext('<p>%s</p>') % formdata.tracking_code
|
||||
r += htmltext('</div>')
|
||||
|
||||
if not formdata.is_draft() and 'general' in management_sidebar_items:
|
||||
r += htmltext('<div class="extra-context sidebar-general-information">')
|
||||
r += htmltext('<h3>%s</h3>') % _('General Information')
|
||||
r += htmltext('<p>')
|
||||
tm = misc.localstrftime(formdata.receipt_time)
|
||||
|
@ -3486,43 +3363,54 @@ class FormBackOfficeStatusPage(FormStatusPage):
|
|||
'date': formdata.anonymised.strftime(misc.date_format())
|
||||
}
|
||||
r += htmltext('</div>')
|
||||
r += htmltext('</div>') # .extra-context
|
||||
|
||||
if formdata.formdef.include_download_all_button:
|
||||
has_attached_files = False
|
||||
for value in (formdata.data or {}).values():
|
||||
if isinstance(value, PicklableUpload):
|
||||
has_attached_files = True
|
||||
if isinstance(value, dict) and isinstance(value.get('data'), list):
|
||||
# block fields
|
||||
for subvalue in value.get('data'):
|
||||
for subvalue_elem in subvalue.values():
|
||||
if isinstance(subvalue_elem, PicklableUpload):
|
||||
has_attached_files = True
|
||||
break
|
||||
if has_attached_files:
|
||||
break
|
||||
|
||||
if not formdata.is_draft() and 'download-files' in management_sidebar_items:
|
||||
has_attached_files = False
|
||||
for value in (formdata.data or {}).values():
|
||||
if isinstance(value, PicklableUpload):
|
||||
has_attached_files = True
|
||||
if isinstance(value, dict) and isinstance(value.get('data'), list):
|
||||
# block fields
|
||||
for subvalue in value.get('data'):
|
||||
for subvalue_elem in subvalue.values():
|
||||
if isinstance(subvalue_elem, PicklableUpload):
|
||||
has_attached_files = True
|
||||
break
|
||||
if has_attached_files:
|
||||
r += htmltext('<p><a class="button" href="download-as-zip">%s</a></p>') % _(
|
||||
'Download all files as .zip'
|
||||
)
|
||||
break
|
||||
|
||||
r += htmltext('</div>')
|
||||
if has_attached_files:
|
||||
r += htmltext('<div class="extra-context sidebar-download-files">')
|
||||
r += htmltext('<p><a class="button" href="download-as-zip">%s</a></p>') % _(
|
||||
'Download all files as .zip'
|
||||
)
|
||||
r += htmltext('</div>')
|
||||
|
||||
r += self.get_extra_submission_context_bar()
|
||||
r += self.get_extra_submission_channel_bar()
|
||||
r += self.get_extra_submission_user_id_bar(parent=parent)
|
||||
r += self.get_extra_geolocation_bar()
|
||||
if formdata.formdef.lateral_template:
|
||||
if 'submission-context' in management_sidebar_items:
|
||||
r += self.get_extra_submission_context_bar()
|
||||
r += self.get_extra_submission_channel_bar()
|
||||
|
||||
if 'user' in management_sidebar_items:
|
||||
r += self.get_extra_submission_user_id_bar(parent=parent)
|
||||
|
||||
if 'geolocation' in management_sidebar_items:
|
||||
r += self.get_extra_geolocation_bar()
|
||||
|
||||
if 'custom-template' in management_sidebar_items and formdata.formdef.lateral_template:
|
||||
r += htmltext('<div data-async-url="%slateral-block"></div>' % formdata.get_url(backoffice=True))
|
||||
|
||||
if not isinstance(formdata.formdef, CardDef) and formdata.user_id:
|
||||
if (
|
||||
'pending-forms' in management_sidebar_items
|
||||
and not isinstance(formdata.formdef, CardDef)
|
||||
and formdata.user_id
|
||||
):
|
||||
r += htmltext(
|
||||
'<div data-async-url="%suser-pending-forms"></div>' % formdata.get_url(backoffice=True)
|
||||
)
|
||||
|
||||
if not formdata.is_draft() and self.can_go_in_inspector():
|
||||
r += htmltext('<div class="extra-context">')
|
||||
r += htmltext('<div class="extra-context sidebar-data-inspector">')
|
||||
r += htmltext('<p><a href="%sinspect">' % formdata.get_url(backoffice=True))
|
||||
r += htmltext('%s</a></p>') % _('Data Inspector')
|
||||
r += htmltext('</div>')
|
||||
|
@ -3534,7 +3422,7 @@ class FormBackOfficeStatusPage(FormStatusPage):
|
|||
r = TemplateIO(html=True)
|
||||
if formdata.submission_context or formdata.submission_channel:
|
||||
extra_context = formdata.submission_context or {}
|
||||
r += htmltext('<div class="extra-context">')
|
||||
r += htmltext('<div class="extra-context sidebar-submission-context">')
|
||||
if extra_context.get('orig_formdef_id'):
|
||||
object_type = extra_context.get('orig_object_type', 'formdef')
|
||||
if object_type == 'formdef':
|
||||
|
@ -3577,7 +3465,7 @@ class FormBackOfficeStatusPage(FormStatusPage):
|
|||
r += htmltext('<p>%s</p>') % extra_context.get('comments')
|
||||
if extra_context.get('summary_url'):
|
||||
r += htmltext('<div data-content-url="%s"></div>' % (extra_context.get('summary_url')))
|
||||
r += htmltext('</div>')
|
||||
r += htmltext('</div>') # closes .extra-context from get_extra_submission_context_bar
|
||||
|
||||
return r.getvalue()
|
||||
|
||||
|
@ -3585,7 +3473,7 @@ class FormBackOfficeStatusPage(FormStatusPage):
|
|||
formdata = self.filled
|
||||
r = TemplateIO(html=True)
|
||||
if formdata and formdata.user_id and formdata.get_user():
|
||||
r += htmltext('<div class="extra-context">')
|
||||
r += htmltext('<div class="extra-context sidebar--user">')
|
||||
r += htmltext('<h3>%s</h3>') % _('Associated User')
|
||||
users_cfg = get_cfg('users', {})
|
||||
sidebar_user_template = users_cfg.get('sidebar_template')
|
||||
|
@ -3622,7 +3510,7 @@ class FormBackOfficeStatusPage(FormStatusPage):
|
|||
formdata = self.filled
|
||||
r = TemplateIO(html=True)
|
||||
if formdata.formdef.geolocations and formdata.geolocations:
|
||||
r += htmltext('<div class="geolocations">')
|
||||
r += htmltext('<div class="extra-context geolocations sidebar-geolocations">')
|
||||
for geoloc_key in formdata.formdef.geolocations:
|
||||
if geoloc_key not in formdata.geolocations:
|
||||
continue
|
||||
|
@ -3681,7 +3569,7 @@ class FormBackOfficeStatusPage(FormStatusPage):
|
|||
self.filled.related_user_forms = formdatas
|
||||
|
||||
if formdatas:
|
||||
r += htmltext('<div class="user-pending-forms">')
|
||||
r += htmltext('<div class="extra-context user-pending-forms">')
|
||||
r += htmltext('<h3>%s</h3>') % _('User Pending Forms')
|
||||
categories = {}
|
||||
formdata_by_category = {}
|
||||
|
@ -4023,7 +3911,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)):
|
||||
|
@ -4064,7 +3952,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>')
|
||||
|
@ -4159,152 +4053,10 @@ class FormBackOfficeStatusPage(FormStatusPage):
|
|||
or get_publisher().get_backoffice_root().is_accessible('workflows')
|
||||
):
|
||||
raise errors.AccessForbiddenError()
|
||||
get_response().filter = {'raw': True}
|
||||
get_response().raw = True
|
||||
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
|
||||
|
||||
|
@ -4474,11 +4226,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
|
||||
|
@ -4486,6 +4237,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)
|
||||
|
|
|
@ -81,6 +81,7 @@ class RootDirectory(AccessControlled, Directory):
|
|||
except KeyError:
|
||||
pass
|
||||
get_response().add_javascript(['jquery.js', 'qommon.js', 'gadjo.js'])
|
||||
get_response().add_css_include('../xstatic/css/godo.css')
|
||||
if path and path[0] == 'categories':
|
||||
# legacy /backoffice/categories/<...>, redirect.
|
||||
return redirect('/backoffice/forms/' + '/'.join(path))
|
||||
|
|
|
@ -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')))
|
||||
|
|
|
@ -23,14 +23,15 @@ 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.backoffice.pagination import pagination_links
|
||||
from wcs.categories import Category
|
||||
from wcs.formdata import FormData
|
||||
from wcs.formdef import FormDef
|
||||
from wcs.forms.common import FormStatusPage
|
||||
from wcs.forms.root import FormPage as PublicFormFillPage
|
||||
from wcs.sql_criterias import Equal, StrictNotEqual
|
||||
from wcs.sql_criterias import Contains, Equal, StrictNotEqual
|
||||
|
||||
from ..qommon import _, errors, misc
|
||||
from ..qommon import _, errors, get_cfg, misc, template
|
||||
from ..qommon.form import Form, HtmlWidget
|
||||
|
||||
|
||||
|
@ -174,7 +175,7 @@ class FormFillPage(PublicFormFillPage):
|
|||
return super()._q_index(*args, **kwargs)
|
||||
|
||||
def lateral_block(self):
|
||||
get_response().filter = {'raw': True}
|
||||
get_response().raw = True
|
||||
response = self.get_lateral_block()
|
||||
return response
|
||||
|
||||
|
@ -427,15 +428,18 @@ class FormFillPage(PublicFormFillPage):
|
|||
|
||||
|
||||
class SubmissionDirectory(Directory):
|
||||
_q_exports = ['', 'count']
|
||||
_q_exports = ['', 'pending', 'count']
|
||||
|
||||
def _q_traverse(self, path):
|
||||
get_response().set_backoffice_section('submission')
|
||||
get_response().breadcrumb.append(('submission/', _('Submission')))
|
||||
return super()._q_traverse(path)
|
||||
|
||||
def is_accessible(self, user, traversal=False):
|
||||
if not user.can_go_in_backoffice():
|
||||
return False
|
||||
if traversal is False and get_cfg('backoffice-submission', {}).get('sidebar_menu_entry') == 'hidden':
|
||||
return False
|
||||
# check user has at least one role set for backoffice submission
|
||||
for role_id in user.roles or []:
|
||||
ids = FormDef.get_ids_with_indexed_value('backoffice_submission_roles', role_id)
|
||||
|
@ -443,7 +447,7 @@ class SubmissionDirectory(Directory):
|
|||
return True
|
||||
return False
|
||||
|
||||
def get_submittable_formdefs(self):
|
||||
def get_submittable_formdefs(self, prefetch=True):
|
||||
user = get_request().user
|
||||
|
||||
agent_ids = set()
|
||||
|
@ -460,113 +464,143 @@ class SubmissionDirectory(Directory):
|
|||
continue
|
||||
list_forms.append(formdef)
|
||||
|
||||
# prefetch formdatas
|
||||
data_class = formdef.data_class()
|
||||
formdef._formdatas = data_class.select(
|
||||
[Equal('status', 'draft'), Equal('backoffice_submission', True)]
|
||||
if prefetch:
|
||||
# prefetch formdatas
|
||||
data_class = formdef.data_class()
|
||||
formdef._formdatas = data_class.select(
|
||||
[Equal('status', 'draft'), Equal('backoffice_submission', True)]
|
||||
)
|
||||
formdef._formdatas.sort(
|
||||
key=lambda x: x.receipt_time or make_aware(datetime.datetime(1900, 1, 1))
|
||||
)
|
||||
agent_ids.update([x.submission_agent_id for x in formdef._formdatas if x.submission_agent_id])
|
||||
|
||||
if prefetch:
|
||||
# prefetch agents
|
||||
self.prefetched_agents = {
|
||||
str(x.id): x
|
||||
for x in get_publisher().user_class.get_ids(list(agent_ids), ignore_errors=True)
|
||||
if x is not None
|
||||
}
|
||||
|
||||
return list_forms
|
||||
|
||||
def get_categories(self, list_formdefs):
|
||||
cats = Category.select()
|
||||
Category.sort_by_position(cats)
|
||||
for cat in cats:
|
||||
cat.formdefs = [x for x in list_formdefs if str(x.category_id) == str(cat.id)]
|
||||
misc_cat = Category(name=_('Misc'))
|
||||
misc_cat.formdefs = [x for x in list_formdefs if not x.category]
|
||||
cats.append(misc_cat)
|
||||
return cats
|
||||
|
||||
def _q_index(self):
|
||||
redirect_url = get_cfg('backoffice-submission', {}).get('redirect')
|
||||
if redirect_url:
|
||||
redirect_url = misc.get_variadic_url(
|
||||
redirect_url, get_publisher().substitutions.get_context_variables(mode='lazy')
|
||||
)
|
||||
formdef._formdatas.sort(key=lambda x: x.receipt_time or make_aware(datetime.datetime(1900, 1, 1)))
|
||||
agent_ids.update([x.submission_agent_id for x in formdef._formdatas if x.submission_agent_id])
|
||||
if redirect_url:
|
||||
return redirect(redirect_url)
|
||||
|
||||
get_response().set_title(_('Submission'))
|
||||
|
||||
list_forms = self.get_submittable_formdefs(prefetch=False)
|
||||
|
||||
context = {'categories': self.get_categories(list_forms)}
|
||||
return template.QommonTemplateResponse(
|
||||
templates=['wcs/backoffice/submission.html'], context=context, is_django_native=True
|
||||
)
|
||||
|
||||
def pending(self):
|
||||
get_response().breadcrumb.append(('pending', _('Pending submissions')))
|
||||
get_response().set_title(_('Pending submissions'))
|
||||
get_response().add_javascript(['wcs.listing.js'])
|
||||
|
||||
limit = misc.get_int_or_400(
|
||||
get_request().form.get('limit', get_publisher().get_site_option('default-page-size') or 20)
|
||||
)
|
||||
offset = misc.get_int_or_400(get_request().form.get('offset', 0))
|
||||
order_by = misc.get_order_by_or_400(
|
||||
get_request().form.get(
|
||||
'order_by', get_publisher().get_site_option('default-sort-order') or '-receipt_time'
|
||||
)
|
||||
)
|
||||
include_submission_channel = misc.get_cfg('submission-channels', {}).get('include-in-global-listing')
|
||||
|
||||
list_formdefs = self.get_submittable_formdefs(prefetch=False)
|
||||
criterias = [
|
||||
Equal('status', 'draft'),
|
||||
Equal('backoffice_submission', True),
|
||||
Contains('formdef_id', [x.id for x in list_formdefs]),
|
||||
]
|
||||
|
||||
r = TemplateIO(html=True)
|
||||
|
||||
r += htmltext('<table id="listing" class="main">')
|
||||
r += htmltext('<thead><tr>')
|
||||
if include_submission_channel:
|
||||
r += htmltext('<th data-field-sort-key="submission_channel"><span>%s</span></th>') % _('Channel')
|
||||
r += htmltext('<th data-field-sort-key="formdef_name"><span>%s</span></th>') % _('Form')
|
||||
r += htmltext('<th data-field-sort-key="receipt_time"><span>%s</span></th>') % _('Created')
|
||||
r += htmltext('<th><span>%s</span></th>') % _('Submission Agent')
|
||||
r += htmltext('</tr></thead>')
|
||||
r += htmltext('<tbody>\n')
|
||||
|
||||
from wcs.sql import AnyFormData
|
||||
|
||||
total_count = AnyFormData.count(criterias)
|
||||
formdatas = AnyFormData.select(criterias, order_by=order_by, limit=limit, offset=offset)
|
||||
|
||||
# prefetch agents
|
||||
agent_ids = set()
|
||||
agent_ids.update([x.submission_agent_id for x in formdatas if x.submission_agent_id])
|
||||
self.prefetched_agents = {
|
||||
str(x.id): x
|
||||
for x in get_publisher().user_class.get_ids(list(agent_ids), ignore_errors=True)
|
||||
if x is not None
|
||||
}
|
||||
|
||||
return list_forms
|
||||
|
||||
def _q_index(self):
|
||||
get_response().breadcrumb.append(('submission/', _('Submission')))
|
||||
get_response().set_title(_('Submission'))
|
||||
|
||||
list_forms = self.get_submittable_formdefs()
|
||||
cats = Category.select()
|
||||
Category.sort_by_position(cats)
|
||||
for cat in cats:
|
||||
cat.formdefs = [x for x in list_forms if str(x.category_id) == str(cat.id)]
|
||||
misc_cat = Category(name=_('Misc'))
|
||||
misc_cat.formdefs = [x for x in list_forms if not x.category]
|
||||
cats.append(misc_cat)
|
||||
|
||||
r = TemplateIO(html=True)
|
||||
r += get_session().display_message()
|
||||
modes = ['empty', 'create', 'existing']
|
||||
for mode in modes:
|
||||
list_content = TemplateIO()
|
||||
for cat in cats:
|
||||
if not cat.formdefs:
|
||||
continue
|
||||
list_content += self.form_list(cat.formdefs, title=cat.name, mode=mode)
|
||||
if not list_content.getvalue().strip():
|
||||
continue
|
||||
r += htmltext('<h2>%s</h2>') % {
|
||||
'create': _('New submission'),
|
||||
'existing': _('Running submission'),
|
||||
'empty': _('Submission to complete'),
|
||||
}.get(mode)
|
||||
r += htmltext(f'<ul class="biglist {mode}">')
|
||||
r += htmltext(list_content.getvalue())
|
||||
r += htmltext('</ul>')
|
||||
|
||||
return r.getvalue()
|
||||
|
||||
def form_list(self, formdefs, title=None, mode='create'):
|
||||
r = TemplateIO(html=True)
|
||||
if mode != 'create':
|
||||
skip = True
|
||||
for formdef in formdefs:
|
||||
skip &= not (bool(formdef._formdatas))
|
||||
if skip:
|
||||
return
|
||||
|
||||
first = True
|
||||
|
||||
for formdef in formdefs:
|
||||
if mode != 'create':
|
||||
formdatas = formdef._formdatas[:]
|
||||
if mode == 'empty':
|
||||
formdatas = [x for x in formdatas if x.has_empty_data()]
|
||||
elif mode == 'existing':
|
||||
formdatas = [x for x in formdatas if not x.has_empty_data()]
|
||||
if not formdatas:
|
||||
continue
|
||||
|
||||
if first and title:
|
||||
r += htmltext('<li><h3>%s</h3></li>') % title
|
||||
first = False
|
||||
|
||||
r += htmltext('<li>')
|
||||
if mode == 'create':
|
||||
r += htmltext('<strong class="label"><a href="%s/">%s</a></strong>') % (
|
||||
formdef.url_name,
|
||||
formdef.name,
|
||||
)
|
||||
for formdata in formdatas:
|
||||
url = f'{formdata.formdef.url_name}/{formdata.id}/'
|
||||
r += htmltext(f'<tr data-link="{url}">')
|
||||
if include_submission_channel:
|
||||
r += htmltext('<td>%s</td>') % formdata.get_submission_channel_label()
|
||||
r += htmltext(f'<td><a href="{url}">{formdata.get_display_name()}')
|
||||
if formdata.default_digest:
|
||||
r += htmltext(' <small>%s</small>') % formdata.default_digest
|
||||
r += htmltext('</a></td>')
|
||||
r += htmltext('<td class="cell-time">%s</td>') % misc.localstrftime(formdata.receipt_time)
|
||||
agent_user = self.prefetched_agents.get(formdata.submission_agent_id)
|
||||
if agent_user:
|
||||
r += htmltext('<td class="cell-user">%s</td>') % agent_user.get_display_name()
|
||||
else:
|
||||
r += htmltext('<strong class="label"><a class="fake">%s</a></strong>') % formdef.name
|
||||
r += htmltext('</li>')
|
||||
if mode == 'create':
|
||||
continue
|
||||
for formdata in formdatas:
|
||||
r += htmltext('<li class="smallitem">')
|
||||
label = ''
|
||||
if formdata.submission_channel:
|
||||
label = '%s ' % formdata.get_submission_channel_label()
|
||||
label += _('#%(id)s, %(time)s') % {
|
||||
'id': formdata.id,
|
||||
'time': misc.localstrftime(formdata.receipt_time)
|
||||
if formdata.receipt_time
|
||||
else _('unknown date'),
|
||||
}
|
||||
if formdata.submission_agent_id:
|
||||
agent_user = self.prefetched_agents.get(formdata.submission_agent_id)
|
||||
if agent_user:
|
||||
label += ' (%s)' % agent_user.display_name
|
||||
r += htmltext('<a href="%s/%s/">%s</a>') % (formdef.url_name, formdata.id, label)
|
||||
r += htmltext('</li>')
|
||||
r += htmltext('<td class="cell-user cell-no-user">-</td>')
|
||||
r += htmltext('</tr>\n')
|
||||
|
||||
return r.getvalue()
|
||||
r += htmltext('</tbody></table>')
|
||||
|
||||
if (offset > 0) or (total_count > limit > 0):
|
||||
r += pagination_links(offset, limit, total_count)
|
||||
|
||||
if get_request().form.get('ajax') == 'true':
|
||||
get_request().ignore_session = True
|
||||
get_response().raw = True
|
||||
return r.getvalue()
|
||||
|
||||
rt = TemplateIO(html=True)
|
||||
rt += htmltext('<div id="appbar">')
|
||||
rt += htmltext('<h2>%s</h2>') % _('Pending submissions')
|
||||
rt += htmltext('</div>')
|
||||
rt += r.getvalue()
|
||||
form = Form(use_tokens=False, id='listing-settings', method='get', action='pending')
|
||||
form.add_hidden('offset', offset)
|
||||
form.add_hidden('limit', limit)
|
||||
form.add_hidden('order_by', order_by)
|
||||
rt += form.render()
|
||||
|
||||
return rt.getvalue()
|
||||
|
||||
def count(self):
|
||||
formdefs = self.get_submittable_formdefs()
|
||||
|
@ -582,5 +616,4 @@ class SubmissionDirectory(Directory):
|
|||
return misc.json_response({'count': count})
|
||||
|
||||
def _q_lookup(self, component):
|
||||
get_response().breadcrumb.append(('submission/', _('Submission')))
|
||||
return FormFillPage(component)
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue