feat: Add column sums to /doc/ad dashboard (#4415)

* feat: Add column sums to /doc/ad dashboard

* Tweak template a bit

* Add trend indicators when logged in as AD

* Shorten column headings more; put full heading into tooltip

* Fix trend colors; add table dividers

* Add note about trend indicators

* Wording fix

* Show which docs make up the delta if there is a trend change

* Fix missing stats

* More space before headings

* Better popover formatting

* Make popover trigger clickable, and add links to docs in the delta

* Improve trends

* Fix tests and shorten headers

* Add button to IESG dashboard to AD dashboards.

* fix: use tz-aware calculations for ad_workload view

Co-authored-by: Jennifer Richards <jennifer@painless-security.com>
This commit is contained in:
Lars Eggert 2022-09-08 22:00:18 +03:00 committed by GitHub
parent 56bd75a4b0
commit 9db1958bdd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 312 additions and 105 deletions

View file

@ -37,6 +37,8 @@
import re
import datetime
from collections import defaultdict
from django import forms
from django.conf import settings
from django.core.cache import cache, caches
@ -44,6 +46,7 @@ from django.urls import reverse as urlreverse
from django.db.models import Q
from django.http import Http404, HttpResponseBadRequest, HttpResponse, HttpResponseRedirect, QueryDict
from django.shortcuts import render
from django.utils import timezone
from django.utils.cache import _generate_cache_key # type: ignore
@ -364,11 +367,42 @@ def ad_dashboard_group(doc):
else:
return "Document"
def shorten_group_name(name):
for s in [' Internet-Draft', ' Conflict Review', ' Status Change', ' (Internal Steering Group/IAB Review) Charter', 'Charter']:
for s in [
" Internet-Draft",
" Conflict Review",
" Status Change",
" (Internal Steering Group/IAB Review) Charter",
"Charter",
]:
if name.endswith(s):
name = name[:-len(s)]
return name
name = name[: -len(s)]
for pat, sub in [
("Writeup", "Write-up"),
("Requested", "Req"),
("Evaluation", "Eval"),
("Publication", "Pub"),
("Waiting", "Wait"),
("Go-Ahead", "OK"),
("Approved-", "App, "),
("announcement", "ann."),
("IESG Eval - ", ""),
("Not currently under review", "Not under review"),
("External Review", "Ext. Review"),
(r"IESG Review \(Charter for Approval, Selected by Secretariat\)", "IESG Review"),
("Needs Shepherd", "Needs Shep."),
("Approved", "App."),
("Replaced", "Repl."),
("Withdrawn", "Withd."),
("Chartering/Rechartering", "Charter"),
(r"\(Message to Community, Selected by Secretariat\)", "")
]:
name = re.sub(pat, sub, name)
return name.strip()
def ad_dashboard_sort_key(doc):
@ -425,106 +459,211 @@ def ad_dashboard_sort_key(doc):
return "3%s" % seed
def ad_workload(request):
delta = datetime.timedelta(days=30)
right_now = timezone.now()
ads = []
responsible = Document.objects.values_list('ad', flat=True).distinct()
responsible = Document.objects.values_list("ad", flat=True).distinct()
for p in Person.objects.filter(
Q(
role__name__in=("pre-ad", "ad"),
role__group__type="area",
role__group__state="active"
role__group__state="active",
)
| Q(pk__in=responsible)
).distinct():
if p in get_active_ads():
ads.append(p)
ads.append(p)
doctypes = list(DocTypeName.objects.filter(used=True).exclude(slug='draft').values_list("pk", flat=True))
doctypes = list(
DocTypeName.objects.filter(used=True)
.exclude(slug="draft")
.values_list("pk", flat=True)
)
up_is_good = {}
group_types = ad_dashboard_group_type(None)
groups = {g: {} for g in group_types}
group_names = {g: [] for g in group_types}
groups = {}
group_names = {}
for g in group_types:
groups[g] = {}
group_names[g] = []
# Prefill groups in preferred sort order
id = 0
for g in [
'Publication Requested Internet-Draft',
'Waiting for Writeup Internet-Draft',
'AD Evaluation Internet-Draft',
'In Last Call Internet-Draft',
'IESG Evaluation - Defer Internet-Draft',
'IESG Evaluation Internet-Draft',
'Waiting for AD Go-Ahead Internet-Draft',
'Approved-announcement to be sent Internet-Draft',
'Approved-announcement sent Internet-Draft']:
groups['I-D'][g] = id
group_names['I-D'].append(g)
id += 1;
id = 0
for g in ['RFC Ed Queue Internet-Draft', 'RFC']:
groups['RFC'][g] = id
group_names['RFC'].append(g)
id += 1;
id = 0
for g in ['AD Review Conflict Review',
'Needs Shepherd Conflict Review',
'IESG Evaluation Conflict Review',
'Approved Conflict Review',
'Withdrawn Conflict Review']:
groups['Conflict Review'][g] = id
group_names['Conflict Review'].append(g)
id += 1;
id = 0
for g in [ 'Start Chartering/Rechartering (Internal Steering Group/IAB Review) Charter',
'Replaced Charter',
'Approved Charter',
'Not currently under review Charter']:
groups['Charter'][g] = id
group_names['Charter'].append(g)
id += 1;
# FIXME: This should really use the database states instead of replicating the logic
for id, (g, uig) in enumerate(
[
("Publication Requested Internet-Draft", False),
("Waiting for Writeup Internet-Draft", False),
("AD Evaluation Internet-Draft", False),
("In Last Call Internet-Draft", None),
("IESG Evaluation - Defer Internet-Draft", None),
("IESG Evaluation Internet-Draft", True),
("Waiting for AD Go-Ahead Internet-Draft", False),
("Approved-announcement to be sent Internet-Draft", True),
("Approved-announcement sent Internet-Draft", True),
]
):
groups["I-D"][g] = id
group_names["I-D"].append(g)
up_is_good[g] = uig
for id, g in enumerate(["RFC Ed Queue Internet-Draft", "RFC"]):
groups["RFC"][g] = id
group_names["RFC"].append(g)
up_is_good[g] = True
for id, (g, uig) in enumerate(
[
("AD Review Conflict Review", False),
("Needs Shepherd Conflict Review", False),
("IESG Evaluation Conflict Review", True),
("Approved Conflict Review", True),
("Withdrawn Conflict Review", None),
]
):
groups["Conflict Review"][g] = id
group_names["Conflict Review"].append(g)
up_is_good[g] = uig
for id, (g, uig) in enumerate(
[
("Publication Requested Status Change", False),
("Waiting for Writeup Status Change", False),
("AD Evaluation Status Change", False),
("In Last Call Status Change", None),
("IESG Evaluation Status Change", True),
("Waiting for AD Go-Ahead Status Change", False),
]
):
groups["Status Change"][g] = id
group_names["Status Change"].append(g)
up_is_good[g] = uig
for id, (g, uig) in enumerate(
[
("Not currently under review Charter", None),
("Draft Charter Charter", None),
("Start Chartering/Rechartering (Internal Steering Group/IAB Review) Charter", False),
("External Review (Message to Community, Selected by Secretariat) Charter", True),
("IESG Review (Charter for Approval, Selected by Secretariat) Charter", True),
("Approved Charter", True),
("Replaced Charter", None),
]
):
groups["Charter"][g] = id
group_names["Charter"].append(g)
up_is_good[g] = uig
for ad in ads:
form = SearchForm({'by':'ad','ad': ad.id,
'rfcs':'on', 'activedrafts':'on',
'olddrafts':'on',
'doctypes': doctypes})
data = retrieve_search_results(form)
ad.dashboard = urlreverse("ietf.doc.views_search.docs_for_ad", kwargs=dict(name=ad.full_name_as_key()))
counts = {}
for g in group_types:
counts[g] = []
for doc in data:
form = SearchForm(
{
"by": "ad",
"ad": ad.id,
"rfcs": "on",
"activedrafts": "on",
"olddrafts": "on",
"doctypes": doctypes,
}
)
ad.dashboard = urlreverse(
"ietf.doc.views_search.docs_for_ad", kwargs=dict(name=ad.full_name_as_key())
)
ad.counts = defaultdict(list)
ad.prev = defaultdict(list)
ad.doc_now = defaultdict(list)
ad.doc_prev = defaultdict(list)
for doc in retrieve_search_results(form):
group_type = ad_dashboard_group_type(doc)
if group_type and group_type in groups: # Right now, anything with group_type "Document", such as a bofreq is not handled.
if group_type and group_type in groups:
# Right now, anything with group_type "Document", such as a bofreq is not handled.
group = ad_dashboard_group(doc)
if group not in groups[group_type]:
groups[group_type][group] = len(groups[group_type])
group_names[group_type].append(group)
if len(counts[group_type]) < len(groups[group_type]):
counts[group_type].extend([0] * (len(groups[group_type]) - len(counts[group_type])))
counts[group_type][groups[group_type][group]] += 1
ad.counts = counts
inc = len(groups[group_type]) - len(ad.counts[group_type])
if inc > 0:
ad.counts[group_type].extend([0] * inc)
ad.prev[group_type].extend([0] * inc)
ad.doc_now[group_type].extend(set() for _ in range(inc))
ad.doc_prev[group_type].extend(set() for _ in range(inc))
ad.counts[group_type][groups[group_type][group]] += 1
ad.doc_now[group_type][groups[group_type][group]].add(doc)
try:
state_date = (
doc.docevent_set.filter(
Q(type="started_iesg_process") | Q(type="changed_state")
)
.order_by("-time")[0]
.time
)
except IndexError:
state_date = datetime.datetime(1990, 1, 1, tzinfo=datetime.timezone.utc)
if right_now - state_date > delta:
ad.prev[group_type][groups[group_type][group]] += 1
ad.doc_prev[group_type][groups[group_type][group]].add(doc)
for ad in ads:
for group_type in group_types:
if len(ad.counts[group_type]) < len(groups[group_type]):
ad.counts[group_type].extend([0] * (len(groups[group_type]) - len(ad.counts[group_type])))
ad.doc_diff = defaultdict(list)
for gt in group_types:
inc = len(groups[gt]) - len(ad.counts[gt])
if inc > 0:
ad.counts[gt].extend([0] * inc)
ad.prev[gt].extend([0] * inc)
ad.doc_now[gt].extend([set()] * inc)
ad.doc_prev[gt].extend([set()] * inc)
ad.doc_diff[gt].extend([set()] * len(groups[gt]))
for idx, g in enumerate(group_names[gt]):
ad.doc_diff[gt][idx] = ad.doc_prev[gt][idx] ^ ad.doc_now[gt][idx]
# Shorten the names of groups
for gt in group_types:
for idx,g in enumerate(group_names[gt]):
group_names[gt][idx] = shorten_group_name(g)
for idx, g in enumerate(group_names[gt]):
group_names[gt][idx] = (
shorten_group_name(g),
g,
up_is_good[g] if g in up_is_good else None,
)
workload = []
for gt in group_types:
workload.append(dict(group_type=gt,group_names=group_names[gt],counts=[(ad, [(group_names[gt][index],ad.counts[gt][index]) for index in range(len(group_names[gt]))]) for ad in ads]))
workload = [
dict(
group_type=gt,
group_names=group_names[gt],
counts=[
(
ad,
[
(
group_names[gt][index],
ad.counts[gt][index],
ad.prev[gt][index],
ad.doc_diff[gt][index],
)
for index in range(len(group_names[gt]))
],
)
for ad in ads
],
sums=[
(
group_names[gt][index],
sum([ad.counts[gt][index] for ad in ads]),
sum([ad.prev[gt][index] for ad in ads]),
)
for index in range(len(group_names[gt]))
],
)
for gt in group_types
]
return render(request, 'doc/ad_list.html', {
'workload': workload
})
return render(request, "doc/ad_list.html", {"workload": workload, "delta": delta})
def docs_for_ad(request, name):
ad = None

View file

@ -0,0 +1,33 @@
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load ietf_filters %}
{% if prev or count %}
<span{% if count == 0 %} class="text-muted"{% endif %}>{{ count }}</span>
{% if user|has_role:"Area Director,Secretariat" %}
<i data-bs-toggle="popover"
{% if count != prev %}
data-bs-content="
<div class='mb-2 fw-bold'>
{{ delta.days }} days ago, the count was {{ prev }}.
</div>
{% if docs_delta %}
{{ group.group_type }}s in the delta are:
<ul>
{% for d in docs_delta %}
<li><a href='{% url "ietf.doc.views_doc.document_main" d.name %}'>{{ d.name }}</a></li>
{% endfor %}
</ul>
{% endif %}"
{% endif %}
{% with label.2 as up_is_good %}
{% if prev < count %}
class="bi bi-arrow-up-right-circle{% if count %}-fill{% endif %} {{ up_is_good|yesno:'text-success,text-danger,text-muted' }}"
{% elif prev > count %}
class="bi bi-arrow-down-right-circle{% if count %}-fill{% endif %} {{ up_is_good|yesno:'text-danger,text-success,text-muted' }}"
{% else %}
class="bi bi-arrow-right-circle text-muted"
{% endif %}
></i>
{% endwith %}
{% endif %}
{% endif %}

View file

@ -3,36 +3,70 @@
{% load origin static %}
{% load ietf_filters %}
{% block pagehead %}
<link rel="stylesheet" href="{% static "ietf/css/list.css" %}">
<link rel="stylesheet" href="{% static "ietf/css/list.css" %}">
{% endblock %}
{% block title %}Area directors{% endblock %}
{% block content %}
{% origin %}
<h1>Area Directors Workload</h1>
{% for group in workload %}
<h2>{{ group.group_type }}</h2>
<table class="table table-sm table-striped tablesorter">
<thead>
<tr>
<th scope="col" data-sort="name">Name</th>
{% for g in group.group_names %}
<th scope="col" class="text-end" data-sort="{{ g|slugify }}-num">{{ g }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for ad, ad_counts in group.counts %}
<tr>
<td><a href="{{ ad.dashboard }}">{{ ad.name }}</a></td>
{% for label, count in ad_counts %}
<td id="{{group.group_type|slugify}}-{{ad.full_name_as_key|slugify}}-{{label|slugify}}">{{count}}</td>
{% endfor %}
</tr>
{% origin %}
<h1>Area Director Workload</h1>
{% if user|has_role:"Area Director,Secretariat" %}
<div class="alert alert-info my-3">
{{ delta.days }}-day trend indicators
are only shown to logged-in Area Directors.
</div>
{% endif %}
{% for group in workload %}
<h2 class="mt-5" id="{{ group.group_type|slugify }}">{{ group.group_type }} State Counts</h2>
<table class="table table-sm table-striped table-bordered tablesorter">
<thead>
<tr>
<th scope="col" data-sort="name">Area Director</th>
{% for g, desc, up_is_good in group.group_names %}
<th scope="col" class="col-1" title="{{ desc }}"
data-sort="{{ g|slugify }}-num">
{{ g|split:'/'|join:'/<wbr>' }}
</th>
{% endfor %}
</tr>
</thead>
<tbody class="table-group-divider">
{% for ad, ad_data in group.counts %}
<tr>
<td>
<a href="{{ ad.dashboard }}">{{ ad.name }}</a>
</td>
{% for label, count, prev, docs_delta in ad_data %}
<td class="col-1"
id="{{ group.group_type|slugify }}-{{ ad.full_name_as_key|slugify }}-{{ label.0|slugify }}">
{% include 'doc/ad_count.html' %}
</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
<tfoot class="table-group-divider">
<tr>
<th scope="row">Sum</th>
{% for label, count, prev in group.sums %}
<td>
{% include 'doc/ad_count.html' %}
</td>
{% endfor %}
</tr>
</tfoot>
</table>
{% endfor %}
</tbody>
</table>
{% endfor %}
{% endblock %}
{% block js %}
<script src="{% static "ietf/js/list.js" %}"></script>
{% endblock %}
<script src="{% static "ietf/js/list.js" %}"></script>
<script>
$(document)
.ready(function () {
$("[data-bs-toggle='popover']")
.popover({
html: true,
trigger: "hover focus click"
});
});
</script>
{% endblock %}

View file

@ -10,8 +10,9 @@
{% block content %}
{% origin %}
<h1>Documents for {{ ad_name }}</h1>
<a class="btn btn-primary my-3" href="{% url 'ietf.doc.views_search.ad_workload' %}">IESG dashboard</a>
{% if blocked_docs %}
<h2>Blocking positions held by {{ ad_name }}</h2>
<h2 class="mt-4">Blocking positions held by {{ ad_name }}</h2>
<table class="table table-sm table-striped tablesorter">
<thead>
<tr>
@ -51,7 +52,7 @@
{% endfor %}
</tbody>
</table>
<h2 class="mt-3">Documents for {{ ad_name }}</h2>
<h2 class="mt-4">Documents for {{ ad_name }}</h2>
{% endif %}
{% include "doc/search/search_results.html" with start_table=True end_table=True %}
{% endblock %}