feat: Revamp AD dashboard (#6534)
* fix: Reorder conflict review columns Fixes #6528 Also remove some redundant computation while I'm here. * Remove some more stuff that isn't needed * Progress * Delivers current functionality * Add some comments * Handle expired docs * Interim commit * Fix tests * Cleanup * More cleanup * Reduce differences to current view * Interim commit * More progress * Getting close * Make page functional again * Remove unused variable * Suppress mypy warning * Fix #6553 * Log in as secretary to execute new code, and remove redundant check * Remove unneeded code * Fix #6608 by adding link to state description to state heading * Missed part of this change in last commit. Also fix an unrelated template nit while I'm here.
This commit is contained in:
parent
5eb2f568a0
commit
c36f63baf9
|
@ -36,15 +36,14 @@ import debug # pyflakes:ignore
|
|||
from ietf.doc.models import ( Document, DocAlias, DocRelationshipName, RelatedDocument, State,
|
||||
DocEvent, BallotPositionDocEvent, LastCallDocEvent, WriteupDocEvent, NewRevisionDocEvent, BallotType,
|
||||
EditedAuthorsDocEvent )
|
||||
from ietf.doc.factories import ( DocumentFactory, DocEventFactory, CharterFactory,
|
||||
from ietf.doc.factories import ( DocumentFactory, DocEventFactory, CharterFactory,
|
||||
ConflictReviewFactory, WgDraftFactory, IndividualDraftFactory, WgRfcFactory,
|
||||
IndividualRfcFactory, StateDocEventFactory, BallotPositionDocEventFactory,
|
||||
BallotDocEventFactory, DocumentAuthorFactory, NewRevisionDocEventFactory,
|
||||
StatusChangeFactory, BofreqFactory, DocExtResourceFactory, RgDraftFactory)
|
||||
StatusChangeFactory, DocExtResourceFactory, RgDraftFactory)
|
||||
from ietf.doc.forms import NotifyForm
|
||||
from ietf.doc.fields import SearchableDocumentsField
|
||||
from ietf.doc.utils import create_ballot_if_not_open, uppercase_std_abbreviated_name
|
||||
from ietf.doc.views_search import ad_dashboard_group, ad_dashboard_group_type, shorten_group_name # TODO: red flag that we're importing from views in tests. Move these to utils.
|
||||
from ietf.group.models import Group, Role
|
||||
from ietf.group.factories import GroupFactory, RoleFactory
|
||||
from ietf.ipr.factories import HolderIprDisclosureFactory
|
||||
|
@ -60,6 +59,7 @@ from ietf.utils.test_utils import login_testing_unauthorized, unicontent
|
|||
from ietf.utils.test_utils import TestCase
|
||||
from ietf.utils.text import normalize_text
|
||||
from ietf.utils.timezone import date_today, datetime_today, DEADLINE_TZINFO, RPC_TZINFO
|
||||
from ietf.doc.utils_search import AD_WORKLOAD
|
||||
|
||||
|
||||
class SearchTests(TestCase):
|
||||
|
@ -279,43 +279,61 @@ class SearchTests(TestCase):
|
|||
self.assertContains(r, "Document Search")
|
||||
|
||||
def test_ad_workload(self):
|
||||
Role.objects.filter(name_id='ad').delete()
|
||||
ad = RoleFactory(name_id='ad',group__type_id='area',group__state_id='active',person__name='Example Areadirector').person
|
||||
doc_type_names = ['bofreq', 'charter', 'conflrev', 'draft', 'statchg']
|
||||
expected = defaultdict(lambda :0)
|
||||
for doc_type_name in doc_type_names:
|
||||
if doc_type_name=='draft':
|
||||
states = State.objects.filter(type='draft-iesg', used=True).values_list('slug', flat=True)
|
||||
else:
|
||||
states = State.objects.filter(type=doc_type_name, used=True).values_list('slug', flat=True)
|
||||
|
||||
for state in states:
|
||||
target_num = random.randint(0,2)
|
||||
Role.objects.filter(name_id="ad").delete()
|
||||
ad = RoleFactory(
|
||||
name_id="ad",
|
||||
group__type_id="area",
|
||||
group__state_id="active",
|
||||
person__name="Example Areadirector",
|
||||
).person
|
||||
expected = defaultdict(lambda: 0)
|
||||
for doc_type_slug in AD_WORKLOAD:
|
||||
for state in AD_WORKLOAD[doc_type_slug]:
|
||||
target_num = random.randint(0, 2)
|
||||
for _ in range(target_num):
|
||||
if doc_type_name == 'draft':
|
||||
doc = IndividualDraftFactory(ad=ad,states=[('draft-iesg', state),('draft','rfc' if state=='pub' else 'active')])
|
||||
elif doc_type_name == 'charter':
|
||||
doc = CharterFactory(ad=ad, states=[(doc_type_name, state)])
|
||||
elif doc_type_name == 'bofreq':
|
||||
# Note that the view currently doesn't handle bofreqs
|
||||
doc = BofreqFactory(states=[(doc_type_name, state)], bofreqresponsibledocevent__responsible=[ad])
|
||||
elif doc_type_name == 'conflrev':
|
||||
doc = ConflictReviewFactory(ad=ad, states=State.objects.filter(type_id=doc_type_name, slug=state))
|
||||
elif doc_type_name == 'statchg':
|
||||
doc = StatusChangeFactory(ad=ad, states=State.objects.filter(type_id=doc_type_name, slug=state))
|
||||
else:
|
||||
# Currently unreachable
|
||||
doc = DocumentFactory(type_id=doc_type_name, ad=ad, states=[(doc_type_name, state)])
|
||||
if (
|
||||
doc_type_slug == "draft"
|
||||
or doc_type_slug == "rfc"
|
||||
and state == "rfcqueue"
|
||||
):
|
||||
IndividualDraftFactory(
|
||||
ad=ad,
|
||||
states=[
|
||||
("draft-iesg", state),
|
||||
("draft", "rfc" if state == "pub" else "active"),
|
||||
],
|
||||
)
|
||||
elif doc_type_slug == "rfc":
|
||||
WgRfcFactory.create(
|
||||
states=[("draft", "rfc"), ("draft-iesg", "pub")]
|
||||
)
|
||||
|
||||
if not slugify(ad_dashboard_group_type(doc)) in ('document', 'none'):
|
||||
expected[(slugify(ad_dashboard_group_type(doc)), slugify(ad.full_name_as_key()), slugify(shorten_group_name(ad_dashboard_group(doc))))] += 1
|
||||
|
||||
url = urlreverse('ietf.doc.views_search.ad_workload')
|
||||
elif doc_type_slug == "charter":
|
||||
CharterFactory(ad=ad, states=[(doc_type_slug, state)])
|
||||
elif doc_type_slug == "conflrev":
|
||||
ConflictReviewFactory(
|
||||
ad=ad,
|
||||
states=State.objects.filter(
|
||||
type_id=doc_type_slug, slug=state
|
||||
),
|
||||
)
|
||||
elif doc_type_slug == "statchg":
|
||||
StatusChangeFactory(
|
||||
ad=ad,
|
||||
states=State.objects.filter(
|
||||
type_id=doc_type_slug, slug=state
|
||||
),
|
||||
)
|
||||
self.client.login(username="ad", password="ad+password")
|
||||
url = urlreverse("ietf.doc.views_search.ad_workload")
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
q = PyQuery(r.content)
|
||||
for group_type, ad, group in expected:
|
||||
self.assertEqual(int(q(f'#{group_type}-{ad}-{group}').text()),expected[(group_type, ad, group)])
|
||||
self.assertEqual(
|
||||
int(q(f"#{group_type}-{ad}-{group}").text()),
|
||||
expected[(group_type, ad, group)],
|
||||
)
|
||||
|
||||
def test_docs_for_ad(self):
|
||||
ad = RoleFactory(name_id='ad',group__type_id='area',group__state_id='active').person
|
||||
|
|
|
@ -9,7 +9,7 @@ from zoneinfo import ZoneInfo
|
|||
|
||||
from django.conf import settings
|
||||
|
||||
from ietf.doc.models import Document, DocAlias, RelatedDocument, DocEvent, TelechatDocEvent, BallotDocEvent
|
||||
from ietf.doc.models import Document, DocAlias, RelatedDocument, DocEvent, TelechatDocEvent, BallotDocEvent, DocTypeName
|
||||
from ietf.doc.expire import expirable_drafts
|
||||
from ietf.doc.utils import augment_docs_and_user_with_user_info
|
||||
from ietf.meeting.models import SessionPresentation, Meeting, Session
|
||||
|
@ -26,7 +26,7 @@ def fill_in_telechat_date(docs, doc_dict=None, doc_ids=None):
|
|||
doc_dict = dict((d.pk, d) for d in docs)
|
||||
doc_ids = list(doc_dict.keys())
|
||||
if doc_ids is None:
|
||||
doc_ids = list(doc_dict.keys())
|
||||
doc_ids = list(doc_dict.keys())
|
||||
|
||||
seen = set()
|
||||
for e in TelechatDocEvent.objects.filter(doc__id__in=doc_ids, type="scheduled_for_telechat").order_by('-time'):
|
||||
|
@ -181,7 +181,7 @@ def augment_docs_with_related_docs_info(docs):
|
|||
continue
|
||||
originalDoc = d.related_that_doc('conflrev')[0].document
|
||||
d.pages = originalDoc.pages
|
||||
|
||||
|
||||
def prepare_document_table(request, docs, query=None, max_results=200):
|
||||
"""Take a queryset of documents and a QueryDict with sorting info
|
||||
and return list of documents with attributes filled in for
|
||||
|
@ -283,3 +283,86 @@ def prepare_document_table(request, docs, query=None, max_results=200):
|
|||
h["sort_url"] = "?" + d.urlencode()
|
||||
|
||||
return (docs, meta)
|
||||
|
||||
|
||||
# The document types and state slugs to include in the AD dashboard
|
||||
# and AD doc list, in the order they should be shown.
|
||||
#
|
||||
# "rfc" is a custom subset of "draft" that we special-case in the code
|
||||
# to break out these docs into a separate table.
|
||||
#
|
||||
AD_WORKLOAD = {
|
||||
"draft": [
|
||||
"pub-req",
|
||||
"ad-eval",
|
||||
"lc-req",
|
||||
"lc",
|
||||
"writeupw",
|
||||
# "defer", # probably not a useful state to show, since it's rare
|
||||
"iesg-eva",
|
||||
"goaheadw",
|
||||
"approved",
|
||||
"ann",
|
||||
],
|
||||
"rfc": [
|
||||
"rfcqueue",
|
||||
"rfc",
|
||||
],
|
||||
"conflrev": [
|
||||
"needshep",
|
||||
"adrev",
|
||||
"iesgeval",
|
||||
"approved", # synthesized state for all the "appr-" states
|
||||
# "withdraw", # probably not a useful state to show
|
||||
],
|
||||
"statchg": [
|
||||
"needshep",
|
||||
"adrev",
|
||||
"lc-req",
|
||||
"in-lc",
|
||||
"iesgeval",
|
||||
"goahead",
|
||||
"appr-sent",
|
||||
# "dead", # probably not a useful state to show
|
||||
],
|
||||
"charter": [
|
||||
"notrev",
|
||||
"infrev",
|
||||
"intrev",
|
||||
"extrev",
|
||||
"iesgrev",
|
||||
"approved",
|
||||
# "replaced", # probably not a useful state to show
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def doc_type(doc):
|
||||
dt = doc.type.slug
|
||||
if (
|
||||
doc.get_state_slug("draft") == "rfc"
|
||||
or doc.get_state_slug("draft-iesg") == "rfcqueue"
|
||||
):
|
||||
dt = "rfc"
|
||||
return dt
|
||||
|
||||
|
||||
def doc_state(doc):
|
||||
dt = doc.type.slug
|
||||
ds = doc.get_state(dt)
|
||||
if dt == "draft":
|
||||
dis = doc.get_state("draft-iesg")
|
||||
if ds.slug == "active" and dis:
|
||||
return dis.slug
|
||||
elif dt == "conflrev":
|
||||
if ds.slug.startswith("appr"):
|
||||
return "approved"
|
||||
return ds.slug
|
||||
|
||||
|
||||
def doc_type_name(doc_type):
|
||||
if doc_type == "rfc":
|
||||
return "RFC"
|
||||
if doc_type == "draft":
|
||||
return "Internet-Draft"
|
||||
return DocTypeName.objects.get(slug=doc_type).name
|
||||
|
|
|
@ -36,8 +36,7 @@
|
|||
|
||||
import re
|
||||
import datetime
|
||||
|
||||
from collections import defaultdict
|
||||
import copy
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
|
@ -47,8 +46,9 @@ 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.html import strip_tags
|
||||
from django.utils.cache import _generate_cache_key # type: ignore
|
||||
|
||||
from django.utils.text import slugify
|
||||
|
||||
|
||||
import debug # pyflakes:ignore
|
||||
|
@ -66,7 +66,8 @@ from ietf.person.models import Person
|
|||
from ietf.person.utils import get_active_ads
|
||||
from ietf.utils.draft_search import normalize_draftname
|
||||
from ietf.utils.log import log
|
||||
from ietf.doc.utils_search import prepare_document_table
|
||||
from ietf.doc.utils_search import prepare_document_table, doc_type, doc_state, doc_type_name, AD_WORKLOAD
|
||||
from ietf.ietfauth.utils import has_role
|
||||
|
||||
|
||||
class SearchForm(forms.Form):
|
||||
|
@ -313,76 +314,23 @@ def search_for_name(request, name):
|
|||
|
||||
return cached_redirect(cache_key, urlreverse('ietf.doc.views_search.search') + search_args)
|
||||
|
||||
def ad_dashboard_group_type(doc):
|
||||
# Return group type for document for dashboard.
|
||||
# If doc is not defined return list of all possible
|
||||
# group types
|
||||
if not doc:
|
||||
return ('I-D', 'RFC', 'Conflict Review', 'Status Change', 'Charter')
|
||||
if doc.type.slug=='draft':
|
||||
if doc.get_state_slug('draft') == 'rfc':
|
||||
return 'RFC'
|
||||
elif doc.get_state_slug('draft') == 'active' and doc.get_state_slug('draft-iesg') and doc.get_state('draft-iesg').name =='RFC Ed Queue':
|
||||
return 'RFC'
|
||||
elif doc.get_state_slug('draft') == 'active' and doc.get_state_slug('draft-iesg') and doc.get_state('draft-iesg').name in ('Dead', 'I-D Exists', 'AD is watching'):
|
||||
return None
|
||||
elif doc.get_state('draft').name in ('Expired', 'Replaced'):
|
||||
return None
|
||||
else:
|
||||
return 'I-D'
|
||||
elif doc.type.slug=='conflrev':
|
||||
return 'Conflict Review'
|
||||
elif doc.type.slug=='statchg':
|
||||
return 'Status Change'
|
||||
elif doc.type.slug=='charter':
|
||||
return "Charter"
|
||||
|
||||
def state_name(doc_type, state, shorten=True):
|
||||
name = ""
|
||||
if doc_type in ["draft", "rfc"] and state not in ["rfc", "expired"]:
|
||||
name = State.objects.get(type__in=["draft", "draft-iesg"], slug=state).name
|
||||
elif state == "rfc":
|
||||
name = "RFC"
|
||||
elif doc_type == "conflrev" and state.startswith("appr"):
|
||||
name = "Approved"
|
||||
else:
|
||||
return "Document"
|
||||
name = State.objects.get(type=doc_type, slug=state).name
|
||||
|
||||
def ad_dashboard_group(doc):
|
||||
|
||||
if doc.type.slug=='draft':
|
||||
if doc.get_state_slug('draft') == 'rfc':
|
||||
return 'RFC'
|
||||
elif doc.get_state_slug('draft') == 'active' and doc.get_state_slug('draft-iesg'):
|
||||
return '%s Internet-Draft' % doc.get_state('draft-iesg').name
|
||||
else:
|
||||
return '%s Internet-Draft' % doc.get_state('draft').name
|
||||
elif doc.type.slug=='conflrev':
|
||||
if doc.get_state_slug('conflrev') in ('appr-reqnopub-sent','appr-noprob-sent'):
|
||||
return 'Approved Conflict Review'
|
||||
elif doc.get_state_slug('conflrev') in ('appr-reqnopub-pend','appr-noprob-pend','appr-reqnopub-pr','appr-noprob-pr'):
|
||||
return "%s Conflict Review" % State.objects.get(type__slug='draft-iesg',slug='approved')
|
||||
else:
|
||||
return '%s Conflict Review' % doc.get_state('conflrev')
|
||||
elif doc.type.slug=='statchg':
|
||||
if doc.get_state_slug('statchg') in ('appr-sent',):
|
||||
return 'Approved Status Change'
|
||||
if doc.get_state_slug('statchg') in ('appr-pend','appr-pr'):
|
||||
return '%s Status Change' % State.objects.get(type__slug='draft-iesg',slug='approved')
|
||||
else:
|
||||
return '%s Status Change' % doc.get_state('statchg')
|
||||
elif doc.type.slug=='charter':
|
||||
if doc.get_state_slug('charter') == 'approved':
|
||||
return "Approved Charter"
|
||||
else:
|
||||
return '%s Charter' % doc.get_state('charter')
|
||||
else:
|
||||
return "Document"
|
||||
|
||||
|
||||
def shorten_group_name(name):
|
||||
for s in [
|
||||
" Internet-Draft",
|
||||
" Conflict Review",
|
||||
" Status Change",
|
||||
" (Internal Steering Group/IAB Review) Charter",
|
||||
"Charter",
|
||||
]:
|
||||
if name.endswith(s):
|
||||
name = name[: -len(s)]
|
||||
if not shorten:
|
||||
return name
|
||||
|
||||
for pat, sub in [
|
||||
(r" \(Internal Steering Group/IAB Review\)", ""),
|
||||
("Writeup", "Write-up"),
|
||||
("Requested", "Req"),
|
||||
("Evaluation", "Eval"),
|
||||
|
@ -390,82 +338,45 @@ def shorten_group_name(name):
|
|||
("Waiting", "Wait"),
|
||||
("Go-Ahead", "OK"),
|
||||
("Approved-", "App, "),
|
||||
("Approved No Problem", "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"),
|
||||
(
|
||||
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\)", "")
|
||||
(r"\(Message to Community, Selected by Secretariat\)", ""),
|
||||
]:
|
||||
name = re.sub(pat, sub, name)
|
||||
|
||||
return name.strip()
|
||||
|
||||
|
||||
def ad_dashboard_sort_key(doc):
|
||||
STATE_SLUGS = {
|
||||
dt: {state_name(dt, ds, shorten=False): ds for ds in AD_WORKLOAD[dt]} # type: ignore
|
||||
for dt in AD_WORKLOAD
|
||||
}
|
||||
|
||||
if doc.type.slug=='draft' and doc.get_state_slug('draft') == 'rfc':
|
||||
return "21%04d" % int(doc.rfc_number())
|
||||
if doc.type.slug=='statchg' and doc.get_state_slug('statchg') == 'appr-sent':
|
||||
return "22%d" % 0 # TODO - get the date of the transition into this state here
|
||||
if doc.type.slug=='conflrev' and doc.get_state_slug('conflrev') in ('appr-reqnopub-sent','appr-noprob-sent'):
|
||||
return "23%d" % 0 # TODO - get the date of the transition into this state here
|
||||
if doc.type.slug=='charter' and doc.get_state_slug('charter') == 'approved':
|
||||
return "24%d" % 0 # TODO - get the date of the transition into this state here
|
||||
IESG_STATES = State.objects.filter(type="draft-iesg").values_list("name", flat=True)
|
||||
|
||||
seed = ad_dashboard_group(doc)
|
||||
|
||||
if doc.type.slug=='conflrev' and doc.get_state_slug('conflrev') == 'adrev':
|
||||
state = State.objects.get(type__slug='draft-iesg',slug='ad-eval')
|
||||
return "1%d%s" % (state.order,seed)
|
||||
|
||||
if doc.type.slug=='charter' and doc.get_state_slug('charter') != 'replaced':
|
||||
if doc.get_state_slug('charter') in ('notrev','infrev'):
|
||||
return "100%s" % seed
|
||||
elif doc.get_state_slug('charter') == 'intrev':
|
||||
state = State.objects.get(type__slug='draft-iesg',slug='ad-eval')
|
||||
return "1%d%s" % (state.order,seed)
|
||||
elif doc.get_state_slug('charter') == 'extrev':
|
||||
state = State.objects.get(type__slug='draft-iesg',slug='lc')
|
||||
return "1%d%s" % (state.order,seed)
|
||||
elif doc.get_state_slug('charter') == 'iesgrev':
|
||||
state = State.objects.get(type__slug='draft-iesg',slug='iesg-eva')
|
||||
return "1%d%s" % (state.order,seed)
|
||||
|
||||
if doc.type.slug=='statchg' and doc.get_state_slug('statchg') == 'adrev':
|
||||
state = State.objects.get(type__slug='draft-iesg',slug='ad-eval')
|
||||
return "1%d%s" % (state.order,seed)
|
||||
|
||||
if seed.startswith('Needs Shepherd'):
|
||||
return "100%s" % seed
|
||||
if seed.endswith(' Document'):
|
||||
seed = seed[:-9]
|
||||
elif seed.endswith(' Internet-Draft'):
|
||||
seed = seed[:-15]
|
||||
elif seed.endswith(' Conflict Review'):
|
||||
seed = seed[:-16]
|
||||
elif seed.endswith(' Status Change'):
|
||||
seed = seed[:-14]
|
||||
state = State.objects.filter(type__slug='draft-iesg',name=seed)
|
||||
if state:
|
||||
ageseconds = 0
|
||||
changetime= doc.latest_event(type='changed_document')
|
||||
if changetime:
|
||||
ad = (timezone.now()-doc.latest_event(type='changed_document').time)
|
||||
ageseconds = (ad.microseconds + (ad.seconds + ad.days * 24 * 3600) * 10**6) / 10**6
|
||||
return "1%d%s%s%010d" % (state[0].order,seed,doc.type.slug,ageseconds)
|
||||
|
||||
return "3%s" % seed
|
||||
def date_to_bucket(date, now, num_buckets):
|
||||
return num_buckets - min(
|
||||
num_buckets, int((now.date() - date.date()).total_seconds() / 60 / 60 / 24)
|
||||
)
|
||||
|
||||
|
||||
def ad_workload(request):
|
||||
delta = datetime.timedelta(days=120)
|
||||
right_now = timezone.now()
|
||||
# number of days (= buckets) to show in the graphs
|
||||
days = 120 if has_role(request.user, ["Area Director", "Secretariat"]) else 1
|
||||
now = timezone.now()
|
||||
|
||||
ads = []
|
||||
responsible = Document.objects.values_list("ad", flat=True).distinct()
|
||||
|
@ -480,209 +391,146 @@ def ad_workload(request):
|
|||
if p in get_active_ads():
|
||||
ads.append(p)
|
||||
|
||||
doctypes = list(
|
||||
DocTypeName.objects.filter(used=True)
|
||||
.exclude(slug__in=("draft", "liai-att"))
|
||||
.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}
|
||||
|
||||
# Prefill groups in preferred sort order
|
||||
# FIXME: This should really use the database states instead of replicating the logic
|
||||
for id, (g, uig) in enumerate(
|
||||
[
|
||||
("Publication Requested Internet-Draft", False),
|
||||
("AD Evaluation Internet-Draft", False),
|
||||
("Last Call Requested Internet-Draft", True),
|
||||
("In Last Call Internet-Draft", True),
|
||||
("Waiting for Writeup Internet-Draft", False),
|
||||
("IESG Evaluation - Defer Internet-Draft", False),
|
||||
("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),
|
||||
("AD Evaluation Status Change", False),
|
||||
("Last Call Requested Status Change", True),
|
||||
("In Last Call Status Change", True),
|
||||
("Waiting for Writeup Status Change", False),
|
||||
("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
|
||||
bucket_template = {
|
||||
dt: {state: [[] for _ in range(days)] for state in STATE_SLUGS[dt].values()}
|
||||
for dt in STATE_SLUGS
|
||||
}
|
||||
sums = copy.deepcopy(bucket_template)
|
||||
|
||||
for ad in ads:
|
||||
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)
|
||||
ad.buckets = copy.deepcopy(bucket_template)
|
||||
|
||||
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.
|
||||
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)
|
||||
for doc in Document.objects.filter(ad=ad):
|
||||
dt = doc_type(doc)
|
||||
state = doc_state(doc)
|
||||
|
||||
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))
|
||||
state_events = doc.docevent_set.filter(
|
||||
Q(type="started_iesg_process")
|
||||
| Q(type="changed_state")
|
||||
| Q(type="published_rfc")
|
||||
| Q(type="closed_ballot"),
|
||||
).order_by("-time")
|
||||
|
||||
ad.counts[group_type][groups[group_type][group]] += 1
|
||||
ad.doc_now[group_type][groups[group_type][group]].add(doc)
|
||||
# compute state history for drafts
|
||||
last = now
|
||||
for e in state_events:
|
||||
to_state = None
|
||||
if dt == "charter":
|
||||
if e.type == "closed_ballot":
|
||||
to_state = state_name(dt, state, shorten=False)
|
||||
elif e.desc.endswith("has been replaced"):
|
||||
# stop tracking
|
||||
break
|
||||
|
||||
last_state_event = (
|
||||
doc.docevent_set.filter(
|
||||
Q(type="started_iesg_process") | Q(type="changed_state")
|
||||
if not to_state:
|
||||
# get the state name this event changed the doc into
|
||||
match = re.search(
|
||||
r"(RFC) published|[Ss]tate changed to (.*?)(?:::.*)? from (.*?)(?=::|$)",
|
||||
strip_tags(e.desc),
|
||||
flags=re.MULTILINE,
|
||||
)
|
||||
.order_by("-time")
|
||||
.first()
|
||||
)
|
||||
if (last_state_event is not None) and (right_now - last_state_event.time) > delta:
|
||||
ad.prev[group_type][groups[group_type][group]] += 1
|
||||
ad.doc_prev[group_type][groups[group_type][group]].add(doc)
|
||||
if not match:
|
||||
# some irrelevant state change for the AD dashboard, ignore it
|
||||
continue
|
||||
to_state = match.group(1) or match.group(2)
|
||||
|
||||
for ad in ads:
|
||||
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)
|
||||
# fix up some states that have been renamed
|
||||
if dt == "conflrev" and to_state.startswith("Approved"):
|
||||
to_state = "Approved"
|
||||
elif dt == "charter" and to_state.startswith(
|
||||
"Start Chartering/Rechartering"
|
||||
):
|
||||
to_state = "Start Chartering/Rechartering (Internal Steering Group/IAB Review)"
|
||||
elif to_state == "RFC Published":
|
||||
to_state = "RFC"
|
||||
|
||||
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]
|
||||
if to_state not in STATE_SLUGS[dt].keys() or to_state == "Replaced":
|
||||
# change into a state the AD dashboard doesn't display
|
||||
if to_state in IESG_STATES or to_state == "Replaced":
|
||||
# if it's an IESG state we don't display, we're done with this doc
|
||||
last = e.time
|
||||
break
|
||||
# if it's not an IESG state, keep going with next event
|
||||
continue
|
||||
|
||||
# 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),
|
||||
g,
|
||||
up_is_good[g] if g in up_is_good else None,
|
||||
)
|
||||
sn = STATE_SLUGS[dt][to_state]
|
||||
buckets_start = date_to_bucket(e.time, now, days)
|
||||
buckets_end = date_to_bucket(last, now, days)
|
||||
|
||||
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
|
||||
if buckets_end >= days:
|
||||
# this event is older than we record in the history
|
||||
if last == now:
|
||||
# but since we didn't record any state yet,
|
||||
# this is the state the doc was in for the
|
||||
# entire history
|
||||
for b in range(buckets_start, days):
|
||||
ad.buckets[dt][sn][b].append(doc.name)
|
||||
sums[dt][sn][b].append(doc.name)
|
||||
last = e.time
|
||||
break
|
||||
|
||||
# record doc state in the indicated buckets
|
||||
for b in range(buckets_start, buckets_end):
|
||||
ad.buckets[dt][sn][b].append(doc.name)
|
||||
sums[dt][sn][b].append(doc.name)
|
||||
last = e.time
|
||||
|
||||
if last == now:
|
||||
s = state_name(dt, state, shorten=False)
|
||||
if s in STATE_SLUGS[dt].keys():
|
||||
# we didn't have a single event for this doc, assume
|
||||
# the current state applied throughput the history
|
||||
for b in range(days):
|
||||
ad.buckets[dt][state][b].append(doc.name)
|
||||
sums[dt][state][b].append(doc.name)
|
||||
|
||||
metadata = [
|
||||
{
|
||||
"type": (dt, doc_type_name(dt)),
|
||||
"states": [(state, state_name(dt, state)) for state in ad.buckets[dt]],
|
||||
"ads": ads,
|
||||
}
|
||||
for dt in AD_WORKLOAD
|
||||
]
|
||||
|
||||
return render(request, "doc/ad_list.html", {"workload": workload, "delta": delta})
|
||||
data = {
|
||||
dt: {slugify(ad): ad.buckets[dt] for ad in ads} | {"sum": sums[dt]}
|
||||
for dt in AD_WORKLOAD
|
||||
}
|
||||
|
||||
return render(
|
||||
request,
|
||||
"doc/ad_list.html",
|
||||
{"metadata": metadata, "data": data, "delta": days},
|
||||
)
|
||||
|
||||
|
||||
def docs_for_ad(request, name):
|
||||
def sort_key(doc):
|
||||
key = list(AD_WORKLOAD.keys()).index(doc_type(doc))
|
||||
return key
|
||||
|
||||
ad = None
|
||||
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")
|
||||
| Q(pk__in=responsible)).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",
|
||||
)
|
||||
| Q(pk__in=responsible)
|
||||
).distinct():
|
||||
if name == p.full_name_as_key():
|
||||
ad = p
|
||||
break
|
||||
if not ad:
|
||||
raise Http404
|
||||
form = SearchForm({'by':'ad','ad': ad.id,
|
||||
'rfcs':'on', 'activedrafts':'on', 'olddrafts':'on',
|
||||
'sort': 'status',
|
||||
'doctypes': list(DocTypeName.objects.filter(used=True).exclude(slug__in=('draft','liai-att')).values_list("pk", flat=True))})
|
||||
results, meta = prepare_document_table(request, retrieve_search_results(form), form.data, max_results=500)
|
||||
results.sort(key=ad_dashboard_sort_key)
|
||||
|
||||
results, meta = prepare_document_table(request, Document.objects.filter(ad=ad))
|
||||
results.sort(key=lambda d: sort_key(d))
|
||||
del meta["headers"][-1]
|
||||
|
||||
# filter out some results
|
||||
|
@ -706,28 +554,37 @@ def docs_for_ad(request, name):
|
|||
]
|
||||
|
||||
for d in results:
|
||||
d.search_heading = ad_dashboard_group(d)
|
||||
dt = d.type.slug
|
||||
d.search_heading = state_name(dt, doc_state(d), shorten=False)
|
||||
if d.search_heading != "RFC":
|
||||
d.search_heading += f" {doc_type_name(dt)}"
|
||||
|
||||
# Additional content showing docs with blocking positions by this AD,
|
||||
# and docs that the AD hasn't balloted on that are lacking ballot positions to progress
|
||||
blocked_docs = []
|
||||
not_balloted_docs = []
|
||||
if ad in get_active_ads():
|
||||
iesg_docs = Document.objects.filter(Q(states__type="draft-iesg",
|
||||
states__slug__in=IESG_BALLOT_ACTIVE_STATES) |
|
||||
Q(states__type="charter",
|
||||
states__slug__in=IESG_CHARTER_ACTIVE_STATES) |
|
||||
Q(states__type__in=("statchg", "conflrev"),
|
||||
states__slug__in=IESG_STATCHG_CONFLREV_ACTIVE_STATES)).distinct()
|
||||
possible_docs = iesg_docs.filter(docevent__ballotpositiondocevent__pos__blocking=True,
|
||||
docevent__ballotpositiondocevent__balloter=ad)
|
||||
iesg_docs = Document.objects.filter(
|
||||
Q(states__type="draft-iesg", states__slug__in=IESG_BALLOT_ACTIVE_STATES)
|
||||
| Q(states__type="charter", states__slug__in=IESG_CHARTER_ACTIVE_STATES)
|
||||
| Q(
|
||||
states__type__in=("statchg", "conflrev"),
|
||||
states__slug__in=IESG_STATCHG_CONFLREV_ACTIVE_STATES,
|
||||
)
|
||||
).distinct()
|
||||
possible_docs = iesg_docs.filter(
|
||||
docevent__ballotpositiondocevent__pos__blocking=True,
|
||||
docevent__ballotpositiondocevent__balloter=ad,
|
||||
)
|
||||
for doc in possible_docs:
|
||||
ballot = doc.active_ballot()
|
||||
if not ballot:
|
||||
continue
|
||||
|
||||
blocking_positions = [p for p in ballot.all_positions() if p.pos.blocking]
|
||||
if not blocking_positions or not any( p.balloter==ad for p in blocking_positions ):
|
||||
if not blocking_positions or not any(
|
||||
p.balloter == ad for p in blocking_positions
|
||||
):
|
||||
continue
|
||||
|
||||
augment_events_with_revision(doc, blocking_positions)
|
||||
|
@ -739,7 +596,12 @@ def docs_for_ad(request, name):
|
|||
|
||||
# latest first
|
||||
if blocked_docs:
|
||||
blocked_docs.sort(key=lambda d: min(p.time for p in d.blocking_positions if p.balloter==ad), reverse=True)
|
||||
blocked_docs.sort(
|
||||
key=lambda d: min(
|
||||
p.time for p in d.blocking_positions if p.balloter == ad
|
||||
),
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
possible_docs = iesg_docs.exclude(
|
||||
Q(docevent__ballotpositiondocevent__balloter=ad)
|
||||
|
@ -750,7 +612,7 @@ def docs_for_ad(request, name):
|
|||
not ballot
|
||||
or doc.get_state_slug("draft") == "repl"
|
||||
or doc.get_state_slug("draft-iesg") == "defer"
|
||||
or (doc.telechat_date() and doc.telechat_date() > timezone.now().date())
|
||||
or not doc.previous_telechat_date()
|
||||
):
|
||||
continue
|
||||
|
||||
|
@ -760,9 +622,19 @@ def docs_for_ad(request, name):
|
|||
if re.search(r"\bNeeds\s+\d+", iesg_ballot_summary):
|
||||
not_balloted_docs.append(doc)
|
||||
|
||||
return render(request, 'doc/drafts_for_ad.html', {
|
||||
'form':form, 'docs':results, 'meta':meta, 'ad_name': ad.plain_name(), 'blocked_docs': blocked_docs, 'not_balloted_docs': not_balloted_docs
|
||||
})
|
||||
return render(
|
||||
request,
|
||||
"doc/drafts_for_ad.html",
|
||||
{
|
||||
"docs": results,
|
||||
"meta": meta,
|
||||
"ad_name": ad.name,
|
||||
"blocked_docs": blocked_docs,
|
||||
"not_balloted_docs": not_balloted_docs,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def drafts_in_last_call(request):
|
||||
lc_state = State.objects.get(type="draft-iesg", slug="lc").pk
|
||||
form = SearchForm({'by':'state','state': lc_state, 'rfcs':'on', 'activedrafts':'on'})
|
||||
|
|
|
@ -1 +1,6 @@
|
|||
@import "npm:highcharts/css/highcharts.css";
|
||||
@import "custom-bs-import";
|
||||
|
||||
.highcharts-container {
|
||||
font-family: $font-family-sans-serif;
|
||||
}
|
||||
|
|
|
@ -6,6 +6,8 @@ import Highcharts_Export_Data from "highcharts/modules/export-data";
|
|||
import Highcharts_Accessibility from "highcharts/modules/accessibility";
|
||||
import Highcharts_Sunburst from "highcharts/modules/sunburst";
|
||||
|
||||
document.documentElement.style.setProperty("--highcharts-background-color", "transparent");
|
||||
|
||||
Highcharts_Exporting(Highcharts);
|
||||
Highcharts_Offline_Exporting(Highcharts);
|
||||
Highcharts_Export_Data(Highcharts);
|
||||
|
@ -27,7 +29,7 @@ window.Highcharts = Highcharts;
|
|||
window.group_stats = function (url, chart_selector) {
|
||||
$.getJSON(url, function (data) {
|
||||
$(chart_selector)
|
||||
.each(function (i, e) {
|
||||
.each(function (_, e) {
|
||||
const dataset = e.dataset.dataset;
|
||||
if (!dataset) {
|
||||
console.log("dataset data attribute not set");
|
||||
|
|
|
@ -5,6 +5,8 @@ import Highcharts_Offline_Exporting from "highcharts/modules/offline-exporting";
|
|||
import Highcharts_Export_Data from "highcharts/modules/export-data";
|
||||
import Highcharts_Accessibility from"highcharts/modules/accessibility";
|
||||
|
||||
document.documentElement.style.setProperty("--highcharts-background-color", "transparent");
|
||||
|
||||
Highcharts_Exporting(Highcharts);
|
||||
Highcharts_Offline_Exporting(Highcharts);
|
||||
Highcharts_Export_Data(Highcharts);
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
{# Copyright The IETF Trust 2015, All Rights Reserved #}
|
||||
{% load ietf_filters %}
|
||||
|
||||
{% if prev or count %}
|
||||
<span{% if count == 0 %} class="text-body-secondary"{% 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-body-secondary' }}"
|
||||
{% elif prev > count %}
|
||||
class="bi bi-arrow-down-right-circle{% if count %}-fill{% endif %} {{ up_is_good|yesno:'text-danger,text-success,text-body-secondary' }}"
|
||||
{% else %}
|
||||
class="bi bi-arrow-right-circle text-body-secondary"
|
||||
{% endif %}
|
||||
></i>
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% endif %}
|
|
@ -3,42 +3,58 @@
|
|||
{% 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' %}">
|
||||
<link rel="stylesheet" href="{% static 'ietf/css/highcharts.css' %}">
|
||||
{% endblock %}
|
||||
{% block title %}Area directors{% endblock %}
|
||||
{% block morecss %}
|
||||
table .border-bottom { border-bottom-color: var(--highcharts-neutral-color-80) !important; }
|
||||
.highcharts-container .highcharts-axis-labels { font-size: .7rem; }
|
||||
.highcharts-container .highcharts-graph { stroke-width: 2.5; }
|
||||
.highcharts-container .highcharts-color-0 {
|
||||
fill: var(--bs-primary);
|
||||
stroke: var(--bs-primary);
|
||||
}
|
||||
.highcharts-container .highcharts-data-label text {
|
||||
font-size: 1rem;
|
||||
font-weight: inherit;
|
||||
}
|
||||
{% endblock %}
|
||||
{% block title %}IESG Dashboard{% endblock %}
|
||||
{% block content %}
|
||||
{% origin %}
|
||||
<h1>Area Director Workload</h1>
|
||||
<h1>IESG Dashboard</h1>
|
||||
{% if user|has_role:"Area Director,Secretariat" %}
|
||||
<div class="alert alert-info my-3">
|
||||
{{ delta.days }}-day trend indicators
|
||||
{{ delta }}-day trend graphs
|
||||
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">
|
||||
{% for dt in metadata %}
|
||||
<h2 class="mt-5" id="{{ dt.type.0 }}">{{ dt.type.1 }} State Counts</h2>
|
||||
<table class="table table-sm table-striped table-bordered tablesorter navskip">
|
||||
<thead class="wrap-anywhere">
|
||||
<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>' }}
|
||||
{% for state, state_name in dt.states %}
|
||||
<th scope="col" class="col-1" title=""
|
||||
data-sort="{{ state }}-num">
|
||||
<a href="{% url 'ietf.doc.views_help.state_help' type='draft-iesg' %}#{{ state }}">
|
||||
{{ state_name|split:'/'|join:'/<wbr>' }}
|
||||
</a>
|
||||
</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="table-group-divider">
|
||||
{% for ad, ad_data in group.counts %}
|
||||
{% for ad in dt.ads %}
|
||||
<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' %}
|
||||
{% for state, state_name in dt.states %}
|
||||
<td class="col-1 align-bottom"
|
||||
id="{{ dt.type.0 }}-{{ ad|slugify }}-{{ state }}">
|
||||
<div id="chart-{{ dt.type.0 }}-{{ ad|slugify }}-{{ state }}"></div>
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
|
@ -47,9 +63,9 @@
|
|||
<tfoot class="table-group-divider">
|
||||
<tr>
|
||||
<th scope="row">Sum</th>
|
||||
{% for label, count, prev in group.sums %}
|
||||
<td>
|
||||
{% include 'doc/ad_count.html' %}
|
||||
{% for state, state_name in dt.states %}
|
||||
<td class="align-bottom">
|
||||
<div id="chart-{{ dt.type.0 }}-sum-{{ state }}"></div>
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
|
@ -69,4 +85,114 @@
|
|||
});
|
||||
});
|
||||
</script>
|
||||
<script src="{% static "ietf/js/highcharts.js" %}"></script>
|
||||
{{ data|json_script:"data" }}
|
||||
<script>
|
||||
const data = JSON.parse(document.getElementById("data").textContent);
|
||||
|
||||
Object.entries(data).forEach(([dt, ads]) => {
|
||||
max = {};
|
||||
Object.entries(ads).forEach(([ad, states]) => {
|
||||
Object.entries(states).forEach(([state, buckets]) => {
|
||||
buckets.series = buckets.map((x) => x.length);
|
||||
if (ad != "sum") {
|
||||
max[state] = Math.max(...buckets.series,
|
||||
max[state] ? max[state] : 0);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Object.entries(ads).forEach(([ad, states]) => {
|
||||
Object.entries(states).forEach(([state, buckets]) => {
|
||||
const cell = `chart-${dt}-${ad}-${state}`;
|
||||
|
||||
// if there is only a single datapoint in the
|
||||
// bucket, display it without a graph
|
||||
if (buckets.series.length == 1) {
|
||||
document.getElementById(cell).innerHTML =
|
||||
buckets.series[0];
|
||||
return;
|
||||
}
|
||||
|
||||
// if a bucket has all zeroes, fake a Highcharts
|
||||
// plot with HTML, to reduce the number of plot
|
||||
// objects on the page
|
||||
if (buckets.series.every((x) => x == 0)) {
|
||||
// document.getElementById(cell).innerHTML = `
|
||||
// <div class="position-relative">
|
||||
// <div class="position-absolute bottom-0 start-0">
|
||||
// <div style="font-size: .7rem;" class="ms-1">0</div>
|
||||
// </div>
|
||||
// <div class="position-absolute bottom-0 end-0 w-100 ps-1">
|
||||
// <div class="border-bottom mb-1 ms-3">0</div>
|
||||
// </div>
|
||||
// </div>
|
||||
// `;
|
||||
return;
|
||||
}
|
||||
|
||||
// else actually create a graph
|
||||
const ymax = Math.max(1, ad != "sum" ? max[state] : Math.max(...buckets.series));
|
||||
Highcharts.chart({
|
||||
title: { text: undefined },
|
||||
chart: {
|
||||
type: "line",
|
||||
animation: false,
|
||||
renderTo: cell,
|
||||
panning: { enabled: false },
|
||||
spacing: [4, 0, 5, 0],
|
||||
height: "45%",
|
||||
},
|
||||
scrollbar: { enabled: false },
|
||||
tooltip: { enabled: false },
|
||||
navigator: { enabled: false },
|
||||
exporting: { enabled: false },
|
||||
legend: { enabled: false },
|
||||
credits: { enabled: false },
|
||||
xAxis: {
|
||||
title: { text: undefined},
|
||||
labels: { enabled: false },
|
||||
zoomEnabled: false,
|
||||
tickLength: 0,
|
||||
},
|
||||
yAxis: {
|
||||
title: { text: undefined},
|
||||
zoomEnabled: false,
|
||||
tickLength: 0,
|
||||
labels: { x: -3 },
|
||||
min: 0,
|
||||
max: ymax,
|
||||
tickInterval: ymax,
|
||||
},
|
||||
plotOptions: {
|
||||
series: {
|
||||
animation: false,
|
||||
dataLabels: {
|
||||
enabled: true,
|
||||
inside: true,
|
||||
padding: 0,
|
||||
formatter: function() {
|
||||
// only label the last
|
||||
// (= current-day) point in
|
||||
// the curve with today's
|
||||
// value
|
||||
if (this.point.index + 1 ==
|
||||
this.series.points.length) {
|
||||
return this.y;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
series: [{
|
||||
name: undefined,
|
||||
data: buckets.series,
|
||||
enableMouseTracking: false,
|
||||
}],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -1,7 +1,6 @@
|
|||
{% extends "base.html" %}
|
||||
{# Copyright The IETF Trust 2015, All Rights Reserved #}
|
||||
{% load origin static ietf_filters textfilters %}
|
||||
%}
|
||||
{% block pagehead %}
|
||||
<link rel="stylesheet" href="{% static "ietf/css/list.css" %}">
|
||||
{% endblock %}
|
||||
|
|
Loading…
Reference in a new issue