Compare commits
75 Commits
f1aba77545
...
dbcbca44e4
Author | SHA1 | Date |
---|---|---|
Frédéric Péters | dbcbca44e4 | |
Frédéric Péters | 838eed8cac | |
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 |
|
@ -368,7 +368,7 @@ Par ailleurs, l’API « Liste de formulaires » accepte un paramètre
|
|||
</p>
|
||||
|
||||
<screen>
|
||||
<input>GET https://www.example.net/api/forms/inscriptions/list??include-anonymised=on</input>
|
||||
<input>GET https://www.example.net/api/forms/inscriptions/list?include-anonymised=on</input>
|
||||
</screen>
|
||||
|
||||
</section>
|
||||
|
|
|
@ -53,6 +53,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/')
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,16 @@ 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
|
||||
|
||||
|
||||
def test_card_digest_template(pub):
|
||||
create_superuser(pub)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -7,11 +7,11 @@ 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 +23,8 @@ 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
|
||||
from wcs.wscalls import NamedWsCall, NamedWsCallImportError
|
||||
|
||||
from ..utilities import clean_temporary_pub, create_temporary_pub, get_app, login
|
||||
from .test_all import create_superuser
|
||||
|
@ -293,7 +293,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
|
||||
|
||||
|
@ -305,7 +305,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
|
||||
|
||||
|
@ -316,19 +316,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
|
||||
|
||||
|
@ -397,7 +397,7 @@ def test_deprecations_document_models(pub):
|
|||
export_to2.by = ['_submitter']
|
||||
workflow.store()
|
||||
|
||||
job = DeprecationsScanAfterJob()
|
||||
job = DeprecationsScan()
|
||||
job.execute()
|
||||
assert job.report_lines == [
|
||||
{
|
||||
|
@ -446,7 +446,7 @@ def test_deprecations_inspect_pages(pub):
|
|||
display.message = 'message with [ezt] info'
|
||||
workflow.store()
|
||||
|
||||
job = DeprecationsScanAfterJob()
|
||||
job = DeprecationsScan()
|
||||
job.execute()
|
||||
|
||||
create_superuser(pub)
|
||||
|
@ -485,7 +485,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')
|
||||
|
@ -506,7 +506,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:
|
||||
|
@ -525,3 +525,124 @@ 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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
|
|
|
@ -242,13 +242,26 @@ 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__'}
|
||||
|
||||
|
||||
def test_forms_edit_tracking_code(pub, formdef):
|
||||
|
@ -510,6 +523,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)
|
||||
|
@ -4036,6 +4077,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)
|
||||
|
@ -4093,6 +4167,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)
|
||||
|
@ -4661,6 +4752,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)
|
||||
|
|
|
@ -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',
|
||||
|
@ -128,6 +135,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
|
||||
|
@ -159,7 +169,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')}
|
||||
|
|
|
@ -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-')]
|
||||
|
|
|
@ -814,6 +814,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
|
||||
|
@ -1046,6 +1050,7 @@ def test_tests_result_inspect(pub):
|
|||
testdef = TestDef.create_from_formdata(formdef, formdata)
|
||||
testdef.name = 'First test'
|
||||
testdef.agent_id = user.id
|
||||
testdef.is_in_backoffice = True
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.ButtonClick(id='1', button_name='Loop on status'),
|
||||
]
|
||||
|
@ -1087,6 +1092,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)
|
||||
|
|
|
@ -990,6 +990,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)
|
||||
|
|
|
@ -32,6 +32,7 @@ def pub():
|
|||
|
||||
FormDef.wipe()
|
||||
TestDef.wipe()
|
||||
WebserviceResponse.wipe()
|
||||
return pub
|
||||
|
||||
|
||||
|
@ -197,6 +198,8 @@ def test_workflow_tests_edit_actions(pub):
|
|||
|
||||
def test_workflow_tests_action_button_click(pub):
|
||||
create_superuser(pub)
|
||||
user = pub.user_class(name='test user')
|
||||
user.store()
|
||||
|
||||
workflow = Workflow(name='Workflow One')
|
||||
new_status = workflow.add_status(name='New status')
|
||||
|
@ -217,6 +220,9 @@ def test_workflow_tests_action_button_click(pub):
|
|||
testdef.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
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
|
||||
|
||||
|
@ -240,6 +246,23 @@ def test_workflow_tests_action_button_click(pub):
|
|||
('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'].force_value(user.id)
|
||||
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
|
||||
|
||||
|
||||
def test_workflow_tests_action_assert_status(pub):
|
||||
create_superuser(pub)
|
||||
|
@ -321,6 +344,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')
|
||||
|
@ -331,10 +355,68 @@ 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
|
||||
|
||||
|
||||
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'
|
||||
resp = resp.form.submit().follow()
|
||||
|
||||
assert 'SMS to 0123456789' in resp.text
|
||||
|
||||
assert_sms = TestDef.get(testdef.id).workflow_tests.actions[0]
|
||||
assert assert_sms.phone_numbers == ['0123456789']
|
||||
assert assert_sms.body == 'Hello'
|
||||
|
||||
assert_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
|
||||
|
||||
|
||||
def test_workflow_tests_action_assert_backoffice_field(pub):
|
||||
create_superuser(pub)
|
||||
|
@ -427,8 +509,8 @@ def test_workflow_tests_action_assert_webservice_call(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'),
|
||||
(str(response.id), False, 'Fake response'),
|
||||
(str(response2.id), False, 'Fake response 2'),
|
||||
]
|
||||
assert resp.form['call_count'].value == '1'
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -177,6 +177,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 +219,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 +240,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'
|
||||
)
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ 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.blocks import BlockDef
|
||||
from wcs.carddef import CardDef
|
||||
|
@ -940,6 +940,50 @@ 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).put(sign_uri('/api/export-import/bundle-import/'), 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).put(sign_uri('/api/export-import/bundle-import/'), 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).put(sign_uri('/api/export-import/bundle-import/'), 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',
|
||||
|
@ -1321,6 +1365,30 @@ def test_export_import_bundle_declare(pub):
|
|||
== []
|
||||
)
|
||||
|
||||
# bad file format
|
||||
resp = get_app(pub).put(sign_uri('/api/export-import/bundle-declare/'), 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).put(sign_uri('/api/export-import/bundle-declare/'), 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()
|
||||
|
@ -1896,6 +1964,38 @@ def test_export_import_bundle_check(pub):
|
|||
},
|
||||
}
|
||||
|
||||
# bad file format
|
||||
resp = get_app(pub).put(sign_uri('/api/export-import/bundle-check/'), 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).put(sign_uri('/api/export-import/bundle-check/'), 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).put(sign_uri('/api/export-import/bundle-check/'), 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()
|
||||
|
@ -1946,6 +2046,97 @@ def test_export_import_workflow_options(pub):
|
|||
assert formdef.workflow_options == {'foo': 'bar2'}
|
||||
|
||||
|
||||
def test_export_import_with_deprecated(pub):
|
||||
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).put(sign_uri('/api/export-import/bundle-import/'), bundle)
|
||||
afterjob_url = resp.json['url']
|
||||
resp = get_app(pub).put(sign_uri(afterjob_url))
|
||||
assert resp.json['data']['status'] == 'failed'
|
||||
|
||||
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).put(sign_uri('/api/export-import/bundle-import/'), 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).put(sign_uri('/api/export-import/bundle-import/'), 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).put(sign_uri('/api/export-import/bundle-import/'), 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).put(sign_uri('/api/export-import/bundle-import/'), 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')
|
||||
|
|
|
@ -1366,6 +1366,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(
|
||||
|
@ -1542,6 +1545,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(
|
||||
|
@ -2011,6 +2017,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(
|
||||
|
@ -2307,6 +2315,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(
|
||||
|
|
|
@ -2184,7 +2184,7 @@ def test_backoffice_download_as_zip(pub):
|
|||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/management/form-title/%s/' % number31.id)
|
||||
assert 'Download all files as .zip' not in resp
|
||||
formdef.include_download_all_button = True
|
||||
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 +2264,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 +3889,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 +5130,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 +5509,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()
|
||||
|
|
|
@ -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()
|
||||
|
@ -1761,6 +1771,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 +1890,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 +1905,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 +1991,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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
@ -2245,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.'
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -3366,7 +3408,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
|
||||
|
@ -6095,3 +6146,45 @@ def test_form_submit_no_csrf_suddenly_single_page(pub):
|
|||
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
|
||||
|
|
|
@ -573,3 +573,22 @@ 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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
||||
|
@ -634,6 +635,7 @@ def test_form_page_item_with_variable_data_source_prefill(pub):
|
|||
def test_form_page_item_with_card_with_custom_id_prefill(pub):
|
||||
create_user(pub)
|
||||
CardDef.wipe()
|
||||
FormDef.wipe()
|
||||
|
||||
carddef = CardDef()
|
||||
carddef.name = 'Test'
|
||||
|
@ -678,6 +680,7 @@ def test_form_page_item_with_card_with_custom_id_prefill(pub):
|
|||
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'
|
||||
|
|
|
@ -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
|
||||
|
@ -1328,3 +1329,347 @@ 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()
|
||||
|
||||
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()
|
||||
|
||||
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_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()
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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'):
|
||||
|
|
|
@ -3742,6 +3742,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 +4246,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 +4506,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 +4557,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
|
||||
|
@ -5312,10 +5328,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
|
||||
|
|
|
@ -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'}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1795,3 +1795,27 @@ def test_convert_image_format_errors(pub):
|
|||
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) == ''
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -129,6 +129,15 @@ def test_workflow_tests_action_not_configured(pub):
|
|||
mocked_perform.assert_called_once()
|
||||
|
||||
|
||||
def test_workflow_tests_new_action_id(pub):
|
||||
wf_tests = workflow_tests.WorkflowTests()
|
||||
|
||||
for i in range(15):
|
||||
wf_tests.add_action(workflow_tests.ButtonClick)
|
||||
|
||||
assert [x.id for x in wf_tests.actions] == [str(i) for i in range(1, 16)]
|
||||
|
||||
|
||||
def test_workflow_tests_button_click(pub):
|
||||
role = pub.role_class(name='test role')
|
||||
role.store()
|
||||
|
@ -195,6 +204,110 @@ def test_workflow_tests_button_click(pub):
|
|||
assert str(excinfo.value) == 'Button "Go to end status" is not displayed.'
|
||||
|
||||
|
||||
def test_workflow_tests_button_click_who(pub):
|
||||
role = pub.role_class(name='test role')
|
||||
role.store()
|
||||
agent_user = pub.user_class(name='agent user')
|
||||
agent_user.roles = [role.id]
|
||||
agent_user.store()
|
||||
other_role = pub.role_class(name='other test role')
|
||||
other_role.store()
|
||||
other_user = pub.user_class(name='other user')
|
||||
other_user.roles = [other_role.id]
|
||||
other_user.store()
|
||||
|
||||
workflow = Workflow(name='Workflow One')
|
||||
new_status = workflow.add_status(name='New status')
|
||||
jump_by_unknown = workflow.add_status(name='Jump by unknown')
|
||||
jump_by_receiver = workflow.add_status(name='Jump by receiver')
|
||||
jump_by_submitter = workflow.add_status(name='Jump by submitter')
|
||||
jump_by_other_user = workflow.add_status(name='Jump by other user')
|
||||
|
||||
jump = new_status.add_action('choice')
|
||||
jump.label = 'Go to next status'
|
||||
jump.status = jump_by_unknown.id
|
||||
jump.by = ['unknown']
|
||||
|
||||
receiver_jump = new_status.add_action('choice')
|
||||
receiver_jump.label = 'Go to next status'
|
||||
receiver_jump.status = jump_by_receiver.id
|
||||
receiver_jump.by = ['_receiver']
|
||||
|
||||
submitter_jump = new_status.add_action('choice')
|
||||
submitter_jump.label = 'Go to next status'
|
||||
submitter_jump.status = jump_by_submitter.id
|
||||
submitter_jump.by = ['_submitter']
|
||||
|
||||
other_user_jump = new_status.add_action('choice')
|
||||
other_user_jump.label = 'Go to next status'
|
||||
other_user_jump.status = jump_by_other_user.id
|
||||
other_user_jump.by = [other_role.id]
|
||||
|
||||
workflow.store()
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test title'
|
||||
formdef.workflow_id = workflow.id
|
||||
formdef.workflow_roles = {'_receiver': role.id}
|
||||
formdef.store()
|
||||
|
||||
formdata = formdef.data_class()()
|
||||
formdata.just_created()
|
||||
|
||||
testdef = TestDef.create_from_formdata(formdef, formdata)
|
||||
testdef.agent_id = agent_user.id
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.ButtonClick(button_name='Go to next status', who='receiver'),
|
||||
workflow_tests.AssertStatus(status_name='Jump by receiver'),
|
||||
]
|
||||
testdef.run(formdef)
|
||||
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.ButtonClick(button_name='Go to next status', who='submitter'),
|
||||
workflow_tests.AssertStatus(status_name='Jump by submitter'),
|
||||
]
|
||||
testdef.run(formdef)
|
||||
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.ButtonClick(button_name='Go to next status', who='other', who_id=other_user.id),
|
||||
workflow_tests.AssertStatus(status_name='Jump by other user'),
|
||||
]
|
||||
testdef.run(formdef)
|
||||
|
||||
other_user.remove_self()
|
||||
with pytest.raises(WorkflowTestError) as excinfo:
|
||||
testdef.run(formdef)
|
||||
assert str(excinfo.value) == 'Broken, missing user'
|
||||
|
||||
# submitter is anonymous
|
||||
submitter_jump.by = ['logged-users']
|
||||
workflow.store()
|
||||
formdef.refresh_from_storage()
|
||||
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.ButtonClick(button_name='Go to next status', who='submitter'),
|
||||
workflow_tests.AssertStatus(status_name='Jump by submitter'),
|
||||
]
|
||||
with pytest.raises(WorkflowTestError) as excinfo:
|
||||
testdef.run(formdef)
|
||||
assert str(excinfo.value) == 'Button "Go to next status" is not displayed.'
|
||||
|
||||
# not anonymous submitter
|
||||
submitter_user = pub.user_class(name='submitter user')
|
||||
submitter_user.email = 'test@example.com'
|
||||
submitter_user.store()
|
||||
|
||||
formdata.user = submitter_user
|
||||
|
||||
testdef = TestDef.create_from_formdata(formdef, formdata)
|
||||
testdef.agent_id = agent_user.id
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.ButtonClick(button_name='Go to next status', who='submitter'),
|
||||
workflow_tests.AssertStatus(status_name='Jump by submitter'),
|
||||
]
|
||||
testdef.run(formdef)
|
||||
|
||||
|
||||
def test_workflow_tests_automatic_jump(pub):
|
||||
user = pub.user_class(name='test user')
|
||||
user.store()
|
||||
|
@ -434,6 +547,62 @@ def test_workflow_tests_sendmail(mocked_send_email, pub):
|
|||
assert str(excinfo.value) == 'Email subject does not contain "In new status".'
|
||||
|
||||
|
||||
def test_workflow_tests_sms(pub):
|
||||
pub.cfg['sms'] = {'sender': 'xxx', 'passerelle_url': 'http://passerelle.invalid/'}
|
||||
|
||||
user = pub.user_class(name='test user')
|
||||
user.store()
|
||||
|
||||
workflow = Workflow(name='Workflow One')
|
||||
new_status = workflow.add_status(name='New status')
|
||||
|
||||
sendsms = new_status.add_action('sendsms')
|
||||
sendsms.to = ['0123456789']
|
||||
sendsms.body = 'Hello'
|
||||
|
||||
workflow.store()
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test title'
|
||||
formdef.workflow_id = workflow.id
|
||||
formdef.store()
|
||||
|
||||
formdata = formdef.data_class()()
|
||||
formdata.just_created()
|
||||
|
||||
testdef = TestDef.create_from_formdata(formdef, formdata)
|
||||
testdef.agent_id = user.id
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.AssertSMS(phone_numbers=['0123456789'], body='Hello'),
|
||||
]
|
||||
|
||||
testdef.run(formdef)
|
||||
|
||||
testdef.workflow_tests.actions.append(workflow_tests.AssertSMS())
|
||||
|
||||
with pytest.raises(WorkflowTestError) as excinfo:
|
||||
testdef.run(formdef)
|
||||
assert str(excinfo.value) == 'No SMS was sent.'
|
||||
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.AssertSMS(phone_numbers=['0612345678']),
|
||||
]
|
||||
|
||||
with pytest.raises(WorkflowTestError) as excinfo:
|
||||
testdef.run(formdef)
|
||||
assert str(excinfo.value) == 'SMS was not sent to 0612345678.'
|
||||
assert 'SMS phone numbers: 0123456789' in excinfo.value.details
|
||||
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.AssertSMS(body='Goodbye'),
|
||||
]
|
||||
|
||||
with pytest.raises(WorkflowTestError) as excinfo:
|
||||
testdef.run(formdef)
|
||||
assert str(excinfo.value) == 'SMS body mismatch.'
|
||||
assert 'SMS body: "Hello"' in excinfo.value.details
|
||||
|
||||
|
||||
def test_workflow_tests_backoffice_fields(pub):
|
||||
user = pub.user_class(name='test user')
|
||||
user.store()
|
||||
|
@ -691,6 +860,10 @@ def test_workflow_tests_create_from_formdata(pub, http_requests, freezer):
|
|||
set_backoffice_fields = transition_status.add_action('set-backoffice-fields')
|
||||
set_backoffice_fields.fields = [{'field_id': 'bo1', 'value': 'xxx'}]
|
||||
|
||||
sendsms = transition_status.add_action('sendsms')
|
||||
sendsms.to = ['0123456789']
|
||||
sendsms.body = 'Hello'
|
||||
|
||||
jump = transition_status.add_action('jump')
|
||||
jump.status = end_status.id
|
||||
|
||||
|
@ -723,7 +896,7 @@ def test_workflow_tests_create_from_formdata(pub, http_requests, freezer):
|
|||
testdef.run(formdef)
|
||||
|
||||
actions = testdef.workflow_tests.actions
|
||||
assert len(actions) == 8
|
||||
assert len(actions) == 9
|
||||
|
||||
assert actions[0].key == 'assert-status'
|
||||
assert actions[0].status_name == 'Status with timeout jump'
|
||||
|
@ -740,6 +913,7 @@ def test_workflow_tests_create_from_formdata(pub, http_requests, freezer):
|
|||
assert actions[4].key == 'assert-webservice-call'
|
||||
assert actions[5].key == 'assert-email'
|
||||
assert actions[6].key == 'assert-backoffice-field'
|
||||
assert actions[7].key == 'assert-sms'
|
||||
|
||||
assert actions[-1].key == 'assert-status'
|
||||
assert actions[-1].status_name == 'End status'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 = {}
|
||||
|
@ -647,3 +652,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'}
|
|
@ -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 }}']
|
||||
|
|
|
@ -36,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
|
||||
|
@ -320,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()
|
||||
|
@ -330,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>'
|
||||
|
@ -341,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'])
|
||||
|
|
|
@ -158,8 +158,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 +177,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 +223,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):
|
||||
|
|
1
tox.ini
1
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
|
||||
|
|
|
@ -27,6 +27,7 @@ from wcs.categories import CardDefCategory, DataSourceCategory
|
|||
from wcs.data_sources import (
|
||||
DataSourceSelectionWidget,
|
||||
NamedDataSource,
|
||||
NamedDataSourceImportError,
|
||||
RefreshAgendas,
|
||||
get_structured_items,
|
||||
has_chrono,
|
||||
|
@ -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)
|
||||
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:
|
||||
|
|
|
@ -332,10 +332,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,
|
||||
|
@ -494,6 +496,7 @@ class OptionsDirectory(Directory):
|
|||
'submission_lateral_template',
|
||||
'drafts_lifespan',
|
||||
'user_support',
|
||||
'management_sidebar_items',
|
||||
]
|
||||
for attr in attrs:
|
||||
widget = form.get_widget(attr)
|
||||
|
@ -503,6 +506,10 @@ class OptionsDirectory(Directory):
|
|||
if has_error:
|
||||
continue
|
||||
new_value = widget.parse()
|
||||
if attr == 'management_sidebar_items':
|
||||
new_value = set(new_value)
|
||||
if new_value == self.formdef.__class__.management_sidebar_items:
|
||||
new_value = {'__default__'}
|
||||
if attr == 'digest_template':
|
||||
if self.formdef.default_digest_template != new_value:
|
||||
self.changed = True
|
||||
|
@ -777,7 +784,10 @@ 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 != {'__default__'}
|
||||
)
|
||||
else _('Default'),
|
||||
),
|
||||
'tracking_code': self.add_option_line(
|
||||
|
|
|
@ -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 = (
|
||||
|
|
|
@ -36,7 +36,7 @@ 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, pgettext_lazy, template
|
||||
|
@ -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
|
||||
|
@ -1512,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:
|
||||
|
|
|
@ -46,12 +46,12 @@ from wcs.qommon.form import (
|
|||
Form,
|
||||
HtmlWidget,
|
||||
RadiobuttonsWidget,
|
||||
RichTextWidget,
|
||||
SingleSelectWidget,
|
||||
SlugWidget,
|
||||
StringWidget,
|
||||
UrlWidget,
|
||||
VarnameWidget,
|
||||
WysiwygTextWidget,
|
||||
)
|
||||
from wcs.sql_criterias import Equal
|
||||
from wcs.workflows import (
|
||||
|
@ -991,7 +991,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,
|
||||
|
@ -1590,7 +1590,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,
|
||||
|
|
|
@ -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:
|
||||
|
@ -317,15 +317,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)
|
||||
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:
|
||||
|
|
|
@ -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
|
||||
|
@ -270,101 +270,118 @@ 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'))
|
||||
try:
|
||||
with tarfile.open(fileobj=tar_io) 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,7 +522,12 @@ 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
|
||||
)
|
||||
|
@ -587,24 +642,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:
|
||||
|
|
|
@ -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/',
|
||||
|
@ -127,7 +134,7 @@ class DeprecationsDirectory(Directory):
|
|||
}
|
||||
|
||||
|
||||
class DeprecationsScanAfterJob(AfterJob):
|
||||
class DeprecationsScan(AfterJob):
|
||||
def done_action_url(self):
|
||||
return self.kwargs['return_url']
|
||||
|
||||
|
@ -155,181 +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
|
||||
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
|
||||
|
||||
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)
|
||||
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
|
||||
)
|
||||
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
|
||||
|
@ -386,6 +237,169 @@ class DeprecationsScanAfterJob(AfterJob):
|
|||
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)
|
||||
|
@ -400,3 +414,32 @@ class DeprecationsScanAfterJob(AfterJob):
|
|||
fd,
|
||||
indent=2,
|
||||
)
|
||||
|
||||
def check_deprecated_elements_in_object(self, obj):
|
||||
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
|
||||
|
|
|
@ -298,7 +298,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)
|
||||
|
@ -1147,6 +1150,7 @@ class FormPage(Directory, TempfileDirectoryMixin):
|
|||
'submission-agent-id',
|
||||
'date',
|
||||
'distance',
|
||||
'criticality-level',
|
||||
]
|
||||
return types
|
||||
|
||||
|
@ -1169,6 +1173,8 @@ class FormPage(Directory, TempfileDirectoryMixin):
|
|||
FakeField('user-function', 'user-function', _('Current User Function')),
|
||||
FakeField('submission-agent', 'submission-agent-id', _('Submission Agent'), addable=False),
|
||||
]
|
||||
if self.formdef.workflow.criticality_levels:
|
||||
fake_fields.append(FakeField('criticality-level', 'criticality-level', _('Criticality Level')))
|
||||
default_filters = self.get_default_filters(mode)
|
||||
|
||||
filter_fields = []
|
||||
|
@ -1376,6 +1382,19 @@ class FormPage(Directory, TempfileDirectoryMixin):
|
|||
)
|
||||
r += render_widget(widget, operators)
|
||||
|
||||
elif filter_field.key == 'criticality-level':
|
||||
options = [('', pgettext_lazy('criticality-level', 'All'), '')] + [
|
||||
(str(i), x.name, str(i)) for i, x in enumerate(self.formdef.workflow.criticality_levels)
|
||||
]
|
||||
widget = SingleSelectWidget(
|
||||
filter_field_key,
|
||||
title=filter_field.label,
|
||||
options=options,
|
||||
value=filter_field_value,
|
||||
render_br=False,
|
||||
)
|
||||
r += render_widget(widget, operators=[])
|
||||
|
||||
elif filter_field.key in ('item', 'items'):
|
||||
filter_field.required = False
|
||||
|
||||
|
@ -1457,18 +1476,12 @@ 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">')
|
||||
|
@ -2002,6 +2015,7 @@ class FormPage(Directory, TempfileDirectoryMixin):
|
|||
FakeField('user-function', 'user-function', _('Current User Function')),
|
||||
FakeField('submission-agent', 'submission-agent-id', _('Submission Agent')),
|
||||
FakeField('distance', 'distance', _('Distance')),
|
||||
FakeField('criticality-level', 'criticality-level', _('Criticality Level')),
|
||||
]
|
||||
criterias = []
|
||||
|
||||
|
@ -2066,6 +2080,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
|
||||
|
@ -2218,7 +2236,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
|
||||
|
@ -2306,6 +2324,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(
|
||||
|
@ -3419,6 +3440,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)
|
||||
|
||||
|
@ -3426,7 +3448,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)
|
||||
|
@ -3435,9 +3456,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)
|
||||
|
@ -3489,43 +3514,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>')
|
||||
|
@ -3537,7 +3573,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':
|
||||
|
@ -3580,7 +3616,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()
|
||||
|
||||
|
@ -3588,7 +3624,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')
|
||||
|
@ -3625,7 +3661,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
|
||||
|
@ -3684,7 +3720,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 = {}
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -183,12 +183,17 @@ class BlockDef(StorableObject, PostConditionsXmlMixin):
|
|||
return root
|
||||
|
||||
@classmethod
|
||||
def import_from_xml(cls, fd, include_id=False, check_datasources=True):
|
||||
def import_from_xml(cls, fd, include_id=False, check_datasources=True, check_deprecated=True):
|
||||
try:
|
||||
tree = ET.parse(fd)
|
||||
except Exception:
|
||||
raise ValueError()
|
||||
blockdef = cls.import_from_xml_tree(tree, include_id=include_id, check_datasources=check_datasources)
|
||||
blockdef = cls.import_from_xml_tree(
|
||||
tree,
|
||||
include_id=include_id,
|
||||
check_datasources=check_datasources,
|
||||
check_deprecated=check_deprecated,
|
||||
)
|
||||
|
||||
if blockdef.slug:
|
||||
try:
|
||||
|
@ -201,7 +206,11 @@ class BlockDef(StorableObject, PostConditionsXmlMixin):
|
|||
return blockdef
|
||||
|
||||
@classmethod
|
||||
def import_from_xml_tree(cls, tree, include_id=False, check_datasources=True, **kwargs):
|
||||
def import_from_xml_tree(
|
||||
cls, tree, include_id=False, check_datasources=True, check_deprecated=True, **kwargs
|
||||
):
|
||||
from wcs.backoffice.deprecations import DeprecatedElementsDetected, DeprecationsScan
|
||||
|
||||
blockdef = cls()
|
||||
if tree.find('name') is None or not tree.find('name').text:
|
||||
raise BlockdefImportError(_('Missing name'))
|
||||
|
@ -286,6 +295,14 @@ class BlockDef(StorableObject, PostConditionsXmlMixin):
|
|||
details[_('Unknown datasources')].update(unknown_datasources)
|
||||
raise BlockdefImportUnknownReferencedError(_('Unknown referenced objects'), details=details)
|
||||
|
||||
if check_deprecated:
|
||||
# check for deprecated elements
|
||||
job = DeprecationsScan()
|
||||
try:
|
||||
job.check_deprecated_elements_in_object(blockdef)
|
||||
except DeprecatedElementsDetected as e:
|
||||
raise BlockdefImportError(str(e))
|
||||
|
||||
return blockdef
|
||||
|
||||
def get_usage_fields(self):
|
||||
|
|
115
wcs/carddata.py
115
wcs/carddata.py
|
@ -14,31 +14,18 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from quixote import get_publisher, get_request, get_session
|
||||
from quixote import get_publisher, get_request, get_response, get_session
|
||||
|
||||
from wcs.formdata import FormData
|
||||
|
||||
from .qommon import _
|
||||
from .sql_criterias import Equal
|
||||
from .qommon.afterjobs import AfterJob
|
||||
from .sql_criterias import Equal, Null, Or, get_field_id
|
||||
|
||||
|
||||
class CardData(FormData):
|
||||
uuid = None
|
||||
|
||||
def get_formdef(self):
|
||||
if self._formdef:
|
||||
return self._formdef
|
||||
from .carddef import CardDef
|
||||
|
||||
id = self._names.split('-', 1)[1]
|
||||
try:
|
||||
self._formdef = CardDef.get_by_urlname(id)
|
||||
except KeyError:
|
||||
self._formdef = None
|
||||
return self._formdef
|
||||
|
||||
formdef = property(get_formdef)
|
||||
|
||||
def get_data_source_structured_item(
|
||||
self, digest_key='default', group_by=None, with_related_urls=False, with_files_urls=False
|
||||
):
|
||||
|
@ -133,3 +120,99 @@ class CardData(FormData):
|
|||
}
|
||||
token = get_session().create_token('card-file-by-token', context)
|
||||
return '/api/card-file-by-token/%s' % token.id
|
||||
|
||||
def update_related(self):
|
||||
if self.formdef.reverse_relations:
|
||||
job = UpdateRelationsAfterJob(carddata=self)
|
||||
if get_response():
|
||||
job.store()
|
||||
get_response().add_after_job(job)
|
||||
else:
|
||||
job.execute()
|
||||
self._has_changed_digest = False
|
||||
|
||||
|
||||
class UpdateRelationsAfterJob(AfterJob):
|
||||
label = _('Updating relations')
|
||||
|
||||
def __init__(self, carddata):
|
||||
super().__init__(carddef_id=carddata.formdef.id, carddata_id=carddata.id)
|
||||
|
||||
def execute(self):
|
||||
from .carddef import CardDef
|
||||
from .formdef import FormDef
|
||||
|
||||
if getattr(get_publisher(), '_update_related_seen', None) is None:
|
||||
get_publisher()._update_related_seen = set()
|
||||
|
||||
# keep track of objects that have been updated, to avoid cycles
|
||||
update_related_seen = get_publisher()._update_related_seen
|
||||
|
||||
try:
|
||||
carddef = CardDef.get(self.kwargs['carddef_id'])
|
||||
carddata = carddef.data_class().get(self.kwargs['carddata_id'])
|
||||
except KeyError:
|
||||
# card got removed (probably the afterjob met some unexpected delay), ignore.
|
||||
return
|
||||
|
||||
klass = {'carddef': CardDef, 'formdef': FormDef}
|
||||
|
||||
# check all known reverse relations
|
||||
for obj_ref in {x['obj'] for x in carddef.reverse_relations}:
|
||||
obj_type, obj_slug = obj_ref.split(':')
|
||||
obj_class = klass.get(obj_type)
|
||||
try:
|
||||
objdef = obj_class.get_by_slug(obj_slug)
|
||||
except KeyError:
|
||||
continue
|
||||
criterias = []
|
||||
fields = []
|
||||
|
||||
# get fields referencing the card model (only item and items fields, as string
|
||||
# field with data source is just for completion, and computed field with data
|
||||
# source, do not store a display value.
|
||||
for field in objdef.iter_fields(include_block_fields=True):
|
||||
if field.key not in ('item', 'items'):
|
||||
continue
|
||||
data_source = getattr(field, 'data_source', None)
|
||||
if not data_source:
|
||||
continue
|
||||
data_source_type = data_source.get('type')
|
||||
if (
|
||||
not data_source_type.startswith('carddef:')
|
||||
or data_source_type.split(':')[1] != carddef.slug
|
||||
):
|
||||
continue
|
||||
fields.append(field)
|
||||
criterias.append(Equal(get_field_id(field), carddata.identifier, field=field))
|
||||
if not criterias:
|
||||
continue
|
||||
|
||||
def update_data(field, data):
|
||||
display_value = data.get(f'{field.id}_display')
|
||||
field.set_value(data, data.get(field.id))
|
||||
return bool(data.get(f'{field.id}_display') != display_value)
|
||||
|
||||
# look for all formdata, including drafts, excluding anonymised
|
||||
select_criterias = [Null('anonymised'), Or(criterias)]
|
||||
for objdata in objdef.data_class().select_iterator(clause=select_criterias, itersize=200):
|
||||
objdata_seen_key = f'{objdata.formdef.xml_root_node}:{objdata.formdef.slug}:{objdata.id}'
|
||||
if objdata_seen_key in update_related_seen:
|
||||
# do not allow updates to cycle back
|
||||
continue
|
||||
objdata_changed = False
|
||||
for field in fields:
|
||||
if getattr(field, 'block_field', None):
|
||||
if objdata.data.get(field.block_field.id):
|
||||
blockdata_changed = False
|
||||
for block_row_data in objdata.data[field.block_field.id]['data']:
|
||||
blockdata_changed |= update_data(field, block_row_data)
|
||||
if blockdata_changed:
|
||||
# if block data changed, maybe block digest changed too
|
||||
update_data(field.block_field, objdata.data)
|
||||
objdata_changed |= blockdata_changed
|
||||
else:
|
||||
objdata_changed |= update_data(field, objdata.data)
|
||||
if objdata_changed:
|
||||
update_related_seen.add(objdata_seen_key)
|
||||
objdata.store()
|
||||
|
|
|
@ -49,21 +49,22 @@ class Condition:
|
|||
local_variables = self.get_data()
|
||||
return getattr(self, 'evaluate_' + self.type)(local_variables)
|
||||
|
||||
def evaluate(self):
|
||||
try:
|
||||
return self.unsafe_evaluate()
|
||||
except Exception as e:
|
||||
if self.record_errors:
|
||||
summary = _('Failed to evaluate condition')
|
||||
get_publisher().record_error(
|
||||
summary,
|
||||
formdata=self.context.get('formdata'),
|
||||
status_item=self.context.get('status_item'),
|
||||
expression=self.value,
|
||||
expression_type=self.type,
|
||||
exception=e,
|
||||
)
|
||||
raise RuntimeError()
|
||||
def evaluate(self, source_label=None, source_url=None):
|
||||
with get_publisher().error_context(
|
||||
condition=self.value, condition_type=self.type, source_label=source_label, source_url=source_url
|
||||
):
|
||||
try:
|
||||
return self.unsafe_evaluate()
|
||||
except Exception as e:
|
||||
if self.record_errors:
|
||||
summary = _('Failed to evaluate condition')
|
||||
get_publisher().record_error(
|
||||
summary,
|
||||
formdata=self.context.get('formdata'),
|
||||
status_item=self.context.get('status_item'),
|
||||
exception=e,
|
||||
)
|
||||
raise RuntimeError()
|
||||
|
||||
def evaluate_python(self, local_variables):
|
||||
global_variables = get_publisher().get_global_eval_dict()
|
||||
|
|
|
@ -64,6 +64,7 @@ class TenantCommand(BaseCommand):
|
|||
except UnknownTenantError:
|
||||
raise CommandError('unknown tenant')
|
||||
publisher.install_lang()
|
||||
publisher.setup_timezone()
|
||||
publisher.substitutions.feed(publisher)
|
||||
return publisher
|
||||
|
||||
|
|
|
@ -51,6 +51,10 @@ from .qommon.xml_storage import XmlStorableObject
|
|||
data_source_functions = {}
|
||||
|
||||
|
||||
class NamedDataSourceImportError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class DataSourceError(Exception):
|
||||
pass
|
||||
|
||||
|
@ -713,8 +717,8 @@ class NamedDataSource(XmlStorableObject):
|
|||
('data_source', 'data_source'),
|
||||
('notify_on_errors', 'bool'),
|
||||
('record_on_errors', 'bool'),
|
||||
('users_included_roles', 'str_list'),
|
||||
('users_excluded_roles', 'str_list'),
|
||||
('users_included_roles', 'ds_roles'),
|
||||
('users_excluded_roles', 'ds_roles'),
|
||||
('include_disabled_users', 'bool'),
|
||||
]
|
||||
|
||||
|
@ -913,9 +917,22 @@ class NamedDataSource(XmlStorableObject):
|
|||
return root
|
||||
|
||||
@classmethod
|
||||
def import_from_xml_tree(cls, tree, include_id=False, **kwargs):
|
||||
data_source = super().import_from_xml_tree(tree, include_id=include_id, **kwargs)
|
||||
def import_from_xml_tree(cls, tree, include_id=False, check_deprecated=True, **kwargs):
|
||||
from wcs.backoffice.deprecations import DeprecatedElementsDetected, DeprecationsScan
|
||||
|
||||
data_source = super().import_from_xml_tree(
|
||||
tree, include_id=include_id, check_deprecated=check_deprecated, **kwargs
|
||||
)
|
||||
DataSourceCategory.object_category_xml_import(data_source, tree, include_id=include_id)
|
||||
|
||||
if check_deprecated:
|
||||
# check for deprecated elements
|
||||
job = DeprecationsScan()
|
||||
try:
|
||||
job.check_deprecated_elements_in_object(data_source)
|
||||
except DeprecatedElementsDetected as e:
|
||||
raise NamedDataSourceImportError(str(e))
|
||||
|
||||
return data_source
|
||||
|
||||
@classmethod
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
# along with this program; if not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import collections
|
||||
import copy
|
||||
import datetime
|
||||
import html
|
||||
import re
|
||||
|
@ -236,6 +237,11 @@ class Field:
|
|||
for k, v in kwargs.items():
|
||||
setattr(self, k.replace('-', '_'), v)
|
||||
|
||||
def __getstate__(self):
|
||||
odict = copy.copy(self.__dict__)
|
||||
odict.pop('_formdef', None)
|
||||
return odict
|
||||
|
||||
@classmethod
|
||||
def init(cls):
|
||||
pass
|
||||
|
@ -243,6 +249,11 @@ class Field:
|
|||
def get_type_label(self):
|
||||
return self.description
|
||||
|
||||
def get_admin_url(self):
|
||||
if not getattr(self, '_formdef', None):
|
||||
return ''
|
||||
return self._formdef.get_field_admin_url(field=self)
|
||||
|
||||
@property
|
||||
def include_in_listing(self):
|
||||
return 'listings' in (self.display_locations or [])
|
||||
|
@ -584,16 +595,24 @@ class Field:
|
|||
return changed
|
||||
|
||||
@staticmethod
|
||||
def evaluate_condition(dict_vars, formdef, condition, record_errors=True):
|
||||
def evaluate_condition(
|
||||
dict_vars, formdef, condition, source_label=None, source_url=None, record_errors=True
|
||||
):
|
||||
from .page import PageCondition
|
||||
|
||||
return PageCondition(
|
||||
condition, {'dict_vars': dict_vars, 'formdef': formdef}, record_errors
|
||||
).evaluate()
|
||||
return PageCondition(condition, {'dict_vars': dict_vars, 'formdef': formdef}, record_errors).evaluate(
|
||||
source_label=source_label, source_url=source_url
|
||||
)
|
||||
|
||||
def is_visible(self, dict, formdef):
|
||||
try:
|
||||
return self.evaluate_condition(dict, formdef, self.condition)
|
||||
return self.evaluate_condition(
|
||||
dict,
|
||||
formdef,
|
||||
self.condition,
|
||||
source_label=_('Field: %s') % self.ellipsized_label,
|
||||
source_url=self.get_admin_url(),
|
||||
)
|
||||
except RuntimeError:
|
||||
return True
|
||||
|
||||
|
|
|
@ -342,9 +342,8 @@ class BlockField(WidgetField):
|
|||
|
||||
def __getstate__(self):
|
||||
# do not store _block cache
|
||||
odict = copy.copy(self.__dict__)
|
||||
if '_block' in odict:
|
||||
del odict['_block']
|
||||
odict = super().__getstate__()
|
||||
odict.pop('_block', None)
|
||||
return odict
|
||||
|
||||
def __setstate__(self, ndict):
|
||||
|
|
|
@ -237,7 +237,10 @@ class FileField(WidgetField):
|
|||
|
||||
def from_json_value(self, value):
|
||||
if value and 'filename' in value and 'content' in value:
|
||||
content = base64.b64decode(value['content'])
|
||||
try:
|
||||
content = base64.b64decode(value['content'])
|
||||
except ValueError:
|
||||
return None
|
||||
content_type = value.get('content_type', 'application/octet-stream')
|
||||
if content_type.startswith('text/'):
|
||||
charset = 'utf-8'
|
||||
|
|
|
@ -310,15 +310,7 @@ class FormData(StorableObject):
|
|||
_formdef = None
|
||||
|
||||
def get_formdef(self):
|
||||
if self._formdef:
|
||||
return self._formdef
|
||||
from .formdef import FormDef
|
||||
|
||||
id = self._names.split('-', 1)[1]
|
||||
try:
|
||||
self._formdef = FormDef.get_by_urlname(id)
|
||||
except KeyError:
|
||||
self._formdef = None
|
||||
assert self._formdef
|
||||
return self._formdef
|
||||
|
||||
formdef = property(get_formdef)
|
||||
|
|
|
@ -107,6 +107,7 @@ class FormDefForm(Form):
|
|||
|
||||
def __init__(self):
|
||||
super().__init__(enctype='multipart/form-data', use_tokens=False)
|
||||
self.attrs['data-warn-on-unsaved-content'] = 'true'
|
||||
|
||||
def _render_error_notice_content(self, errors):
|
||||
t = TemplateIO(html=True)
|
||||
|
@ -174,6 +175,14 @@ class FormDef(StorableObject):
|
|||
expiration_date = None
|
||||
has_captcha = False
|
||||
skip_from_360_view = False
|
||||
management_sidebar_items = {
|
||||
'general',
|
||||
'submission-context',
|
||||
'user',
|
||||
'geolocation',
|
||||
'custom-template',
|
||||
'pending-forms',
|
||||
}
|
||||
include_download_all_button = False
|
||||
appearance_keywords = None
|
||||
digest_templates = None
|
||||
|
@ -221,7 +230,6 @@ class FormDef(StorableObject):
|
|||
'enable_tracking_codes',
|
||||
'confirmation',
|
||||
'always_advertise',
|
||||
'include_download_all_button',
|
||||
'has_captcha',
|
||||
'skip_from_360_view',
|
||||
]
|
||||
|
@ -231,6 +239,7 @@ class FormDef(StorableObject):
|
|||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields = []
|
||||
self.management_sidebar_items = {'__default__'}
|
||||
|
||||
def __eq__(self, other):
|
||||
return bool(
|
||||
|
@ -267,6 +276,12 @@ class FormDef(StorableObject):
|
|||
changed = True
|
||||
break
|
||||
|
||||
if self.include_download_all_button: # 2023-12-30
|
||||
self.management_sidebar_items = self.__class__.management_sidebar_items.copy()
|
||||
self.management_sidebar_items.add('download-files')
|
||||
self.include_download_all_button = False
|
||||
changed = True
|
||||
|
||||
for f in self.fields or []:
|
||||
changed |= f.migrate()
|
||||
|
||||
|
@ -287,6 +302,29 @@ class FormDef(StorableObject):
|
|||
sql.clean_global_views(conn, cur)
|
||||
cur.close()
|
||||
|
||||
def get_management_sidebar_available_items(self):
|
||||
return [
|
||||
('general', _('General Information')),
|
||||
('download-files', _('Button to download all files')),
|
||||
('submission-context', _('Submission context')),
|
||||
('user', _('Associated User')),
|
||||
('geolocation', _('Geolocation')),
|
||||
('custom-template', _('Custom template')),
|
||||
('pending-forms', _('User Pending Forms')),
|
||||
]
|
||||
|
||||
def management_sidebar_items_labels(self):
|
||||
# return ordered labels
|
||||
management_sidebar_items = self.get_management_sidebar_items()
|
||||
for key, label in self.get_management_sidebar_available_items():
|
||||
if key in management_sidebar_items:
|
||||
yield label
|
||||
|
||||
def get_management_sidebar_items(self):
|
||||
if self.management_sidebar_items == {'__default__'}:
|
||||
return self.__class__.management_sidebar_items
|
||||
return self.management_sidebar_items or []
|
||||
|
||||
@property
|
||||
def data_class_name(self):
|
||||
return '_wcs_%s' % self.url_name.title()
|
||||
|
@ -1080,6 +1118,9 @@ class FormDef(StorableObject):
|
|||
if self.required_authentication_contexts:
|
||||
root['required_authentication_contexts'] = self.required_authentication_contexts[:]
|
||||
|
||||
if self.management_sidebar_items:
|
||||
root['management_sidebar_items'] = sorted(self.management_sidebar_items)
|
||||
|
||||
if isinstance(self, CardDef):
|
||||
all_carddefs = CardDef.select(ignore_errors=True)
|
||||
all_carddefs = [c for c in all_carddefs if c]
|
||||
|
@ -1247,6 +1288,9 @@ class FormDef(StorableObject):
|
|||
str(x) for x in value.get('required_authentication_contexts')
|
||||
]
|
||||
|
||||
if value.get('management_sidebar_items'):
|
||||
formdef.management_sidebar_items = {str(x) for x in value.get('management_sidebar_items')}
|
||||
|
||||
return formdef
|
||||
|
||||
def export_to_xml(self, include_id=False):
|
||||
|
@ -1396,6 +1440,11 @@ class FormDef(StorableObject):
|
|||
for auth_context in self.required_authentication_contexts:
|
||||
ET.SubElement(element, 'method').text = force_str(auth_context)
|
||||
|
||||
if self.management_sidebar_items:
|
||||
element = ET.SubElement(root, 'management_sidebar_items')
|
||||
for item in sorted(self.management_sidebar_items):
|
||||
ET.SubElement(element, 'item').text = force_str(item)
|
||||
|
||||
if self.digest_templates:
|
||||
digest_templates = ET.SubElement(root, 'digest_templates')
|
||||
for key, value in self.digest_templates.items():
|
||||
|
@ -1416,7 +1465,9 @@ class FormDef(StorableObject):
|
|||
return root
|
||||
|
||||
@classmethod
|
||||
def import_from_xml(cls, fd, include_id=False, fix_on_error=False, check_datasources=True):
|
||||
def import_from_xml(
|
||||
cls, fd, include_id=False, fix_on_error=False, check_datasources=True, check_deprecated=True
|
||||
):
|
||||
try:
|
||||
tree = ET.parse(fd)
|
||||
except Exception:
|
||||
|
@ -1426,6 +1477,7 @@ class FormDef(StorableObject):
|
|||
include_id=include_id,
|
||||
fix_on_error=fix_on_error,
|
||||
check_datasources=check_datasources,
|
||||
check_deprecated=check_deprecated,
|
||||
)
|
||||
|
||||
if formdef.url_name:
|
||||
|
@ -1448,8 +1500,15 @@ class FormDef(StorableObject):
|
|||
|
||||
@classmethod
|
||||
def import_from_xml_tree(
|
||||
cls, tree, include_id=False, fix_on_error=False, snapshot=False, check_datasources=True
|
||||
cls,
|
||||
tree,
|
||||
include_id=False,
|
||||
fix_on_error=False,
|
||||
snapshot=False,
|
||||
check_datasources=True,
|
||||
check_deprecated=True,
|
||||
):
|
||||
from wcs.backoffice.deprecations import DeprecatedElementsDetected, DeprecationsScan
|
||||
from wcs.carddef import CardDef
|
||||
|
||||
formdef = cls()
|
||||
|
@ -1604,6 +1663,12 @@ class FormDef(StorableObject):
|
|||
for child in node:
|
||||
formdef.required_authentication_contexts.append(str(child.text))
|
||||
|
||||
if tree.find('management_sidebar_items') is not None:
|
||||
node = tree.find('management_sidebar_items')
|
||||
formdef.management_sidebar_items = set()
|
||||
for child in node:
|
||||
formdef.management_sidebar_items.add(str(child.text))
|
||||
|
||||
if tree.find('digest_templates') is not None:
|
||||
digest_templates_node = tree.find('digest_templates')
|
||||
formdef.digest_templates = {}
|
||||
|
@ -1659,6 +1724,14 @@ class FormDef(StorableObject):
|
|||
details[_('Unknown datasources')].update(unknown_datasources)
|
||||
raise FormdefImportUnknownReferencedError(_('Unknown referenced objects'), details=details)
|
||||
|
||||
if check_deprecated:
|
||||
# check for deprecated elements
|
||||
job = DeprecationsScan()
|
||||
try:
|
||||
job.check_deprecated_elements_in_object(formdef)
|
||||
except DeprecatedElementsDetected as e:
|
||||
raise FormdefImportError(str(e))
|
||||
|
||||
return formdef
|
||||
|
||||
def finish_tests_xml_import(self):
|
||||
|
@ -1956,6 +2029,8 @@ class FormDef(StorableObject):
|
|||
o.fields = pickle.load(fd, **PICKLE_KWARGS)
|
||||
except EOFError:
|
||||
pass # old format
|
||||
for field in o.fields or []:
|
||||
field._formdef = o # keep formdef reference
|
||||
return o
|
||||
|
||||
@classmethod
|
||||
|
|
|
@ -77,6 +77,10 @@ class FileDirectory(Directory):
|
|||
if component and component not in (file.base_filename, urllib.parse.quote(file.base_filename)):
|
||||
raise errors.TraversalError()
|
||||
|
||||
if not hasattr(file, 'has_redirect_url'):
|
||||
# not an appropriate file object
|
||||
raise errors.TraversalError()
|
||||
|
||||
if file.has_redirect_url():
|
||||
redirect_url = file.get_redirect_url(backoffice=get_request().is_in_backoffice())
|
||||
if not redirect_url:
|
||||
|
|
|
@ -422,6 +422,7 @@ class FormPage(Directory, TempfileDirectoryMixin, FormTemplateMixin):
|
|||
'current_page_index': current_position,
|
||||
'current_page_no': current_position, # legacy, for themes
|
||||
'page_labels': page_labels,
|
||||
'pages': self.pages,
|
||||
}
|
||||
|
||||
def step(self):
|
||||
|
@ -945,9 +946,13 @@ class FormPage(Directory, TempfileDirectoryMixin, FormTemplateMixin):
|
|||
# create a fake FormData with current submission data
|
||||
formdata.user = get_request().user
|
||||
formdata._formdef = self.formdef
|
||||
if draft_formdata and draft_formdata.submission_context:
|
||||
# restore submission context, this is required to get access to form_parent_* variables
|
||||
formdata.submission_context = draft_formdata.submission_context
|
||||
if draft_formdata:
|
||||
if draft_formdata.submission_context:
|
||||
# restore submission context, this is required to get access to form_parent_* variables
|
||||
formdata.submission_context = draft_formdata.submission_context
|
||||
if draft_formdata.workflow_data:
|
||||
# restore workflow_data, this is used for partial edit
|
||||
formdata.workflow_data = draft_formdata.workflow_data
|
||||
formdata.data = session_data
|
||||
formdata.prefilling_data = formdata.data.get('prefilling_data', {})
|
||||
computed_values = get_session().get_by_magictoken('%s-computed' % magictoken) or {}
|
||||
|
@ -1013,6 +1018,7 @@ class FormPage(Directory, TempfileDirectoryMixin, FormTemplateMixin):
|
|||
# if there's a form with a single page (at all, not as the result of conditions),
|
||||
# and no confirmation page, add native quixote CSRF protection.
|
||||
form.add(FormTokenWidget, form.TOKEN_NAME)
|
||||
form.add_hidden('previous-page-id', '')
|
||||
form.attrs['data-live-url'] = self.formdef.get_url(language=get_publisher().current_language) + 'live'
|
||||
form.attrs['data-live-validation-url'] = (
|
||||
self.formdef.get_url(language=get_publisher().current_language) + 'live-validation'
|
||||
|
@ -1027,7 +1033,9 @@ class FormPage(Directory, TempfileDirectoryMixin, FormTemplateMixin):
|
|||
return form
|
||||
|
||||
def create_view_form(self, *args, **kwargs):
|
||||
return self.formdef.create_view_form(*args, **kwargs)
|
||||
form = self.formdef.create_view_form(*args, **kwargs)
|
||||
form.add_hidden('previous-page-id', '')
|
||||
return form
|
||||
|
||||
def check_authentication_context(self):
|
||||
if not self.formdef.required_authentication_contexts:
|
||||
|
@ -1070,7 +1078,8 @@ class FormPage(Directory, TempfileDirectoryMixin, FormTemplateMixin):
|
|||
def pages(self):
|
||||
if self._pages:
|
||||
return self._pages
|
||||
current_data = self.get_transient_formdata().data
|
||||
transient_formdata = self.get_transient_formdata()
|
||||
current_data = transient_formdata.data
|
||||
|
||||
pages = [x for x in self.formdef.fields if x.key == 'page']
|
||||
has_page_fields = bool(pages)
|
||||
|
@ -1082,12 +1091,27 @@ class FormPage(Directory, TempfileDirectoryMixin, FormTemplateMixin):
|
|||
# submitted data) (#27247).
|
||||
hidden_pages = [x for x in pages if not x.is_visible(current_data, self.formdef)]
|
||||
|
||||
if self.edit_mode and self.edit_action and self.edit_action.operation_mode in ('single', 'partial'):
|
||||
if self.edit_mode and self.edit_action:
|
||||
operation_mode = self.edit_action.operation_mode
|
||||
page_identifier = self.edit_action.page_identifier
|
||||
elif (
|
||||
not self.edit_mode
|
||||
and transient_formdata.workflow_data
|
||||
and '_create_formdata_draft_edit' in transient_formdata.workflow_data
|
||||
):
|
||||
operation_mode = transient_formdata.workflow_data['_create_formdata_draft_edit']['operation_mode']
|
||||
page_identifier = transient_formdata.workflow_data['_create_formdata_draft_edit'][
|
||||
'page_identifier'
|
||||
]
|
||||
else:
|
||||
operation_mode = 'full'
|
||||
|
||||
if operation_mode in ('single', 'partial'):
|
||||
edit_pages = []
|
||||
for page in pages:
|
||||
if self.edit_action.page_identifier == page.varname or edit_pages:
|
||||
if page_identifier == page.varname or edit_pages:
|
||||
edit_pages.append(page)
|
||||
if self.edit_action.operation_mode == 'single':
|
||||
if operation_mode == 'single':
|
||||
break
|
||||
edit_pages = [x for x in edit_pages if x not in hidden_pages]
|
||||
if not edit_pages:
|
||||
|
@ -1374,7 +1398,7 @@ class FormPage(Directory, TempfileDirectoryMixin, FormTemplateMixin):
|
|||
if errored:
|
||||
form.add(HiddenErrorWidget, 'post_condition%d' % i)
|
||||
form.set_error('post_condition%d' % i, 'error')
|
||||
page_error_messages.append(error_message)
|
||||
page_error_messages.append(get_publisher().translate(error_message))
|
||||
|
||||
honeypot_error = False
|
||||
if get_request().form.get('f00'): # 🍯
|
||||
|
@ -1595,8 +1619,14 @@ class FormPage(Directory, TempfileDirectoryMixin, FormTemplateMixin):
|
|||
|
||||
def previous_page(self, page_no, magictoken):
|
||||
try:
|
||||
new_page_no = page_no - 1
|
||||
previous_page = self.pages[new_page_no]
|
||||
previous_page_id = get_request().form.get('previous-page-id')
|
||||
if previous_page_id:
|
||||
new_page_no, previous_page = [
|
||||
x for x in enumerate(self.pages[:page_no]) if x[1].id == previous_page_id
|
||||
][0]
|
||||
else:
|
||||
new_page_no = page_no - 1
|
||||
previous_page = self.pages[new_page_no]
|
||||
except IndexError:
|
||||
new_page_no = 0
|
||||
previous_page = self.pages[0]
|
||||
|
@ -2470,14 +2500,42 @@ TextsDirectory.register(
|
|||
'form-recorded',
|
||||
_('Message when a form has been recorded'),
|
||||
category=_('Forms'),
|
||||
default=_('The form has been recorded on {{ form_receipt_datetime }} with the number {{ form_number }}.'),
|
||||
default=_(
|
||||
'''
|
||||
The form has been recorded on {{ form_receipt_datetime }} with the number {{ form_number }}.
|
||||
{% if form_submission_agent_display_name %}
|
||||
It has been submitted for you by {{ form_submission_agent_display_name }}
|
||||
{% if form_submission_channel == "phone" %}after a phone call.
|
||||
{% elif form_submission_channel == "email" %}after an email.
|
||||
{% elif form_submission_channel == "mail" %}after a mail.
|
||||
{% elif form_submission_channel == "social-network" %}after a message on a social network.
|
||||
{% elif form_submission_channel == "counter" %}after your passage at the counter.
|
||||
{% else %}.
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
'''
|
||||
),
|
||||
)
|
||||
|
||||
TextsDirectory.register(
|
||||
'form-recorded-allow-one',
|
||||
_('Message when a form has been recorded, and the form is set to only allow one per user'),
|
||||
category=_('Forms'),
|
||||
default=_('The form has been recorded on {{ form_receipt_datetime }}.'),
|
||||
default=_(
|
||||
'''
|
||||
The form has been recorded on {{ form_receipt_datetime }}.
|
||||
{% if form_submission_agent_display_name %}
|
||||
It has been submitted for you by {{ form_submission_agent_display_name }}
|
||||
{% if form_submission_channel == "phone" %}after a phone call.
|
||||
{% elif form_submission_channel == "email" %}after an email.
|
||||
{% elif form_submission_channel == "mail" %}after a mail.
|
||||
{% elif form_submission_channel == "social-network" %}after a message on a social network.
|
||||
{% elif form_submission_channel == "counter" %}after your passage at the counter.
|
||||
{% else %}.
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
'''
|
||||
),
|
||||
)
|
||||
|
||||
TextsDirectory.register(
|
||||
|
|
|
@ -4,8 +4,8 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: wcs 0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-03-12 09:34+0100\n"
|
||||
"PO-Revision-Date: 2024-03-12 09:34+0100\n"
|
||||
"POT-Creation-Date: 2024-03-18 14:22+0100\n"
|
||||
"PO-Revision-Date: 2024-03-18 14:22+0100\n"
|
||||
"Last-Translator: Thomas Noël <tnoel@entrouvert.com>\n"
|
||||
"Language-Team: french\n"
|
||||
"Language: fr\n"
|
||||
|
@ -174,6 +174,21 @@ msgstr "Ce bloc de champs contient plus de %d champs."
|
|||
msgid "Applications"
|
||||
msgstr "Applications"
|
||||
|
||||
#: admin/blocks.py admin/comment_templates.py admin/data_sources.py
|
||||
#: admin/mail_templates.py admin/settings.py admin/wscalls.py
|
||||
#: backoffice/i18n.py backoffice/management.py
|
||||
#: templates/wcs/backoffice/category.html templates/wcs/backoffice/formdef.html
|
||||
#: templates/wcs/backoffice/i18n.html templates/wcs/backoffice/snapshots.html
|
||||
#: templates/wcs/backoffice/test_sidebar.html
|
||||
#: templates/wcs/backoffice/workflow.html
|
||||
msgid "Export"
|
||||
msgstr "Exporter"
|
||||
|
||||
#: admin/blocks.py admin/settings.py backoffice/root.py
|
||||
#: templates/wcs/backoffice/settings/import.html
|
||||
msgid "Settings"
|
||||
msgstr "Paramètres"
|
||||
|
||||
#: admin/blocks.py admin/fields.py
|
||||
msgid "There are not yet any fields defined."
|
||||
msgstr "Il n’y a pas encore de champs configurés."
|
||||
|
@ -183,6 +198,23 @@ msgstr "Il n’y a pas encore de champs configurés."
|
|||
msgid "Usage"
|
||||
msgstr "Utilisation"
|
||||
|
||||
#: admin/blocks.py templates/wcs/backoffice/blocks.html
|
||||
#: templates/wcs/backoffice/cards.html templates/wcs/backoffice/categories.html
|
||||
#: templates/wcs/backoffice/comment-templates.html
|
||||
#: templates/wcs/backoffice/data-sources.html
|
||||
#: templates/wcs/backoffice/formdef.html templates/wcs/backoffice/forms.html
|
||||
#: templates/wcs/backoffice/mail-templates.html
|
||||
#: templates/wcs/backoffice/snapshots.html
|
||||
#: templates/wcs/backoffice/test-webservice-responses.html
|
||||
#: templates/wcs/backoffice/test_sidebar.html
|
||||
#: templates/wcs/backoffice/tests.html
|
||||
#: templates/wcs/backoffice/workflow-global-action.html
|
||||
#: templates/wcs/backoffice/workflow.html
|
||||
#: templates/wcs/backoffice/workflows.html
|
||||
#: templates/wcs/backoffice/wscalls.html
|
||||
msgid "Actions"
|
||||
msgstr "Actions"
|
||||
|
||||
#: admin/blocks.py admin/comment_templates.py admin/data_sources.py
|
||||
#: admin/fields.py admin/forms.py admin/mail_templates.py admin/tests.py
|
||||
#: admin/workflows.py qommon/admin/menu.py
|
||||
|
@ -194,22 +226,28 @@ msgstr "Utilisation"
|
|||
msgid "Duplicate"
|
||||
msgstr "Dupliquer"
|
||||
|
||||
#: admin/blocks.py admin/comment_templates.py admin/data_sources.py
|
||||
#: admin/mail_templates.py admin/settings.py admin/wscalls.py
|
||||
#: backoffice/i18n.py backoffice/management.py
|
||||
#: templates/wcs/backoffice/category.html templates/wcs/backoffice/formdef.html
|
||||
#: templates/wcs/backoffice/i18n.html templates/wcs/backoffice/snapshots.html
|
||||
#: templates/wcs/backoffice/test_sidebar.html
|
||||
#: templates/wcs/backoffice/workflow.html
|
||||
msgid "Export"
|
||||
msgstr "Exporter"
|
||||
|
||||
#: admin/blocks.py admin/comment_templates.py admin/data_sources.py
|
||||
#: admin/mail_templates.py admin/wscalls.py backoffice/snapshots.py
|
||||
#: templates/wcs/backoffice/formdef.html templates/wcs/backoffice/workflow.html
|
||||
msgid "Save snapshot"
|
||||
msgstr "Enregistrer une sauvegarde"
|
||||
|
||||
#: admin/blocks.py admin/forms.py
|
||||
msgid "Overwrite"
|
||||
msgstr "Écraser"
|
||||
|
||||
#: admin/blocks.py templates/wcs/backoffice/blocks.html
|
||||
#: templates/wcs/backoffice/cards.html templates/wcs/backoffice/category.html
|
||||
#: templates/wcs/backoffice/comment-templates.html
|
||||
#: templates/wcs/backoffice/data-sources.html
|
||||
#: templates/wcs/backoffice/formdef.html templates/wcs/backoffice/forms.html
|
||||
#: templates/wcs/backoffice/mail-templates.html
|
||||
#: templates/wcs/backoffice/test_sidebar.html
|
||||
#: templates/wcs/backoffice/tests.html templates/wcs/backoffice/workflow.html
|
||||
#: templates/wcs/backoffice/workflows.html
|
||||
msgid "Navigation"
|
||||
msgstr "Navigation"
|
||||
|
||||
#: admin/blocks.py admin/comment_templates.py admin/data_sources.py
|
||||
#: admin/mail_templates.py admin/wscalls.py backoffice/snapshots.py
|
||||
#: templates/wcs/backoffice/category.html templates/wcs/backoffice/formdef.html
|
||||
|
@ -223,11 +261,6 @@ msgstr "Historique"
|
|||
msgid "Inspector"
|
||||
msgstr "Inspecteur"
|
||||
|
||||
#: admin/blocks.py admin/settings.py backoffice/root.py
|
||||
#: templates/wcs/backoffice/settings/import.html
|
||||
msgid "Settings"
|
||||
msgstr "Paramètres"
|
||||
|
||||
#: admin/blocks.py
|
||||
msgid "You are about to irrevocably delete this block."
|
||||
msgstr "Vous allez définitivement supprimer ce bloc."
|
||||
|
@ -259,6 +292,23 @@ msgstr "%(name)s (Copie %(no)d)"
|
|||
msgid "Duplicate Fields Block"
|
||||
msgstr "Dupliquer le bloc de champs"
|
||||
|
||||
#: admin/blocks.py
|
||||
msgid "Field data will be lost if overwriting with an incompatible block."
|
||||
msgstr ""
|
||||
"Les données des champs seront perdues si le bloc est écrasé par un bloc "
|
||||
"incompatible."
|
||||
|
||||
#: admin/blocks.py admin/categories.py admin/comment_templates.py
|
||||
#: admin/data_sources.py admin/forms.py admin/mail_templates.py
|
||||
#: admin/settings.py admin/tests.py admin/workflows.py admin/wscalls.py
|
||||
#: backoffice/data_management.py backoffice/i18n.py wf/export_to_model.py
|
||||
msgid "File"
|
||||
msgstr "Fichier"
|
||||
|
||||
#: admin/blocks.py admin/forms.py
|
||||
msgid "Overwritten"
|
||||
msgstr "Écrasement"
|
||||
|
||||
#: admin/blocks.py
|
||||
msgid "The identifier can not be modified as the block is in use."
|
||||
msgstr "L’identifiant ne peut pas être modifié car le bloc est utilisé."
|
||||
|
@ -310,13 +360,6 @@ msgstr "Ajouter"
|
|||
msgid "New Fields Block"
|
||||
msgstr "Nouveau bloc de champs"
|
||||
|
||||
#: admin/blocks.py admin/categories.py admin/comment_templates.py
|
||||
#: admin/data_sources.py admin/forms.py admin/mail_templates.py
|
||||
#: admin/settings.py admin/tests.py admin/workflows.py admin/wscalls.py
|
||||
#: backoffice/data_management.py backoffice/i18n.py wf/export_to_model.py
|
||||
msgid "File"
|
||||
msgstr "Fichier"
|
||||
|
||||
#: admin/blocks.py
|
||||
msgid "Import Fields Block"
|
||||
msgstr "Importer un bloc de champs"
|
||||
|
@ -340,7 +383,8 @@ msgid "You can install a new fields block by uploading a file."
|
|||
msgstr ""
|
||||
"Vous pouvez installer un nouveau bloc de champs en téléchargeant un fichier."
|
||||
|
||||
#: admin/blocks.py admin/forms.py admin/workflows.py
|
||||
#: admin/blocks.py admin/data_sources.py admin/forms.py admin/workflows.py
|
||||
#: admin/wscalls.py
|
||||
#, python-format
|
||||
msgid "Invalid File (%s)"
|
||||
msgstr "Fichier invalide (%s)"
|
||||
|
@ -973,7 +1017,7 @@ msgstr ""
|
|||
msgid "Previous page"
|
||||
msgstr "Page précédente"
|
||||
|
||||
#: admin/fields.py wf/editable.py
|
||||
#: admin/fields.py wf/create_formdata.py wf/editable.py
|
||||
msgid "All pages"
|
||||
msgstr "Toutes les pages"
|
||||
|
||||
|
@ -1105,7 +1149,7 @@ msgstr "Inclure une page de confirmation"
|
|||
msgid "Confirmation Page"
|
||||
msgstr "Page de confirmation"
|
||||
|
||||
#: admin/forms.py api.py formdata.py
|
||||
#: admin/forms.py api.py formdata.py wf/create_formdata.py
|
||||
msgid "Draft"
|
||||
msgstr "Brouillon"
|
||||
|
||||
|
@ -1157,9 +1201,9 @@ msgstr "Commencer par un CAPTCHA pour les utilisateurs anonymes"
|
|||
msgid "CAPTCHA"
|
||||
msgstr "CAPTCHA"
|
||||
|
||||
#: admin/forms.py templates/wcs/backoffice/formdef-inspect.html
|
||||
msgid "Include button to download all files"
|
||||
msgstr "Inclure un bouton pour télécharger tous les fichiers"
|
||||
#: admin/forms.py
|
||||
msgid "Sidebar elements"
|
||||
msgstr "Contenu de la barre latérale"
|
||||
|
||||
#: admin/forms.py templates/wcs/backoffice/formdef-inspect.html
|
||||
msgid "Skip from per user view"
|
||||
|
@ -1597,10 +1641,6 @@ msgstr "Attention les demandes seront supprimées."
|
|||
msgid "Address"
|
||||
msgstr "Adresse"
|
||||
|
||||
#: admin/forms.py
|
||||
msgid "Overwrite"
|
||||
msgstr "Écraser"
|
||||
|
||||
#: admin/forms.py admin/workflows.py
|
||||
#, python-format
|
||||
msgid "Error loading form (%s)."
|
||||
|
@ -1614,10 +1654,6 @@ msgstr "Vous devez entrer un fichier ou une URL."
|
|||
msgid "Overwritten (removal of incompatible fields)"
|
||||
msgstr "Écrasement (avec suppression des champs incompatibles)"
|
||||
|
||||
#: admin/forms.py
|
||||
msgid "Overwritten"
|
||||
msgstr "Écrasement"
|
||||
|
||||
#: admin/forms.py
|
||||
msgid "Summary of changes"
|
||||
msgstr "Résumé des modifications"
|
||||
|
@ -1724,6 +1760,14 @@ msgstr "Ré-indexation des données pour le nouveau workflow"
|
|||
msgid "Back"
|
||||
msgstr "Retour"
|
||||
|
||||
#: admin/logged_errors.py
|
||||
msgid "Condition"
|
||||
msgstr "Condition"
|
||||
|
||||
#: admin/logged_errors.py
|
||||
msgid "Condition type"
|
||||
msgstr "Type de condition"
|
||||
|
||||
#: admin/logged_errors.py
|
||||
#, python-format
|
||||
msgid "Logged Errors - %s"
|
||||
|
@ -1842,6 +1886,7 @@ msgid "Subject"
|
|||
msgstr "Sujet"
|
||||
|
||||
#: admin/mail_templates.py wf/notification.py wf/sendmail.py wf/sms.py
|
||||
#: workflow_tests.py
|
||||
msgid "Body"
|
||||
msgstr "Corps"
|
||||
|
||||
|
@ -2504,11 +2549,11 @@ msgstr "Erreur : ce n’est pas un fichier valide"
|
|||
|
||||
#: admin/settings.py
|
||||
#, python-format
|
||||
msgid "Failed to import a workflow (%s); site import did not complete."
|
||||
msgid "Failed to import objects (%s); site import did not complete."
|
||||
msgstr ""
|
||||
"Erreur à l’import d’un workflow (%s). L’import du site n’a pas pu terminer."
|
||||
"Erreur à l’import d’éléments (%s). L’import du site n’a pas pu terminer."
|
||||
|
||||
#: admin/settings.py
|
||||
#: admin/settings.py api_export_import.py
|
||||
#, python-format
|
||||
msgid "Error: %s"
|
||||
msgstr "Erreur : %s"
|
||||
|
@ -2826,7 +2871,7 @@ msgstr "Modifier l’action"
|
|||
msgid "Deleting action:"
|
||||
msgstr "Suppression de l’action :"
|
||||
|
||||
#: admin/workflow_tests.py
|
||||
#: admin/workflow_tests.py workflow_tests.py
|
||||
msgid "Backoffice user"
|
||||
msgstr "Utilisateur agent"
|
||||
|
||||
|
@ -3645,6 +3690,27 @@ msgstr "Catégories (sources de données)"
|
|||
msgid "Category (data Sources)"
|
||||
msgstr "Catégorie (sources de données)"
|
||||
|
||||
#: api_export_import.py
|
||||
msgid "Invalid tar file, missing manifest"
|
||||
msgstr "Fichier tar invalide, manifeste manquant"
|
||||
|
||||
#: api_export_import.py
|
||||
#, python-format
|
||||
msgid "Invalid tar file, missing component %s/%s"
|
||||
msgstr "Fichier tar invalide, composant %s/%s manquant"
|
||||
|
||||
#: api_export_import.py
|
||||
msgid "Invalid tar file"
|
||||
msgstr "Fichier tar invalide"
|
||||
|
||||
#: api_export_import.py
|
||||
msgid "Invalid tar file, missing manifest."
|
||||
msgstr "Fichier tar invalide, manifeste manquant."
|
||||
|
||||
#: api_export_import.py
|
||||
msgid "Invalid tar file."
|
||||
msgstr "Fichier tar invalide."
|
||||
|
||||
#: api_export_import.py
|
||||
#, python-format
|
||||
msgid "Application (%s) initial installation"
|
||||
|
@ -4085,6 +4151,10 @@ msgstr "Source de données"
|
|||
msgid "Webservice"
|
||||
msgstr "Webservice"
|
||||
|
||||
#: backoffice/deprecations.py
|
||||
msgid "Python expression detected"
|
||||
msgstr "Expression Python détectée"
|
||||
|
||||
#: backoffice/i18n.py backoffice/root.py templates/wcs/backoffice/i18n.html
|
||||
msgid "Multilinguism"
|
||||
msgstr "Multilinguisme"
|
||||
|
@ -4382,6 +4452,10 @@ msgstr "Fonction de l’utilisateur connecté"
|
|||
msgid "Submission Agent"
|
||||
msgstr "Agent à la saisie"
|
||||
|
||||
#: backoffice/management.py
|
||||
msgid "Criticality Level"
|
||||
msgstr "Niveau de criticité"
|
||||
|
||||
#: backoffice/management.py
|
||||
msgid "Current view"
|
||||
msgstr "Vue actuelle"
|
||||
|
@ -4414,6 +4488,11 @@ msgstr "Statuts à afficher"
|
|||
msgid "Current user"
|
||||
msgstr "Utilisateur connecté"
|
||||
|
||||
#: backoffice/management.py
|
||||
msgctxt "criticality-level"
|
||||
msgid "All"
|
||||
msgstr "Tous"
|
||||
|
||||
#: backoffice/management.py fields/base.py fields/bool.py formdata.py
|
||||
#: statistics/views.py templates/wcs/backoffice/data-source.html workflows.py
|
||||
msgid "No"
|
||||
|
@ -4651,7 +4730,7 @@ msgstr ""
|
|||
"Vous avez accédé à ce formulaire via son code de suivi, vous le voyez donc "
|
||||
"aussi comme l’usager."
|
||||
|
||||
#: backoffice/management.py
|
||||
#: backoffice/management.py formdef.py
|
||||
msgid "General Information"
|
||||
msgstr "Informations générales"
|
||||
|
||||
|
@ -4700,11 +4779,11 @@ msgstr "Ouvrir"
|
|||
msgid "Comments"
|
||||
msgstr "Commentaire"
|
||||
|
||||
#: backoffice/management.py
|
||||
#: backoffice/management.py formdef.py
|
||||
msgid "Associated User"
|
||||
msgstr "Usager associé"
|
||||
|
||||
#: backoffice/management.py
|
||||
#: backoffice/management.py formdef.py
|
||||
msgid "User Pending Forms"
|
||||
msgstr "Formulaires de cet usager en attente"
|
||||
|
||||
|
@ -5118,6 +5197,10 @@ msgstr "Web"
|
|||
msgid "File Import"
|
||||
msgstr "Importation de fichier"
|
||||
|
||||
#: carddata.py
|
||||
msgid "Updating relations"
|
||||
msgstr "Mise à jour des relations"
|
||||
|
||||
#: carddef.py
|
||||
#, python-format
|
||||
msgid "No such card model: %s"
|
||||
|
@ -5354,6 +5437,11 @@ msgstr "Tableaux de traitement"
|
|||
msgid "Failed to evaluate prefill on field \"%s\""
|
||||
msgstr "Erreur à l’évaluation du préremplissage du champ « %s »"
|
||||
|
||||
#: fields/base.py
|
||||
#, python-format
|
||||
msgid "Field: %s"
|
||||
msgstr "Champ : %s"
|
||||
|
||||
#: fields/base.py fields/item.py fields/items.py
|
||||
#, python-format
|
||||
msgid "datasource is unavailable (field id: %s)"
|
||||
|
@ -6184,6 +6272,18 @@ msgctxt "item"
|
|||
msgid "forms"
|
||||
msgstr "demandes"
|
||||
|
||||
#: formdef.py
|
||||
msgid "Button to download all files"
|
||||
msgstr "Bouton pour télécharger tous les fichiers"
|
||||
|
||||
#: formdef.py
|
||||
msgid "Submission context"
|
||||
msgstr "Informations sur la saisie"
|
||||
|
||||
#: formdef.py
|
||||
msgid "Custom template"
|
||||
msgstr "Gabarit personnalisé"
|
||||
|
||||
#: formdef.py
|
||||
#, python-format
|
||||
msgid "Could not render submission lateral template (%s)"
|
||||
|
@ -6684,11 +6784,39 @@ msgstr "Message quand un formulaire a été enregistré"
|
|||
|
||||
#: forms/root.py
|
||||
msgid ""
|
||||
"\n"
|
||||
"The form has been recorded on {{ form_receipt_datetime }} with the number "
|
||||
"{{ form_number }}."
|
||||
"{{ form_number }}.\n"
|
||||
"{% if form_submission_agent_display_name %}\n"
|
||||
"It has been submitted for you by {{ form_submission_agent_display_name }}\n"
|
||||
"{% if form_submission_channel == \"phone\" %}after a phone call.\n"
|
||||
"{% elif form_submission_channel == \"email\" %}after an email.\n"
|
||||
"{% elif form_submission_channel == \"mail\" %}after a mail.\n"
|
||||
"{% elif form_submission_channel == \"social-network\" %}after a message on a "
|
||||
"social network.\n"
|
||||
"{% elif form_submission_channel == \"counter\" %}after your passage at the "
|
||||
"counter.\n"
|
||||
"{% else %}.\n"
|
||||
"{% endif %}\n"
|
||||
"{% endif %}\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
"Le formulaire a été enregistré le {{ form_receipt_datetime }} avec le numéro "
|
||||
"{{ form_number }}."
|
||||
"{{ form_number }}.\n"
|
||||
"{% if form_submission_agent_display_name %}\n"
|
||||
"Il a été saisie pour vous par {{ form_submission_agent_display_name }}\n"
|
||||
"{% if form_submission_channel == \"phone\" %}suite à un appel téléphonique.\n"
|
||||
"{% elif form_submission_channel == \"email\" %}suite à un courriel.\n"
|
||||
"{% elif form_submission_channel == \"mail\" %}suite à un courrier.\n"
|
||||
"{% elif form_submission_channel == \"social-network\" %}suite à un message "
|
||||
"sur un réseau social.\n"
|
||||
"{% elif form_submission_channel == \"counter\" %}suite à votre passage au "
|
||||
"guichet.\n"
|
||||
"{% else %}.\n"
|
||||
"{% endif %}\n"
|
||||
"{% endif %}\n"
|
||||
" "
|
||||
|
||||
#: forms/root.py
|
||||
msgid ""
|
||||
|
@ -6699,8 +6827,38 @@ msgstr ""
|
|||
"pour n’autoriser qu’un seul exemplaire par utilisateur"
|
||||
|
||||
#: forms/root.py
|
||||
msgid "The form has been recorded on {{ form_receipt_datetime }}."
|
||||
msgstr "Le formulaire a été enregistré le {{ form_receipt_datetime }}."
|
||||
msgid ""
|
||||
"\n"
|
||||
"The form has been recorded on {{ form_receipt_datetime }}.\n"
|
||||
"{% if form_submission_agent_display_name %}\n"
|
||||
"It has been submitted for you by {{ form_submission_agent_display_name }}\n"
|
||||
"{% if form_submission_channel == \"phone\" %}after a phone call.\n"
|
||||
"{% elif form_submission_channel == \"email\" %}after an email.\n"
|
||||
"{% elif form_submission_channel == \"mail\" %}after a mail.\n"
|
||||
"{% elif form_submission_channel == \"social-network\" %}after a message on a "
|
||||
"social network.\n"
|
||||
"{% elif form_submission_channel == \"counter\" %}after your passage at the "
|
||||
"counter.\n"
|
||||
"{% else %}.\n"
|
||||
"{% endif %}\n"
|
||||
"{% endif %}\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
"Le formulaire a été enregistré le {{ form_receipt_datetime }}.\n"
|
||||
"{% if form_submission_agent_display_name %}\n"
|
||||
"Il a été saisie pour vous par {{ form_submission_agent_display_name }}\n"
|
||||
"{% if form_submission_channel == \"phone\" %}suite à un appel téléphonique.\n"
|
||||
"{% elif form_submission_channel == \"email\" %}suite à un courriel.\n"
|
||||
"{% elif form_submission_channel == \"mail\" %}suite à un courrier.\n"
|
||||
"{% elif form_submission_channel == \"social-network\" %}suite à un message "
|
||||
"sur un réseau social.\n"
|
||||
"{% elif form_submission_channel == \"counter\" %}suite à votre passage au "
|
||||
"guichet.\n"
|
||||
"{% else %}.\n"
|
||||
"{% endif %}\n"
|
||||
"{% endif %}\n"
|
||||
" "
|
||||
|
||||
#: forms/root.py
|
||||
msgid "Message when a form is displayed before validation"
|
||||
|
@ -9295,39 +9453,10 @@ msgstr[1] "%(fields_count)s champs"
|
|||
msgid "Field Blocks"
|
||||
msgstr "Blocs de champs"
|
||||
|
||||
#: templates/wcs/backoffice/blocks.html templates/wcs/backoffice/cards.html
|
||||
#: templates/wcs/backoffice/categories.html
|
||||
#: templates/wcs/backoffice/comment-templates.html
|
||||
#: templates/wcs/backoffice/data-sources.html
|
||||
#: templates/wcs/backoffice/formdef.html templates/wcs/backoffice/forms.html
|
||||
#: templates/wcs/backoffice/mail-templates.html
|
||||
#: templates/wcs/backoffice/snapshots.html
|
||||
#: templates/wcs/backoffice/test-webservice-responses.html
|
||||
#: templates/wcs/backoffice/test_sidebar.html
|
||||
#: templates/wcs/backoffice/tests.html
|
||||
#: templates/wcs/backoffice/workflow-global-action.html
|
||||
#: templates/wcs/backoffice/workflow.html
|
||||
#: templates/wcs/backoffice/workflows.html
|
||||
#: templates/wcs/backoffice/wscalls.html
|
||||
msgid "Actions"
|
||||
msgstr "Actions"
|
||||
|
||||
#: templates/wcs/backoffice/blocks.html
|
||||
msgid "New field block"
|
||||
msgstr "Nouveau bloc de champs"
|
||||
|
||||
#: templates/wcs/backoffice/blocks.html templates/wcs/backoffice/cards.html
|
||||
#: templates/wcs/backoffice/category.html
|
||||
#: templates/wcs/backoffice/comment-templates.html
|
||||
#: templates/wcs/backoffice/data-sources.html
|
||||
#: templates/wcs/backoffice/formdef.html templates/wcs/backoffice/forms.html
|
||||
#: templates/wcs/backoffice/mail-templates.html
|
||||
#: templates/wcs/backoffice/test_sidebar.html
|
||||
#: templates/wcs/backoffice/tests.html templates/wcs/backoffice/workflow.html
|
||||
#: templates/wcs/backoffice/workflows.html
|
||||
msgid "Navigation"
|
||||
msgstr "Navigation"
|
||||
|
||||
#: templates/wcs/backoffice/card-data-import-form.html
|
||||
msgid "You can add data to this card by uploading a JSON file."
|
||||
msgstr ""
|
||||
|
@ -9592,12 +9721,12 @@ msgid "Display to unlogged users"
|
|||
msgstr "Afficher aux utilisateurs non-connectés"
|
||||
|
||||
#: templates/wcs/backoffice/formdef-inspect.html
|
||||
msgid "Tracking codes"
|
||||
msgstr "Codes de suivi"
|
||||
msgid "Management sidebar elements"
|
||||
msgstr "Contenu de la barre latéral pour le traitement"
|
||||
|
||||
#: templates/wcs/backoffice/formdef-inspect.html
|
||||
msgid "default value"
|
||||
msgstr "valeur par défaut"
|
||||
msgid "Tracking codes"
|
||||
msgstr "Codes de suivi"
|
||||
|
||||
#: templates/wcs/backoffice/formdef-inspect.html
|
||||
msgid "Redirection when disabled"
|
||||
|
@ -9723,6 +9852,21 @@ msgstr "non trouvé"
|
|||
msgid "There are no agendas."
|
||||
msgstr "Il n’y a pas d’agendas."
|
||||
|
||||
#: templates/wcs/backoffice/includes/forms.html
|
||||
#, python-format
|
||||
msgid "Published from %(date1)s until %(date2)s"
|
||||
msgstr "Publié du %(date1)s au %(date2)s"
|
||||
|
||||
#: templates/wcs/backoffice/includes/forms.html
|
||||
#, python-format
|
||||
msgid "Published from %(date1)s"
|
||||
msgstr "Publié à partir du %(date1)s"
|
||||
|
||||
#: templates/wcs/backoffice/includes/forms.html
|
||||
#, python-format
|
||||
msgid "Published until %(date2)s"
|
||||
msgstr "Publié jusqu’au %(date2)s"
|
||||
|
||||
#: templates/wcs/backoffice/includes/mail-templates.html
|
||||
msgid "There are no mail templates defined."
|
||||
msgstr "Il n’y a pas de modèle de courriel défini."
|
||||
|
@ -10049,6 +10193,10 @@ msgstr "Vous pouvez créer la réponse webservice correspondante ici."
|
|||
msgid "Result"
|
||||
msgstr "Résultat"
|
||||
|
||||
#: templates/wcs/backoffice/test-result.html
|
||||
msgid "Run tests again"
|
||||
msgstr "Relancer les tests"
|
||||
|
||||
#: templates/wcs/backoffice/test-result.html
|
||||
msgid "Details"
|
||||
msgstr "Détails"
|
||||
|
@ -10423,6 +10571,11 @@ msgstr "Étapes"
|
|||
msgid "Step %(page_no)s: %(page_label)s"
|
||||
msgstr "Étape %(page_no)s : %(page_label)s"
|
||||
|
||||
#: templates/wcs/formdata_steps.html
|
||||
#, python-format
|
||||
msgid "Go back to step: %(page_label)s"
|
||||
msgstr "REtourner à l’étape : %(page_label)s"
|
||||
|
||||
#: templates/wcs/formdata_steps.html
|
||||
#, python-format
|
||||
msgid "Step %(page_no)s of %(page_count)s:"
|
||||
|
@ -10638,6 +10791,10 @@ msgstr "dans"
|
|||
msgid "not in"
|
||||
msgstr "pas dans"
|
||||
|
||||
#: variables.py
|
||||
msgid "contains"
|
||||
msgstr "contient"
|
||||
|
||||
#: variables.py
|
||||
#, python-format
|
||||
msgid "Invalid value \"%(value)s\" for filter \"%(filter)s\""
|
||||
|
@ -11029,6 +11186,22 @@ msgstr "Libellé de l’action"
|
|||
msgid "Create new draft"
|
||||
msgstr "Créer en tant que brouillon"
|
||||
|
||||
#: wf/create_formdata.py
|
||||
msgid "Operation mode when a draft is created"
|
||||
msgstr "Mode d’édition quand un brouillon est créé"
|
||||
|
||||
#: wf/create_formdata.py wf/editable.py
|
||||
msgid "Single page"
|
||||
msgstr "Une seule page"
|
||||
|
||||
#: wf/create_formdata.py wf/editable.py
|
||||
msgid "From specific page"
|
||||
msgstr "À partir d’une page"
|
||||
|
||||
#: wf/create_formdata.py wf/editable.py
|
||||
msgid "Page Identifier"
|
||||
msgstr "Identifiant de page"
|
||||
|
||||
#: wf/create_formdata.py
|
||||
msgid "Backoffice submission"
|
||||
msgstr "Saisie backoffice"
|
||||
|
@ -11280,18 +11453,6 @@ msgstr "Statut après modification"
|
|||
msgid "Don't select any if you don't want status change processing"
|
||||
msgstr "N’en sélectionnez aucun si vous ne voulez pas de changement de statut"
|
||||
|
||||
#: wf/editable.py
|
||||
msgid "Single page"
|
||||
msgstr "Une seule page"
|
||||
|
||||
#: wf/editable.py
|
||||
msgid "From specific page"
|
||||
msgstr "À partir d’une page"
|
||||
|
||||
#: wf/editable.py
|
||||
msgid "Page Identifier"
|
||||
msgstr "Identifiant de page"
|
||||
|
||||
#: wf/editable.py wf/wscall.py workflows.py
|
||||
msgid "Set marker to jump back to current status"
|
||||
msgstr "Poser un marqueur qui permettra de revenir au statut actuel"
|
||||
|
@ -11841,7 +12002,7 @@ msgstr "Erreur dans le gabarit du sujet, le courriel ne peut pas être généré
|
|||
msgid "Email too big to be sent"
|
||||
msgstr "Courriel trop volumineux"
|
||||
|
||||
#: wf/sms.py
|
||||
#: wf/sms.py workflow_tests.py
|
||||
msgid "Submitter"
|
||||
msgstr "Demandeur"
|
||||
|
||||
|
@ -12027,10 +12188,26 @@ msgstr "Clic sur un bouton d’action"
|
|||
msgid "Workflow has no action that displays a button."
|
||||
msgstr "Le workflow ne contient pas d’action qui affiche un bouton."
|
||||
|
||||
#: workflow_tests.py
|
||||
msgid "backoffice user"
|
||||
msgstr "utilisateur agent"
|
||||
|
||||
#: workflow_tests.py
|
||||
msgid "submitter"
|
||||
msgstr "demandeur"
|
||||
|
||||
#: workflow_tests.py
|
||||
msgid "missing user"
|
||||
msgstr "utilisateur manquant"
|
||||
|
||||
#: workflow_tests.py
|
||||
#, python-format
|
||||
msgid "Click on \"%s\""
|
||||
msgstr "Clic sur « %s »"
|
||||
msgid "Click on \"%(button_name)s\" by %(user)s"
|
||||
msgstr "Clic sur « %(button_name)s » par %(user)s"
|
||||
|
||||
#: workflow_tests.py
|
||||
msgid "Broken, missing user"
|
||||
msgstr "Cassé, utilisateur manquant"
|
||||
|
||||
#: workflow_tests.py
|
||||
#, python-format
|
||||
|
@ -12045,6 +12222,14 @@ msgstr "pas disponible"
|
|||
msgid "Button name"
|
||||
msgstr "Texte du bouton"
|
||||
|
||||
#: workflow_tests.py
|
||||
msgid "User who clicks on button"
|
||||
msgstr "Usager qui clique sur le bouton"
|
||||
|
||||
#: workflow_tests.py
|
||||
msgid "Other user"
|
||||
msgstr "Autre utilisateur"
|
||||
|
||||
#: workflow_tests.py
|
||||
msgid "Assert form status"
|
||||
msgstr "Vérifier le statut de la demande"
|
||||
|
@ -12197,6 +12382,46 @@ msgstr "Réponse webservice"
|
|||
msgid "Call count"
|
||||
msgstr "Nombre d’appels"
|
||||
|
||||
#: workflow_tests.py
|
||||
msgid "Assert SMS is sent"
|
||||
msgstr "Vérifier l’envoi d’un SMS"
|
||||
|
||||
#: workflow_tests.py
|
||||
#, python-format
|
||||
msgid "SMS to %s"
|
||||
msgstr "SMS vers %s"
|
||||
|
||||
#: workflow_tests.py
|
||||
msgid "No SMS was sent."
|
||||
msgstr "Aucun SMS n’a été envoyé."
|
||||
|
||||
#: workflow_tests.py
|
||||
#, python-format
|
||||
msgid "SMS phone numbers: %s"
|
||||
msgstr "Numéros de téléphones pour le SMS : %s"
|
||||
|
||||
#: workflow_tests.py
|
||||
#, python-format
|
||||
msgid "SMS was not sent to %s."
|
||||
msgstr "Un SMS n’a pas été envoyé à %s."
|
||||
|
||||
#: workflow_tests.py
|
||||
#, python-format
|
||||
msgid "SMS body: \"%s\""
|
||||
msgstr "Contenu du SMS : « %s »"
|
||||
|
||||
#: workflow_tests.py
|
||||
msgid "SMS body mismatch."
|
||||
msgstr "Différence dans le contenu du SMS."
|
||||
|
||||
#: workflow_tests.py
|
||||
msgid "Phone numbers"
|
||||
msgstr "Numéros de téléphone"
|
||||
|
||||
#: workflow_tests.py
|
||||
msgid "Add phone number"
|
||||
msgstr "Ajouter un numéro de téléphone"
|
||||
|
||||
#: workflow_traces.py
|
||||
msgid "Created (by API)"
|
||||
msgstr "Création (par l’API)"
|
||||
|
|
|
@ -18,6 +18,7 @@ import re
|
|||
|
||||
from django.utils.formats import number_format
|
||||
from django.utils.timezone import now
|
||||
from quixote import get_publisher
|
||||
from quixote.html import htmlescape, htmltext
|
||||
|
||||
from wcs.carddef import CardDef
|
||||
|
@ -42,6 +43,7 @@ class LoggedError:
|
|||
status_item_id = None
|
||||
expression = None
|
||||
expression_type = None
|
||||
context = None
|
||||
traceback = None
|
||||
exception_class = None
|
||||
exception_message = None
|
||||
|
@ -93,6 +95,8 @@ class LoggedError:
|
|||
if status:
|
||||
error.status_id = status.id
|
||||
|
||||
error.context = get_publisher().get_error_context()
|
||||
|
||||
error.first_occurence_timestamp = now()
|
||||
error.tech_id = error.build_tech_id()
|
||||
error.occurences_count += 1
|
||||
|
@ -111,6 +115,7 @@ class LoggedError:
|
|||
self.traceback = error.traceback
|
||||
self.expression = error.expression
|
||||
self.expression_type = error.expression_type
|
||||
self.context = error.context
|
||||
# exception should be the same (same tech_id), record just in case
|
||||
self.exception_class = error.exception_class
|
||||
self.exception_message = error.exception_message
|
||||
|
|
|
@ -154,8 +154,10 @@ class MaintenanceMiddleware(MiddlewareMixin):
|
|||
pub = get_publisher()
|
||||
maintenance_mode = pub.get_site_option('maintenance_page', 'variables')
|
||||
if maintenance_mode and not pass_through(request, pub):
|
||||
pub.install_lang()
|
||||
context = pub.get_site_options('variables')
|
||||
maintenance_message = pub.get_site_option('maintenance_page_message', 'variables')
|
||||
context = {'maintenance_message': maintenance_message or ''}
|
||||
context['maintenance_message'] = maintenance_message or ''
|
||||
return TemplateResponse(
|
||||
request,
|
||||
['hobo/maintenance/maintenance_page.html', 'wcs/maintenance_page.html'],
|
||||
|
|
|
@ -24,8 +24,10 @@ import shutil
|
|||
import sys
|
||||
import traceback
|
||||
import zipfile
|
||||
import zoneinfo
|
||||
from contextlib import ExitStack, contextmanager
|
||||
|
||||
from django.utils import timezone
|
||||
from django.utils.timezone import localtime
|
||||
|
||||
from . import custom_views, data_sources, formdef, sessions
|
||||
|
@ -181,9 +183,9 @@ class WcsPublisher(QommonPublisher):
|
|||
formdef.register_cronjobs()
|
||||
|
||||
def update_deprecations_report(self, **kwargs):
|
||||
from .backoffice.deprecations import DeprecationsScanAfterJob
|
||||
from .backoffice.deprecations import DeprecationsScan
|
||||
|
||||
DeprecationsScanAfterJob().execute()
|
||||
DeprecationsScan().execute()
|
||||
|
||||
def has_postgresql_config(self):
|
||||
return bool(self.cfg.get('postgresql', {}))
|
||||
|
@ -233,6 +235,16 @@ class WcsPublisher(QommonPublisher):
|
|||
self.session_manager_class = sessions.StorageSessionManager
|
||||
self.set_session_manager(self.session_manager_class(session_class=self.session_class))
|
||||
|
||||
def start_request(self):
|
||||
self.setup_timezone()
|
||||
super().start_request()
|
||||
|
||||
def setup_timezone(self):
|
||||
try:
|
||||
timezone.activate(zoneinfo.ZoneInfo(self.get_site_option('timezone')))
|
||||
except zoneinfo.ZoneInfoNotFoundError:
|
||||
timezone.deactivate() # use value from django settings
|
||||
|
||||
def get_enabled_languages(self):
|
||||
return self.cfg.get('language', {}).get('languages') or []
|
||||
|
||||
|
@ -280,6 +292,8 @@ class WcsPublisher(QommonPublisher):
|
|||
'workflows_xml',
|
||||
'blockdefs_xml',
|
||||
'roles_xml',
|
||||
'datasources',
|
||||
'wscalls',
|
||||
):
|
||||
continue
|
||||
path = os.path.join(self.app_dir, f)
|
||||
|
@ -334,6 +348,22 @@ class WcsPublisher(QommonPublisher):
|
|||
if os.path.split(f)[0] in results:
|
||||
results[os.path.split(f)[0]] += 1
|
||||
|
||||
# import datasources and wscalls
|
||||
from wcs.data_sources import NamedDataSource
|
||||
from wcs.wscalls import NamedWsCall
|
||||
|
||||
for f in z.namelist():
|
||||
if os.path.dirname(f) == 'datasources' and os.path.basename(f):
|
||||
with z.open(f) as fd:
|
||||
data_source = NamedDataSource.import_from_xml(fd, include_id=True)
|
||||
data_source.store()
|
||||
results['datasources'] += 1
|
||||
if os.path.dirname(f) == 'wscalls' and os.path.basename(f):
|
||||
with z.open(f) as fd:
|
||||
wscall = NamedWsCall.import_from_xml(fd, include_id=True)
|
||||
wscall.store()
|
||||
results['wscalls'] += 1
|
||||
|
||||
# second pass, fields blocks
|
||||
from wcs.blocks import BlockDef
|
||||
|
||||
|
@ -592,9 +622,12 @@ class WcsPublisher(QommonPublisher):
|
|||
|
||||
def cleanup(self):
|
||||
self._cached_user_fields_formdef = None
|
||||
self._update_related_seen = None
|
||||
self._error_context = None
|
||||
from . import sql
|
||||
|
||||
sql.cleanup_connection()
|
||||
timezone.deactivate()
|
||||
|
||||
@contextmanager
|
||||
def complex_data(self):
|
||||
|
@ -659,6 +692,22 @@ class WcsPublisher(QommonPublisher):
|
|||
finally:
|
||||
self.keep_all_block_rows_mode = False
|
||||
|
||||
# stacked contexts to include in logged errors
|
||||
_error_context = None
|
||||
|
||||
@contextmanager
|
||||
def error_context(self, **kwargs):
|
||||
if not self._error_context:
|
||||
self._error_context = []
|
||||
self._error_context.append(kwargs)
|
||||
try:
|
||||
yield True
|
||||
finally:
|
||||
self._error_context.pop()
|
||||
|
||||
def get_error_context(self):
|
||||
return {'stack': self._error_context} if self._error_context else None
|
||||
|
||||
def clean_deleted_users(self, **kwargs):
|
||||
for user_id in self.user_class.get_to_delete_ids():
|
||||
self.user_class.remove_object(user_id)
|
||||
|
|
|
@ -121,6 +121,7 @@ def cron_worker(publisher, since, job_name=None):
|
|||
CronJob.log('running jobs: %r' % sorted([x.name or x for x in jobs]))
|
||||
for job in jobs:
|
||||
publisher.install_lang()
|
||||
publisher.setup_timezone()
|
||||
publisher.reset_formdata_state()
|
||||
publisher.set_sql_application_name(f'wcs-cron-{job.name}')
|
||||
try:
|
||||
|
|
|
@ -909,7 +909,7 @@ class FileWithPreviewWidget(CompositeWidget):
|
|||
attrs['data-url'] += '?storage=%s' % self.storage
|
||||
self.file_type = kwargs.pop('file_type', None)
|
||||
if self.file_type:
|
||||
attrs['accept'] = ','.join(self.file_type)
|
||||
attrs['accept'] = ','.join([x for x in self.file_type if x])
|
||||
if self.max_file_size_bytes:
|
||||
# this could be used for client size validation of file size
|
||||
attrs['data-max-file-size'] = str(self.max_file_size_bytes)
|
||||
|
@ -1080,6 +1080,15 @@ class FileWithPreviewWidget(CompositeWidget):
|
|||
'.pif',
|
||||
'.php',
|
||||
'.js',
|
||||
'.pht',
|
||||
'.phtml',
|
||||
'.shtml',
|
||||
'.asa',
|
||||
'.asax',
|
||||
'.cer',
|
||||
'.swf',
|
||||
'.xap',
|
||||
'.ps1',
|
||||
'application/x-ms-dos-executable',
|
||||
'text/x-php',
|
||||
]
|
||||
|
@ -3711,7 +3720,9 @@ class HiddenErrorWidget(HiddenWidget):
|
|||
|
||||
class SingleSelectWidgetWithOther(CompositeWidget):
|
||||
def __init__(self, name, value=None, **kwargs):
|
||||
other_widget_class = kwargs.pop('other_widget_class', StringWidget)
|
||||
CompositeWidget.__init__(self, name, value=value, **kwargs)
|
||||
kwargs.pop('attrs', None)
|
||||
if 'title' in kwargs:
|
||||
del kwargs['title']
|
||||
options = kwargs.get('options')[:]
|
||||
|
@ -3727,8 +3738,23 @@ class SingleSelectWidgetWithOther(CompositeWidget):
|
|||
else:
|
||||
choice_value = '__other'
|
||||
other_value = value
|
||||
self.add(SingleSelectWidget, 'choice', value=choice_value, **kwargs)
|
||||
self.add(StringWidget, 'other', value=other_value, size=35)
|
||||
self.add(
|
||||
SingleSelectWidget,
|
||||
'choice',
|
||||
value=choice_value,
|
||||
attrs={'data-dynamic-display-parent': 'true'},
|
||||
**kwargs,
|
||||
)
|
||||
self.add(
|
||||
other_widget_class,
|
||||
'other',
|
||||
value=other_value,
|
||||
size=35,
|
||||
attrs={
|
||||
'data-dynamic-display-value': '__other',
|
||||
'data-dynamic-display-child-of': f'{name}$choice',
|
||||
},
|
||||
)
|
||||
|
||||
def _parse(self, request):
|
||||
self.value = self.get('choice')
|
||||
|
|
|
@ -450,7 +450,11 @@ def get_variadic_url(url, variables, encode_query=True):
|
|||
return url
|
||||
|
||||
# django template
|
||||
if '{{' in url or '{%' in url:
|
||||
if (
|
||||
'{{' in url
|
||||
or '{%' in url
|
||||
or (get_publisher() and get_publisher().has_site_option('disable-ezt-support'))
|
||||
):
|
||||
try:
|
||||
with no_complex(variables):
|
||||
url = Template(url).render(variables)
|
||||
|
|
|
@ -442,11 +442,13 @@ class QommonPublisher(Publisher):
|
|||
'allow-tracking-code-in-url': 'true',
|
||||
'disabled-fields': 'ranked-items, table, table-select, tablerows',
|
||||
'disable-rtf-support': 'true',
|
||||
'enable-card-identifier-template': 'true',
|
||||
'enable-intermediate-anonymisation': 'true',
|
||||
'relatable-hosts': '',
|
||||
'sync-map-and-address-fields': 'true',
|
||||
'unused-files-behaviour': 'remove',
|
||||
'rich-text-wf-displaymsg': 'auto-ckeditor',
|
||||
'timezone': 'Europe/Paris',
|
||||
},
|
||||
}
|
||||
if self.site_options is None:
|
||||
|
@ -821,6 +823,14 @@ class QommonPublisher(Publisher):
|
|||
)
|
||||
return attrs
|
||||
|
||||
def get_nominatim_extra_params(self):
|
||||
params = {}
|
||||
for option_name, query_name in (('nominatim_key', 'key'), ('nominatim_contact_email', 'email')):
|
||||
value = self.get_site_option(option_name)
|
||||
if value:
|
||||
params[query_name] = value
|
||||
return params
|
||||
|
||||
def get_reverse_geocoding_service_url(self):
|
||||
url = self.get_site_option('reverse_geocoding_service_url')
|
||||
if url:
|
||||
|
@ -832,11 +842,9 @@ class QommonPublisher(Publisher):
|
|||
)
|
||||
url += '/reverse'
|
||||
reverse_zoom_level = self.get_site_option('nominatim_reverse_zoom_level') or 18
|
||||
url += '?zoom=%s' % reverse_zoom_level
|
||||
key = self.get_site_option('nominatim_key')
|
||||
if key:
|
||||
url += '&key=%s' % key
|
||||
return url
|
||||
params = {'zoom': reverse_zoom_level}
|
||||
params.update(self.get_nominatim_extra_params())
|
||||
return urllib.parse.urljoin(url, '?' + urllib.parse.urlencode(params))
|
||||
|
||||
def get_geocoding_service_url(self):
|
||||
url = self.get_site_option('geocoding_service_url')
|
||||
|
@ -848,15 +856,13 @@ class QommonPublisher(Publisher):
|
|||
or 'https://nominatim.entrouvert.org'
|
||||
)
|
||||
url += '/search'
|
||||
key = self.get_site_option('nominatim_key')
|
||||
if key:
|
||||
url += '?key=%s' % key
|
||||
params = self.get_nominatim_extra_params()
|
||||
if self.get_site_option('map-bounds-top-left'):
|
||||
url += '&' if '?' in url else '?'
|
||||
top, left = self.get_site_option('map-bounds-top-left').split(';')
|
||||
bottom, right = self.get_site_option('map-bounds-bottom-right').split(';')
|
||||
url += 'viewbox=%s,%s,%s,%s&bounded=1' % (left, top, right, bottom)
|
||||
return url
|
||||
params['viewbox'] = f'{left},{top},{right},{bottom}'
|
||||
params['bounded'] = 1
|
||||
return urllib.parse.urljoin(url, '?' + urllib.parse.urlencode(params))
|
||||
|
||||
def get_working_day_calendar(self):
|
||||
return self.get_site_option('working_day_calendar') or settings.WORKING_DAY_CALENDAR
|
||||
|
|
|
@ -3136,3 +3136,25 @@ div[role="tabpanel"] > div.infonotice:first-child {
|
|||
form div.widget[data-widget-name="model_file_mode"] {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.extra-info.publication-dates {
|
||||
&::before {
|
||||
content: "";
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
#panel-general ul.logged-error-frames {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
li {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.logged-error-frames--context {
|
||||
list-style: none;
|
||||
}
|
||||
> li:nth-child(2n) {
|
||||
background: #eee;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ div.list-add {
|
|||
display: block;
|
||||
}
|
||||
|
||||
div.SingleSelectWidgetWithOther .content .widget {
|
||||
div.SingleSelectWidgetWithOther .content .widget:not(.widget-hidden) {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
|
|
|
@ -9,9 +9,13 @@ $(function() {
|
|||
$('a[rel=popup], a[data-popup]').data('title-selector', 'h2');
|
||||
$('a[rel=popup], a[data-popup]').data('close-button-text', WCS_I18N.close);
|
||||
$(document).on('gadjo:dialog-loaded', function(e, dialog) {
|
||||
window.disable_beforeunload = true;
|
||||
if ($(dialog).find('[name$=add_element]').length) {
|
||||
prepare_widget_list_elements();
|
||||
}
|
||||
if ($(dialog).data('enable-select2')) {
|
||||
add_js_behaviours($(dialog));
|
||||
}
|
||||
if (jQuery.fn.colourPicker !== undefined) {
|
||||
jQuery('select.colour-picker').colourPicker({title: ''});
|
||||
}
|
||||
|
|
|
@ -461,7 +461,7 @@ $.WcsFileUpload = {
|
|||
image_preview: function(base_widget, img_token) {
|
||||
var file_button = base_widget.find('.file-button');
|
||||
if(file_button.hasClass("file-image")) {
|
||||
file_button[0].style.setProperty('--image-preview-url', `url(${window.location.href}tempfile?t=${img_token}&thumbnail=1)`);
|
||||
file_button[0].style.setProperty('--image-preview-url', `url(${window.location.pathname}tempfile?t=${img_token}&thumbnail=1)`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -123,6 +123,212 @@ Responsive_table_widget.prototype.init = function () {
|
|||
});
|
||||
};
|
||||
|
||||
function add_js_behaviours($base) {
|
||||
$base.find('input[type=email]').on('change wcs:change', function() {
|
||||
var $email_input = $(this);
|
||||
var val = $email_input.val();
|
||||
var val_domain = val.split('@')[1];
|
||||
var $domain_hint_div = this.domain_hint_div;
|
||||
var highest_ratio = 0;
|
||||
var suggestion = null;
|
||||
|
||||
if (typeof val_domain === 'undefined' || known_domains.indexOf(val_domain) > -1) {
|
||||
// domain not yet typed in, or known domain, don't suggest anything.
|
||||
if ($domain_hint_div) {
|
||||
$domain_hint_div.hide();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i=0; i < well_known_domains.length; i++) {
|
||||
var domain = well_known_domains[i];
|
||||
var ratio = val_domain.similarity(domain);
|
||||
if (ratio > highest_ratio) {
|
||||
highest_ratio = ratio;
|
||||
suggestion = domain;
|
||||
}
|
||||
}
|
||||
if (highest_ratio > 0.80 && highest_ratio < 1) {
|
||||
if ($domain_hint_div === undefined) {
|
||||
$domain_hint_div = $('<div class="field-live-hint"><p class="message"></p><button type="button" class="action"></button><button type="button" class="close"><span class="sr-only"></span></button></div>');
|
||||
this.domain_hint_div = $domain_hint_div;
|
||||
$(this).after($domain_hint_div);
|
||||
$domain_hint_div.find('button.action').on('click', function() {
|
||||
$email_input.val($email_input.val().replace(/@.*/, '@' + $(this).data('suggestion')));
|
||||
$email_input.trigger('wcs:change');
|
||||
$domain_hint_div.hide();
|
||||
return false;
|
||||
});
|
||||
$domain_hint_div.find('button.close').on('click', function() {
|
||||
$domain_hint_div.hide();
|
||||
return false;
|
||||
});
|
||||
}
|
||||
$domain_hint_div.find('p').text(WCS_I18N.email_domain_suggest + ' @' + suggestion + ' ?');
|
||||
$domain_hint_div.find('button.action').text(WCS_I18N.email_domain_fix);
|
||||
$domain_hint_div.find('button.action').data('suggestion', suggestion);
|
||||
$domain_hint_div.find('button.close span.sr-only').text(WCS_I18N.close);
|
||||
$domain_hint_div.show();
|
||||
} else if ($domain_hint_div) {
|
||||
$domain_hint_div.hide();
|
||||
}
|
||||
});
|
||||
$base.find('.date-pick').each(function() {
|
||||
if (this.type == "date" || this.type == "time") {
|
||||
return; // prefer native date/time widgets
|
||||
}
|
||||
var $date_input = $(this);
|
||||
$date_input.attr('type', 'text');
|
||||
if ($date_input.data('formatted-value')) {
|
||||
$date_input.val($date_input.data('formatted-value'));
|
||||
}
|
||||
var options = Object();
|
||||
options.autoclose = true;
|
||||
options.weekStart = 1;
|
||||
options.format = $date_input.data('date-format');
|
||||
options.minView = $date_input.data('min-view');
|
||||
options.maxView = $date_input.data('max-view');
|
||||
options.startView = $date_input.data('start-view');
|
||||
if ($date_input.data('start-date')) options.startDate = $date_input.data('start-date');
|
||||
if ($date_input.data('end-date')) options.endDate = $date_input.data('end-date');
|
||||
$date_input.datetimepicker(options);
|
||||
});
|
||||
|
||||
/* searchable select */
|
||||
$base.find('select[data-autocomplete]').each(function(i, elem) {
|
||||
var required = $(elem).data('required');
|
||||
var options = {
|
||||
language: {
|
||||
errorLoading: function() { return WCS_I18N.s2_errorloading; },
|
||||
noResults: function () { return WCS_I18N.s2_nomatches; },
|
||||
inputTooShort: function (input, min) { return WCS_I18N.s2_tooshort; },
|
||||
loadingMore: function () { return WCS_I18N.s2_loadmore; },
|
||||
searching: function () { return WCS_I18N.s2_searching; }
|
||||
}
|
||||
};
|
||||
options.placeholder = $(elem).find('[data-hint]').data('hint');
|
||||
if (!required) {
|
||||
if (!options.placeholder) options.placeholder = '...';
|
||||
options.allowClear = true;
|
||||
}
|
||||
$(elem).select2(options);
|
||||
});
|
||||
|
||||
/* searchable select using a data source */
|
||||
$base.find('select[data-select2-url]').each(function(i, elem) {
|
||||
var required = $(elem).data('required');
|
||||
// create an additional hidden field to hold the label of the selected
|
||||
// option, it is necessary as the server may not have any knowledge of
|
||||
// possible options.
|
||||
var $input_display_value = $('<input>', {
|
||||
type: 'hidden',
|
||||
name: $(elem).attr('name') + '_display',
|
||||
value: $(elem).data('initial-display-value')
|
||||
});
|
||||
$input_display_value.insertAfter($(elem));
|
||||
var options = {
|
||||
minimumInputLength: 1,
|
||||
formatResult: function(result) { return result.text; },
|
||||
language: {
|
||||
errorLoading: function() { return WCS_I18N.s2_errorloading; },
|
||||
noResults: function () { return WCS_I18N.s2_nomatches; },
|
||||
inputTooShort: function (input, min) { return WCS_I18N.s2_tooshort; },
|
||||
loadingMore: function () { return WCS_I18N.s2_loadmore; },
|
||||
searching: function () { return WCS_I18N.s2_searching; }
|
||||
},
|
||||
templateSelection: function(data, container) {
|
||||
if (data.edit_related_url) {
|
||||
$(data.element).attr('data-edit-related-url', data.edit_related_url);
|
||||
}
|
||||
if (data.view_related_url) {
|
||||
$(data.element).attr('data-view-related-url', data.view_related_url);
|
||||
}
|
||||
return data.text;
|
||||
}
|
||||
};
|
||||
if (!required) {
|
||||
options.placeholder = '...';
|
||||
options.allowClear = true;
|
||||
}
|
||||
var url = $(elem).data('select2-url');
|
||||
if (url.indexOf('/api/') == 0) { // local proxying
|
||||
var data_type = 'json';
|
||||
} else {
|
||||
var data_type = 'jsonp';
|
||||
}
|
||||
options.ajax = {
|
||||
delay: 250,
|
||||
dataType: data_type,
|
||||
data: function(params) {
|
||||
return {q: params.term, page_limit: 50};
|
||||
},
|
||||
processResults: function (data, params) {
|
||||
return {results: data.data};
|
||||
},
|
||||
url: function() {
|
||||
var url = $(elem).data('select2-url');
|
||||
url = url.replace(/\[var_.+?\]/g, function(match, g1, g2) {
|
||||
// compatibility: if there are [var_...] references in the URL
|
||||
// replace them by looking for other select fields on the same
|
||||
// page.
|
||||
var related_select = $('#' + match.slice(1, -1));
|
||||
var value_container_id = $(related_select).data('valuecontainerid');
|
||||
return $('#' + value_container_id).val() || '';
|
||||
});
|
||||
return url;
|
||||
}
|
||||
};
|
||||
var select2 = $(elem).select2(options);
|
||||
$(elem).on('change', function() {
|
||||
// update _display hidden field with selected text
|
||||
var $selected = $(elem).find(':selected').first();
|
||||
var text = $selected.text();
|
||||
$input_display_value.val(text);
|
||||
// update edit-related button and view-related link href
|
||||
$(elem).siblings('.edit-related').attr('href', '').hide();
|
||||
$(elem).siblings('.view-related').attr('href', '').hide();
|
||||
if ($selected.attr('data-edit-related-url')) {
|
||||
$(elem).siblings('.edit-related').attr('href', $selected.attr('data-edit-related-url') + '?_popup=1').show();
|
||||
}
|
||||
if ($selected.attr('data-view-related-url')) {
|
||||
$(elem).siblings('.view-related').attr('href', $selected.attr('data-view-related-url')).show();
|
||||
}
|
||||
});
|
||||
if ($input_display_value.val()) {
|
||||
// if the _display hidden field was created with an initial value take it
|
||||
// and create a matching <option> in the real <select> widget, and use it
|
||||
// to set select2 initial state.
|
||||
var option = $('<option></option>', {value: $(elem).data('value')});
|
||||
option.appendTo($(elem));
|
||||
option.text($input_display_value.val());
|
||||
if ($(elem).data('initial-edit-related-url')) {
|
||||
option.attr('data-edit-related-url', $(elem).data('initial-edit-related-url'));
|
||||
}
|
||||
if ($(elem).data('initial-view-related-url')) {
|
||||
option.attr('data-view-related-url', $(elem).data('initial-view-related-url'));
|
||||
}
|
||||
select2.val($(elem).data('value')).trigger('change');
|
||||
$(elem).select2('data', {id: $(elem).data('value'), text: $(elem).data('initial-display-value')});
|
||||
}
|
||||
});
|
||||
|
||||
/* Make table widgets responsive */
|
||||
$base.find('.TableWidget, .SingleSelectTableWidget, .TableListRowsWidget').each(function (i, elem) {
|
||||
const table = elem.querySelector('table');
|
||||
new Responsive_table_widget(table);
|
||||
});
|
||||
|
||||
/* Add class to reset error style on change */
|
||||
$base.find('.widget-with-error').each(function(i, elem) {
|
||||
$(elem).find('input, select, textarea').on('change', function() {
|
||||
$(this).parents('.widget-with-error').addClass('widget-reset-error');
|
||||
});
|
||||
});
|
||||
$base.find('div.widget-prefilled').on('change input paste', function(ev) {
|
||||
$(this).removeClass('widget-prefilled');
|
||||
});
|
||||
}
|
||||
|
||||
$(function() {
|
||||
$('.section.foldable').addClass('gadjo-foldable-ignore');
|
||||
$('.section.foldable > h2 [role=button]').each(function() {
|
||||
|
@ -147,10 +353,24 @@ $(function() {
|
|||
var autosave_timeout_id = null;
|
||||
var autosave_is_running = false;
|
||||
var autosave_button_to_click_on_complete = null;
|
||||
var last_auto_save = $('form[data-has-draft]').serialize();
|
||||
|
||||
if ($('form[data-warn-on-unsaved-content]').length) {
|
||||
window.addEventListener('beforeunload', function (e) {
|
||||
var $form = $('form[data-warn-on-unsaved-content]');
|
||||
var current_data = $form.serialize();
|
||||
if (last_auto_save == current_data) return;
|
||||
if (window.disable_beforeunload) return;
|
||||
// preventDefault() and returnValue will trigger the browser alert
|
||||
// warning user about closing tag/window and losing data.
|
||||
e.preventDefault();
|
||||
e.returnValue = true;
|
||||
});
|
||||
}
|
||||
|
||||
if ($('form[data-has-draft]:not([data-autosave=false])').length == 1) {
|
||||
var last_auto_save = $('form[data-has-draft]').serialize();
|
||||
var error_counter = 0;
|
||||
|
||||
function autosave() {
|
||||
var $form = $('form[data-has-draft]');
|
||||
if ($form.hasClass('disabled-during-submit')) return;
|
||||
|
@ -232,213 +452,8 @@ $(function() {
|
|||
var known_domains = WCS_VALID_KNOWN_DOMAINS;
|
||||
}
|
||||
|
||||
function add_js_behaviours($base) {
|
||||
$base.find('input[type=email]').on('change wcs:change', function() {
|
||||
var $email_input = $(this);
|
||||
var val = $email_input.val();
|
||||
var val_domain = val.split('@')[1];
|
||||
var $domain_hint_div = this.domain_hint_div;
|
||||
var highest_ratio = 0;
|
||||
var suggestion = null;
|
||||
|
||||
if (typeof val_domain === 'undefined' || known_domains.indexOf(val_domain) > -1) {
|
||||
// domain not yet typed in, or known domain, don't suggest anything.
|
||||
if ($domain_hint_div) {
|
||||
$domain_hint_div.hide();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i=0; i < well_known_domains.length; i++) {
|
||||
var domain = well_known_domains[i];
|
||||
var ratio = val_domain.similarity(domain);
|
||||
if (ratio > highest_ratio) {
|
||||
highest_ratio = ratio;
|
||||
suggestion = domain;
|
||||
}
|
||||
}
|
||||
if (highest_ratio > 0.80 && highest_ratio < 1) {
|
||||
if ($domain_hint_div === undefined) {
|
||||
$domain_hint_div = $('<div class="field-live-hint"><p class="message"></p><button type="button" class="action"></button><button type="button" class="close"><span class="sr-only"></span></button></div>');
|
||||
this.domain_hint_div = $domain_hint_div;
|
||||
$(this).after($domain_hint_div);
|
||||
$domain_hint_div.find('button.action').on('click', function() {
|
||||
$email_input.val($email_input.val().replace(/@.*/, '@' + $(this).data('suggestion')));
|
||||
$email_input.trigger('wcs:change');
|
||||
$domain_hint_div.hide();
|
||||
return false;
|
||||
});
|
||||
$domain_hint_div.find('button.close').on('click', function() {
|
||||
$domain_hint_div.hide();
|
||||
return false;
|
||||
});
|
||||
}
|
||||
$domain_hint_div.find('p').text(WCS_I18N.email_domain_suggest + ' @' + suggestion + ' ?');
|
||||
$domain_hint_div.find('button.action').text(WCS_I18N.email_domain_fix);
|
||||
$domain_hint_div.find('button.action').data('suggestion', suggestion);
|
||||
$domain_hint_div.find('button.close span.sr-only').text(WCS_I18N.close);
|
||||
$domain_hint_div.show();
|
||||
} else if ($domain_hint_div) {
|
||||
$domain_hint_div.hide();
|
||||
}
|
||||
});
|
||||
$base.find('.date-pick').each(function() {
|
||||
if (this.type == "date" || this.type == "time") {
|
||||
return; // prefer native date/time widgets
|
||||
}
|
||||
var $date_input = $(this);
|
||||
$date_input.attr('type', 'text');
|
||||
if ($date_input.data('formatted-value')) {
|
||||
$date_input.val($date_input.data('formatted-value'));
|
||||
}
|
||||
var options = Object();
|
||||
options.autoclose = true;
|
||||
options.weekStart = 1;
|
||||
options.format = $date_input.data('date-format');
|
||||
options.minView = $date_input.data('min-view');
|
||||
options.maxView = $date_input.data('max-view');
|
||||
options.startView = $date_input.data('start-view');
|
||||
if ($date_input.data('start-date')) options.startDate = $date_input.data('start-date');
|
||||
if ($date_input.data('end-date')) options.endDate = $date_input.data('end-date');
|
||||
$date_input.datetimepicker(options);
|
||||
});
|
||||
|
||||
/* searchable select */
|
||||
$base.find('select[data-autocomplete]').each(function(i, elem) {
|
||||
var required = $(elem).data('required');
|
||||
var options = {
|
||||
language: {
|
||||
errorLoading: function() { return WCS_I18N.s2_errorloading; },
|
||||
noResults: function () { return WCS_I18N.s2_nomatches; },
|
||||
inputTooShort: function (input, min) { return WCS_I18N.s2_tooshort; },
|
||||
loadingMore: function () { return WCS_I18N.s2_loadmore; },
|
||||
searching: function () { return WCS_I18N.s2_searching; }
|
||||
}
|
||||
};
|
||||
options.placeholder = $(elem).find('[data-hint]').data('hint');
|
||||
if (!required) {
|
||||
if (!options.placeholder) options.placeholder = '...';
|
||||
options.allowClear = true;
|
||||
}
|
||||
$(elem).select2(options);
|
||||
});
|
||||
|
||||
/* searchable select using a data source */
|
||||
$base.find('select[data-select2-url]').each(function(i, elem) {
|
||||
var required = $(elem).data('required');
|
||||
// create an additional hidden field to hold the label of the selected
|
||||
// option, it is necessary as the server may not have any knowledge of
|
||||
// possible options.
|
||||
var $input_display_value = $('<input>', {
|
||||
type: 'hidden',
|
||||
name: $(elem).attr('name') + '_display',
|
||||
value: $(elem).data('initial-display-value')
|
||||
});
|
||||
$input_display_value.insertAfter($(elem));
|
||||
var options = {
|
||||
minimumInputLength: 1,
|
||||
formatResult: function(result) { return result.text; },
|
||||
language: {
|
||||
errorLoading: function() { return WCS_I18N.s2_errorloading; },
|
||||
noResults: function () { return WCS_I18N.s2_nomatches; },
|
||||
inputTooShort: function (input, min) { return WCS_I18N.s2_tooshort; },
|
||||
loadingMore: function () { return WCS_I18N.s2_loadmore; },
|
||||
searching: function () { return WCS_I18N.s2_searching; }
|
||||
},
|
||||
templateSelection: function(data, container) {
|
||||
if (data.edit_related_url) {
|
||||
$(data.element).attr('data-edit-related-url', data.edit_related_url);
|
||||
}
|
||||
if (data.view_related_url) {
|
||||
$(data.element).attr('data-view-related-url', data.view_related_url);
|
||||
}
|
||||
return data.text;
|
||||
}
|
||||
};
|
||||
if (!required) {
|
||||
options.placeholder = '...';
|
||||
options.allowClear = true;
|
||||
}
|
||||
var url = $(elem).data('select2-url');
|
||||
if (url.indexOf('/api/autocomplete/') == 0) { // local proxying
|
||||
var data_type = 'json';
|
||||
} else {
|
||||
var data_type = 'jsonp';
|
||||
}
|
||||
options.ajax = {
|
||||
delay: 250,
|
||||
dataType: data_type,
|
||||
data: function(params) {
|
||||
return {q: params.term, page_limit: 50};
|
||||
},
|
||||
processResults: function (data, params) {
|
||||
return {results: data.data};
|
||||
},
|
||||
url: function() {
|
||||
var url = $(elem).data('select2-url');
|
||||
url = url.replace(/\[var_.+?\]/g, function(match, g1, g2) {
|
||||
// compatibility: if there are [var_...] references in the URL
|
||||
// replace them by looking for other select fields on the same
|
||||
// page.
|
||||
var related_select = $('#' + match.slice(1, -1));
|
||||
var value_container_id = $(related_select).data('valuecontainerid');
|
||||
return $('#' + value_container_id).val() || '';
|
||||
});
|
||||
return url;
|
||||
}
|
||||
};
|
||||
var select2 = $(elem).select2(options);
|
||||
$(elem).on('change', function() {
|
||||
// update _display hidden field with selected text
|
||||
var $selected = $(elem).find(':selected').first();
|
||||
var text = $selected.text();
|
||||
$input_display_value.val(text);
|
||||
// update edit-related button and view-related link href
|
||||
$(elem).siblings('.edit-related').attr('href', '').hide();
|
||||
$(elem).siblings('.view-related').attr('href', '').hide();
|
||||
if ($selected.attr('data-edit-related-url')) {
|
||||
$(elem).siblings('.edit-related').attr('href', $selected.attr('data-edit-related-url') + '?_popup=1').show();
|
||||
}
|
||||
if ($selected.attr('data-view-related-url')) {
|
||||
$(elem).siblings('.view-related').attr('href', $selected.attr('data-view-related-url')).show();
|
||||
}
|
||||
});
|
||||
if ($input_display_value.val()) {
|
||||
// if the _display hidden field was created with an initial value take it
|
||||
// and create a matching <option> in the real <select> widget, and use it
|
||||
// to set select2 initial state.
|
||||
var option = $('<option></option>', {value: $(elem).data('value')});
|
||||
option.appendTo($(elem));
|
||||
option.text($input_display_value.val());
|
||||
if ($(elem).data('initial-edit-related-url')) {
|
||||
option.attr('data-edit-related-url', $(elem).data('initial-edit-related-url'));
|
||||
}
|
||||
if ($(elem).data('initial-view-related-url')) {
|
||||
option.attr('data-view-related-url', $(elem).data('initial-view-related-url'));
|
||||
}
|
||||
select2.val($(elem).data('value')).trigger('change');
|
||||
$(elem).select2('data', {id: $(elem).data('value'), text: $(elem).data('initial-display-value')});
|
||||
}
|
||||
});
|
||||
|
||||
/* Make table widgets responsive */
|
||||
$base.find('.TableWidget, .SingleSelectTableWidget, .TableListRowsWidget').each(function (i, elem) {
|
||||
const table = elem.querySelector('table');
|
||||
new Responsive_table_widget(table);
|
||||
});
|
||||
|
||||
/* Add class to reset error style on change */
|
||||
$base.find('.widget-with-error').each(function(i, elem) {
|
||||
$(elem).find('input, select, textarea').on('change', function() {
|
||||
$(this).parents('.widget-with-error').addClass('widget-reset-error');
|
||||
});
|
||||
});
|
||||
$base.find('div.widget-prefilled').on('change input paste', function(ev) {
|
||||
$(this).removeClass('widget-prefilled');
|
||||
});
|
||||
}
|
||||
|
||||
add_js_behaviours($('form[data-live-url], form[data-backoffice-preview]'));
|
||||
add_js_behaviours($('form[data-live-url], form[data-backoffice-preview], form[data-enable-select2]'));
|
||||
last_auto_save = $('form[data-has-draft]').serialize();
|
||||
|
||||
// Form with error
|
||||
const errornotice = document.querySelector('form:not([data-backoffice-preview]) .errornotice');
|
||||
|
@ -469,6 +484,7 @@ $(function() {
|
|||
});
|
||||
$('form').on('submit', function(event) {
|
||||
var $form = $(this);
|
||||
window.disable_beforeunload = true;
|
||||
/* prevent more autosave */
|
||||
if (autosave_timeout_id !== null) {
|
||||
window.clearTimeout(autosave_timeout_id);
|
||||
|
@ -1083,3 +1099,20 @@ document.addEventListener('DOMContentLoaded', function(){
|
|||
initLiveValidation(blockWidgets)
|
||||
})
|
||||
})
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function(){
|
||||
const previous_page_id_input = document.querySelector('[name="previous-page-id"]')
|
||||
if (!previous_page_id_input) return
|
||||
document.querySelectorAll('.wcs-step[data-page-id]').forEach((step, idx) => {
|
||||
step.addEventListener('keydown', function(e) {
|
||||
if (e.key !== "Enter" && e.key !== " ") return
|
||||
e.preventDefault()
|
||||
previous_page_id_input.value = step.dataset.pageId
|
||||
document.querySelector('button[name="previous"]').dispatchEvent(new MouseEvent('click'))
|
||||
})
|
||||
step.addEventListener('click', function() {
|
||||
previous_page_id_input.value = step.dataset.pageId
|
||||
document.querySelector('button[name="previous"]').dispatchEvent(new MouseEvent('click'))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -17,11 +17,11 @@ function geoloc_prefill(element_type, element_values, widget_name=null)
|
|||
}
|
||||
if ($input_widget.length) {
|
||||
$input_widget.val(element_value)
|
||||
$input_widget.each((idx, elt) => elt.dispatchEvent(new Event('change')))
|
||||
$input_widget.each((idx, elt) => elt.dispatchEvent(new Event('change', {'bubbles': true})))
|
||||
found = true;
|
||||
} else if ($text_widget.length) {
|
||||
$text_widget.val(element_value)
|
||||
$text_widget.each((idx, elt) => elt.dispatchEvent(new Event('change')))
|
||||
$text_widget.each((idx, elt) => elt.dispatchEvent(new Event('change', {'bubbles': true})))
|
||||
found = true;
|
||||
} else if ($select_widget.length) {
|
||||
if ($options.length == 0) break;
|
||||
|
@ -31,7 +31,7 @@ function geoloc_prefill(element_type, element_values, widget_name=null)
|
|||
if ($.slugify($option.val()) == slugified_value ||
|
||||
$.slugify($option.text()) == slugified_value) {
|
||||
$option.prop('selected', true);
|
||||
$option.parent().each((idx, elt) => elt.dispatchEvent(new Event('change')))
|
||||
$option.parent().each((idx, elt) => elt.dispatchEvent(new Event('change', {'bubbles': true})))
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -51,21 +51,6 @@ $(function() {
|
|||
});
|
||||
}
|
||||
|
||||
function prepate_select_or_other_widgets() {
|
||||
$('.SingleSelectWidgetWithOther').each(function(idx, elem) {
|
||||
var $widget = $(elem);
|
||||
$widget.find('select').off('change input').on('change input', function() {
|
||||
var val = $(this).val();
|
||||
if (val == '__other') {
|
||||
$widget.find('.StringWidget').show();
|
||||
} else {
|
||||
$widget.find('.StringWidget').hide();
|
||||
}
|
||||
});
|
||||
$widget.find('select').trigger('change');
|
||||
});
|
||||
}
|
||||
|
||||
function prepare_confirmation_buttons() {
|
||||
$('button[data-ask-for-confirmation]').off('click').on('click', function() {
|
||||
var text = $(this).data('ask-for-confirmation');
|
||||
|
@ -108,7 +93,6 @@ $(function() {
|
|||
function prepare_widgets() {
|
||||
prepare_dynamic_widgets();
|
||||
prepare_autocomplete_widgets();
|
||||
prepate_select_or_other_widgets();
|
||||
prepare_select_empty_label();
|
||||
prepare_confirmation_buttons();
|
||||
}
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
{% extends "qommon/forms/widget.html" %}
|
||||
{% block widget-control %}
|
||||
<textarea hidden id="form_{{widget.get_name_for_id}}" name="{{widget.name}}">
|
||||
{{widget.value|default:""}}
|
||||
</textarea>
|
||||
<textarea hidden id="form_{{widget.get_name_for_id}}" name="{{widget.name}}">{{widget.value|default:""}}</textarea>
|
||||
<godo-editor
|
||||
style="width: 100%"
|
||||
{% for attr in widget.attrs.items %}{{attr.0}}="{{attr.1}}" {% endfor %}
|
||||
|
|
|
@ -922,6 +922,11 @@ def between(queryset):
|
|||
return queryset.apply_between()
|
||||
|
||||
|
||||
@register_queryset_filter(name='icontains', attr='apply_icontains')
|
||||
def icontains(queryset):
|
||||
return queryset.apply_icontains()
|
||||
|
||||
|
||||
@register.filter
|
||||
def count(queryset):
|
||||
if hasattr(queryset, '__len__'):
|
||||
|
@ -1308,7 +1313,11 @@ def temporary_access_url(
|
|||
from wcs.formdef import FormDef
|
||||
|
||||
formdef = FormDef.get_by_urlname(formdef_urlname)
|
||||
formdata = formdef.data_class().get(formdata_id)
|
||||
try:
|
||||
formdata = formdef.data_class().get(formdata_id)
|
||||
except KeyError:
|
||||
# formdata somehow got removed, ignore
|
||||
return ''
|
||||
|
||||
duration = 0
|
||||
for amount, unit in ((days, 86400), (hours, 3600), (minutes, 60), (seconds, 1)):
|
||||
|
|
|
@ -20,7 +20,7 @@ import xml.etree.ElementTree as ET
|
|||
from quixote import get_publisher
|
||||
|
||||
from .misc import indent_xml, xml_node_text
|
||||
from .storage import Equal, Or, StorableObject
|
||||
from .storage import Contains, Equal, Or, StorableObject
|
||||
|
||||
|
||||
class XmlStorableObject(StorableObject):
|
||||
|
@ -31,7 +31,7 @@ class XmlStorableObject(StorableObject):
|
|||
first_byte = fd.read(1)
|
||||
fd.seek(0)
|
||||
if first_byte == b'<':
|
||||
return cls.import_from_xml(fd, include_id=True)
|
||||
return cls.import_from_xml(fd, include_id=True, check_deprecated=False)
|
||||
else:
|
||||
obj = StorableObject.storage_load(fd)
|
||||
obj._upgrade_must_store = True
|
||||
|
@ -84,15 +84,15 @@ class XmlStorableObject(StorableObject):
|
|||
sub.text = role.name
|
||||
|
||||
@classmethod
|
||||
def import_from_xml(cls, fd, include_id=False):
|
||||
def import_from_xml(cls, fd, include_id=False, check_deprecated=True):
|
||||
try:
|
||||
tree = ET.parse(fd)
|
||||
except Exception:
|
||||
raise ValueError()
|
||||
return cls.import_from_xml_tree(tree, include_id=include_id)
|
||||
return cls.import_from_xml_tree(tree, include_id=include_id, check_deprecated=check_deprecated)
|
||||
|
||||
@classmethod
|
||||
def import_from_xml_tree(cls, tree, include_id=False, **kwargs):
|
||||
def import_from_xml_tree(cls, tree, include_id=False, check_deprecated=True, **kwargs):
|
||||
obj = cls()
|
||||
|
||||
# if the tree we get is actually a ElementTree for real, we get its
|
||||
|
@ -141,6 +141,8 @@ class XmlStorableObject(StorableObject):
|
|||
def import_roles_from_xml(self, element, include_id=False, **kwargs):
|
||||
criterias = []
|
||||
for sub in element:
|
||||
if sub.tag != 'role':
|
||||
continue
|
||||
if include_id and 'role-id' in sub.attrib:
|
||||
criterias.append(Equal('id', sub.attrib['role-id']))
|
||||
elif 'role-slug' in sub.attrib:
|
||||
|
@ -157,6 +159,25 @@ class XmlStorableObject(StorableObject):
|
|||
|
||||
return lazy_roles
|
||||
|
||||
def import_ds_roles_from_xml(self, element, include_id=False, **kwargs):
|
||||
imported_roles = self.import_roles_from_xml(element, include_id=include_id, **kwargs)
|
||||
if callable(imported_roles):
|
||||
imported_roles = imported_roles()
|
||||
role_ids = [x.id for x in imported_roles]
|
||||
for sub in element:
|
||||
if sub.tag == 'item': # legacy support for <item>{id}</item>
|
||||
role_ids.append(xml_node_text(sub))
|
||||
return role_ids
|
||||
|
||||
def export_ds_roles_to_xml(self, element, attribute_name, include_id=False, **kwargs):
|
||||
for role in get_publisher().role_class.select(
|
||||
[Contains('id', getattr(self, attribute_name, None) or [])]
|
||||
):
|
||||
sub = ET.SubElement(element, 'role')
|
||||
sub.attrib['role-id'] = role.id # always include id
|
||||
sub.attrib['role-slug'] = role.slug
|
||||
sub.text = role.name
|
||||
|
||||
|
||||
class PostConditionsXmlMixin:
|
||||
def post_conditions_init_with_xml(self, node, include_id=False, snapshot=False):
|
||||
|
|
20
wcs/sql.py
20
wcs/sql.py
|
@ -1177,6 +1177,7 @@ def do_loggederrors_table():
|
|||
status_item_id VARCHAR,
|
||||
expression VARCHAR,
|
||||
expression_type VARCHAR,
|
||||
context JSONB,
|
||||
traceback TEXT,
|
||||
exception_class VARCHAR,
|
||||
exception_message VARCHAR,
|
||||
|
@ -1199,6 +1200,8 @@ def do_loggederrors_table():
|
|||
# migrations
|
||||
if 'kind' not in existing_fields:
|
||||
cur.execute('''ALTER TABLE %s ADD COLUMN kind VARCHAR''' % table_name)
|
||||
if 'context' not in existing_fields:
|
||||
cur.execute('''ALTER TABLE %s ADD COLUMN context JSONB''' % table_name)
|
||||
|
||||
# delete obsolete fields
|
||||
for field in existing_fields - needed_fields:
|
||||
|
@ -2267,6 +2270,7 @@ class SqlDataMixin(SqlMixin):
|
|||
def __init__(self, id=None):
|
||||
self.id = id
|
||||
self.data = {}
|
||||
self._has_changed_digest = False
|
||||
|
||||
_evolution = None
|
||||
|
||||
|
@ -2403,6 +2407,7 @@ class SqlDataMixin(SqlMixin):
|
|||
|
||||
def _set_auto_fields(self, cur):
|
||||
if self.set_auto_fields():
|
||||
self._has_changed_digest = True
|
||||
sql_statement = (
|
||||
'''UPDATE %s
|
||||
SET id_display = %%(id_display)s,
|
||||
|
@ -2619,6 +2624,11 @@ class SqlDataMixin(SqlMixin):
|
|||
if isinstance(value, str) and len(value) < 10000:
|
||||
# avoid overlong strings, typically base64-encoded values
|
||||
fts_strings[weight].add(value)
|
||||
# normalize values looking like phonenumbers, because
|
||||
# phonenumbers are normalized by the FTS criteria
|
||||
if len(value) < 30 and value != normalize_phone_number_for_fts_if_needed(value):
|
||||
# use weight 'D' to give preference to fields with the phonenumber validation
|
||||
fts_strings['D'].add(normalize_phone_number_for_fts_if_needed(value))
|
||||
elif type(value) in (tuple, list):
|
||||
for val in value:
|
||||
fts_strings[weight].add(val)
|
||||
|
@ -2848,7 +2858,9 @@ class SqlCardData(SqlDataMixin, wcs.carddata.CardData):
|
|||
def store(self, *args, **kwargs):
|
||||
if self.uuid is None:
|
||||
self.uuid = str(uuid.uuid4())
|
||||
return super().store(*args, **kwargs)
|
||||
super().store(*args, **kwargs)
|
||||
if self._has_changed_digest:
|
||||
self.update_related()
|
||||
|
||||
|
||||
class SqlUser(SqlMixin, wcs.users.User):
|
||||
|
@ -3727,6 +3739,7 @@ class LoggedError(SqlMixin, wcs.logged_errors.LoggedError):
|
|||
('status_item_id', 'varchar'),
|
||||
('expression', 'varchar'),
|
||||
('expression_type', 'varchar'),
|
||||
('context', 'jsonb'),
|
||||
('traceback', 'text'),
|
||||
('exception_class', 'varchar'),
|
||||
('exception_message', 'varchar'),
|
||||
|
@ -5107,7 +5120,7 @@ def get_period_total(
|
|||
# latest migration, number + description (description is not used
|
||||
# programmaticaly but will make sure git conflicts if two migrations are
|
||||
# separately added with the same number)
|
||||
SQL_LEVEL = (105, 'change test result json structure')
|
||||
SQL_LEVEL = (106, 'add context column to logged_errors table')
|
||||
|
||||
|
||||
def migrate_global_views(conn, cur):
|
||||
|
@ -5235,10 +5248,11 @@ def migrate():
|
|||
# 50: switch role uuid column to varchar
|
||||
do_role_table()
|
||||
migrate_legacy_roles()
|
||||
if sql_level < 53:
|
||||
if sql_level < 106:
|
||||
# 47: store LoggedErrors in SQL
|
||||
# 48: remove acked attribute from LoggedError
|
||||
# 53: add kind column to logged_errors table
|
||||
# 106: add context column to logged_errors table
|
||||
do_loggederrors_table()
|
||||
if sql_level < 94:
|
||||
# 3: introduction of _structured for user fields
|
||||
|
|
|
@ -25,7 +25,7 @@ from wcs.qommon import misc
|
|||
|
||||
|
||||
def like_escape(value):
|
||||
value = value.replace('\\', '\\\\')
|
||||
value = str(value or '').replace('\\', '\\\\')
|
||||
value = value.replace('_', '\\_')
|
||||
value = value.replace('%', '\\%')
|
||||
return value
|
||||
|
@ -336,12 +336,31 @@ class Intersects(Criteria):
|
|||
|
||||
|
||||
class ILike(Criteria):
|
||||
sql_op = 'ILIKE'
|
||||
|
||||
def __init__(self, attribute, value, **kwargs):
|
||||
super().__init__(attribute, value, **kwargs)
|
||||
self.value = '%' + like_escape(self.value) + '%'
|
||||
|
||||
def as_sql(self):
|
||||
return '%s ILIKE %%(c%s)s' % (self.attribute, id(self.value))
|
||||
|
||||
phone_re = re.compile(
|
||||
r'''.*?(?P<phone> # a phone number
|
||||
((\+[1-9])|(\b0)) # starting with an international prefix, or 0
|
||||
[-\(\)\d\.\s/]{6,20} # then a bunch of numbers/symbols
|
||||
\b) # till the end of the "word"''',
|
||||
re.X,
|
||||
)
|
||||
|
||||
|
||||
def normalize_phone_number_for_fts_if_needed(value):
|
||||
phone_match = phone_re.match(value)
|
||||
if phone_match and not re.match(r'^\d+-\d+$', phone_match.group('phone').strip()):
|
||||
# if it looks like a phone number, normalize it to its
|
||||
# "international/E164" format to match what's stored in the
|
||||
# database.
|
||||
phone_value = misc.normalize_phone_number_for_fts(phone_match.group('phone').strip())
|
||||
value = value.replace(phone_match.group('phone').strip(), phone_value)
|
||||
return value
|
||||
|
||||
|
||||
class FtsMatch(Criteria):
|
||||
|
@ -350,20 +369,7 @@ class FtsMatch(Criteria):
|
|||
self.attribute = 'fts'
|
||||
self.value = self.get_fts_value(value)
|
||||
if extra_normalize:
|
||||
phone_match = re.match(
|
||||
r'''.*?(?P<phone> # a phone number
|
||||
((\+[1-9])|(\b0)) # starting with an international prefix, or 0
|
||||
[-\(\)\d\.\s/]{6,20} # then a bunch of numbers/symbols
|
||||
\b) # till the end of the "word"''',
|
||||
self.value,
|
||||
re.X,
|
||||
)
|
||||
if phone_match and not re.match(r'^\d+-\d+$', phone_match.group('phone').strip()):
|
||||
# if it looks like a phone number, normalize it to its
|
||||
# "international/E164" format to match what's stored in the
|
||||
# database.
|
||||
phone_value = misc.normalize_phone_number_for_fts(phone_match.group('phone').strip())
|
||||
self.value = self.value.replace(phone_match.group('phone').strip(), phone_value)
|
||||
self.value = normalize_phone_number_for_fts_if_needed(self.value)
|
||||
|
||||
@classmethod
|
||||
def get_fts_value(cls, value):
|
||||
|
|
|
@ -56,13 +56,17 @@
|
|||
{% if formdef.roles %}
|
||||
<li><span class="parameter">{% trans "Display to unlogged users" %}{% trans ":" %}</span> {{ formdef.always_advertise|yesno }}</li>
|
||||
{% endif %}
|
||||
<li><span class="parameter">{% trans "Include button to download all files" %}{% trans ":" %}</span> {{ formdef.include_download_all_button|yesno }}</li>
|
||||
<li><span class="parameter">{% trans "Management sidebar elements" %}{% trans ":" %}</span>
|
||||
<ul>
|
||||
{% for label in formdef.management_sidebar_items_labels %} <li>{{ label }}</li>{% endfor %}
|
||||
</ul>
|
||||
</li>
|
||||
<li><span class="parameter">{% trans "Skip from per user view" %}{% trans ":" %}</span> {{ formdef.skip_from_360_view|yesno }}</li>
|
||||
<li><span class="parameter">{% trans "Tracking codes" %}{% trans ":" %}</span> {{ formdef.enable_tracking_codes|yesno }}</li>
|
||||
{% if formdef.enable_tracking_codes %}
|
||||
<li><span class="parameter">{% trans "Fields to check after entering the tracking code" %}{% trans ":" %}</span> {{ tracking_code_verify_fields_labels|default:"-" }}</li>
|
||||
{% endif %}
|
||||
<li><span class="parameter">{% trans "Lifespan of drafts (in days)" %}{% trans ":" %}</span> {{ formdef.drafts_lifespan|default_if_none:_('default value') }}</li>
|
||||
<li><span class="parameter">{% trans "Lifespan of drafts (in days)" %}{% trans ":" %}</span> {{ formdef.get_drafts_lifespan }}</li>
|
||||
<li><span class="parameter">{% trans "Templates" %}</span>
|
||||
<ul>
|
||||
<li><span class="parameter">{% trans "Digest" %}{% trans ":" %}</span> {{ formdef.default_digest_template|default:"-" }}</li>
|
||||
|
|
|
@ -10,6 +10,23 @@
|
|||
<li {% if item.disabled %}class="disabled"{% endif %}><a href="{{ item.get_admin_url }}">
|
||||
{% if not application %}{% include 'wcs/backoffice/includes/application_icons.html' with object=item %}{% endif %}
|
||||
{{ item.name }}
|
||||
{% if item.publication_date or item.expiration_date %}
|
||||
<span class="extra-info publication-dates">
|
||||
{% if item.publication_date and item.expiration_date %}
|
||||
{% blocktrans trimmed with date1=item.publication_date|date:"DATETIME_FORMAT" date2=item.expiration_date|date:"DATETIME_FORMAT" %}
|
||||
Published from {{ date1 }} until {{ date2 }}
|
||||
{% endblocktrans %}
|
||||
{% elif item.publication_date %}
|
||||
{% blocktrans trimmed with date1=item.publication_date|date:"DATETIME_FORMAT" %}
|
||||
Published from {{ date1 }}
|
||||
{% endblocktrans %}
|
||||
{% else %}
|
||||
{% blocktrans trimmed with date2=item.expiration_date|date:"DATETIME_FORMAT" %}
|
||||
Published until {{ date2 }}
|
||||
{% endblocktrans %}
|
||||
{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if item.disabled and item.disabled_redirection %}
|
||||
<span class="extra-info">- {% trans "redirection" %}</span>
|
||||
{% endif %}
|
||||
|
|
|
@ -43,6 +43,17 @@
|
|||
{% if error.expression or error.expression_type %}
|
||||
<li>{{ view.error_expression_type_label }}{% trans ":" %} <code>{{ error.expression }}</code></li>
|
||||
{% endif %}
|
||||
{% if error.context %}
|
||||
<li><ul class="logged-error-frames">
|
||||
{% for frame in view.get_context_frames %}
|
||||
<li>{% if frame.source %}<a href="{{ frame.source.url }}">{{ frame.source.label }}</a>{% endif %}
|
||||
<ul class="logged-error-frames--context">
|
||||
{% for frame_context in frame.get_frame_lines %}
|
||||
<li>{{ frame_context.label }}{% trans ":" %} <code>{{ frame_context.value }}</code></li>
|
||||
{% endfor %}
|
||||
</ul></li>
|
||||
{% endfor %}</ul></li>
|
||||
{% endif %}
|
||||
{% if error.exception_class or error.exception_message %}
|
||||
<li>{% trans "Error message:" %} <code>{{ error.exception_class }}: {{ error.exception_message }}</code></li>
|
||||
{% endif %}
|
||||
|
|
|
@ -3,6 +3,10 @@
|
|||
|
||||
{% block appbar-title %}{% trans "Result" %} #{{ test_result.id }}{% endblock %}
|
||||
|
||||
{% block appbar-actions %}
|
||||
<a href="../run">{% trans "Run tests again" %}</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="section">
|
||||
<h3>{% trans "Details" %}</h3>
|
||||
|
|
|
@ -17,7 +17,14 @@
|
|||
{% if forloop.last %}last{% endif %}
|
||||
{% if forloop.counter == current_page_no %}current{% endif %}
|
||||
{% if forloop.counter < current_page_no %}step-before{% endif %}
|
||||
{% if forloop.counter > current_page_no %}step-after{% endif %}" >
|
||||
{% if forloop.counter > current_page_no %}step-after{% endif %}"
|
||||
{% if forloop.counter < current_page_no %}
|
||||
aria-label="{% blocktrans with page_label=page_label %}Go back to step: {{ page_label }}{% endblocktrans %}"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
data-page-id="{% with page=pages|get:forloop.counter0 %}{{ page.id }}{% endwith %}"
|
||||
{% endif %}
|
||||
>
|
||||
<abbr
|
||||
aria-label="{% blocktrans with page_no=forloop.counter page_count=page_labels|length %}Step {{ page_no }} of {{ page_count }}:{% endblocktrans %}"
|
||||
title="{% blocktrans with page_no=forloop.counter page_count=page_labels|length %}Step {{ page_no }} of {{ page_count }}{% endblocktrans %}"
|
||||
|
|
|
@ -222,6 +222,7 @@ class TestDef(sql.TestDef):
|
|||
def build_formdata(self, objectdef, include_fields=False):
|
||||
formdata = objectdef.data_class()()
|
||||
formdata.just_created()
|
||||
formdata.backoffice_submission = self.is_in_backoffice
|
||||
|
||||
formdata.workflow_traces = []
|
||||
|
||||
|
@ -292,8 +293,7 @@ class TestDef(sql.TestDef):
|
|||
def _run(self, objectdef):
|
||||
formdata = self.run_form_fill(objectdef)
|
||||
if self.agent_id and self.workflow_tests.actions:
|
||||
agent_user = get_publisher().user_class.get(self.agent_id)
|
||||
self.workflow_tests.run(formdata, agent_user)
|
||||
self.workflow_tests.run(formdata)
|
||||
|
||||
def run_form_fill(self, objectdef):
|
||||
self.formdata = formdata = self.build_formdata(objectdef)
|
||||
|
@ -369,6 +369,7 @@ class TestDef(sql.TestDef):
|
|||
'carddef:'
|
||||
):
|
||||
x.data_source = None
|
||||
x.had_data_source = True
|
||||
|
||||
value = self.data['fields'].get(field.id)
|
||||
if value is not None:
|
||||
|
@ -479,10 +480,14 @@ class TestDef(sql.TestDef):
|
|||
testdef.missing_required_fields.append(label)
|
||||
return False
|
||||
|
||||
if widget.error != get_selection_error_text():
|
||||
return True
|
||||
ignore_invalid_selection = bool(
|
||||
widget.error == get_selection_error_text()
|
||||
and (widget.field.data_source or hasattr(widget.field, 'had_data_source'))
|
||||
)
|
||||
if ignore_invalid_selection:
|
||||
return False
|
||||
|
||||
return False
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def get_error_widget(cls, widget, testdef=None):
|
||||
|
|
|
@ -30,6 +30,7 @@ from wcs.sql_criterias import (
|
|||
Equal,
|
||||
Greater,
|
||||
GreaterOrEqual,
|
||||
ILike,
|
||||
Less,
|
||||
LessOrEqual,
|
||||
Not,
|
||||
|
@ -303,9 +304,21 @@ class LazyFormDefObjectsManager:
|
|||
('in', _('in')),
|
||||
('not_in', _('not in')),
|
||||
]
|
||||
text_operators = [
|
||||
('icontains', _('contains')),
|
||||
]
|
||||
if field.key == 'internal-id':
|
||||
return equality_operators + comparison_operators
|
||||
if field.key in ['date', 'item', 'items', 'string', 'text', 'numeric']:
|
||||
if field.key in ['string', 'text']:
|
||||
return (
|
||||
equality_operators
|
||||
+ comparison_operators
|
||||
+ more_comparison_operators
|
||||
+ in_operators
|
||||
+ empty_operators
|
||||
+ text_operators
|
||||
)
|
||||
if field.key in ['date', 'item', 'items', 'numeric']:
|
||||
return (
|
||||
equality_operators
|
||||
+ comparison_operators
|
||||
|
@ -316,7 +329,7 @@ class LazyFormDefObjectsManager:
|
|||
if field.key == 'bool':
|
||||
return equality_operators + empty_operators
|
||||
if field.key == 'email':
|
||||
return equality_operators + in_operators + empty_operators
|
||||
return equality_operators + in_operators + empty_operators + text_operators
|
||||
return None
|
||||
|
||||
def format_value(self, op, value, field):
|
||||
|
@ -412,6 +425,7 @@ class LazyFormDefObjectsManager:
|
|||
'gt': Greater,
|
||||
'gte': GreaterOrEqual,
|
||||
'in': Contains,
|
||||
'icontains': ILike,
|
||||
}
|
||||
|
||||
if isinstance(value, list) and op in ['eq', 'ne']:
|
||||
|
@ -588,6 +602,9 @@ class LazyFormDefObjectsManager:
|
|||
def apply_between(self):
|
||||
return self.apply_op('between')
|
||||
|
||||
def apply_icontains(self):
|
||||
return self.apply_op('icontains')
|
||||
|
||||
def getlist(self, key):
|
||||
return LazyList(self, key)
|
||||
|
||||
|
@ -1886,7 +1903,7 @@ class LazyUser:
|
|||
self._user = user
|
||||
|
||||
def inspect_keys(self):
|
||||
return ['display_name', 'email', 'var', 'nameid']
|
||||
return ['display_name', 'email', 'var', 'nameid', 'has_deleted_account']
|
||||
|
||||
@property
|
||||
def display_name(self):
|
||||
|
@ -1896,6 +1913,10 @@ class LazyUser:
|
|||
def email(self):
|
||||
return self._user.email
|
||||
|
||||
@property
|
||||
def has_deleted_account(self):
|
||||
return bool(self._user.deleted_timestamp)
|
||||
|
||||
@property
|
||||
def var(self):
|
||||
return LazyFormDataVar(self._user.get_formdef().fields, self._user.form_data)
|
||||
|
|
|
@ -167,9 +167,10 @@ class AddAttachmentWorkflowStatusItem(WorkflowStatusItem):
|
|||
self.store_in_backoffice_filefield(
|
||||
formdata, self.backoffice_filefield_id, filename, content_type, outstream.read()
|
||||
)
|
||||
if self.attach_to_history:
|
||||
f.fp.seek(0)
|
||||
evo.add_part(AttachmentEvolutionPart.from_upload(f, varname=self.varname))
|
||||
f.fp.seek(0)
|
||||
evo_part = AttachmentEvolutionPart.from_upload(f, varname=self.varname)
|
||||
evo_part.display_in_history = self.attach_to_history
|
||||
evo.add_part(evo_part)
|
||||
|
||||
def get_parameters(self):
|
||||
parameters = (
|
||||
|
|
|
@ -387,6 +387,8 @@ class CreateFormdataWorkflowStatusItem(WorkflowStatusItem):
|
|||
map_fields_by_varname = False
|
||||
attach_to_history = False
|
||||
cached_field_labels = None
|
||||
draft_edit_operation_mode = 'full' # or 'single' or 'partial'
|
||||
page_identifier = None
|
||||
|
||||
def migrate(self):
|
||||
changed = super().migrate()
|
||||
|
@ -412,6 +414,13 @@ class CreateFormdataWorkflowStatusItem(WorkflowStatusItem):
|
|||
parameters = super().get_inspect_parameters()
|
||||
if self.user_association_mode != 'custom' and 'user_association_template' in parameters:
|
||||
parameters.remove('user_association_template')
|
||||
if not self.draft:
|
||||
if 'draft_edit_operation_mode' in parameters:
|
||||
parameters.remove('draft_edit_operation_mode')
|
||||
if 'page_identifier' in parameters:
|
||||
parameters.remove('page_identifier')
|
||||
if self.draft_edit_operation_mode not in ('single', 'partial') and 'page_identifier' in parameters:
|
||||
parameters.remove('page_identifier')
|
||||
return parameters
|
||||
|
||||
def add_parameters_widgets(self, form, parameters, prefix='', formdef=None, **kwargs):
|
||||
|
@ -443,7 +452,46 @@ class CreateFormdataWorkflowStatusItem(WorkflowStatusItem):
|
|||
**{'data-autocomplete': 'true'},
|
||||
)
|
||||
if 'draft' in parameters:
|
||||
form.add(CheckboxWidget, '%sdraft' % prefix, title=_('Create new draft'), value=self.draft)
|
||||
form.add(
|
||||
CheckboxWidget,
|
||||
'%sdraft' % prefix,
|
||||
title=_('Create new draft'),
|
||||
value=self.draft,
|
||||
attrs={'data-dynamic-display-parent': 'true'},
|
||||
tab=('draft', _('Draft')),
|
||||
)
|
||||
if 'draft_edit_operation_mode' in parameters:
|
||||
form.add(
|
||||
RadiobuttonsWidget,
|
||||
'%sdraft_edit_operation_mode' % prefix,
|
||||
title=_('Operation mode when a draft is created'),
|
||||
options=[
|
||||
('full', _('All pages'), 'full'),
|
||||
('single', _('Single page'), 'single'),
|
||||
('partial', _('From specific page'), 'partial'),
|
||||
],
|
||||
tab=('draft', _('Draft')),
|
||||
value=self.draft_edit_operation_mode,
|
||||
attrs={
|
||||
'data-dynamic-display-parent': 'true',
|
||||
'data-dynamic-display-child-of': f'{prefix}draft',
|
||||
'data-dynamic-display-checked': 'true',
|
||||
},
|
||||
extra_css_class='widget-inline-radio',
|
||||
default_value=self.__class__.draft_edit_operation_mode,
|
||||
)
|
||||
if 'page_identifier' in parameters:
|
||||
form.add(
|
||||
StringWidget,
|
||||
'%spage_identifier' % prefix,
|
||||
title=_('Page Identifier'),
|
||||
value=self.page_identifier,
|
||||
tab=('draft', _('Draft')),
|
||||
attrs={
|
||||
'data-dynamic-display-child-of': '%sdraft_edit_operation_mode' % prefix,
|
||||
'data-dynamic-display-value-in': 'single|partial',
|
||||
},
|
||||
)
|
||||
if 'backoffice_submission' in parameters:
|
||||
form.add(
|
||||
CheckboxWidget,
|
||||
|
@ -566,6 +614,12 @@ class CreateFormdataWorkflowStatusItem(WorkflowStatusItem):
|
|||
mapped_subfield_id = f'{field.id}${subfield.id}'
|
||||
if mapped_subfield_id in mapped_field_ids:
|
||||
self.cached_field_labels[mapped_subfield_id] = f'{field.label} - {subfield.label}'
|
||||
if not self.draft:
|
||||
# cleanup
|
||||
if 'draft_edit_operation_mode' in self.get_parameters():
|
||||
delattr(self, 'draft_edit_operation_mode')
|
||||
if 'page_identifier' in self.get_parameters():
|
||||
delattr(self, 'page_identifier')
|
||||
|
||||
def get_mappings_parameter_view_value(self):
|
||||
to_id_fields = {str(field.id): field for field in self.formdef.get_widget_fields()}
|
||||
|
@ -595,11 +649,13 @@ class CreateFormdataWorkflowStatusItem(WorkflowStatusItem):
|
|||
def get_parameters(self):
|
||||
return (
|
||||
'action_label',
|
||||
'draft',
|
||||
'formdef_slug',
|
||||
'map_fields_by_varname',
|
||||
'mappings',
|
||||
'backoffice_submission',
|
||||
'draft',
|
||||
'draft_edit_operation_mode',
|
||||
'page_identifier',
|
||||
'user_association_mode',
|
||||
'user_association_template',
|
||||
'keep_submission_context',
|
||||
|
@ -737,6 +793,13 @@ class CreateFormdataWorkflowStatusItem(WorkflowStatusItem):
|
|||
if self.draft:
|
||||
new_formdata.status = 'draft'
|
||||
new_formdata.receipt_time = localtime()
|
||||
if self.draft_edit_operation_mode != 'full':
|
||||
new_formdata.workflow_data = {
|
||||
'_create_formdata_draft_edit': {
|
||||
'operation_mode': self.draft_edit_operation_mode,
|
||||
'page_identifier': self.page_identifier,
|
||||
},
|
||||
}
|
||||
new_formdata.store()
|
||||
if formdef.enable_tracking_codes:
|
||||
code.formdata = new_formdata # this will .store() the code
|
||||
|
|
|
@ -938,7 +938,11 @@ class ExportToModel(WorkflowStatusItem):
|
|||
model_file = self.get_model_file()
|
||||
if not model_file:
|
||||
return
|
||||
outstream = self.apply_template_to_formdata(formdata, model_file)
|
||||
try:
|
||||
outstream = self.apply_template_to_formdata(formdata, model_file)
|
||||
except UploadValidationError as e:
|
||||
get_publisher().record_error(str(e), formdata=formdata, exception=e)
|
||||
return
|
||||
filename = self.get_filename(model_file)
|
||||
content_type = model_file.content_type
|
||||
if self.convert_to_pdf:
|
||||
|
|
|
@ -174,6 +174,7 @@ class SendmailWorkflowStatusItem(WorkflowStatusItem):
|
|||
add_element_label=self.get_add_role_label(),
|
||||
element_kwargs={
|
||||
'render_br': False,
|
||||
'other_widget_class': ComputedExpressionWidget,
|
||||
'options': [(None, '---', None)] + self.get_list_of_roles(include_logged_in_users=False),
|
||||
},
|
||||
)
|
||||
|
|
|
@ -151,13 +151,23 @@ class SendSMSWorkflowStatusItem(WorkflowStatusItem):
|
|||
sms_cfg = get_cfg('sms', {})
|
||||
sender = sms_cfg.get('sender', 'AuQuotidien')[:11]
|
||||
try:
|
||||
sms.SMS.get_sms_class().send(sender, recipients, sms_body)
|
||||
self.send_sms(sender, recipients, sms_body)
|
||||
except errors.SMSError as e:
|
||||
get_publisher().record_error(_('Could not send SMS'), formdata=formdata, exception=e)
|
||||
|
||||
def send_sms(self, sender, recipients, sms_body):
|
||||
sms.SMS.get_sms_class().send(sender, recipients, sms_body)
|
||||
|
||||
def i18n_scan(self, base_location):
|
||||
location = '%sitems/%s/' % (base_location, self.id)
|
||||
yield location, None, self.body
|
||||
|
||||
def get_workflow_test_action(self, formdata, *args, **kwargs):
|
||||
def record_sms(sender, recipients, sms_body):
|
||||
formdata.sent_sms.append({'phone_numbers': recipients, 'body': sms_body})
|
||||
|
||||
setattr(self, 'send_sms', record_sms)
|
||||
return self
|
||||
|
||||
|
||||
register_item_class(SendSMSWorkflowStatusItem)
|
||||
|
|
|
@ -17,9 +17,19 @@
|
|||
import datetime
|
||||
import uuid
|
||||
|
||||
from quixote import get_publisher, get_session
|
||||
|
||||
from wcs import wf
|
||||
from wcs.qommon import _
|
||||
from wcs.qommon.form import EmailWidget, IntWidget, SingleSelectWidget, StringWidget, WidgetList
|
||||
from wcs.qommon.form import (
|
||||
EmailWidget,
|
||||
IntWidget,
|
||||
JsonpSingleSelectWidget,
|
||||
RadiobuttonsWidget,
|
||||
SingleSelectWidget,
|
||||
StringWidget,
|
||||
WidgetList,
|
||||
)
|
||||
from wcs.qommon.humantime import humanduration2seconds, seconds2humanduration, timewords
|
||||
from wcs.qommon.xml_storage import XmlStorableObject
|
||||
from wcs.testdef import TestError, WebserviceResponse
|
||||
|
@ -52,7 +62,7 @@ class WorkflowTests(XmlStorableObject):
|
|||
_names = 'workflow_tests'
|
||||
xml_root_node = 'workflow_tests'
|
||||
testdef_id = None
|
||||
actions = None
|
||||
_actions = None
|
||||
|
||||
XML_NODES = [
|
||||
('testdef_id', 'int'),
|
||||
|
@ -61,15 +71,26 @@ class WorkflowTests(XmlStorableObject):
|
|||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.actions = []
|
||||
self._actions = []
|
||||
|
||||
def run(self, formdata, agent_user):
|
||||
@property
|
||||
def actions(self):
|
||||
return self._actions
|
||||
|
||||
@actions.setter
|
||||
def actions(self, actions):
|
||||
self._actions = actions
|
||||
for action in actions:
|
||||
action.parent = self
|
||||
|
||||
def run(self, formdata):
|
||||
self.mock_formdata_methods(formdata)
|
||||
|
||||
# mark formdata as running workflow tests
|
||||
formdata.workflow_test = True
|
||||
|
||||
formdata.frozen_receipt_time = formdata.receipt_time
|
||||
formdata.sent_sms = []
|
||||
formdata.sent_emails = []
|
||||
formdata.used_webservice_responses = self.testdef.used_webservice_responses = []
|
||||
|
||||
|
@ -81,11 +102,12 @@ class WorkflowTests(XmlStorableObject):
|
|||
continue
|
||||
|
||||
if not action.is_assertion:
|
||||
formdata.sent_sms.clear()
|
||||
formdata.sent_emails.clear()
|
||||
formdata.used_webservice_responses.clear()
|
||||
|
||||
try:
|
||||
action.perform(formdata, agent_user)
|
||||
action.perform(formdata)
|
||||
except WorkflowTestError as e:
|
||||
e.action_uuid = action.uuid
|
||||
e.details.append(_('Form status when error occured: %s') % status.name)
|
||||
|
@ -109,7 +131,7 @@ class WorkflowTests(XmlStorableObject):
|
|||
if not self.actions:
|
||||
return '1'
|
||||
|
||||
return str(int(max(x.id for x in self.actions)) + 1)
|
||||
return str(max(int(x.id) for x in self.actions) + 1)
|
||||
|
||||
def add_action(self, action_class):
|
||||
action = action_class(id=self.get_new_action_id())
|
||||
|
@ -119,6 +141,7 @@ class WorkflowTests(XmlStorableObject):
|
|||
def add_actions_from_formdata(self, formdata):
|
||||
test_action_class_by_trace_id = {
|
||||
'sendmail': AssertEmail,
|
||||
'sendsms': AssertSMS,
|
||||
'webservice_call': AssertWebserviceCall,
|
||||
'set-backoffice-fields': AssertBackofficeFieldValues,
|
||||
'button': ButtonClick,
|
||||
|
@ -147,11 +170,6 @@ class WorkflowTests(XmlStorableObject):
|
|||
action = self.add_action(AssertStatus)
|
||||
action.set_attributes_from_trace(formdata.formdef, workflow_traces[-1])
|
||||
|
||||
def store(self, *args, **kwargs):
|
||||
super().store(*args, **kwargs)
|
||||
for action in self.actions:
|
||||
action.parent = self
|
||||
|
||||
def export_actions_to_xml(self, element, attribute_name, **kwargs):
|
||||
for action in self.actions:
|
||||
element.append(action.export_to_xml())
|
||||
|
@ -166,9 +184,7 @@ class WorkflowTests(XmlStorableObject):
|
|||
except KeyError:
|
||||
continue
|
||||
|
||||
action = klass.import_from_xml_tree(sub)
|
||||
action.parent = self
|
||||
actions.append(action)
|
||||
actions.append(klass.import_from_xml_tree(sub))
|
||||
|
||||
return actions
|
||||
|
||||
|
@ -222,16 +238,32 @@ class ButtonClick(WorkflowTestAction):
|
|||
|
||||
key = 'button-click'
|
||||
button_name = None
|
||||
who = 'receiver'
|
||||
who_id = None
|
||||
|
||||
optional_fields = ['who_id']
|
||||
|
||||
is_assertion = False
|
||||
|
||||
XML_NODES = WorkflowTestAction.XML_NODES + [
|
||||
('button_name', 'str'),
|
||||
('who', 'str'),
|
||||
('who_id', 'int'),
|
||||
]
|
||||
|
||||
@property
|
||||
def details_label(self):
|
||||
return _('Click on "%s"') % self.button_name
|
||||
if self.who == 'receiver':
|
||||
user = _('backoffice user')
|
||||
elif self.who == 'submitter':
|
||||
user = _('submitter')
|
||||
else:
|
||||
try:
|
||||
user = get_publisher().user_class.get(self.who_id)
|
||||
except KeyError:
|
||||
user = _('missing user')
|
||||
|
||||
return _('Click on "%(button_name)s" by %(user)s') % {'button_name': self.button_name, 'user': user}
|
||||
|
||||
def set_attributes_from_trace(self, formdef, trace, previous_trace=None):
|
||||
try:
|
||||
|
@ -243,7 +275,21 @@ class ButtonClick(WorkflowTestAction):
|
|||
|
||||
self.button_name = item.label
|
||||
|
||||
def perform(self, formdata, user):
|
||||
def perform(self, formdata):
|
||||
if self.who == 'receiver':
|
||||
user = get_publisher().user_class.get(self.parent.testdef.agent_id)
|
||||
elif self.who == 'submitter':
|
||||
if formdata.user_id:
|
||||
user = get_publisher().user_class.get(formdata.user_id)
|
||||
else:
|
||||
get_session().mark_anonymous_formdata(formdata)
|
||||
user = None
|
||||
else:
|
||||
try:
|
||||
user = get_publisher().user_class.get(self.who_id)
|
||||
except KeyError:
|
||||
raise WorkflowTestError(_('Broken, missing user'))
|
||||
|
||||
status = formdata.get_status()
|
||||
form = status.get_action_form(formdata, user)
|
||||
if not form or not any(
|
||||
|
@ -282,6 +328,31 @@ class ButtonClick(WorkflowTestAction):
|
|||
value=value,
|
||||
)
|
||||
|
||||
form.add(
|
||||
RadiobuttonsWidget,
|
||||
'who',
|
||||
title=_('User who clicks on button'),
|
||||
options=[
|
||||
('receiver', _('Backoffice user'), 'receiver'),
|
||||
('submitter', _('Submitter'), 'submitter'),
|
||||
('other', _('Other user'), 'other'),
|
||||
],
|
||||
value=self.who,
|
||||
attrs={'data-dynamic-display-parent': 'true'},
|
||||
)
|
||||
|
||||
form.attrs['data-enable-select2'] = 'on'
|
||||
form.add(
|
||||
JsonpSingleSelectWidget,
|
||||
'who_id',
|
||||
url='/api/users/',
|
||||
value=self.who_id,
|
||||
attrs={
|
||||
'data-dynamic-display-child-of': 'who',
|
||||
'data-dynamic-display-value-in': 'other',
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class AssertStatus(WorkflowTestAction):
|
||||
label = _('Assert form status')
|
||||
|
@ -305,7 +376,7 @@ class AssertStatus(WorkflowTestAction):
|
|||
|
||||
self.status_name = status.name
|
||||
|
||||
def perform(self, formdata, user):
|
||||
def perform(self, formdata):
|
||||
status = formdata.get_status()
|
||||
if status.name != self.status_name:
|
||||
raise WorkflowTestError(
|
||||
|
@ -365,7 +436,7 @@ class AssertEmail(WorkflowTestAction):
|
|||
|
||||
return label
|
||||
|
||||
def perform(self, formdata, user):
|
||||
def perform(self, formdata):
|
||||
try:
|
||||
email = formdata.sent_emails.pop(0)
|
||||
except IndexError:
|
||||
|
@ -443,7 +514,7 @@ class SkipTime(WorkflowTestAction):
|
|||
formdata.receipt_time = rewind_time(formdata.receipt_time)
|
||||
formdata.evolution[-1].time = rewind_time(formdata.evolution[-1].time)
|
||||
|
||||
def perform(self, formdata, user):
|
||||
def perform(self, formdata):
|
||||
self.rewind(formdata)
|
||||
|
||||
jump_actions = []
|
||||
|
@ -504,7 +575,7 @@ class AssertBackofficeFieldValues(WorkflowTestAction):
|
|||
def details_label(self):
|
||||
return ''
|
||||
|
||||
def perform(self, formdata, user):
|
||||
def perform(self, formdata):
|
||||
for field_dict in self.fields:
|
||||
field_id = field_dict['field_id']
|
||||
expected_value = field_dict['value']
|
||||
|
@ -591,7 +662,7 @@ class AssertWebserviceCall(WorkflowTestAction):
|
|||
)
|
||||
return r
|
||||
|
||||
def perform(self, formdata, user):
|
||||
def perform(self, formdata):
|
||||
try:
|
||||
response = WebserviceResponse.get(self.webservice_response_id)
|
||||
except KeyError:
|
||||
|
@ -627,3 +698,65 @@ class AssertWebserviceCall(WorkflowTestAction):
|
|||
value=self.webservice_response_id,
|
||||
)
|
||||
form.add(IntWidget, 'call_count', title=_('Call count'), required=True, value=self.call_count)
|
||||
|
||||
|
||||
class AssertSMS(WorkflowTestAction):
|
||||
label = _('Assert SMS is sent')
|
||||
|
||||
key = 'assert-sms'
|
||||
phone_numbers = None
|
||||
body = None
|
||||
|
||||
optional_fields = ['phone_numbers', 'body']
|
||||
|
||||
XML_NODES = WorkflowTestAction.XML_NODES + [
|
||||
('phone_numbers', 'str_list'),
|
||||
('body', 'str'),
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.phone_numbers = self.phone_numbers or []
|
||||
|
||||
@property
|
||||
def details_label(self):
|
||||
if not self.phone_numbers:
|
||||
return ''
|
||||
|
||||
label = _('SMS to %s') % self.phone_numbers[0]
|
||||
|
||||
if len(self.phone_numbers) > 1:
|
||||
label = '%s (+%s)' % (label, len(self.phone_numbers) - 1)
|
||||
|
||||
return label
|
||||
|
||||
def perform(self, formdata):
|
||||
try:
|
||||
sms = formdata.sent_sms.pop(0)
|
||||
except IndexError:
|
||||
raise WorkflowTestError(_('No SMS was sent.'))
|
||||
|
||||
for recipient in self.phone_numbers:
|
||||
if recipient not in sms['phone_numbers']:
|
||||
details = [_('SMS phone numbers: %s') % ', '.join(sms['phone_numbers'])]
|
||||
raise WorkflowTestError(_('SMS was not sent to %s.') % recipient, details=details)
|
||||
|
||||
if self.body != sms['body']:
|
||||
details = [_('SMS body: "%s"') % sms['body']]
|
||||
raise WorkflowTestError(_('SMS body mismatch.'), details=details)
|
||||
|
||||
def fill_admin_form(self, form, formdef):
|
||||
form.add(
|
||||
WidgetList,
|
||||
'phone_numbers',
|
||||
title=_('Phone numbers'),
|
||||
value=self.phone_numbers,
|
||||
add_element_label=_('Add phone number'),
|
||||
element_kwargs={'render_br': False, 'size': 50},
|
||||
)
|
||||
form.add(
|
||||
StringWidget,
|
||||
'body',
|
||||
title=_('Body'),
|
||||
value=self.body,
|
||||
)
|
||||
|
|
|
@ -372,6 +372,7 @@ class AttachmentEvolutionPart(EvolutionPart):
|
|||
render_for_fts = None
|
||||
storage = None
|
||||
storage_attrs = None
|
||||
display_in_history = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
@ -452,6 +453,9 @@ class AttachmentEvolutionPart(EvolutionPart):
|
|||
|
||||
return odict
|
||||
|
||||
def is_hidden(self):
|
||||
return bool(not self.display_in_history)
|
||||
|
||||
def view(self):
|
||||
show_link = True
|
||||
if self.has_redirect_url():
|
||||
|
@ -730,6 +734,18 @@ class WorkflowVariablesFieldsFormDef(FormDef):
|
|||
def is_readonly(self):
|
||||
return self.workflow.is_readonly()
|
||||
|
||||
def migrate(self):
|
||||
changed = False
|
||||
for field in self.fields or []:
|
||||
changed |= field.migrate()
|
||||
if getattr(field, 'prefill', None): # 2024-03-11
|
||||
# prefill attribute is no longer advertised for workflow variables,
|
||||
# reset its value if it had one, so ancient python prefills do not
|
||||
# persist.
|
||||
field.prefill = None
|
||||
changed = True
|
||||
return changed
|
||||
|
||||
|
||||
class WorkflowBackofficeFieldsFormDef(FormDef):
|
||||
"""Class to handle workflow backoffice fields, it loads and saves from/to
|
||||
|
@ -816,6 +832,9 @@ class Workflow(StorableObject):
|
|||
for field in self.backoffice_fields_formdef.fields:
|
||||
changed |= field.migrate()
|
||||
|
||||
if self.variables_formdef:
|
||||
changed |= self.variables_formdef.migrate()
|
||||
|
||||
if not self.global_actions:
|
||||
self.global_actions = []
|
||||
|
||||
|
@ -1214,12 +1233,17 @@ class Workflow(StorableObject):
|
|||
return root
|
||||
|
||||
@classmethod
|
||||
def import_from_xml(cls, fd, include_id=False, check_datasources=True):
|
||||
def import_from_xml(cls, fd, include_id=False, check_datasources=True, check_deprecated=True):
|
||||
try:
|
||||
tree = ET.parse(fd)
|
||||
except Exception:
|
||||
raise ValueError()
|
||||
workflow = cls.import_from_xml_tree(tree, include_id=include_id, check_datasources=check_datasources)
|
||||
workflow = cls.import_from_xml_tree(
|
||||
tree,
|
||||
include_id=include_id,
|
||||
check_datasources=check_datasources,
|
||||
check_deprecated=check_deprecated,
|
||||
)
|
||||
|
||||
if workflow.slug and cls.get_by_slug(workflow.slug):
|
||||
# slug already in use, reset so a new one will be generated on store()
|
||||
|
@ -1228,7 +1252,11 @@ class Workflow(StorableObject):
|
|||
return workflow
|
||||
|
||||
@classmethod
|
||||
def import_from_xml_tree(cls, tree, include_id=False, snapshot=False, check_datasources=True):
|
||||
def import_from_xml_tree(
|
||||
cls, tree, include_id=False, snapshot=False, check_datasources=True, check_deprecated=True
|
||||
):
|
||||
from wcs.backoffice.deprecations import DeprecatedElementsDetected, DeprecationsScan
|
||||
|
||||
workflow = cls()
|
||||
if tree.find('name') is None or not tree.find('name').text:
|
||||
raise WorkflowImportError(_('Missing name'))
|
||||
|
@ -1340,6 +1368,14 @@ class Workflow(StorableObject):
|
|||
_('Unknown referenced objects'), details=unknown_referenced_objects_details
|
||||
)
|
||||
|
||||
if check_deprecated:
|
||||
# check for deprecated elements
|
||||
job = DeprecationsScan()
|
||||
try:
|
||||
job.check_deprecated_elements_in_object(workflow)
|
||||
except DeprecatedElementsDetected as e:
|
||||
raise WorkflowImportError(str(e))
|
||||
|
||||
return workflow
|
||||
|
||||
def get_list_of_roles(
|
||||
|
@ -3049,7 +3085,9 @@ class WorkflowStatusItem(XmlSerialisable):
|
|||
return ''
|
||||
|
||||
def get_admin_url(self):
|
||||
return self.parent.get_admin_url() + 'items/%s/' % self.id
|
||||
if self.parent:
|
||||
return self.parent.get_admin_url() + 'items/%s/' % self.id
|
||||
return ''
|
||||
|
||||
def get_inspect_details(self):
|
||||
return getattr(self, 'label', '')
|
||||
|
@ -3114,7 +3152,10 @@ class WorkflowStatusItem(XmlSerialisable):
|
|||
def check_condition(self, formdata, record_errors=True):
|
||||
context = {'formdata': formdata, 'status_item': self}
|
||||
try:
|
||||
return Condition(self.condition, context, record_errors=record_errors).evaluate()
|
||||
return Condition(self.condition, context, record_errors=record_errors).evaluate(
|
||||
source_label=str(self.description),
|
||||
source_url=self.get_admin_url(),
|
||||
)
|
||||
except RuntimeError:
|
||||
return False
|
||||
|
||||
|
@ -3514,7 +3555,7 @@ class WorkflowStatusItem(XmlSerialisable):
|
|||
uploads.append(complex_value)
|
||||
|
||||
# 2. python expressions
|
||||
if attachments:
|
||||
if attachments and not get_publisher().has_site_option('forbid-python-expressions'):
|
||||
global_eval_dict = get_publisher().get_global_eval_dict()
|
||||
local_eval_dict = get_publisher().substitutions.get_context_variables()
|
||||
for attachment in attachments:
|
||||
|
|
|
@ -44,6 +44,10 @@ from .qommon.template import Template
|
|||
from .qommon.xml_storage import XmlStorableObject
|
||||
|
||||
|
||||
class NamedWsCallImportError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class PayloadError(Exception):
|
||||
pass
|
||||
|
||||
|
@ -280,6 +284,24 @@ class NamedWsCall(XmlStorableObject):
|
|||
if self.request.get('post_data'):
|
||||
yield from self.request.get('post_data').values()
|
||||
|
||||
@classmethod
|
||||
def import_from_xml_tree(cls, tree, include_id=False, check_deprecated=True, **kwargs):
|
||||
from wcs.backoffice.deprecations import DeprecatedElementsDetected, DeprecationsScan
|
||||
|
||||
wscall = super().import_from_xml_tree(
|
||||
tree, include_id=include_id, check_deprecated=check_deprecated, **kwargs
|
||||
)
|
||||
|
||||
if check_deprecated:
|
||||
# check for deprecated elements
|
||||
job = DeprecationsScan()
|
||||
try:
|
||||
job.check_deprecated_elements_in_object(wscall)
|
||||
except DeprecatedElementsDetected as e:
|
||||
raise NamedWsCallImportError(str(e))
|
||||
|
||||
return wscall
|
||||
|
||||
def export_request_to_xml(self, element, attribute_name, **kwargs):
|
||||
request = getattr(self, attribute_name)
|
||||
for attr in ('url', 'request_signature_key', 'method', 'timeout', 'cache_duration'):
|
||||
|
|
Loading…
Reference in New Issue