From 734ecef92954ed03a080dd8abeca0510a90e8eee Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Fri, 5 Jun 2020 11:42:02 +0200
Subject: [PATCH] cells: display invalidity date or delay (#43605)

---
 combo/data/models.py                         | 20 ++++++++-
 combo/manager/templates/combo/page_view.html | 12 +++--
 tests/test_cells.py                          | 39 ++++++++++++++++-
 tests/test_manager.py                        | 46 +++++++++++---------
 4 files changed, 89 insertions(+), 28 deletions(-)

diff --git a/combo/data/models.py b/combo/data/models.py
index dedbabad..df1e58ff 100644
--- a/combo/data/models.py
+++ b/combo/data/models.py
@@ -547,6 +547,12 @@ class ValidityInfo(models.Model):
     class Meta:
         unique_together = [('content_type', 'object_id')]
 
+    @property
+    def invalid_datetime(self):
+        if not self.invalid_since:
+            return
+        return self.invalid_since + datetime.timedelta(days=2)
+
 
 class CellMeta(MediaDefiningClass, ModelBase):
     pass
@@ -891,6 +897,17 @@ class CellBase(six.with_metaclass(CellMeta, models.Model)):
             # 5xx error: can not retrieve data, don't report cell as invalid
             self.mark_as_valid()
 
+    def is_valid(self):
+        validity_info = self.get_validity_info()
+        if validity_info is None:
+            return True
+        if not validity_info.invalid_since:
+            return True
+        if validity_info.invalid_datetime >= now():
+            # not yet
+            return True
+        return False
+
     def get_invalid_reason(self):
         validity_info = self.get_validity_info()
         if validity_info is None:
@@ -916,8 +933,7 @@ class CellBase(six.with_metaclass(CellMeta, models.Model)):
 
     def is_visible(self, user=None, check_validity_info=True):
         if check_validity_info:
-            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):
+            if not self.is_valid():
                 return False
         return element_is_visible(self, user=user)
 
diff --git a/combo/manager/templates/combo/page_view.html b/combo/manager/templates/combo/page_view.html
index e9a14bbb..f1446d11 100644
--- a/combo/manager/templates/combo/page_view.html
+++ b/combo/manager/templates/combo/page_view.html
@@ -150,11 +150,15 @@
                   <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>
-                  {% with cell.get_invalid_reason as invalid_reason %}
-                  {% if invalid_reason %}
-                  <span class="invalid">{{ invalid_reason }}</span>
+                  {% if cell.get_invalid_reason %}
+                  <span class="invalid">{{ cell.get_invalid_reason }} -
+                    {% if cell.is_valid %}
+                      {% blocktrans with cell.get_validity_info.invalid_datetime|timeuntil as invalidity_delay %}This cell will no longer be displayed in {{ invalidity_delay }}.{% endblocktrans %}
+                    {% else %}
+                      {% blocktrans with cell.get_validity_info.invalid_datetime|date:"DATETIME_FORMAT" as invalidity_date %}This cell is no longer displayed since {{ invalidity_date }}.{% endblocktrans %}
+                    {% endif %}
+                  </span>
                   {% endif %}
-                  {% endwith %}
                 </span>
                 {% if not cell.public %}
                   <span class="visibility-summary
diff --git a/tests/test_cells.py b/tests/test_cells.py
index 04567631..f816f6e0 100644
--- a/tests/test_cells.py
+++ b/tests/test_cells.py
@@ -13,7 +13,6 @@ from django.apps import apps
 from django.conf import settings
 from django.db import connection
 from django.forms.widgets import Media
-from django.template import Context
 from django.test import override_settings
 from django.test.client import RequestFactory
 from django.test.utils import CaptureQueriesContext
@@ -1127,6 +1126,44 @@ def test_cell_is_visible():
     assert cell.is_visible() is False
 
 
+def test_cell_invalidity_marker():
+    page = Page.objects.create()
+    cell = TextCell.objects.create(page=page, order=0)
+    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
+
+    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
+
+
+def test_cell_is_valid(freezer):
+    freezer.move_to('2020-06-05 12:00:01')
+    page = Page.objects.create()
+    cell = TextCell.objects.create(page=page, order=0)
+    assert cell.is_valid() is True
+    validity_info = ValidityInfo.objects.create(content_object=cell)
+    validity_info.invalid_reason_code = 'FOO'
+    validity_info.save()
+    assert cell.is_valid() is True  # invalid_since is not set
+
+    ValidityInfo.objects.update(invalid_since=now() - datetime.timedelta(days=2, minutes=10*60+2))
+    assert cell.is_valid() is False
+    ValidityInfo.objects.update(invalid_since=now() - datetime.timedelta(minutes=1))
+    assert cell.is_valid() is True
+
+
 def test_hourly():
     appconfig = apps.get_app_config('data')
     page = Page.objects.create(title='xxx', slug='test_current_forms_cell_render', template_name='standard')
diff --git a/tests/test_manager.py b/tests/test_manager.py
index ce5c5af9..b0c06947 100644
--- a/tests/test_manager.py
+++ b/tests/test_manager.py
@@ -1,29 +1,27 @@
 import base64
-import json
+import datetime
+import mock
 import os
 import re
 import shutil
 
-import mock
-
 from django.core.files.storage import default_storage
 from django.urls import reverse
 from django.conf import settings
-from django.contrib.auth.models import User, Group
+from django.contrib.auth.models import Group
 from django.db import connection
 from django.template import TemplateSyntaxError
 from django.test import override_settings
 from django.test.client import RequestFactory
 from django.test.utils import CaptureQueriesContext
 from django.utils.http import urlencode
+from django.utils.timezone import now
 from django.utils.six import BytesIO
 from django.utils.six.moves.urllib import parse as urlparse
 
 import pytest
-from webtest import TestApp
 from webtest import Upload
 
-from combo.wsgi import application
 from combo.data.forms import LinkCellForm
 from combo.data.models import (
     Page, CellBase, TextCell, LinkCell, ConfigJsonCell, JsonCell, PageSnapshot,
@@ -169,39 +167,45 @@ 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):
+def test_edit_page_cell_invalid_placeholder(freezer, app, admin_user):
+    freezer.move_to('2020-06-05 12:00:01')
     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)
+    ValidityInfo.objects.update(invalid_since=now() - datetime.timedelta(minutes=1))
     resp = app.get('/manage/pages/%s/' % page.pk)
-    assert '<span class="invalid">foo_bar_reason</span>' in resp.text
+    assert '<span class="invalid">foo_bar_reason' in resp.text
+    assert 'This cell will no longer be displayed in 1 day, 23 hours' 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
+    ValidityInfo.objects.update(invalid_since=now() - datetime.timedelta(minutes=47*60-1))
+    resp = app.get('/manage/pages/%s/' % page.pk)
+    assert '<span class="invalid">foo_bar_reason' in resp.text
+    assert 'This cell will no longer be displayed in 1 hour, 1 minute.' in resp.text
+
+    ValidityInfo.objects.update(invalid_since=now() - datetime.timedelta(minutes=47*60+29))
+    resp = app.get('/manage/pages/%s/' % page.pk)
+    assert '<span class="invalid">foo_bar_reason' in resp.text
+    assert 'This cell will no longer be displayed in 31 minutes.' in resp.text
 
-    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
+    ValidityInfo.objects.update(invalid_since=now() - datetime.timedelta(days=2, minutes=10*60+2))
+    resp = app.get('/manage/pages/%s/' % page.pk)
+    assert '<span class="invalid">foo_bar_reason' in resp.text
+    assert 'This cell is no longer displayed since June 5, 2020, 1:58 a.m.' in resp.text
 
     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
+    assert '<span class="invalid">foo_bar_reason' not in resp.text
 
     cell2 = LinkListCell.objects.create(order=0, placeholder='content', page=page)
     item = LinkCell.objects.create(page=page, placeholder=cell2.link_placeholder, order=0)
     item.mark_as_invalid('foo_bar_reason')
     cell2.check_validity()
     resp = app.get('/manage/pages/%s/' % page.pk)
-    assert '<span class="invalid">Invalid link</span>' in resp.text
+    assert '<span class="invalid">Invalid link' in resp.text
+    assert 'This cell will no longer be displayed in 2 days.' in resp.text
     assert '<span class="invalid">foo_bar_reason</span>' in resp.text
 
 
-- 
2.20.1

