Skip to content

Commit dac430c

Browse files
committed
Add branch from trunk @r12628 for the author statistics project, add document statistics page with the first statistics with the number of authors per document
- Legacy-Id: 12629
1 parent 334445d commit dac430c

7 files changed

Lines changed: 270 additions & 26 deletions

File tree

ietf/static/ietf/css/ietf.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,6 +568,11 @@ table.simple-table td:last-child {
568568
width: 7em;
569569
}
570570

571+
.popover .docname {
572+
padding-left: 1em;
573+
text-indent: -1em;
574+
}
575+
571576
.stats-time-graph {
572577
height: 15em;
573578
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
$(document).ready(function () {
2+
if (window.chartConf) {
3+
var chart = Highcharts.chart('chart', window.chartConf);
4+
}
5+
6+
$(".popover-docnames").each(function () {
7+
var stdNameRegExp = new RegExp("^(rfc|bcp|fyi|std)[0-9]+$", 'i');
8+
9+
var html = [];
10+
$.each(($(this).data("docnames") || "").split(" "), function (i, docname) {
11+
if (!$.trim(docname))
12+
return;
13+
14+
var displayName = docname;
15+
16+
if (stdNameRegExp.test(docname))
17+
displayName = docname.slice(0, 3).toUpperCase() + " " + docname.slice(3);
18+
19+
html.push('<div class="docname"><a href="/doc/' + docname + '/">' + displayName + '</a></div>');
20+
});
21+
22+
if ($(this).data("sliced"))
23+
html.push('<div class="text-center">&hellip;</div>');
24+
25+
$(this).popover({
26+
trigger: "focus",
27+
template: '<div class="popover" role="tooltip"><div class="arrow"></div><h3 class="popover-title"></h3><div class="popover-content"></div></div>',
28+
content: html.join(""),
29+
html: true
30+
}).on("click", function (e) {
31+
e.preventDefault();
32+
});
33+
});
34+
});

ietf/stats/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@
55

66
urlpatterns = patterns('',
77
url("^$", ietf.stats.views.stats_index),
8+
url("^document/(?:(?P<stats_type>authors|pages|format|spectech)/)?(?:(?P<document_state>all|rfc|draft)/)?$", ietf.stats.views.document_stats),
89
url("^review/(?:(?P<stats_type>completion|results|states|time)/)?(?:%(acronym)s/)?$" % settings.URL_REGEXPS, ietf.stats.views.review_stats),
910
)

ietf/stats/views.py

Lines changed: 109 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import datetime, itertools, json, calendar
2+
from collections import defaultdict
23

34
from django.shortcuts import render
45
from django.contrib.auth.decorators import login_required
56
from django.core.urlresolvers import reverse as urlreverse
67
from django.http import HttpResponseRedirect, HttpResponseForbidden
8+
from django.db.models import Count
9+
from django.utils.safestring import mark_safe
710

811
import dateutil.relativedelta
912

@@ -15,11 +18,116 @@
1518
from ietf.group.models import Role, Group
1619
from ietf.person.models import Person
1720
from ietf.name.models import ReviewRequestStateName, ReviewResultName
21+
from ietf.doc.models import Document
1822
from ietf.ietfauth.utils import has_role
1923

2024
def stats_index(request):
2125
return render(request, "stats/index.html")
2226

27+
def generate_query_string(query_dict, overrides):
28+
query_part = u""
29+
30+
if query_dict or overrides:
31+
d = query_dict.copy()
32+
for k, v in overrides.iteritems():
33+
if type(v) in (list, tuple):
34+
if not v:
35+
if k in d:
36+
del d[k]
37+
else:
38+
d.setlist(k, v)
39+
else:
40+
if v is None or v == u"":
41+
if k in d:
42+
del d[k]
43+
else:
44+
d[k] = v
45+
46+
if d:
47+
query_part = u"?" + d.urlencode()
48+
49+
return query_part
50+
51+
52+
def document_stats(request, stats_type=None, document_state=None):
53+
def build_document_stats_url(stats_type_override=Ellipsis, document_state_override=Ellipsis, get_overrides={}):
54+
kwargs = {
55+
"stats_type": stats_type if stats_type_override is Ellipsis else stats_type_override,
56+
"document_state": document_state if document_state_override is Ellipsis else document_state_override,
57+
}
58+
59+
return urlreverse(document_stats, kwargs={ k: v for k, v in kwargs.iteritems() if v is not None }) + generate_query_string(request.GET, get_overrides)
60+
61+
# statistics type - one of the tables or the chart
62+
possible_stats_types = [
63+
("authors", "Number of authors"),
64+
# ("pages", "Pages"),
65+
# ("format", "Format"),
66+
# ("spectech", "Specification techniques"),
67+
]
68+
69+
possible_stats_types = [ (slug, label, build_document_stats_url(stats_type_override=slug))
70+
for slug, label in possible_stats_types ]
71+
72+
if not stats_type:
73+
return HttpResponseRedirect(build_document_stats_url(stats_type_override=possible_stats_types[0][0]))
74+
75+
possible_document_states = [
76+
("all", "All"),
77+
("rfc", "RFCs"),
78+
("draft", "Drafts (not published as RFC)"),
79+
]
80+
81+
possible_document_states = [ (slug, label, build_document_stats_url(document_state_override=slug))
82+
for slug, label in possible_document_states ]
83+
84+
if not document_state:
85+
return HttpResponseRedirect(build_document_stats_url(document_state_override=possible_document_states[0][0]))
86+
87+
88+
# filter documents
89+
doc_qs = Document.objects.filter(type="draft")
90+
91+
if document_state == "rfc":
92+
doc_qs = doc_qs.filter(states__type="draft", states__slug="rfc")
93+
elif document_state == "draft":
94+
doc_qs = doc_qs.exclude(states__type="draft", states__slug="rfc")
95+
96+
chart_data = []
97+
table_data = []
98+
stats_title = ""
99+
100+
if stats_type == "authors":
101+
stats_title = "Number of authors for each document"
102+
103+
groups = defaultdict(list)
104+
105+
for name, author_count in doc_qs.values_list("name").annotate(Count("authors")).iterator():
106+
groups[author_count].append(name)
107+
108+
total_docs = sum(len(names) for author_count, names in groups.iteritems())
109+
110+
series_data = []
111+
for author_count, names in sorted(groups.iteritems(), key=lambda t: t[0]):
112+
series_data.append((author_count, len(names) * 100.0 / total_docs))
113+
table_data.append((author_count, names))
114+
115+
chart_data.append({
116+
"data": series_data,
117+
"name": "Percentage of documents",
118+
})
119+
120+
121+
return render(request, "stats/document_stats.html", {
122+
"chart_data": mark_safe(json.dumps(chart_data)),
123+
"table_data": table_data,
124+
"stats_title": stats_title,
125+
"possible_stats_types": possible_stats_types,
126+
"stats_type": stats_type,
127+
"possible_document_states": possible_document_states,
128+
"document_state": document_state,
129+
})
130+
23131
@login_required
24132
def review_stats(request, stats_type=None, acronym=None):
25133
# This view is a bit complex because we want to show a bunch of
@@ -39,29 +147,7 @@ def build_review_stats_url(stats_type_override=Ellipsis, acronym_override=Ellips
39147
if acr:
40148
kwargs["acronym"] = acr
41149

42-
base_url = urlreverse(review_stats, kwargs=kwargs)
43-
query_part = u""
44-
45-
if request.GET or get_overrides:
46-
d = request.GET.copy()
47-
for k, v in get_overrides.iteritems():
48-
if type(v) in (list, tuple):
49-
if not v:
50-
if k in d:
51-
del d[k]
52-
else:
53-
d.setlist(k, v)
54-
else:
55-
if v is None or v == u"":
56-
if k in d:
57-
del d[k]
58-
else:
59-
d[k] = v
60-
61-
if d:
62-
query_part = u"?" + d.urlencode()
63-
64-
return base_url + query_part
150+
return urlreverse(review_stats, kwargs=kwargs) + generate_query_string(request.GET, get_overrides)
65151

66152
def get_choice(get_parameter, possible_choices, multiple=False):
67153
values = request.GET.getlist(get_parameter)
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
{% extends "base.html" %}
2+
3+
{% load origin %}
4+
5+
{% load ietf_filters staticfiles bootstrap3 %}
6+
7+
{% block title %}{{ stats_title }}{% endblock %}
8+
9+
{% block pagehead %}
10+
<link rel="stylesheet" href="{% static 'bootstrap-datepicker/css/bootstrap-datepicker3.min.css' %}">
11+
{% endblock %}
12+
13+
{% block content %}
14+
{% origin %}
15+
16+
<h1>Document statistics</h1>
17+
18+
<div class="stats-options well">
19+
<div>
20+
Show:
21+
<div class="btn-group">
22+
{% for slug, label, url in possible_stats_types %}
23+
<a class="btn btn-default {% if slug == stats_type %}active{% endif %}" href="{{ url }}">{{ label }}</a>
24+
{% endfor %}
25+
</div>
26+
</div>
27+
28+
<div>
29+
Document types:
30+
<div class="btn-group">
31+
{% for slug, label, url in possible_document_states %}
32+
<a class="btn btn-default {% if slug == document_state %}active{% endif %}" href="{{ url }}">{{ label }}</a>
33+
{% endfor %}
34+
</div>
35+
</div>
36+
</div>
37+
38+
{% if stats_type == "authors" %}
39+
{% include "stats/document_stats_authors.html" %}
40+
{% endif %}
41+
{% endblock %}
42+
43+
{% block js %}
44+
<script src="{% static 'highcharts/highcharts.js' %}"></script>
45+
<script src="{% static 'highcharts/modules/exporting.js' %}"></script>
46+
<script src="{% static 'highcharts/modules/offline-exporting.js' %}"></script>
47+
<script src="{% static 'ietf/js/document-stats.js' %}"></script>
48+
{% endblock %}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<h3>{{ stats_title }}</h3>
2+
3+
<div id="chart"></div>
4+
5+
<script>
6+
var chartConf = {
7+
chart: {
8+
type: 'column'
9+
},
10+
title: {
11+
text: '{{ stats_title|escapejs }}'
12+
},
13+
xAxis: {
14+
tickInterval: 1,
15+
title: {
16+
text: 'Number of authors'
17+
}
18+
},
19+
yAxis: {
20+
title: {
21+
text: 'Percentage of documents'
22+
},
23+
labels: {
24+
formatter: function () {
25+
return this.value + '%';
26+
}
27+
}
28+
},
29+
legend: {
30+
enabled: false,
31+
},
32+
tooltip: {
33+
formatter: function () {
34+
var s = '<b>' + this.x + ' ' + (this.x == 1 ? "author" : 'authors') + '</b>';
35+
console.log(this.points)
36+
37+
$.each(this.points, function () {
38+
s += '<br/>' + this.series.name + ': ' +
39+
this.y.toFixed(1) + '%';
40+
});
41+
42+
return s;
43+
},
44+
shared: true
45+
},
46+
series: {{ chart_data }}
47+
};
48+
</script>
49+
50+
<h3>Data</h3>
51+
52+
<table class="table table-condensed">
53+
<thead>
54+
<tr>
55+
<th>Authors</th>
56+
<th>Documents</th>
57+
</tr>
58+
</thead>
59+
<tbody>
60+
{% for author_count, names in table_data %}
61+
<tr>
62+
<td>{{ author_count }}</td>
63+
<td><a class="popover-docnames"
64+
href=""
65+
data-docnames="{% for n in names|slice:":20" %}{{ n }}{% if not forloop.last %} {% endif %}{% endfor %}"
66+
data-sliced="{% if names|length > 20 %}1{% endif %}"
67+
>{{ names|length }}</a></td>
68+
</tr>
69+
{% endfor %}
70+
</tbody>
71+
</table>

ietf/templates/stats/index.html

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{% extends "base.html" %}
22

3-
{% load origin %}{% origin %}
3+
{% load origin %}
44

55
{% load ietf_filters staticfiles bootstrap3 %}
66

@@ -9,9 +9,8 @@
99

1010
<h1>{% block title %}Statistics{% endblock %}</h1>
1111

12-
<p>Currently, there are statistics for:</p>
13-
1412
<ul>
13+
<li><a href="{% url "ietf.stats.views.document_stats" %}">Documents (number of authors, size, formats used)</a></li>
1514
<li><a rel="nofollow" href="{% url "ietf.stats.views.review_stats" %}">Reviews in review teams</a> (requires login)</li>
1615
</ul>
1716

0 commit comments

Comments
 (0)