From 215209fb98bdd4ade58b624a32c23e19d8911b0c Mon Sep 17 00:00:00 2001 From: Nicolas ROCHE Date: Thu, 10 Oct 2019 18:01:34 +0200 Subject: [PATCH] admin: protect datasources in use from deletion or slug change (#15163) --- tests/test_admin_pages.py | 56 +++++++++++++++++++++++++++++++++++++++ tests/test_datasource.py | 19 +++++++++++++ wcs/admin/data_sources.py | 30 +++++++++++---------- wcs/data_sources.py | 10 +++++++ 4 files changed, 101 insertions(+), 14 deletions(-) diff --git a/tests/test_admin_pages.py b/tests/test_admin_pages.py index aae45e21f..c12b6c195 100644 --- a/tests/test_admin_pages.py +++ b/tests/test_admin_pages.py @@ -4817,6 +4817,7 @@ def test_data_sources_edit(pub): data_source.data_source = {'type': 'formula', 'value': '[]'} data_source.store() + FormDef.wipe() app = login(get_app(pub)) resp = app.get('/backoffice/settings/data-sources/1/') @@ -4872,6 +4873,33 @@ def test_data_sources_delete(pub): resp = resp.follow() assert NamedDataSource.count() == 0 +def test_data_sources_in_use_delete(pub): + create_superuser(pub) + NamedDataSource.wipe() + category = NamedDataSource(name='foobar') + category.store() + + FormDef.wipe() + formdef = FormDef() + formdef.name = 'form title' + formdef.fields = [ + fields.ItemField(id='0', label='string', type='item', + data_source={'type': 'foobar'}), + ] + formdef.store() + + app = login(get_app(pub)) + resp = app.get('/backoffice/settings/data-sources/1/') + resp = resp.click(href='delete') + assert 'This datasource is still used, it cannot be deleted.' in resp.text + assert 'delete-button' not in resp.text + + formdef.fields = [] + formdef.store() + resp = app.get('/backoffice/settings/data-sources/1/') + resp = resp.click(href='delete') + assert 'delete-button' in resp.text + def test_data_sources_edit_slug(pub): create_superuser(pub) NamedDataSource.wipe() @@ -4906,6 +4934,34 @@ def test_data_sources_edit_slug(pub): resp = resp.forms[0].submit('submit') assert resp.location == 'http://example.net/backoffice/settings/data-sources/1/' +def test_data_sources_in_use_edit_slug(pub): + create_superuser(pub) + NamedDataSource.wipe() + data_source = NamedDataSource(name='foobar') + data_source.data_source = {'type': 'formula', 'value': '[]'} + data_source.store() + assert NamedDataSource.get(1).slug == 'foobar' + + FormDef.wipe() + formdef = FormDef() + formdef.name = 'form title' + formdef.fields = [ + fields.ItemField(id='0', label='string', type='item', + data_source={'type': 'foobar'}), + ] + formdef.store() + + app = login(get_app(pub)) + resp = app.get('/backoffice/settings/data-sources/1/') + resp = resp.click(href='edit') + assert 'form_slug' not in resp.text + + formdef.fields = [] + formdef.store() + resp = app.get('/backoffice/settings/data-sources/1/') + resp = resp.click(href='edit') + assert 'form_slug' in resp.text + def test_wscalls_new(pub): create_superuser(pub) NamedWsCall.wipe() diff --git a/tests/test_datasource.py b/tests/test_datasource.py index 8c14d6dad..73798c89d 100644 --- a/tests/test_datasource.py +++ b/tests/test_datasource.py @@ -477,3 +477,22 @@ def test_named_datasource_json_cache(requests_pub): assert data_sources.get_structured_items({'type': 'foobar'}) == [ {'id': '1', 'text': 'foo'}, {'id': '2', 'text': 'bar'}] assert urlopen.call_count == 3 + +def test_named_datasource_in_formdef(): + from wcs.formdef import FormDef + datasource = NamedDataSource(name='foobar') + datasource.data_source = {'type': 'json', 'value': 'http://whatever/'} + datasource.store() + assert datasource.slug == 'foobar' + + formdef = FormDef() + assert not datasource.is_used_in_formdef(formdef) + + formdef.fields = [ + fields.ItemField(id='0', label='string', type='item', + data_source={'type': 'foobar'}), + ] + assert datasource.is_used_in_formdef(formdef) + + datasource.slug = 'barfoo' + assert not datasource.is_used_in_formdef(formdef) diff --git a/wcs/admin/data_sources.py b/wcs/admin/data_sources.py index 8da1fc04c..31a083bb2 100644 --- a/wcs/admin/data_sources.py +++ b/wcs/admin/data_sources.py @@ -25,7 +25,7 @@ from ..qommon.misc import json_response from ..qommon.backoffice.menu import html_top from wcs.data_sources import (NamedDataSource, DataSourceSelectionWidget, get_structured_items) -from wcs.formdef import FormDef +from wcs.formdef import FormDef, get_formdefs_of_all_kinds class NamedDataSourceUI(object): def __init__(self, datasource): @@ -33,6 +33,12 @@ class NamedDataSourceUI(object): if self.datasource is None: self.datasource = NamedDataSource() + def is_used(self): + for formdef in get_formdefs_of_all_kinds(): + if self.datasource.is_used_in_formdef(formdef): + return True + return False + def get_form(self): form = Form(enctype='multipart/form-data', advanced_label=_('Additional options')) @@ -78,11 +84,10 @@ class NamedDataSourceUI(object): 'data-dynamic-display-child-of': 'data_source$type', 'data-dynamic-display-value': 'json', }) - if self.datasource.slug: + if self.datasource.slug and not self.is_used(): form.add(StringWidget, 'slug', value=self.datasource.slug, title=_('Identifier'), - hint=_('Beware it is risky to change it'), required=True, advanced=True, ) form.add_submit('submit', _('Submit')) @@ -138,15 +143,8 @@ class NamedDataSourcePage(Directory): def usage_in_formdefs(self): formdefs = [] for formdef in FormDef.select(ignore_errors=True, ignore_migration=True, order_by='name'): - for field in (formdef.fields or []): - data_source = getattr(field, 'data_source', None) - if not data_source: - continue - if data_source.get('type') == self.datasource.slug: - formdefs.append(formdef) - break - else: - continue + if self.datasource.is_used_in_formdef(formdef): + formdefs.append(formdef) return formdefs def preview_block(self): @@ -202,9 +200,13 @@ class NamedDataSourcePage(Directory): def delete(self): form = Form(enctype='multipart/form-data') - form.widgets.append(HtmlWidget('

%s

' % _( + if not self.datasource_ui.is_used(): + form.widgets.append(HtmlWidget('

%s

' % _( 'You are about to irrevocably delete this data source.'))) - form.add_submit('delete', _('Submit')) + form.add_submit('delete', _('Submit')) + else: + form.widgets.append(HtmlWidget('

%s

' % _( + 'This datasource is still used, it cannot be deleted.'))) form.add_submit('cancel', _('Cancel')) if form.get_widget('cancel').parse(): return redirect('..') diff --git a/wcs/data_sources.py b/wcs/data_sources.py index 1fbce3536..5bafd4d06 100644 --- a/wcs/data_sources.py +++ b/wcs/data_sources.py @@ -419,6 +419,16 @@ class NamedDataSource(XmlStorableObject): def humanized_cache_duration(self): return seconds2humanduration(int(self.cache_duration)) + def is_used_in_formdef(self, formdef): + from .fields import WidgetField + for field in formdef.fields or []: + data_source = getattr(field, 'data_source', None) + if not data_source: + continue + if data_source.get('type') == self.slug: + return True + return False + class DataSourcesSubstitutionProxy(object): def __getattr__(self, attr):