From 10f9f891884102001107cc7eaa6ca82802fcf16a Mon Sep 17 00:00:00 2001
From: Nicolas ROCHE <nroche@entrouvert.com>
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 aae45e21..c12b6c19 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 8c14d6da..73798c89 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 8da1fc04..31a083bb 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('<p>%s</p>' % _(
+        if not self.datasource_ui.is_used():
+            form.widgets.append(HtmlWidget('<p>%s</p>' % _(
                         'You are about to irrevocably delete this data source.')))
-        form.add_submit('delete', _('Submit'))
+            form.add_submit('delete', _('Submit'))
+        else:
+            form.widgets.append(HtmlWidget('<p>%s</p>' % _(
+                        '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 1fbce353..5bafd4d0 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):
-- 
2.23.0

