From d67ee9aec598a9622d21f3b272b89c277ca8ce35 Mon Sep 17 00:00:00 2001
From: Nicolas ROCHE <nroche@entrouvert.com>
Date: Thu, 10 Oct 2019 18:01:34 +0200
Subject: [PATCH 1/2] datasources: refuse to delete or change slug of
 datasources in use (#15163)

---
 tests/test_admin_pages.py | 43 +++++++++++++++++++++++++++++++++++++++
 wcs/admin/data_sources.py | 24 +++++++++++++---------
 wcs/formdef.py            |  9 ++++++++
 3 files changed, 66 insertions(+), 10 deletions(-)

diff --git a/tests/test_admin_pages.py b/tests/test_admin_pages.py
index 6cbcfbed..ad1ecd9c 100644
--- a/tests/test_admin_pages.py
+++ b/tests/test_admin_pages.py
@@ -4855,6 +4855,25 @@ 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 = app.get('/backoffice/settings/data-sources/1/delete', status=404)
+
 def test_data_sources_edit_slug(pub):
     create_superuser(pub)
     NamedDataSource.wipe()
@@ -4889,6 +4908,30 @@ 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()
+
+    FormDef.wipe()
+    formdef = FormDef()
+    app = login(get_app(pub))
+    resp = app.get('/backoffice/settings/data-sources/1/')
+    resp = resp.click(href='edit')
+
+    formdef.name = 'form title'
+    formdef.fields = [
+        fields.ItemField(id='0', label='string', type='item',
+                data_source={'type': 'foobar'}),
+    ]
+    formdef.store()
+
+    assert NamedDataSource.get(1).slug == 'foobar'
+    resp.forms[0]['slug'] = 'barfoo'
+    resp = resp.forms[0].submit('submit', status=404)
+
 def test_wscalls_new(pub):
     create_superuser(pub)
     NamedWsCall.wipe()
diff --git a/wcs/admin/data_sources.py b/wcs/admin/data_sources.py
index 06c32712..8f206e40 100644
--- a/wcs/admin/data_sources.py
+++ b/wcs/admin/data_sources.py
@@ -20,12 +20,13 @@ from quixote.html import TemplateIO, htmltext
 
 from ..qommon import _
 from ..qommon import template
+from ..qommon import errors
 from ..qommon.form import *
 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 +34,12 @@ class NamedDataSourceUI(object):
         if self.datasource is None:
             self.datasource = NamedDataSource()
 
+    def is_datasource_in_use(self):
+        for formdef in get_formdefs_of_all_kinds():
+            if formdef.is_using_datasource(self.datasource):
+                return True
+        return False
+
     def get_form(self):
         form = Form(enctype='multipart/form-data',
                 advanced_label=_('Additional options'))
@@ -95,6 +102,8 @@ class NamedDataSourceUI(object):
             slug = form.get_widget('slug').parse()
         else:
             slug = None
+        if self.datasource.slug != slug and self.is_datasource_in_use():
+            raise errors.TraversalError()
 
         for nds in NamedDataSource.select():
             if nds.id == self.datasource.id:
@@ -135,15 +144,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 formdef.is_using_datasource(self.datasource):
+                formdefs.append(formdef)
         return formdefs
 
     def preview_block(self):
@@ -198,6 +200,8 @@ class NamedDataSourcePage(Directory):
         return r.getvalue()
 
     def delete(self):
+        if self.datasource_ui.is_datasource_in_use():
+            raise errors.TraversalError()
         form = Form(enctype='multipart/form-data')
         form.widgets.append(HtmlWidget('<p>%s</p>' % _(
                         'You are about to irrevocably delete this data source.')))
diff --git a/wcs/formdef.py b/wcs/formdef.py
index c339e792..d3b459dc 100644
--- a/wcs/formdef.py
+++ b/wcs/formdef.py
@@ -1414,6 +1414,15 @@ class FormDef(StorableObject):
             if changed:
                 formdef.store()
 
+    def is_using_datasource(self, datasource):
+        for field in (self.fields or []):
+            data_source = getattr(field, 'data_source', None)
+            if not data_source:
+                continue
+            if data_source.get('type') == datasource.slug:
+                return True
+        return False
+
     class _EmptyClass(object):  # helper for instance creation without calling __init__
         pass
 
-- 
2.23.0

