Repository: ietf-tools/datatracker

Issue:
Document needs attention indicators have become too subtle.
The IESG would like to have the datatracker be more assertive in pointing out that a document has been in a state for a long time. The egg-timer takes less screen space, but doesn't capture enough attention to drive action.

Conversation about this began with the AD dashboard, but it was noted that any search result page should have stronger indication when documents are in such states.


Discussion:
Some questions:
1. Should this change be made only for people where the role is "AD"?
2. Should this change be made only for those documents that are _severely_ over there time budget for a state?

(The reason I made this change is that the document metadata table is _way_ too busy.)
My take: 

1) Nudging other people (chairs, shepherds, authors) to pay attention can only help, even if it's the ADs action next, and will help even _more_ if it's not the ADs action.

2) Nudging documents that are only starting to lag is more helpful than screaming about long-known-stale documents with difficult problems holding them back. (The overall volume of things facilitated will be higher). Focus _this_ tooling on nudging things nearing sticking points back into the main stream rather than prying stuck things out of cracks.
Anyone got an idea that is more visible than the new way but uses less screen real estate than the old way?
(not intending to pick on Roman)
Instead of
![image](https://user-images.githubusercontent.com/10996692/171749248-bde9aa98-e034-4777-9471-ab46795efee5.png)
Perhaps the entire iesg state could be badged with the color that the hourglass is now, and the label for action holders could also be badged with those colors. Maybe only one hourglass would be needed on action holders (which would remove some visual noise) since right now, they're always all on the same deadline.

Resolving patch:
diff --git a/ietf/doc/templatetags/ballot_icon.py b/ietf/doc/templatetags/ballot_icon.py
--- a/ietf/doc/templatetags/ballot_icon.py
+++ b/ietf/doc/templatetags/ballot_icon.py
@@ -162,23 +162,27 @@ def ballotposition(doc, user):
 
 @register.filter
 def state_age_colored(doc):
-    if doc.type_id == 'draft':
+    if doc.type_id == "draft":
         if not doc.get_state_slug() in ["active", "rfc"]:
             # Don't show anything for expired/withdrawn/replaced drafts
             return ""
-        iesg_state = doc.get_state_slug('draft-iesg')
+        iesg_state = doc.get_state_slug("draft-iesg")
         if not iesg_state:
             return ""
 
         if iesg_state in ["dead", "watching", "pub", "idexists"]:
             return ""
         try:
-            state_date = doc.docevent_set.filter(
-                              Q(type="started_iesg_process")|
-                              Q(type="changed_state", statedocevent__state_type="draft-iesg")
-            ).order_by('-time')[0].time.date()
+            state_date = (
+                doc.docevent_set.filter(
+                    Q(type="started_iesg_process")
+                    | Q(type="changed_state", statedocevent__state_type="draft-iesg")
+                )
+                .order_by("-time")[0]
+                .time.date()
+            )
         except IndexError:
-            state_date = datetime.date(1990,1,1)
+            state_date = datetime.date(1990, 1, 1)
         days = (datetime.date.today() - state_date).days
         # loosely based on
         # https://trac.ietf.org/trac/iesg/wiki/PublishPath
@@ -191,7 +195,7 @@ def state_age_colored(doc):
         elif iesg_state in ["lc-req", "ann"]:
             goal1 = 4
             goal2 = 7
-        elif 'need-rev' in [x.slug for x in doc.tags.all()]:
+        elif "need-rev" in [x.slug for x in doc.tags.all()]:
             goal1 = 14
             goal2 = 28
         elif iesg_state == "pub-req":
@@ -204,21 +208,29 @@ def state_age_colored(doc):
             goal1 = 14
             goal2 = 28
         if days > goal2:
-            class_name = "text-danger"
+            class_name = "bg-danger"
         elif days > goal1:
-            class_name = "text-warning"
+            class_name = "bg-warning"
         else:
             # don't show a badge when things are in the green; clutters display
             # class_name = "text-success"
             return ""
         if days > goal1:
-            title = ' title="In state for %d day%s; goal is &lt;%d days."' % (days, 's' if days != 1 else '', goal1,)
+            title = ' title="In state for %d day%s; goal is &lt;%d days."' % (
+                days,
+                "s" if days != 1 else "",
+                goal1,
+            )
         else:
-            title = ''
-        return mark_safe('<span class="%s"><i class="bi bi-hourglass-split"%s></i></span>' % (class_name, title))
+            title = ""
+        return mark_safe(
+            '<span class="badge %s" %s><i class="bi bi-clock-fill"></i> %d</span>'
+            % (class_name, title, days)
+        )
     else:
         return ""
 
+
 @register.filter
 def auth48_alert_badge(doc):
     """Return alert badge, if any, for a document"""
diff --git a/ietf/doc/templatetags/ietf_filters.py b/ietf/doc/templatetags/ietf_filters.py
--- a/ietf/doc/templatetags/ietf_filters.py
+++ b/ietf/doc/templatetags/ietf_filters.py
@@ -705,36 +705,38 @@ def can_ballot(user,doc):
     else:
         return user.person.role_set.filter(name="ad", group__type="area", group__state="active")
 
+
 @register.filter
 def action_holder_badge(action_holder):
     """Add a warning tag if action holder age exceeds limit
-    
+
     >>> from ietf.doc.factories import DocumentActionHolderFactory
     >>> old_limit = settings.DOC_ACTION_HOLDER_AGE_LIMIT_DAYS
     >>> settings.DOC_ACTION_HOLDER_AGE_LIMIT_DAYS = 15
     >>> action_holder_badge(DocumentActionHolderFactory())
     ''
-    
+
     >>> action_holder_badge(DocumentActionHolderFactory(time_added=datetime.datetime.now() - datetime.timedelta(days=15)))
     ''
 
     >>> action_holder_badge(DocumentActionHolderFactory(time_added=datetime.datetime.now() - datetime.timedelta(days=16)))
-    '<span class="text-danger"><i class="bi bi-hourglass-split" title="In state for 16 days; goal is &lt;15 days."></i></span>'
+    '<span class="badge bg-danger" title="In state for 16 days; goal is &lt;15 days."><i class="bi bi-clock-fill"></i> 16</span>'
 
     >>> action_holder_badge(DocumentActionHolderFactory(time_added=datetime.datetime.now() - datetime.timedelta(days=30)))
-    '<span class="text-danger"><i class="bi bi-hourglass-split" title="In state for 30 days; goal is &lt;15 days."></i></span>'
+    '<span class="badge bg-danger" title="In state for 30 days; goal is &lt;15 days."><i class="bi bi-clock-fill"></i> 30</span>'
 
     >>> settings.DOC_ACTION_HOLDER_AGE_LIMIT_DAYS = old_limit
     """
     age_limit = settings.DOC_ACTION_HOLDER_AGE_LIMIT_DAYS
     age = (datetime.datetime.now() - action_holder.time_added).days
     if age > age_limit:
-        return mark_safe('<span class="text-danger"><i class="bi bi-hourglass-split" title="In state for %d day%s; goal is &lt;%d days."></i></span>' % (
-            age,
-            's' if age != 1 else '',
-            age_limit))
+        return mark_safe(
+            '<span class="badge bg-danger" title="In state for %d day%s; goal is &lt;%d days."><i class="bi bi-clock-fill"></i> %d</span>'
+            % (age, "s" if age != 1 else "", age_limit, age)
+        )
     else:
-        return ''  # no alert needed
+        return ""  # no alert needed
+
 
 @register.filter
 def is_regular_agenda_item(assignment):


Test patch:
diff --git a/ietf/doc/tests.py b/ietf/doc/tests.py
--- a/ietf/doc/tests.py
+++ b/ietf/doc/tests.py
@@ -356,8 +356,8 @@ def test_recent_drafts(self):
         self.assertEqual(r.status_code, 200)
         q = PyQuery(r.content)
         self.assertEqual(len(q('td.doc')),3)
-        self.assertTrue(q('td.status span.text-warning *[title*="%s"]' % "for 15 days"))
-        self.assertTrue(q('td.status span.text-danger *[title*="%s"]' % "for 29 days"))
+        self.assertTrue(q('td.status span.bg-warning[title*="%s"]' % "for 15 days"))
+        self.assertTrue(q('td.status span.bg-danger[title*="%s"]' % "for 29 days"))
         for ah in [draft.action_holders.first() for draft in drafts]:
             self.assertContains(r, escape(ah.name))