From dad968dd6c6d2b7efb42434c090dc06c41268f00 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Tue, 20 Oct 2020 16:57:37 +0200
Subject: [PATCH] cards: empty digest template when datasource custom views
 (#46862)

---
 tests/test_cards_admin_pages.py | 60 +++++++++++++++++++++++++++++++++
 wcs/admin/forms.py              | 33 +++++++++++++++---
 wcs/backoffice/cards.py         | 25 ++------------
 wcs/carddef.py                  | 20 ++++++++++-
 4 files changed, 110 insertions(+), 28 deletions(-)

diff --git a/tests/test_cards_admin_pages.py b/tests/test_cards_admin_pages.py
index 9ac76f56..60e1a52b 100644
--- a/tests/test_cards_admin_pages.py
+++ b/tests/test_cards_admin_pages.py
@@ -137,6 +137,66 @@ def test_card_workflow_change(pub, studio):
     resp = resp.form.submit('submit').follow()
 
 
+def test_card_digest_template(pub, studio):
+    create_superuser(pub)
+
+    CardDef.wipe()
+    carddef = CardDef()
+    carddef.name = 'foo'
+    carddef.fields = [
+        fields.StringField(id='1', label='Test', type='string', varname='foo'),
+    ]
+    carddef.digest_template = 'X{{ form_var_foo }}Y'
+    carddef.store()
+    carddef.data_class().wipe()
+    carddata = carddef.data_class()()
+    carddata.data = {'1': 'bar'}
+    carddata.store()
+
+    custom_view = pub.custom_view_class()
+    custom_view.title = 'card view'
+    custom_view.formdef = carddef
+    custom_view.columns = {'list': [{'id': 'id'}]}
+    custom_view.filters = {'filter-1': True, 'filter-1-value': 'FOO'}
+    custom_view.visibility = 'datasource'
+    custom_view.store()
+
+    # carddef not used in formdef, it's ok to empty digest_template
+    FormDef.wipe()
+
+    app = login(get_app(pub))
+    resp = app.get('/backoffice/cards/%s/options/templates' % carddef.id)
+    resp.form['digest_template'] = ''
+    resp = resp.form.submit().follow()
+    carddef = CardDef.get(carddef.id)
+    assert carddef.digest_template is None
+    assert 'Existing cards will be updated in the background.' in resp.text
+
+    carddef.digest_template = '{{ form_var_foo }}'
+    carddef.store()
+
+    # a formdef using the carddef as datasource
+    FormDef.wipe()
+    formdef = FormDef()
+    formdef.name = 'test card def on data source'
+    formdef.fields = [
+        fields.ItemField(id='1', label='item', data_source={'type': 'carddef:foo'})
+    ]
+    formdef.store()
+
+    # carddef used in formdef, can not empty digest_template
+    resp = app.get('/backoffice/cards/%s/options/templates' % carddef.id)
+    resp.form['digest_template'] = ''
+    resp = resp.form.submit()
+    assert 'Can not empty digest template: this card model is used as data source in some forms.' in resp.text
+    carddef = CardDef.get(carddef.id)
+    assert carddef.digest_template is not None
+
+    # error: not stored, and no after jobs
+    resp = app.get('/backoffice/cards/%s/' % carddef.id)
+    assert 'Existing cards will be updated in the background.' not in resp.text
+
+
 def test_card_custom_view_data_source(pub, studio):
     user = create_superuser(pub)
     Role.wipe()
diff --git a/wcs/admin/forms.py b/wcs/admin/forms.py
index 81103819..2d267de4 100644
--- a/wcs/admin/forms.py
+++ b/wcs/admin/forms.py
@@ -40,6 +40,7 @@ from wcs.qommon.misc import C_
 from wcs.qommon.afterjobs import AfterJob
 
 from wcs.formdef import FormDef, FormdefImportError, FormdefImportRecoverableError
+from wcs.carddef import CardDef
 from wcs.categories import Category
 from wcs.roles import Role, logged_users_role, get_user_roles
 from wcs.workflows import Workflow
@@ -279,8 +280,12 @@ class OptionsDirectory(Directory):
                 for formdata in self.formdef.data_class().select(order_by='id'):
                     formdata.store()
             job = get_response().add_after_job(N_('Updating digests'), update)
-            get_session().message = ('info',
-                    _('Existing forms will be updated in the background.'))
+            if isinstance(self.formdef, CardDef):
+                get_session().message = ('info',
+                        _('Existing cards will be updated in the background.'))
+            else:
+                get_session().message = ('info',
+                        _('Existing forms will be updated in the background.'))
         return result
 
     def handle(self, form, title):
@@ -302,6 +307,10 @@ class OptionsDirectory(Directory):
             for attr in attrs:
                 widget = form.get_widget(attr)
                 if widget:
+                    if hasattr(self, 'clean_%s' % attr):
+                        has_error = getattr(self, 'clean_%s' % attr)(form)
+                        if has_error:
+                            continue
                     if attr == 'geoloc_label':
                         if widget.parse():
                             self.formdef.geolocations = {'base': widget.parse()}
@@ -312,8 +321,9 @@ class OptionsDirectory(Directory):
                         if getattr(self.formdef, attr, None) != new_value:
                             self.changed = True
                             setattr(self.formdef, attr, new_value)
-            self.formdef.store(comment=_('Changed "%s" parameters') % title)
-            return redirect('..')
+            if not form.has_errors():
+                self.formdef.store(comment=_('Changed "%s" parameters') % title)
+                return redirect('..')
 
         html_top('forms', title=self.formdef.name)
         r = TemplateIO(html=True)
@@ -321,6 +331,21 @@ class OptionsDirectory(Directory):
         r += form.render()
         return r.getvalue()
 
+    def clean_digest_template(self, form):
+        if not isinstance(self.formdef, CardDef):
+            return False
+
+        widget = form.get_widget('digest_template')
+        new_value = widget.parse()
+        if new_value:
+            return False
+
+        if any(self.formdef.usage_in_formdefs()):
+            widget.set_error(_('Can not empty digest template: this card model is used as data source in some forms.'))
+            return True
+
+        return False
+
 
 class WorkflowRoleDirectory(Directory):
     def __init__(self, formdef):
diff --git a/wcs/backoffice/cards.py b/wcs/backoffice/cards.py
index 276f40d5..3d97a653 100644
--- a/wcs/backoffice/cards.py
+++ b/wcs/backoffice/cards.py
@@ -27,7 +27,6 @@ from ..qommon.misc import C_
 from ..qommon.storage import NotEqual, Null
 
 from wcs.carddef import CardDef
-from wcs.formdef import get_formdefs_of_all_kinds
 from wcs.roles import Role
 from wcs.workflows import Workflow
 from wcs.admin.forms import FormsDirectory, FormDefPage, FormDefUI, html_top
@@ -167,8 +166,9 @@ class CardDefPage(FormDefPage):
         r += self.get_preview()
         r += htmltext('</div>')
 
-        formdefs = self.usage_in_formdefs()
+        formdefs = list(self.formdef.usage_in_formdefs())
         if formdefs:
+            formdefs.sort(key=lambda x: x.name.lower())
             r += htmltext('<div class="bo-block">')
             r += htmltext('<h3>%s</h3>') % _('Forms')
             r += htmltext('<p>%s ') % _('This card model is used as data source in the following forms:')
@@ -177,27 +177,6 @@ class CardDefPage(FormDefPage):
             r += htmltext('</div>')
         return r.getvalue()
 
-    def usage_in_formdefs(self):
-        def is_used_in_formdef(formdef):
-            for field in formdef.fields or []:
-                data_source = getattr(field, 'data_source', None)
-                if not (data_source and data_source.get('type')):
-                    continue
-                data_source_id = 'carddef:%s' % self.formdef.url_name
-                if data_source.get('type') == data_source_id:
-                    return True
-                if data_source.get('type').startswith('%s:' % data_source_id):
-                    # custom view
-                    return True
-            return False
-
-        formdefs = []
-        for formdef in get_formdefs_of_all_kinds():
-            if is_used_in_formdef(formdef):
-                formdefs.append(formdef)
-        formdefs.sort(key=lambda x: x.name.lower())
-        return formdefs
-
     def duplicate(self):
         response = super(CardDefPage, self).duplicate()
         self.formdefui.formdef.disabled = False
diff --git a/wcs/carddef.py b/wcs/carddef.py
index 71db46a4..534aee89 100644
--- a/wcs/carddef.py
+++ b/wcs/carddef.py
@@ -22,7 +22,7 @@ from .qommon import _, N_, misc
 from .qommon.storage import Contains, Equal, NotEqual, ILike
 
 from wcs.carddata import CardData
-from wcs.formdef import FormDef
+from wcs.formdef import FormDef, get_formdefs_of_all_kinds
 
 if not hasattr(types, 'ClassType'):
     types.ClassType = type
@@ -209,3 +209,21 @@ class CardDef(FormDef):
         if order_by is None:
             items.sort(key=lambda x: misc.simplify(x['text']))
         return items
+
+    def usage_in_formdefs(self):
+        def is_used_in_formdef(formdef):
+            for field in formdef.fields or []:
+                data_source = getattr(field, 'data_source', None)
+                if not (data_source and data_source.get('type')):
+                    continue
+                data_source_id = 'carddef:%s' % self.url_name
+                if data_source.get('type') == data_source_id:
+                    return True
+                if data_source.get('type').startswith('%s:' % data_source_id):
+                    # custom view
+                    return True
+            return False
+
+        for formdef in get_formdefs_of_all_kinds():
+            if is_used_in_formdef(formdef):
+                yield formdef
-- 
2.28.0

