Skip to content

Commit 6a4142e

Browse files
rjsparkskivinen
andauthored
feat: Area director workload summary view (ietf-tools#4315)
* feat: Add new page as requested in ietf-tools#4242 to list all area directors and their current workload. Include links to the specific dashboards for each area director. This new page is in doc/ad/. * feat: Add new page as requested in ietf-tools#4242 to list all area directors and their current workload. Include links to the specific dashboards for each area director. This new page is in doc/ad/. * Fixed issues from the previous commit by renaming hash to get_hash. * Making outer () to be non matching * Fixed RFC Ed Queue Internet-Draft to RFC Ed Queue * refactor: split the /ad view apart from the /ad/name view. * fix: make the new template html valid. * test: start building a test for the new view * refactor: make the view testable and test it. * chore: remove unneeded commented lines * fix: avoid parenthsized-string-looks-like-tuple bug. * fix: repair bad closing tag in template Co-authored-by: Tero Kivinen <kivinen@iki.fi>
1 parent 8604740 commit 6a4142e

5 files changed

Lines changed: 225 additions & 5 deletions

File tree

ietf/doc/factories.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,12 @@ def states(obj, create, extracted, **kwargs):
290290

291291
class ConflictReviewFactory(BaseDocumentFactory):
292292
type_id='conflrev'
293+
294+
group = factory.SubFactory('ietf.group.factories.GroupFactory',acronym='none')
295+
296+
@factory.lazy_attribute_sequence
297+
def name(self, n):
298+
return draft_name_generator(self.type_id,self.group,n).replace('conflrev-','conflict-review-')
293299

294300
@factory.post_generation
295301
def review_of(obj, create, extracted, **kwargs):
@@ -298,7 +304,8 @@ def review_of(obj, create, extracted, **kwargs):
298304
if extracted:
299305
obj.relateddocument_set.create(relationship_id='conflrev',target=extracted.docalias.first())
300306
else:
301-
obj.relateddocument_set.create(relationship_id='conflrev',target=DocumentFactory(type_id='draft',group=Group.objects.get(type_id='individ')).docalias.first())
307+
obj.relateddocument_set.create(relationship_id='conflrev',target=DocumentFactory(name=obj.name.replace('conflict-review-','draft-'),type_id='draft',group=Group.objects.get(type_id='individ')).docalias.first())
308+
302309

303310
@factory.post_generation
304311
def states(obj, create, extracted, **kwargs):

ietf/doc/tests.py

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,14 @@
1010
import mock
1111
import json
1212
import copy
13+
import random
1314

1415
from http.cookies import SimpleCookie
1516
from pathlib import Path
1617
from pyquery import PyQuery
1718
from urllib.parse import urlparse, parse_qs
1819
from tempfile import NamedTemporaryFile
20+
from collections import defaultdict
1921

2022
from django.core.management import call_command
2123
from django.urls import reverse as urlreverse
@@ -36,10 +38,11 @@
3638
ConflictReviewFactory, WgDraftFactory, IndividualDraftFactory, WgRfcFactory,
3739
IndividualRfcFactory, StateDocEventFactory, BallotPositionDocEventFactory,
3840
BallotDocEventFactory, DocumentAuthorFactory, NewRevisionDocEventFactory,
39-
StatusChangeFactory)
41+
StatusChangeFactory, BofreqFactory)
4042
from ietf.doc.fields import SearchableDocumentsField
4143
from ietf.doc.utils import create_ballot_if_not_open, uppercase_std_abbreviated_name
42-
from ietf.group.models import Group
44+
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.
45+
from ietf.group.models import Group, Role
4346
from ietf.group.factories import GroupFactory, RoleFactory
4447
from ietf.ipr.factories import HolderIprDisclosureFactory
4548
from ietf.meeting.models import Meeting, SessionPresentation, SchedulingEvent
@@ -228,6 +231,45 @@ def test_frontpage(self):
228231
self.assertEqual(r.status_code, 200)
229232
self.assertContains(r, "Document Search")
230233

234+
def test_ad_workload(self):
235+
Role.objects.filter(name_id='ad').delete()
236+
ad = RoleFactory(name_id='ad',group__type_id='area',group__state_id='active',person__name='Example Areadirector').person
237+
doc_type_names = ['bofreq', 'charter', 'conflrev', 'draft', 'statchg']
238+
expected = defaultdict(lambda :0)
239+
for doc_type_name in doc_type_names:
240+
if doc_type_name=='draft':
241+
states = State.objects.filter(type='draft-iesg', used=True).values_list('slug', flat=True)
242+
else:
243+
states = State.objects.filter(type=doc_type_name, used=True).values_list('slug', flat=True)
244+
245+
for state in states:
246+
target_num = random.randint(0,2)
247+
for _ in range(target_num):
248+
if doc_type_name == 'draft':
249+
doc = IndividualDraftFactory(ad=ad,states=[('draft-iesg', state),('draft','rfc' if state=='pub' else 'active')])
250+
elif doc_type_name == 'charter':
251+
doc = CharterFactory(ad=ad, states=[(doc_type_name, state)])
252+
elif doc_type_name == 'bofreq':
253+
# Note that the view currently doesn't handle bofreqs
254+
doc = BofreqFactory(states=[(doc_type_name, state)], bofreqresponsibledocevent__responsible=[ad])
255+
elif doc_type_name == 'conflrev':
256+
doc = ConflictReviewFactory(ad=ad, states=State.objects.filter(type_id=doc_type_name, slug=state))
257+
elif doc_type_name == 'statchg':
258+
doc = StatusChangeFactory(ad=ad, states=State.objects.filter(type_id=doc_type_name, slug=state))
259+
else:
260+
# Currently unreachable
261+
doc = DocumentFactory(type_id=doc_type_name, ad=ad, states=[(doc_type_name, state)])
262+
263+
if not slugify(ad_dashboard_group_type(doc)) in ('document', 'none'):
264+
expected[(slugify(ad_dashboard_group_type(doc)), slugify(ad.full_name_as_key()), slugify(shorten_group_name(ad_dashboard_group(doc))))] += 1
265+
266+
url = urlreverse('ietf.doc.views_search.ad_workload')
267+
r = self.client.get(url)
268+
self.assertEqual(r.status_code, 200)
269+
q = PyQuery(r.content)
270+
for group_type, ad, group in expected:
271+
self.assertEqual(int(q(f'#{group_type}-{ad}-{group}').text()),expected[(group_type, ad, group)])
272+
231273
def test_docs_for_ad(self):
232274
ad = RoleFactory(name_id='ad',group__type_id='area',group__state_id='active').person
233275
draft = IndividualDraftFactory(ad=ad)

ietf/doc/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
url(r'^$', views_search.search),
5151
url(r'^search/?$', views_search.search),
5252
url(r'^in-last-call/?$', views_search.drafts_in_last_call),
53+
url(r'^ad/?$', views_search.ad_workload),
5354
url(r'^ad/(?P<name>[^/]+)/?$', views_search.docs_for_ad),
5455
url(r'^ad2/(?P<name>[\w.-]+)/$', RedirectView.as_view(url='/doc/ad/%(name)s/', permanent=True)),
5556
url(r'^rfc-status-changes/?$', views_status_change.rfc_status_changes),

ietf/doc/views_search.py

Lines changed: 134 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright The IETF Trust 2009-2020, All Rights Reserved
1+
# Copyright The IETF Trust 2009-2022, All Rights Reserved
22
# -*- coding: utf-8 -*-
33
#
44
# Some parts Copyright (C) 2009-2010 Nokia Corporation and/or its subsidiary(-ies).
@@ -307,6 +307,32 @@ def cached_redirect(cache_key, url):
307307

308308
return cached_redirect(cache_key, urlreverse('ietf.doc.views_search.search') + search_args)
309309

310+
def ad_dashboard_group_type(doc):
311+
# Return group type for document for dashboard.
312+
# If doc is not defined return list of all possible
313+
# group types
314+
if not doc:
315+
return ('I-D', 'RFC', 'Conflict Review', 'Status Change', 'Charter')
316+
if doc.type.slug=='draft':
317+
if doc.get_state_slug('draft') == 'rfc':
318+
return 'RFC'
319+
elif doc.get_state_slug('draft') == 'active' and doc.get_state_slug('draft-iesg') and doc.get_state('draft-iesg').name =='RFC Ed Queue':
320+
return 'RFC'
321+
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'):
322+
return None
323+
elif doc.get_state('draft').name in ('Expired', 'Replaced'):
324+
return None
325+
else:
326+
return 'I-D'
327+
elif doc.type.slug=='conflrev':
328+
return 'Conflict Review'
329+
elif doc.type.slug=='statchg':
330+
return 'Status Change'
331+
elif doc.type.slug=='charter':
332+
return "Charter"
333+
else:
334+
return "Document"
335+
310336
def ad_dashboard_group(doc):
311337

312338
if doc.type.slug=='draft':
@@ -338,6 +364,12 @@ def ad_dashboard_group(doc):
338364
else:
339365
return "Document"
340366

367+
def shorten_group_name(name):
368+
for s in [' Internet-Draft', ' Conflict Review', ' Status Change', ' (Internal Steering Group/IAB Review) Charter', 'Charter']:
369+
if name.endswith(s):
370+
name = name[:-len(s)]
371+
return name
372+
341373
def ad_dashboard_sort_key(doc):
342374

343375
if doc.type.slug=='draft' and doc.get_state_slug('draft') == 'rfc':
@@ -393,6 +425,107 @@ def ad_dashboard_sort_key(doc):
393425

394426
return "3%s" % seed
395427

428+
def ad_workload(request):
429+
ads = []
430+
responsible = Document.objects.values_list('ad', flat=True).distinct()
431+
for p in Person.objects.filter(
432+
Q(
433+
role__name__in=("pre-ad", "ad"),
434+
role__group__type="area",
435+
role__group__state="active"
436+
)
437+
| Q(pk__in=responsible)
438+
).distinct():
439+
if p in get_active_ads():
440+
ads.append(p)
441+
442+
doctypes = list(DocTypeName.objects.filter(used=True).exclude(slug='draft').values_list("pk", flat=True))
443+
444+
group_types = ad_dashboard_group_type(None)
445+
446+
groups = {}
447+
group_names = {}
448+
for g in group_types:
449+
groups[g] = {}
450+
group_names[g] = []
451+
452+
# Prefill groups in preferred sort order
453+
id = 0
454+
for g in [
455+
'Publication Requested Internet-Draft',
456+
'Waiting for Writeup Internet-Draft',
457+
'AD Evaluation Internet-Draft',
458+
'In Last Call Internet-Draft',
459+
'IESG Evaluation - Defer Internet-Draft',
460+
'IESG Evaluation Internet-Draft',
461+
'Waiting for AD Go-Ahead Internet-Draft',
462+
'Approved-announcement to be sent Internet-Draft',
463+
'Approved-announcement sent Internet-Draft']:
464+
groups['I-D'][g] = id
465+
group_names['I-D'].append(g)
466+
id += 1;
467+
id = 0
468+
for g in ['RFC Ed Queue Internet-Draft', 'RFC']:
469+
groups['RFC'][g] = id
470+
group_names['RFC'].append(g)
471+
id += 1;
472+
id = 0
473+
for g in ['AD Review Conflict Review',
474+
'Needs Shepherd Conflict Review',
475+
'IESG Evaluation Conflict Review',
476+
'Approved Conflict Review',
477+
'Withdrawn Conflict Review']:
478+
groups['Conflict Review'][g] = id
479+
group_names['Conflict Review'].append(g)
480+
id += 1;
481+
id = 0
482+
for g in [ 'Start Chartering/Rechartering (Internal Steering Group/IAB Review) Charter',
483+
'Replaced Charter',
484+
'Approved Charter',
485+
'Not currently under review Charter']:
486+
groups['Charter'][g] = id
487+
group_names['Charter'].append(g)
488+
id += 1;
489+
490+
for ad in ads:
491+
form = SearchForm({'by':'ad','ad': ad.id,
492+
'rfcs':'on', 'activedrafts':'on',
493+
'olddrafts':'on',
494+
'doctypes': doctypes})
495+
data = retrieve_search_results(form)
496+
ad.dashboard = urlreverse("ietf.doc.views_search.docs_for_ad", kwargs=dict(name=ad.full_name_as_key()))
497+
counts = {}
498+
for g in group_types:
499+
counts[g] = []
500+
for doc in data:
501+
group_type = ad_dashboard_group_type(doc)
502+
if group_type and group_type in groups: # Right now, anything with group_type "Document", such as a bofreq is not handled.
503+
group = ad_dashboard_group(doc)
504+
if group not in groups[group_type]:
505+
groups[group_type][group] = len(groups[group_type])
506+
group_names[group_type].append(group)
507+
if len(counts[group_type]) < len(groups[group_type]):
508+
counts[group_type].extend([0] * (len(groups[group_type]) - len(counts[group_type])))
509+
counts[group_type][groups[group_type][group]] += 1
510+
ad.counts = counts
511+
for ad in ads:
512+
for group_type in group_types:
513+
if len(ad.counts[group_type]) < len(groups[group_type]):
514+
ad.counts[group_type].extend([0] * (len(groups[group_type]) - len(ad.counts[group_type])))
515+
# Shorten the names of groups
516+
for gt in group_types:
517+
for idx,g in enumerate(group_names[gt]):
518+
group_names[gt][idx] = shorten_group_name(g)
519+
520+
workload = []
521+
for gt in group_types:
522+
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]))
523+
524+
return render(request, 'doc/ad_list.html', {
525+
'workload': workload
526+
})
527+
528+
396529
def docs_for_ad(request, name):
397530
ad = None
398531
responsible = Document.objects.values_list('ad', flat=True).distinct()
@@ -454,7 +587,6 @@ def docs_for_ad(request, name):
454587
return render(request, 'doc/drafts_for_ad.html', {
455588
'form':form, 'docs':results, 'meta':meta, 'ad_name': ad.plain_name(), 'blocked_docs': blocked_docs
456589
})
457-
458590
def drafts_in_last_call(request):
459591
lc_state = State.objects.get(type="draft-iesg", slug="lc").pk
460592
form = SearchForm({'by':'state','state': lc_state, 'rfcs':'on', 'activedrafts':'on'})

ietf/templates/doc/ad_list.html

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{% extends "base.html" %}
2+
{# Copyright The IETF Trust 2015, All Rights Reserved #}
3+
{% load origin static %}
4+
{% load ietf_filters %}
5+
{% block pagehead %}
6+
<link rel="stylesheet" href="{% static "ietf/css/list.css" %}">
7+
{% endblock %}
8+
{% block title %}Area directors{% endblock %}
9+
{% block content %}
10+
{% origin %}
11+
<h1>Area Directors Workload</h1>
12+
{% for group in workload %}
13+
<h2>{{ group.group_type }}</h2>
14+
<table class="table table-sm table-striped tablesorter">
15+
<thead>
16+
<tr>
17+
<th scope="col" data-sort="name">Name</th>
18+
{% for g in group.group_names %}
19+
<th scope="col" class="text-end" data-sort="{{ g|slugify }}-num">{{ g }}</th>
20+
{% endfor %}
21+
</tr>
22+
</thead>
23+
<tbody>
24+
{% for ad, ad_counts in group.counts %}
25+
<tr>
26+
<td><a href="{{ ad.dashboard }}">{{ ad.name }}</a></td>
27+
{% for label, count in ad_counts %}
28+
<td id="{{group.group_type|slugify}}-{{ad.full_name_as_key|slugify}}-{{label|slugify}}">{{count}}</td>
29+
{% endfor %}
30+
</tr>
31+
{% endfor %}
32+
</tbody>
33+
</table>
34+
{% endfor %}
35+
{% endblock %}
36+
{% block js %}
37+
<script src="{% static "ietf/js/list.js" %}"></script>
38+
{% endblock %}

0 commit comments

Comments
 (0)