depreciations: errors from deprecated elements on import (#72093)

This commit is contained in:
Lauréline Guérin 2024-03-04 15:02:16 +01:00 committed by Lauréline Guérin
parent 86f28b8037
commit eba79fdc77
13 changed files with 364 additions and 13 deletions

View File

@ -220,6 +220,25 @@ def test_block_export_import(pub):
assert 'Invalid File (Unknown referenced objects)' in resp assert 'Invalid File (Unknown referenced objects)' in resp
assert '<ul><li>Unknown datasources: foobar</li></ul>' 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): def test_block_delete(pub):
create_superuser(pub) create_superuser(pub)

View File

@ -1039,6 +1039,23 @@ def test_data_sources_import(pub):
resp = resp.form.submit() resp = resp.form.submit()
assert 'Invalid File' in resp.text 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): def test_data_sources_edit_slug(pub):
create_superuser(pub) create_superuser(pub)

View File

@ -4077,6 +4077,39 @@ def test_form_overwrite(pub):
assert resp.pyquery('.error').text() == 'Invalid File' 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): def test_form_export_import_export_overwrite(pub):
create_superuser(pub) create_superuser(pub)
create_role(pub) create_role(pub)
@ -4134,6 +4167,23 @@ def test_form_export_import_export_overwrite(pub):
field_ow = formdef_overwrited.fields[i] field_ow = formdef_overwrited.fields[i]
assert (field.id, field.label, field.key) == (field_ow.id, field_ow.label, field_ow.key) 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): def test_form_overwrite_from_url(pub):
create_superuser(pub) create_superuser(pub)

View File

@ -399,6 +399,93 @@ def test_settings_export_import(pub):
resp.form['file'] = Upload('export.wcs', zip_content.getvalue()) resp.form['file'] = Upload('export.wcs', zip_content.getvalue())
resp = resp.form.submit('submit').follow() resp = resp.form.submit('submit').follow()
assert 'Unknown referenced objects [Unknown datasources: foobar]' in resp 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 # check a backup of settings has been created
assert [x for x in os.listdir(pub.app_dir) if x.startswith('config.pck.backup-')] assert [x for x in os.listdir(pub.app_dir) if x.startswith('config.pck.backup-')]

View File

@ -990,6 +990,28 @@ def test_workflows_export_import(pub):
assert 'Invalid File' in resp.text assert 'Invalid File' in resp.text
assert Workflow.count() == 2 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): def test_workflows_import_from_url(pub):
create_superuser(pub) create_superuser(pub)

View File

@ -1,4 +1,6 @@
import io import io
import os
import re
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
import pytest import pytest
@ -207,6 +209,23 @@ def test_wscalls_import(pub, wscall):
resp = resp.form.submit() resp = resp.form.submit()
assert 'Invalid File' in resp.text 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): def test_wscalls_empty_param_values(pub):
create_superuser(pub) create_superuser(pub)

View File

@ -2046,6 +2046,97 @@ def test_export_import_workflow_options(pub):
assert formdef.workflow_options == {'foo': 'bar2'} 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): def test_api_export_import_invalid_slug(pub):
pub.role_class.wipe() pub.role_class.wipe()
role1 = pub.role_class(name='Test role 1') role1 = pub.role_class(name='Test role 1')

View File

@ -27,6 +27,7 @@ from wcs.categories import CardDefCategory, DataSourceCategory
from wcs.data_sources import ( from wcs.data_sources import (
DataSourceSelectionWidget, DataSourceSelectionWidget,
NamedDataSource, NamedDataSource,
NamedDataSourceImportError,
RefreshAgendas, RefreshAgendas,
get_structured_items, get_structured_items,
has_chrono, has_chrono,
@ -667,15 +668,22 @@ class NamedDataSourcesDirectory(Directory):
def import_submit(self, form): def import_submit(self, form):
fp = form.get_widget('file').parse().fp fp = form.get_widget('file').parse().fp
error = False error, reason = False, None
try: try:
datasource = NamedDataSource.import_from_xml(fp) datasource = NamedDataSource.import_from_xml(fp)
get_session().message = ('info', _('This datasource has been successfully imported.')) get_session().message = ('info', _('This datasource has been successfully imported.'))
except NamedDataSourceImportError as e:
error = True
reason = str(e)
except ValueError: except ValueError:
error = True error = True
if error: 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() raise ValueError()
try: try:

View File

@ -36,7 +36,7 @@ from quixote.html import TemplateIO, htmltext
from wcs.api_access import ApiAccess from wcs.api_access import ApiAccess
from wcs.blocks import BlockDef, BlockdefImportError from wcs.blocks import BlockDef, BlockdefImportError
from wcs.carddef import CardDef 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.fields.map import MapOptionsMixin
from wcs.formdef import FormDef, FormdefImportError, get_formdefs_of_all_kinds from wcs.formdef import FormDef, FormdefImportError, get_formdefs_of_all_kinds
from wcs.qommon import _, audit, errors, get_cfg, ident, misc, pgettext_lazy, template from wcs.qommon import _, audit, errors, get_cfg, ident, misc, pgettext_lazy, template
@ -61,6 +61,7 @@ from wcs.qommon.form import (
TextWidget, TextWidget,
) )
from wcs.workflows import Workflow, WorkflowImportError from wcs.workflows import Workflow, WorkflowImportError
from wcs.wscalls import NamedWsCallImportError
from .api_access import ApiAccessDirectory from .api_access import ApiAccessDirectory
from .data_sources import NamedDataSourcesDirectory from .data_sources import NamedDataSourcesDirectory
@ -1512,7 +1513,10 @@ class SiteImportAfterJob(AfterJob):
msg = _(e.msg) % e.msg_args msg = _(e.msg) % e.msg_args
if e.details: if e.details:
msg += ' [%s]' % 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 self.results = results
if error: if error:

View File

@ -26,7 +26,7 @@ from wcs.backoffice.snapshots import SnapshotsDirectory
from wcs.qommon import _, errors, misc, template from wcs.qommon import _, errors, misc, template
from wcs.qommon.form import CheckboxWidget, FileWidget, Form, HtmlWidget, SlugWidget, StringWidget, TextWidget from wcs.qommon.form import CheckboxWidget, FileWidget, Form, HtmlWidget, SlugWidget, StringWidget, TextWidget
from wcs.utils import grep_strings from wcs.utils import grep_strings
from wcs.wscalls import NamedWsCall, WsCallRequestWidget from wcs.wscalls import NamedWsCall, NamedWsCallImportError, WsCallRequestWidget
class NamedWsCallUI: class NamedWsCallUI:
@ -317,15 +317,22 @@ class NamedWsCallsDirectory(Directory):
def import_submit(self, form): def import_submit(self, form):
fp = form.get_widget('file').parse().fp fp = form.get_widget('file').parse().fp
error = False error, reason = False, None
try: try:
wscall = NamedWsCall.import_from_xml(fp) wscall = NamedWsCall.import_from_xml(fp)
get_session().message = ('info', _('This webservice call has been successfully imported.')) get_session().message = ('info', _('This webservice call has been successfully imported.'))
except NamedWsCallImportError as e:
error = True
reason = str(e)
except ValueError: except ValueError:
error = True error = True
if error: 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() raise ValueError()
try: try:

View File

@ -26,7 +26,7 @@ from quixote import get_publisher, get_response
from wcs.api_utils import is_url_signed from wcs.api_utils import is_url_signed
from wcs.applications import Application, ApplicationElement from wcs.applications import Application, ApplicationElement
from wcs.blocks import BlockDef from wcs.blocks import BlockDef, BlockdefImportError
from wcs.carddef import CardDef from wcs.carddef import CardDef
from wcs.categories import ( from wcs.categories import (
BlockCategory, BlockCategory,
@ -38,12 +38,12 @@ from wcs.categories import (
WorkflowCategory, WorkflowCategory,
) )
from wcs.comment_templates import CommentTemplate from wcs.comment_templates import CommentTemplate
from wcs.data_sources import NamedDataSource from wcs.data_sources import NamedDataSource, NamedDataSourceImportError
from wcs.formdef import FormDef from wcs.formdef import FormDef, FormdefImportError
from wcs.mail_templates import MailTemplate from wcs.mail_templates import MailTemplate
from wcs.sql import Equal, Role from wcs.sql import Equal, Role
from wcs.workflows import Workflow from wcs.workflows import Workflow, WorkflowImportError
from wcs.wscalls import NamedWsCall from wcs.wscalls import NamedWsCall, NamedWsCallImportError
from .qommon import _ from .qommon import _
from .qommon.afterjobs import AfterJob from .qommon.afterjobs import AfterJob
@ -454,6 +454,15 @@ class BundleImportJob(AfterJob):
# remove obsolete application elements # remove obsolete application elements
self.unlink_obsolete_objects() self.unlink_obsolete_objects()
except (
BlockdefImportError,
FormdefImportError,
WorkflowImportError,
NamedDataSourceImportError,
NamedWsCallImportError,
) as e:
error = str(e)
except tarfile.TarError: except tarfile.TarError:
error = _('Invalid tar file.') error = _('Invalid tar file.')
except BundleKeyError as e: except BundleKeyError as e:

View File

@ -292,6 +292,8 @@ class WcsPublisher(QommonPublisher):
'workflows_xml', 'workflows_xml',
'blockdefs_xml', 'blockdefs_xml',
'roles_xml', 'roles_xml',
'datasources',
'wscalls',
): ):
continue continue
path = os.path.join(self.app_dir, f) path = os.path.join(self.app_dir, f)
@ -346,6 +348,22 @@ class WcsPublisher(QommonPublisher):
if os.path.split(f)[0] in results: if os.path.split(f)[0] in results:
results[os.path.split(f)[0]] += 1 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 # second pass, fields blocks
from wcs.blocks import BlockDef from wcs.blocks import BlockDef

View File

@ -92,7 +92,7 @@ class XmlStorableObject(StorableObject):
return cls.import_from_xml_tree(tree, include_id=include_id, check_deprecated=check_deprecated) return cls.import_from_xml_tree(tree, include_id=include_id, check_deprecated=check_deprecated)
@classmethod @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() obj = cls()
# if the tree we get is actually a ElementTree for real, we get its # if the tree we get is actually a ElementTree for real, we get its