Moved default column chart settings to settings.py. Split chart config and data into separate ajax urls to permit drawing base chart details before having data. Added a 'Loading...' notification while loading chart data. Added more test cases. Changed chart settings to eliminate empty data points from data transfer.
- Legacy-Id: 11930
This commit is contained in:
parent
0b9ed9183a
commit
f726f5c785
|
@ -1102,32 +1102,58 @@ class DocumentMeetingTests(TestCase):
|
|||
|
||||
|
||||
class ChartTests(ResourceTestCaseMixin, TestCase):
|
||||
def test_search_charts(self):
|
||||
def test_search_chart_conf(self):
|
||||
doc = DocumentFactory.create(states=[('draft','active')])
|
||||
|
||||
data_url = urlreverse("ietf.doc.views_stats.chart_data_newrevisiondocevent")
|
||||
conf_url = urlreverse('ietf.doc.views_stats.chart_conf_newrevisiondocevent')
|
||||
|
||||
# No qurey arguments; expect an empty json object
|
||||
r = self.client.get(data_url)
|
||||
r = self.client.get(conf_url)
|
||||
self.assertValidJSONResponse(r)
|
||||
self.assertEqual(r.content, "{}")
|
||||
self.assertEqual(r.content, '{}')
|
||||
|
||||
# No match
|
||||
r = self.client.get(data_url + "?activedrafts=on&name=thisisnotadocumentname")
|
||||
r = self.client.get(conf_url + '?activedrafts=on&name=thisisnotadocumentname')
|
||||
self.assertValidJSONResponse(r)
|
||||
d = json.loads(r.content)
|
||||
self.assertEqual(r.content, "{}")
|
||||
self.assertEqual(d['chart']['type'], settings.CHART_TYPE_COLUMN_OPTIONS['chart']['type'])
|
||||
|
||||
r = self.client.get(data_url + "?activedrafts=on&name=%s"%doc.name[6:12])
|
||||
r = self.client.get(conf_url + '?activedrafts=on&name=%s'%doc.name[6:12])
|
||||
self.assertValidJSONResponse(r)
|
||||
d = json.loads(r.content)
|
||||
self.assertEqual(len(d['series'][0]['data']), 1)
|
||||
self.assertEqual(d['chart']['type'], settings.CHART_TYPE_COLUMN_OPTIONS['chart']['type'])
|
||||
self.assertEqual(len(d['series'][0]['data']), 0)
|
||||
|
||||
chart_url = urlreverse("ietf.doc.views_stats.chart_newrevisiondocevent")
|
||||
def test_search_chart_data(self):
|
||||
doc = DocumentFactory.create(states=[('draft','active')])
|
||||
|
||||
data_url = urlreverse('ietf.doc.views_stats.chart_data_newrevisiondocevent')
|
||||
|
||||
# No qurey arguments; expect an empty json list
|
||||
r = self.client.get(data_url)
|
||||
self.assertValidJSONResponse(r)
|
||||
self.assertEqual(r.content, '[]')
|
||||
|
||||
# No match
|
||||
r = self.client.get(data_url + '?activedrafts=on&name=thisisnotadocumentname')
|
||||
self.assertValidJSONResponse(r)
|
||||
d = json.loads(r.content)
|
||||
self.assertEqual(r.content, '[]')
|
||||
|
||||
r = self.client.get(data_url + '?activedrafts=on&name=%s'%doc.name[6:12])
|
||||
self.assertValidJSONResponse(r)
|
||||
d = json.loads(r.content)
|
||||
self.assertEqual(len(d), 1)
|
||||
self.assertEqual(len(d[0]), 2)
|
||||
|
||||
def test_search_chart(self):
|
||||
doc = DocumentFactory.create(states=[('draft','active')])
|
||||
|
||||
chart_url = urlreverse('ietf.doc.views_stats.chart_newrevisiondocevent')
|
||||
r = self.client.get(chart_url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
r = self.client.get(chart_url + "?activedrafts=on&name=%s"%doc.name[6:12])
|
||||
r = self.client.get(chart_url + '?activedrafts=on&name=%s'%doc.name[6:12])
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
def test_personal_chart(self):
|
||||
|
@ -1137,12 +1163,23 @@ class ChartTests(ResourceTestCaseMixin, TestCase):
|
|||
authors=[person.email(), ],
|
||||
)
|
||||
|
||||
data_url = urlreverse("ietf.doc.views_stats.chart_data_person_drafts", kwargs=dict(id=person.id))
|
||||
conf_url = urlreverse('ietf.doc.views_stats.chart_conf_person_drafts', kwargs=dict(id=person.id))
|
||||
|
||||
r = self.client.get(conf_url)
|
||||
self.assertValidJSONResponse(r)
|
||||
d = json.loads(r.content)
|
||||
self.assertEqual(d['chart']['type'], settings.CHART_TYPE_COLUMN_OPTIONS['chart']['type'])
|
||||
self.assertEqual("New draft revisions over time for %s" % person.name, d['title']['text'])
|
||||
|
||||
data_url = urlreverse('ietf.doc.views_stats.chart_data_person_drafts', kwargs=dict(id=person.id))
|
||||
|
||||
r = self.client.get(data_url)
|
||||
self.assertValidJSONResponse(r)
|
||||
d = json.loads(r.content)
|
||||
self.assertEqual(len(d['series'][0]['data']), 1)
|
||||
self.assertEqual(len(d), 1)
|
||||
self.assertEqual(len(d[0]), 2)
|
||||
|
||||
page_url = urlreverse('ietf.person.views.profile', kwargs=dict(email_or_name=person.name))
|
||||
r = self.client.get(page_url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -54,8 +54,10 @@ urlpatterns = patterns('',
|
|||
url(r'^iesg/(?P<last_call_only>[A-Za-z0-9.-]+/)?$', views_search.drafts_in_iesg_process, name="drafts_in_iesg_process"),
|
||||
url(r'^email-aliases/$', views_doc.email_aliases),
|
||||
url(r'^stats/newrevisiondocevent/?$', views_stats.chart_newrevisiondocevent),
|
||||
url(r'^stats/data/newrevisiondocevent/?$', views_stats.chart_data_newrevisiondocevent),
|
||||
url(r'^stats/data/person/(?P<id>[0-9]+)/drafts/?$', views_stats.chart_data_person_drafts),
|
||||
url(r'^stats/newrevisiondocevent/conf/?$', views_stats.chart_conf_newrevisiondocevent),
|
||||
url(r'^stats/newrevisiondocevent/data/?$', views_stats.chart_data_newrevisiondocevent),
|
||||
url(r'^stats/person/(?P<id>[0-9]+)/drafts/conf/?$', views_stats.chart_conf_person_drafts),
|
||||
url(r'^stats/person/(?P<id>[0-9]+)/drafts/data/?$', views_stats.chart_data_person_drafts),
|
||||
|
||||
url(r'^all/$', views_search.index_all_drafts, name="index_all_drafts"),
|
||||
url(r'^active/$', views_search.index_active_drafts, name="index_active_drafts"),
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
# Copyright The IETF Trust 2016, All Rights Reserved
|
||||
|
||||
import copy
|
||||
import datetime
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.core.urlresolvers import reverse as urlreverse
|
||||
from django.db.models.aggregates import Count
|
||||
|
@ -19,8 +23,41 @@ from ietf.person.models import Person
|
|||
|
||||
epochday = datetime.datetime.utcfromtimestamp(0).date().toordinal()
|
||||
|
||||
def ms(t):
|
||||
return (t.toordinal() - epochday)*1000*60*60*24
|
||||
column_chart_conf = settings.CHART_TYPE_COLUMN_OPTIONS
|
||||
|
||||
def dt(s):
|
||||
"Convert the date string returned by sqlite's date() to a datetime.date"
|
||||
ys, ms, ds = s.split('-')
|
||||
return datetime.date(int(ys), int(ms), int(ds))
|
||||
|
||||
def model_to_timeline_data(model, field='time', **kwargs):
|
||||
"""Takes a Django model and a set of queryset filter arguments, and
|
||||
returns a dictionary with highchart settings and data, suitable as
|
||||
a JsonResponse() argument. The model must have a DateTimeField field.
|
||||
If the time field is named something else than 'time', the name must
|
||||
be supplied."""
|
||||
assert field in model._meta.get_all_field_names()
|
||||
|
||||
objects = ( model.objects.filter(**kwargs)
|
||||
.order_by('date')
|
||||
.extra(select={'date': 'date(%s.%s)'% (model._meta.db_table, field) })
|
||||
.values('date')
|
||||
.annotate(count=Count('id')))
|
||||
if objects.exists():
|
||||
obj_list = list(objects)
|
||||
# This is needed for sqlite, when we're running tests:
|
||||
if type(obj_list[0]['date']) != datetime.date:
|
||||
obj_list = [ {'date': dt(e['date']), 'count': e['count']} for e in obj_list ]
|
||||
today = datetime.date.today()
|
||||
if not obj_list[-1]['date'] == today:
|
||||
obj_list += [ {'date': today, 'count': 0} ]
|
||||
data = [ ((e['date'].toordinal()-epochday)*1000*60*60*24, e['count']) for e in obj_list ]
|
||||
else:
|
||||
data = []
|
||||
|
||||
return data
|
||||
|
||||
|
||||
|
||||
def get_doctypes(queryargs, pluralize=False):
|
||||
doctypes = []
|
||||
|
@ -47,7 +84,7 @@ def make_title(queryargs):
|
|||
# radio choices
|
||||
by = queryargs.get('by')
|
||||
if by == "author":
|
||||
title += ' with author "%s"' % queryargs['author'].title()
|
||||
title += ' with author %s' % queryargs['author'].title()
|
||||
elif by == "group":
|
||||
group = queryargs['group']
|
||||
if group:
|
||||
|
@ -76,127 +113,64 @@ def make_title(queryargs):
|
|||
def chart_newrevisiondocevent(request):
|
||||
return render_to_response("doc/stats/highstock.html", {
|
||||
"title": "Document Statistics",
|
||||
"confurl": urlreverse("ietf.doc.views_stats.chart_conf_newrevisiondocevent"),
|
||||
"dataurl": urlreverse("ietf.doc.views_stats.chart_data_newrevisiondocevent"),
|
||||
"queryargs": request.GET.urlencode(),
|
||||
},
|
||||
context_instance=RequestContext(request))
|
||||
|
||||
def dt(s):
|
||||
"convert the string from sqlite's date() to a datetime.date"
|
||||
ys, ms, ds = s.split('-')
|
||||
return datetime.date(int(ys), int(ms), int(ds))
|
||||
|
||||
def model_to_timeline(model, **kwargs):
|
||||
"""Takes a Django model and a set of queryset filter arguments, and
|
||||
returns a dictionary with highchart settings and data, suitable as
|
||||
a JsonResponse() argument. The model must have a time field."""
|
||||
#debug.pprint('model._meta.fields')
|
||||
assert 'time' in model._meta.get_all_field_names()
|
||||
|
||||
objects = ( model.objects.filter(**kwargs)
|
||||
.order_by('date')
|
||||
.extra(select={'date': 'date(doc_docevent.time)'})
|
||||
.values('date')
|
||||
.annotate(count=Count('id')))
|
||||
if objects.exists():
|
||||
# debug.lap('got event query')
|
||||
obj_list = list(objects)
|
||||
# debug.lap('got event list')
|
||||
# This is needed for sqlite, when we're running tests:
|
||||
if type(obj_list[0]['date']) != datetime.date:
|
||||
# debug.say('converting string dates to datetime.date')
|
||||
obj_list = [ {'date': dt(e['date']), 'count': e['count']} for e in obj_list ]
|
||||
points = [ ((e['date'].toordinal()-epochday)*1000*60*60*24, e['count']) for e in obj_list ]
|
||||
# debug.lap('got event points')
|
||||
counts = dict(points)
|
||||
# debug.lap('got points dictionary')
|
||||
day_ms = 1000*60*60*24
|
||||
days = range(points[0][0], points[-1][0]+day_ms, day_ms)
|
||||
# debug.lap('got days array')
|
||||
data = [ (d, counts[d] if d in counts else 0) for d in days ]
|
||||
# debug.lap('merged points into days')
|
||||
else:
|
||||
data = []
|
||||
|
||||
info = {
|
||||
"chart": {
|
||||
"type": 'column'
|
||||
},
|
||||
"rangeSelector" : {
|
||||
"selected": 4,
|
||||
"allButtonsEnabled": True,
|
||||
},
|
||||
"title" : {
|
||||
"text" : "%s items over time" % model._meta.model_name
|
||||
},
|
||||
"credits": {
|
||||
"enabled": False,
|
||||
},
|
||||
"series" : [{
|
||||
"name" : "Items",
|
||||
"type" : "column",
|
||||
"data" : data,
|
||||
"dataGrouping": {
|
||||
"units": [[
|
||||
'week', # unit name
|
||||
[1,], # allowed multiples
|
||||
], [
|
||||
'month',
|
||||
[1, 4,],
|
||||
]]
|
||||
},
|
||||
"turboThreshold": 1, # Only check format of first data point. All others are the same
|
||||
"pointInterval": 24*60*60*1000,
|
||||
"pointPadding": 0.05,
|
||||
}]
|
||||
}
|
||||
return info
|
||||
|
||||
|
||||
|
||||
@cache_page(60*15)
|
||||
#@cache_page(60*15)
|
||||
def chart_data_newrevisiondocevent(request):
|
||||
# debug.mark()
|
||||
queryargs = request.GET
|
||||
if queryargs:
|
||||
# debug.lap('got queryargs')
|
||||
key = get_search_cache_key(queryargs)
|
||||
# debug.lap('got cache key')
|
||||
results = cache.get(key)
|
||||
# debug.lap('did cache lookup')
|
||||
if not results:
|
||||
# debug.say('doing new search')
|
||||
form = SearchForm(queryargs)
|
||||
# debug.lap('set up search form')
|
||||
if not form.is_valid():
|
||||
return HttpResponseBadRequest("form not valid: %s" % form.errors)
|
||||
results = retrieve_search_results(form)
|
||||
# debug.lap('got search result')
|
||||
if results.exists():
|
||||
cache.set(key, results)
|
||||
# debug.lap('cached search result')
|
||||
if results.exists():
|
||||
info = model_to_timeline(DocEvent, doc__in=results, type='new_revision')
|
||||
info['title']['text'] = make_title(queryargs)
|
||||
info['series'][0]['name'] = "Submitted %s" % get_doctypes(queryargs, pluralize=True).lower(),
|
||||
data = model_to_timeline_data(DocEvent, doc__in=results, type='new_revision')
|
||||
else:
|
||||
info = {}
|
||||
# debug.clock('set up info dict')
|
||||
data = []
|
||||
else:
|
||||
info = {}
|
||||
return JsonResponse(info)
|
||||
data = []
|
||||
return JsonResponse(data, safe=False)
|
||||
|
||||
|
||||
@cache_page(60*15)
|
||||
def chart_data_person_drafts(request, id):
|
||||
# debug.mark()
|
||||
def chart_conf_newrevisiondocevent(request):
|
||||
queryargs = request.GET
|
||||
if queryargs:
|
||||
conf = copy.deepcopy(settings.CHART_TYPE_COLUMN_OPTIONS)
|
||||
conf['title']['text'] = make_title(queryargs)
|
||||
conf['series'][0]['name'] = "Submitted %s" % get_doctypes(queryargs, pluralize=True).lower(),
|
||||
else:
|
||||
conf = {}
|
||||
return JsonResponse(conf)
|
||||
|
||||
|
||||
@cache_page(60*15)
|
||||
def chart_conf_person_drafts(request, id):
|
||||
person = Person.objects.filter(id=id).first()
|
||||
if not person:
|
||||
info = {}
|
||||
conf = {}
|
||||
else:
|
||||
info = model_to_timeline(DocEvent, doc__authors__person=person, type='new_revision')
|
||||
info['title']['text'] = "New draft revisions over time for %s" % person.name
|
||||
info['series'][0]['name'] = "Submitted drafts"
|
||||
return JsonResponse(info)
|
||||
conf = copy.deepcopy(settings.CHART_TYPE_COLUMN_OPTIONS)
|
||||
conf['title']['text'] = "New draft revisions over time for %s" % person.name
|
||||
conf['series'][0]['name'] = "Submitted drafts"
|
||||
return JsonResponse(conf)
|
||||
|
||||
@cache_page(60*15)
|
||||
def chart_data_person_drafts(request, id):
|
||||
person = Person.objects.filter(id=id).first()
|
||||
if not person:
|
||||
data = []
|
||||
else:
|
||||
data = model_to_timeline_data(DocEvent, doc__authors__person=person, type='new_revision')
|
||||
return JsonResponse(data, safe=False)
|
||||
|
||||
|
||||
|
|
|
@ -693,6 +693,45 @@ MAILMAN_LIB_DIR = '/usr/lib/mailman'
|
|||
LIST_ACCOUNT_DELAY = 60*60*25 # 25 hours
|
||||
ACCOUNT_REQUEST_EMAIL = 'account-request@ietf.org'
|
||||
|
||||
CHART_TYPE_COLUMN_OPTIONS = {
|
||||
"chart": {
|
||||
"type": 'column',
|
||||
},
|
||||
"credits": {
|
||||
"enabled": False,
|
||||
},
|
||||
"rangeSelector" : {
|
||||
"selected": 5,
|
||||
"allButtonsEnabled": True,
|
||||
},
|
||||
"series" : [{
|
||||
"name" : "Items",
|
||||
"type" : "column",
|
||||
"data" : [],
|
||||
"dataGrouping": {
|
||||
"units": [[
|
||||
'week', # unit name
|
||||
[1,], # allowed multiples
|
||||
], [
|
||||
'month',
|
||||
[1, 4,],
|
||||
]]
|
||||
},
|
||||
"turboThreshold": 1, # Only check format of first data point. All others are the same
|
||||
"pointIntervalUnit": 'day',
|
||||
"pointPadding": 0.05,
|
||||
}],
|
||||
"title" : {
|
||||
"text" : "Items over time"
|
||||
},
|
||||
"xAxis": {
|
||||
"type": "datetime",
|
||||
# This makes the axis use the given coordinates, rather than
|
||||
# squashing them to equidistant columns
|
||||
"ordinal": False,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# Put the production SECRET_KEY in settings_local.py, and also any other
|
||||
# sensitive or site-specific changes. DO NOT commit settings_local.py to svn.
|
||||
|
|
|
@ -9,9 +9,14 @@
|
|||
<script src="{% static 'highcharts/modules/exporting.js' %}"></script>
|
||||
<script>
|
||||
$(function () {
|
||||
$.getJSON('{{ dataurl }}?{{ queryargs }}', function (info) {
|
||||
// Create the chart
|
||||
$('#chart').highcharts('StockChart', info);
|
||||
var chart;
|
||||
$.getJSON('{{ confurl }}?{{ queryargs }}', function (conf) {
|
||||
chart = Highcharts.stockChart('chart', conf);
|
||||
chart.showLoading();
|
||||
$.getJSON('{{ dataurl }}?{{ queryargs }}', function (data) {
|
||||
chart.series[0].setData(data);
|
||||
chart.hideLoading();
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -82,11 +82,17 @@
|
|||
<script src="{% static 'highcharts/modules/exporting.js' %}"></script>
|
||||
<script>
|
||||
$(function () {
|
||||
$.getJSON('{% url "ietf.doc.views_stats.chart_data_person_drafts" id=persons.0.id %}', function (info) {
|
||||
$.getJSON('{% url "ietf.doc.views_stats.chart_conf_person_drafts" id=persons.0.id %}', function (conf) {
|
||||
// Create the chart
|
||||
$('#chart').highcharts('StockChart', info);
|
||||
chart = Highcharts.stockChart('chart', conf);
|
||||
chart.showLoading();
|
||||
$.getJSON('{% url "ietf.doc.views_stats.chart_data_person_drafts" id=persons.0.id %}', function (data) {
|
||||
chart.series[0].setData(data);
|
||||
chart.hideLoading();
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
|
Loading…
Reference in a new issue