From 1eab0d758e8f19d71e8b41a95a5599e54ebc25c7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Mon, 17 Feb 2020 14:09:56 +0100
Subject: [PATCH 3/6] cells: if a cell is invalid, display it (#38009)

---
 combo/data/models.py                         | 54 +++++++++++++++++++-
 combo/manager/static/css/combo.manager.css   |  5 ++
 combo/manager/static/js/combo.manager.js     |  5 ++
 combo/manager/templates/combo/page_view.html | 10 ++--
 combo/manager/views.py                       |  7 ++-
 tests/test_manager.py                        | 32 +++++++++++-
 6 files changed, 103 insertions(+), 10 deletions(-)

diff --git a/combo/data/models.py b/combo/data/models.py
index a38cd47..d3cb3b6 100644
--- a/combo/data/models.py
+++ b/combo/data/models.py
@@ -564,6 +564,7 @@ class CellBase(six.with_metaclass(CellMeta, models.Model)):
     last_update_timestamp = models.DateTimeField(auto_now=True)
 
     validity_info = GenericRelation(ValidityInfo)
+    invalid_reason_codes = {}
 
     default_form_class = None
     manager_form_factory_kwargs = {}
@@ -641,7 +642,7 @@ class CellBase(six.with_metaclass(CellMeta, models.Model)):
         return cell_types
 
     @classmethod
-    def get_cells(cls, cell_filter=None, skip_cell_cache=False, **kwargs):
+    def get_cells(cls, cell_filter=None, skip_cell_cache=False, prefetch_validity_info=False, **kwargs):
         """Returns the list of cells of various classes matching **kwargs"""
         cells = []
         pages = []
@@ -670,6 +671,12 @@ class CellBase(six.with_metaclass(CellMeta, models.Model)):
             if cell_filter and not cell_filter(klass):
                 continue
             cells.extend(klass.objects.filter(**kwargs))
+        if prefetch_validity_info:
+            validity_info_list = list(ValidityInfo.objects.select_related('content_type'))
+            for cell in cells:
+                cell.prefetched_validity_info = [
+                    v for v in validity_info_list
+                    if v.object_id == cell.pk and v.content_type.model_class() == cell.__class__]
         cells.sort(key=lambda x: x.order)
         return cells
 
@@ -737,8 +744,51 @@ class CellBase(six.with_metaclass(CellMeta, models.Model)):
     def get_extra_manager_context(self):
         return {}
 
-    def is_visible(self, user=None):
+    def mark_as_invalid(self, reason_code, force=True, save=True):
+        validity_info = self.validity_info.all().first()
+        if validity_info is None:
+            validity_info = ValidityInfo(content_object=self)
+
+        if not force and validity_info.invalid_since is not None:
+            # don't overwrite invalid reason already set
+            return
+
+        if validity_info.invalid_reason_code == reason_code:
+            # don't overwrite invalid_since if same reason already set
+            return
+
+        validity_info.invalid_reason_code = reason_code
+        validity_info.invalid_since = now()
+        if save:
+            validity_info.save()
+        return validity_info
+
+    def mark_as_valid(self, save=True):
         validity_info = self.validity_info.all().first()
+        if validity_info is None:
+            return
+        if save:
+            validity_info.delete()
+        return validity_info
+
+    def get_validity_info(self):
+        if hasattr(self, 'prefetched_validity_info'):
+            if not self.prefetched_validity_info:
+                return
+            return self.prefetched_validity_info[0]
+        return self.validity_info.all().first()
+
+    def get_invalid_reason(self):
+        validity_info = self.get_validity_info()
+        if validity_info is None:
+            return
+        if not validity_info.invalid_since:
+            return
+        return self.invalid_reason_codes.get(
+            validity_info.invalid_reason_code, validity_info.invalid_reason_code)
+
+    def is_visible(self, user=None):
+        validity_info = self.get_validity_info()
         if validity_info is not None and validity_info.invalid_since and validity_info.invalid_since < now() - datetime.timedelta(days=2):
             return False
         return element_is_visible(self, user=user)
diff --git a/combo/manager/static/css/combo.manager.css b/combo/manager/static/css/combo.manager.css
index 3b581a0..f45aefa 100644
--- a/combo/manager/static/css/combo.manager.css
+++ b/combo/manager/static/css/combo.manager.css
@@ -80,6 +80,7 @@ div.cell h3 span.extra-css-class {
 }
 
 div.cell h3 span.additional-label,
+div.cell h3 span.invalid,
 div.cell h3 span.visibility-summary,
 div.page span.visibility-summary {
 	font-size: 80%;
@@ -111,6 +112,10 @@ div.cell h3 span.visibility-summary {
 	max-width: 30%;
 }
 
+div.cell h3 span.invalid {
+	color: red;
+}
+
 div.cell-list div h3:after {
 	font-family: FontAwesome;
 	content: "\f107"; /* angle-down */
diff --git a/combo/manager/static/js/combo.manager.js b/combo/manager/static/js/combo.manager.js
index a7d264c..a60403f 100644
--- a/combo/manager/static/js/combo.manager.js
+++ b/combo/manager/static/js/combo.manager.js
@@ -212,6 +212,11 @@ $(function() {
               $.getJSON($form.data('label-url'),
                 function(data) {
                     $form.parents('div.cell').find('.additional-label i').text(data['label']);
+                    if (data['invalid_reason']) {
+                      $form.parents('div.cell').find('.invalid').text('/!\\ ' + data['invalid_reason']);
+                    } else {
+                      $form.parents('div.cell').find('.invalid').text('');
+                    }
                 }
               );
             }
diff --git a/combo/manager/templates/combo/page_view.html b/combo/manager/templates/combo/page_view.html
index 3d9471d..2fb472f 100644
--- a/combo/manager/templates/combo/page_view.html
+++ b/combo/manager/templates/combo/page_view.html
@@ -146,11 +146,11 @@
                 <span class="group1">
                         {{ cell.get_label }}
                         {% if cell.slug %} [{{cell.slug}}] {% endif %}
-                {% if cell.extra_css_class %}
-                <span class="extra-css-class">[{{ cell.extra_css_class }}]</span>
-                {% endif %}
-                <span class="additional-label">
-                        <i>{{cell.get_additional_label|default_if_none:""}}</i></span>
+                  {% if cell.extra_css_class %}
+                  <span class="extra-css-class">[{{ cell.extra_css_class }}]</span>
+                  {% endif %}
+                  <span class="additional-label"><i>{{cell.get_additional_label|default_if_none:""}}</i></span>
+                  <span class="invalid">{% with cell.get_invalid_reason as invalid_reason %}{% if invalid_reason %}/!\ {{ cell.get_invalid_reason }}{% endif %}{% endwith %}</span>
                 </span>
                 {% if not cell.public %}
                   <span class="visibility-summary
diff --git a/combo/manager/views.py b/combo/manager/views.py
index c8508d6..b0206cc 100644
--- a/combo/manager/views.py
+++ b/combo/manager/views.py
@@ -261,7 +261,7 @@ class PageView(DetailView):
         context['cell_type_groups'] = list(cell_type_groups.items())
         context['cell_type_groups'].sort(key=lambda x: x[0])
 
-        cells = CellBase.get_cells(page=self.object)
+        cells = CellBase.get_cells(page=self.object, prefetch_validity_info=True)
         self.object.prefetched_cells = cells
         template = self.object.template_name
         placeholders = []
@@ -568,7 +568,10 @@ def page_order(request):
 def page_get_additional_label(request, page_pk, cell_reference):
     cell = CellBase.get_cell(cell_reference, page_id=page_pk)
     response = HttpResponse(content_type='application/json')
-    json.dump({'label': force_text(cell.get_additional_label()) or ''}, response)
+    json.dump({
+        'label': force_text(cell.get_additional_label()) or '',
+        'invalid_reason': force_text(cell.get_invalid_reason() or '')
+    }, response)
     return response
 
 
diff --git a/tests/test_manager.py b/tests/test_manager.py
index a3c7eae..8617b13 100644
--- a/tests/test_manager.py
+++ b/tests/test_manager.py
@@ -23,7 +23,9 @@ from webtest import TestApp
 from webtest import Upload
 
 from combo.wsgi import application
-from combo.data.models import Page, CellBase, TextCell, LinkCell, ConfigJsonCell, JsonCell, PageSnapshot, LinkListCell, ParentContentCell, MenuCell
+from combo.data.models import (
+    Page, CellBase, TextCell, LinkCell, ConfigJsonCell, JsonCell, PageSnapshot,
+    LinkListCell, ParentContentCell, MenuCell, ValidityInfo)
 from combo.apps.assets.models import Asset
 from combo.apps.family.models import FamilyInfosCell
 from combo.apps.search.models import SearchCell
@@ -153,6 +155,34 @@ def test_edit_page(app, admin_user):
     assert Page.objects.all()[0].exclude_from_navigation is False
 
 
+def test_edit_page_cell_invalid_placeholder(app, admin_user):
+    page = Page.objects.create(title='One', slug='one', template_name='standard')
+    cell = TextCell.objects.create(page=page, placeholder='content', text='Foobar', order=1)
+    cell.mark_as_invalid('foo_bar_reason')
+    validity_info = ValidityInfo.objects.latest('pk')
+    old_reason = validity_info.invalid_reason_code
+    old_date = validity_info.invalid_since
+
+    app = login(app)
+    resp = app.get('/manage/pages/%s/' % page.pk)
+    assert '<span class="invalid">/!\\ foo_bar_reason</span>' in resp.text
+
+    cell.mark_as_invalid('another_foo_bar_reason', force=False)
+    validity_info.refresh_from_db()
+    assert old_reason == validity_info.invalid_reason_code
+    assert old_date == validity_info.invalid_since
+
+    cell.mark_as_invalid('another_foo_bar_reason')
+    validity_info.refresh_from_db()
+    assert validity_info.invalid_reason_code == 'another_foo_bar_reason'
+    assert old_date < validity_info.invalid_since
+
+    cell.mark_as_valid()
+    assert ValidityInfo.objects.exists() is False
+    resp = app.get('/manage/pages/%s/' % page.pk)
+    assert '<span class="invalid">/!\\ foo_bar_reason</span>' not in resp.text
+
+
 def test_edit_page_optional_placeholder(app, admin_user):
     Page.objects.all().delete()
     page = Page.objects.create(title='One', slug='one', template_name='standard')
-- 
2.20.1

