Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add group stats sunburst plots to active WG page #5126

Merged
merged 19 commits into from
Feb 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion ietf/group/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import debug # pyflakes:ignore

from ietf.doc.factories import DocumentFactory, WgDraftFactory
from ietf.doc.models import DocEvent, RelatedDocument
from ietf.doc.models import DocEvent, RelatedDocument, Document
from ietf.group.models import Role, Group
from ietf.group.utils import get_group_role_emails, get_child_group_role_emails, get_group_ad_emails
from ietf.group.factories import GroupFactory, RoleFactory
Expand Down Expand Up @@ -58,6 +58,32 @@ def test_stream_edit(self):
self.assertTrue(Role.objects.filter(name="delegate", group__acronym=stream_acronym, email__address="[email protected]"))


class GroupStatsTests(TestCase):
def setUp(self):
super().setUp()
a = WgDraftFactory()
b = WgDraftFactory()
RelatedDocument.objects.create(
source=a, target=b.docalias.first(), relationship_id="refnorm"
)

def test_group_stats(self):
client = Client(Accept="application/json")
url = urlreverse("ietf.group.views.group_stats_data")
r = client.get(url)
self.assertTrue(r.status_code == 200, "Failed to receive group stats")
self.assertGreater(len(r.content), 0, "Group stats have no content")

try:
data = json.loads(r.content)
except Exception as e:
self.fail("JSON load failed: %s" % e)

ids = [d["id"] for d in data]
for doc in Document.objects.all():
self.assertIn(doc.name, ids)


class GroupDocDependencyTests(TestCase):
def setUp(self):
super().setUp()
Expand Down
1 change: 1 addition & 0 deletions ietf/group/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@

group_urls = [
url(r'^$', views.active_groups),
url(r'^groupstats.json', views.group_stats_data, None, 'ietf.group.views.group_stats_data'),
url(r'^groupmenu.json', views.group_menu_data, None, 'ietf.group.views.group_menu_data'),
url(r'^chartering/$', views.chartering_groups),
url(r'^chartering/create/(?P<group_type>(wg|rg))/$', views.edit, {'action': "charter"}),
Expand Down
79 changes: 79 additions & 0 deletions ietf/group/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -1354,6 +1354,85 @@ def group_menu_data(request):
return JsonResponse(groups_by_parent)


@cache_control(public=True, max_age=30 * 60)
@cache_page(30 * 60)
def group_stats_data(request, years="3", only_active=True):
when = timezone.now() - datetime.timedelta(days=int(years) * 365)
docs = (
Document.objects.filter(type="draft", stream="ietf")
.filter(
Q(docevent__newrevisiondocevent__time__gte=when)
| Q(docevent__type="published_rfc", docevent__time__gte=when)
)
.exclude(states__type="draft", states__slug="repl")
.distinct()
)

data = []
for a in Group.objects.filter(type="area"):
if only_active and not a.is_active:
continue

area_docs = docs.filter(group__parent=a).exclude(group__acronym="none")
if not area_docs:
continue

area_page_cnt = 0
area_doc_cnt = 0
for wg in Group.objects.filter(type="wg", parent=a):
if only_active and not wg.is_active:
continue

wg_docs = area_docs.filter(group=wg)
if not wg_docs:
continue

wg_page_cnt = 0
for doc in wg_docs:
# add doc data
data.append(
{
"id": doc.name,
"active": True,
"parent": wg.acronym,
"grandparent": a.acronym,
"pages": doc.pages,
"docs": 1,
}
)
wg_page_cnt += doc.pages

area_doc_cnt += len(wg_docs)
area_docs = area_docs.exclude(group=wg)

# add WG data
data.append(
{
"id": wg.acronym,
"active": wg.is_active,
"parent": a.acronym,
"grandparent": "ietf",
"pages": wg_page_cnt,
"docs": len(wg_docs),
}
)
area_page_cnt += wg_page_cnt

# add area data
data.append(
{
"id": a.acronym,
"active": a.is_active,
"parent": "ietf",
"pages": area_page_cnt,
"docs": area_doc_cnt,
}
)

data.append({"id": "ietf", "active": True})
return JsonResponse(data, safe=False)


# --- Review views -----------------------------------------------------

def get_open_review_requests_for_team(team, assignment_status=None):
Expand Down
110 changes: 109 additions & 1 deletion ietf/static/js/highcharts.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,119 @@ import Highcharts from "highcharts";
import Highcharts_Exporting from "highcharts/modules/exporting";
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";
import Highcharts_Accessibility from "highcharts/modules/accessibility";
import Highcharts_Sunburst from "highcharts/modules/sunburst";

Highcharts_Exporting(Highcharts);
Highcharts_Offline_Exporting(Highcharts);
Highcharts_Export_Data(Highcharts);
Highcharts_Accessibility(Highcharts);
Highcharts_Sunburst(Highcharts);

Highcharts.setOptions({
// use colors from https://colorbrewer2.org/#type=qualitative&scheme=Paired&n=12
colors: ['#a6cee3', '#1f78b4', '#b2df8a', '#33a02c', '#fb9a99',
'#e31a1c', '#fdbf6f', '#ff7f00', '#cab2d6', '#6a3d9a',
'#ffff99', '#b15928'
],
chart: {
height: "100%",
style: {
fontFamily: getComputedStyle(document.body)
.getPropertyValue('--bs-body-font-family')
}
},
credits: {
enabled: false
},
});

window.Highcharts = Highcharts;

window.group_stats = function (url, chart_selector) {
$.getJSON(url, function (data) {
$(chart_selector)
.each(function (i, e) {
const dataset = e.dataset.dataset;
if (!dataset) {
console.log("dataset data attribute not set");
return;
}
const area = e.dataset.area;
if (!area) {
console.log("area data attribute not set");
return;
}

const chart = Highcharts.chart(e, {
title: {
text: `${dataset == "docs" ? "Documents" : "Pages"} in ${area.toUpperCase()}`
},
series: [{
type: "sunburst",
data: [],
tooltip: {
pointFormatter: function () {
return `There ${this.value == 1 ? "is" : "are"} ${this.value} ${dataset == "docs" ? "documents" : "pages"} in ${this.name}.`;
}
},
dataLabels: {
formatter() {
return this.point.active ? this.point.name : `(${this.point.name})`;
}
},
allowDrillToNode: true,
cursor: 'pointer',
levels: [{
level: 1,
color: "transparent",
levelSize: {
value: .5
}
}, {
level: 2,
colorByPoint: true
}, {
level: 3,
colorVariation: {
key: "brightness",
to: 0.5
}
}]
}],
});

// limit data to area if set and (for now) drop docs
const slice = data.filter(d => (area == "ietf" && d.grandparent == area) || d.parent == area || d.id == area)
.map((d) => {
return {
value: d[dataset],
id: d.id,
parent: d.parent,
grandparent: d.grandparent,
active: d.active,
};
})
.sort((a, b) => {
if (a.parent != b.parent) {
if (a.parent < b.parent) {
return -1;
}
if (a.parent > b.parent) {
return 1;
}
} else if (a.parent == area) {
if (a.id < b.id) {
return 1;
}
if (a.id > b.id) {
return -1;
}
return 0;
}
return b.value - a.value;
});
chart.series[0].setData(slice);
});
});
}
29 changes: 27 additions & 2 deletions ietf/templates/group/active_areas.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin textfilters ietf_filters%}
{% load origin textfilters ietf_filters static %}
{% block title %}Active areas{% endblock %}
{% block content %}
{% origin %}
Expand Down Expand Up @@ -43,8 +43,24 @@ <h1>Areas</h1>
</li>
{% endfor %}
</ul>
<p>
The following diagrams show the sizes of the different areas and working groups,
based on the number of documents - and pages - a group has worked on in the last three years.
</p>
<div class="row mt-3">
<div class="col-sm chart text-center" data-area="ietf" data-dataset="docs">
<div class="spinner-border" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
<div class="col-sm chart text-center" data-area="ietf" data-dataset="pages">
<div class="spinner-border" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
</div>
{% for area in areas %}
<h2 class="mt-3" id="id-{{ area.acronym|slugify }}">
<h2 class="mt-5" id="id-{{ area.acronym|slugify }}">
{{ area.name }}
<a href="{% url 'ietf.group.views.active_groups' group_type='wg' %}#{{ area.acronym }}">({{ area.acronym|upper }})</a>
</h2>
Expand All @@ -53,5 +69,14 @@ <h2 class="mt-3" id="id-{{ area.acronym|slugify }}">
{{ area.description|urlize_ietf_docs|linkify|safe }}
</p>
{% endif %}
{% include "group/group_stats_modal.html" with group=area only %}
{% endfor %}
{% endblock %}
{% block js %}
<script src="{% static 'ietf/js/highcharts.js' %}"></script>
<script>
$(function () {
group_stats("{% url 'ietf.group.views.group_stats_data' %}", ".chart");
});
</script>
{% endblock %}
18 changes: 18 additions & 0 deletions ietf/templates/group/group_about.html
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,18 @@
</tr>
{% endif %}
{% endwith %}
{% if group.type.slug == "area" %}
<tr>
<td></td>
<th scope="row">
Group statistics
</th>
<td class="edit"></td>
<td>
{% include "group/group_stats_modal.html" with group=group only %}
</td>
</tr>
{% endif %}
</tbody>
{% if group.personnel %}
<tbody class="meta border-top">
Expand Down Expand Up @@ -393,4 +405,10 @@ <h2 class="mt-3">
{% block js %}
<script src="{% static 'ietf/js/d3.js' %}"></script>
<script src="{% static 'ietf/js/document_relations.js' %}"></script>
<script src="{% static 'ietf/js/highcharts.js' %}"></script>
<script>
$(function () {
group_stats("{% url 'ietf.group.views.group_stats_data' %}", ".chart");
});
</script>
{% endblock %}
45 changes: 45 additions & 0 deletions ietf/templates/group/group_stats_modal.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<button type="button"
class="btn btn-sm btn-primary"
data-bs-toggle="modal"
data-bs-target="#stats-modal-{{ group.acronym|slugify }}">
<i class="bi bi-pie-chart-fill"></i> Show {{ group.acronym|upper }} statistics
</button>
<div class="modal fade"
id="stats-modal-{{ group.acronym|slugify }}"
tabindex="-1"
aria-labelledby="stats-modal-label-{{ group.acronym|slugify }}"
aria-hidden="true">
<div class="modal-dialog modal-dialog-scrollable modal-xl">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5"
id="stats-modal-label-{{ group.acronym|slugify }}">{{ group.acronym|upper }} statistics</h1>
<button type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="row">
<div class="col-lg chart text-center"
data-area="{{ group.acronym|lower }}"
data-dataset="docs">
<div class="spinner-border" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
<div class="col-lg chart text-center"
data-area="{{ group.acronym|lower }}"
data-dataset="pages">
<div class="spinner-border" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>