From dac430c84e1c6eecc8d2534afa17254b46894ed2 Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Fri, 6 Jan 2017 15:10:49 +0000 Subject: [PATCH] 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 --- ietf/static/ietf/css/ietf.css | 5 + ietf/static/ietf/js/document-stats.js | 34 +++++ ietf/stats/urls.py | 1 + ietf/stats/views.py | 132 +++++++++++++++--- ietf/templates/stats/document_stats.html | 48 +++++++ .../stats/document_stats_authors.html | 71 ++++++++++ ietf/templates/stats/index.html | 5 +- 7 files changed, 270 insertions(+), 26 deletions(-) create mode 100644 ietf/static/ietf/js/document-stats.js create mode 100644 ietf/templates/stats/document_stats.html create mode 100644 ietf/templates/stats/document_stats_authors.html diff --git a/ietf/static/ietf/css/ietf.css b/ietf/static/ietf/css/ietf.css index 025bc0168..76b391d58 100644 --- a/ietf/static/ietf/css/ietf.css +++ b/ietf/static/ietf/css/ietf.css @@ -568,6 +568,11 @@ table.simple-table td:last-child { width: 7em; } +.popover .docname { + padding-left: 1em; + text-indent: -1em; +} + .stats-time-graph { height: 15em; } diff --git a/ietf/static/ietf/js/document-stats.js b/ietf/static/ietf/js/document-stats.js new file mode 100644 index 000000000..f9a08fa2c --- /dev/null +++ b/ietf/static/ietf/js/document-stats.js @@ -0,0 +1,34 @@ +$(document).ready(function () { + if (window.chartConf) { + var chart = Highcharts.chart('chart', window.chartConf); + } + + $(".popover-docnames").each(function () { + var stdNameRegExp = new RegExp("^(rfc|bcp|fyi|std)[0-9]+$", 'i'); + + var html = []; + $.each(($(this).data("docnames") || "").split(" "), function (i, docname) { + if (!$.trim(docname)) + return; + + var displayName = docname; + + if (stdNameRegExp.test(docname)) + displayName = docname.slice(0, 3).toUpperCase() + " " + docname.slice(3); + + html.push('
' + displayName + '
'); + }); + + if ($(this).data("sliced")) + html.push('
'); + + $(this).popover({ + trigger: "focus", + template: '', + content: html.join(""), + html: true + }).on("click", function (e) { + e.preventDefault(); + }); + }); +}); diff --git a/ietf/stats/urls.py b/ietf/stats/urls.py index 8a05f9659..641187350 100644 --- a/ietf/stats/urls.py +++ b/ietf/stats/urls.py @@ -5,5 +5,6 @@ import ietf.stats.views urlpatterns = patterns('', url("^$", ietf.stats.views.stats_index), + url("^document/(?:(?Pauthors|pages|format|spectech)/)?(?:(?Pall|rfc|draft)/)?$", ietf.stats.views.document_stats), url("^review/(?:(?Pcompletion|results|states|time)/)?(?:%(acronym)s/)?$" % settings.URL_REGEXPS, ietf.stats.views.review_stats), ) diff --git a/ietf/stats/views.py b/ietf/stats/views.py index 57c9cf5bf..63b3da920 100644 --- a/ietf/stats/views.py +++ b/ietf/stats/views.py @@ -1,9 +1,12 @@ import datetime, itertools, json, calendar +from collections import defaultdict from django.shortcuts import render from django.contrib.auth.decorators import login_required from django.core.urlresolvers import reverse as urlreverse from django.http import HttpResponseRedirect, HttpResponseForbidden +from django.db.models import Count +from django.utils.safestring import mark_safe import dateutil.relativedelta @@ -15,11 +18,116 @@ from ietf.review.utils import (extract_review_request_data, from ietf.group.models import Role, Group from ietf.person.models import Person from ietf.name.models import ReviewRequestStateName, ReviewResultName +from ietf.doc.models import Document from ietf.ietfauth.utils import has_role def stats_index(request): return render(request, "stats/index.html") +def generate_query_string(query_dict, overrides): + query_part = u"" + + if query_dict or overrides: + d = query_dict.copy() + for k, v in overrides.iteritems(): + if type(v) in (list, tuple): + if not v: + if k in d: + del d[k] + else: + d.setlist(k, v) + else: + if v is None or v == u"": + if k in d: + del d[k] + else: + d[k] = v + + if d: + query_part = u"?" + d.urlencode() + + return query_part + + +def document_stats(request, stats_type=None, document_state=None): + def build_document_stats_url(stats_type_override=Ellipsis, document_state_override=Ellipsis, get_overrides={}): + kwargs = { + "stats_type": stats_type if stats_type_override is Ellipsis else stats_type_override, + "document_state": document_state if document_state_override is Ellipsis else document_state_override, + } + + 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) + + # statistics type - one of the tables or the chart + possible_stats_types = [ + ("authors", "Number of authors"), +# ("pages", "Pages"), +# ("format", "Format"), +# ("spectech", "Specification techniques"), + ] + + possible_stats_types = [ (slug, label, build_document_stats_url(stats_type_override=slug)) + for slug, label in possible_stats_types ] + + if not stats_type: + return HttpResponseRedirect(build_document_stats_url(stats_type_override=possible_stats_types[0][0])) + + possible_document_states = [ + ("all", "All"), + ("rfc", "RFCs"), + ("draft", "Drafts (not published as RFC)"), + ] + + possible_document_states = [ (slug, label, build_document_stats_url(document_state_override=slug)) + for slug, label in possible_document_states ] + + if not document_state: + return HttpResponseRedirect(build_document_stats_url(document_state_override=possible_document_states[0][0])) + + + # filter documents + doc_qs = Document.objects.filter(type="draft") + + if document_state == "rfc": + doc_qs = doc_qs.filter(states__type="draft", states__slug="rfc") + elif document_state == "draft": + doc_qs = doc_qs.exclude(states__type="draft", states__slug="rfc") + + chart_data = [] + table_data = [] + stats_title = "" + + if stats_type == "authors": + stats_title = "Number of authors for each document" + + groups = defaultdict(list) + + for name, author_count in doc_qs.values_list("name").annotate(Count("authors")).iterator(): + groups[author_count].append(name) + + total_docs = sum(len(names) for author_count, names in groups.iteritems()) + + series_data = [] + for author_count, names in sorted(groups.iteritems(), key=lambda t: t[0]): + series_data.append((author_count, len(names) * 100.0 / total_docs)) + table_data.append((author_count, names)) + + chart_data.append({ + "data": series_data, + "name": "Percentage of documents", + }) + + + return render(request, "stats/document_stats.html", { + "chart_data": mark_safe(json.dumps(chart_data)), + "table_data": table_data, + "stats_title": stats_title, + "possible_stats_types": possible_stats_types, + "stats_type": stats_type, + "possible_document_states": possible_document_states, + "document_state": document_state, + }) + @login_required def review_stats(request, stats_type=None, acronym=None): # This view is a bit complex because we want to show a bunch of @@ -39,29 +147,7 @@ def review_stats(request, stats_type=None, acronym=None): if acr: kwargs["acronym"] = acr - base_url = urlreverse(review_stats, kwargs=kwargs) - query_part = u"" - - if request.GET or get_overrides: - d = request.GET.copy() - for k, v in get_overrides.iteritems(): - if type(v) in (list, tuple): - if not v: - if k in d: - del d[k] - else: - d.setlist(k, v) - else: - if v is None or v == u"": - if k in d: - del d[k] - else: - d[k] = v - - if d: - query_part = u"?" + d.urlencode() - - return base_url + query_part + return urlreverse(review_stats, kwargs=kwargs) + generate_query_string(request.GET, get_overrides) def get_choice(get_parameter, possible_choices, multiple=False): values = request.GET.getlist(get_parameter) diff --git a/ietf/templates/stats/document_stats.html b/ietf/templates/stats/document_stats.html new file mode 100644 index 000000000..e11aaed4e --- /dev/null +++ b/ietf/templates/stats/document_stats.html @@ -0,0 +1,48 @@ +{% extends "base.html" %} + +{% load origin %} + +{% load ietf_filters staticfiles bootstrap3 %} + +{% block title %}{{ stats_title }}{% endblock %} + +{% block pagehead %} + +{% endblock %} + +{% block content %} + {% origin %} + +

Document statistics

+ +
+
+ Show: +
+ {% for slug, label, url in possible_stats_types %} + {{ label }} + {% endfor %} +
+
+ +
+ Document types: +
+ {% for slug, label, url in possible_document_states %} + {{ label }} + {% endfor %} +
+
+
+ + {% if stats_type == "authors" %} + {% include "stats/document_stats_authors.html" %} + {% endif %} +{% endblock %} + +{% block js %} + + + + +{% endblock %} diff --git a/ietf/templates/stats/document_stats_authors.html b/ietf/templates/stats/document_stats_authors.html new file mode 100644 index 000000000..3ecc19562 --- /dev/null +++ b/ietf/templates/stats/document_stats_authors.html @@ -0,0 +1,71 @@ +

{{ stats_title }}

+ +
+ + + +

Data

+ + + + + + + + + + {% for author_count, names in table_data %} + + + + + {% endfor %} + +
AuthorsDocuments
{{ author_count }}{{ names|length }}
diff --git a/ietf/templates/stats/index.html b/ietf/templates/stats/index.html index 11b9bb8e5..77b8b7925 100644 --- a/ietf/templates/stats/index.html +++ b/ietf/templates/stats/index.html @@ -1,6 +1,6 @@ {% extends "base.html" %} -{% load origin %}{% origin %} +{% load origin %} {% load ietf_filters staticfiles bootstrap3 %} @@ -9,9 +9,8 @@

{% block title %}Statistics{% endblock %}

-

Currently, there are statistics for:

-