From 0d203390b4a2abf81c3b5d0ad1f2a422c9c4b1ff 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                         |  8 ++-
 combo/manager/templates/combo/page_view.html | 12 +++--
 combo/public/templatetags/combo.py           | 11 ++++-
 tests/test_cells.py                          | 23 ++++++++-
 tests/test_manager.py                        | 51 ++++++++++++--------
 5 files changed, 77 insertions(+), 28 deletions(-)

diff --git a/combo/data/models.py b/combo/data/models.py
index dedbabad..97cb88f2 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
@@ -917,7 +923,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 validity_info is not None and validity_info.invalid_datetime and validity_info.invalid_datetime <= now():
                 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..2b4d6885 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.get_validity_info.invalid_datetime|date_in_past %}
+                      {% blocktrans with cell.get_validity_info.invalid_datetime|date:"DATETIME_FORMAT" as invalidity_date %}This cell is no longer displayed since {{ invalidity_date }}.{% endblocktrans %}
+                    {% else %}
+                      {% blocktrans with cell.get_validity_info.invalid_datetime|timeuntil as invalidity_delay %}This cell will no longer be displayed in {{ invalidity_delay }}.{% endblocktrans %}
+                    {% endif %}
+                  </span>
                   {% endif %}
-                  {% endwith %}
                 </span>
                 {% if not cell.public %}
                   <span class="visibility-summary
diff --git a/combo/public/templatetags/combo.py b/combo/public/templatetags/combo.py
index cdc4b4e5..cbd38001 100644
--- a/combo/public/templatetags/combo.py
+++ b/combo/public/templatetags/combo.py
@@ -25,7 +25,6 @@ import math
 import time
 
 from django import template
-from django.conf import settings
 from django.core import signing
 from django.core.exceptions import PermissionDenied
 from django.core.serializers.json import DjangoJSONEncoder
@@ -43,6 +42,7 @@ from django.utils import dateparse, six
 from django.utils.encoding import force_text
 from django.utils.html import format_html
 from django.utils.safestring import mark_safe
+from django.utils.timezone import now
 
 from combo.data.models import Page, Placeholder
 from combo.public.menu import get_menu_context
@@ -450,6 +450,15 @@ def age_in_months(value, today=None):
         return ''
     return years * 12 + months
 
+
+@register.filter()
+def date_in_past(value):
+    try:
+        return value <= now()
+    except TypeError:
+        return False
+
+
 def parse_decimal(value, default=Decimal(0)):
     if isinstance(value, six.string_types):
         # replace , by . for French users comfort
diff --git a/tests/test_cells.py b/tests/test_cells.py
index 04567631..4c39c64e 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,28 @@ 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_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..b6368df5 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,50 @@ 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
+
+    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
 
-    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+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))
+    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, noon.' in resp.text
+
+    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

