Added document information and submission timeline to profile pages. Refactored the chart data generation.
- Legacy-Id: 11928
This commit is contained in:
parent
74f7c5782b
commit
03d5b07e51
22
changelog
22
changelog
|
@ -1,3 +1,25 @@
|
|||
ietfdb (6.32.0) ietf; urgency=low
|
||||
|
||||
**Initial charting support**
|
||||
|
||||
This release brings in some basic charting support, and a set of initial
|
||||
charts showing new-revision timelines for document search results. There
|
||||
are also a few bugfixes:
|
||||
|
||||
* Fixed a blowup which could happen if an rfc doesn't have its standard
|
||||
level set.
|
||||
|
||||
* Fixed a bug in the rfceditor index sync introduced by the event saving
|
||||
refactoring.
|
||||
|
||||
* Fixed document methods .get_file_path() and .href() for historic
|
||||
meeting documents, to make urls like /doc/minutes-96-detnet/1/ work.
|
||||
|
||||
* Fixed a bug in bin/mkrelease.
|
||||
|
||||
-- Henrik Levkowetz <henrik@levkowetz.com> 06 Sep 2016 06:20:52 -0700
|
||||
|
||||
|
||||
ietfdb (6.31.1) ietf; urgency=low
|
||||
|
||||
This release adds more proceedings generation functionality, adds
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import factory
|
||||
|
||||
from ietf.doc.models import Document, DocEvent, NewRevisionDocEvent, DocAlias, State
|
||||
from ietf.doc.models import Document, DocEvent, NewRevisionDocEvent, DocAlias, State, DocumentAuthor
|
||||
|
||||
def draft_name_generator(type_id,group,n):
|
||||
return '%s-%s-%s-%s%d'%(
|
||||
|
@ -42,6 +42,14 @@ class DocumentFactory(factory.DjangoModelFactory):
|
|||
for (state_type_id,state_slug) in extracted:
|
||||
self.set_state(State.objects.get(type_id=state_type_id,slug=state_slug))
|
||||
|
||||
@factory.post_generation
|
||||
def authors(self, create, extracted, **kwargs):
|
||||
if create and extracted:
|
||||
order = 0
|
||||
for email in extracted:
|
||||
DocumentAuthor.objects.create(document=self, author=email, order=order)
|
||||
order += 1
|
||||
|
||||
@classmethod
|
||||
def _after_postgeneration(cls, obj, create, results=None):
|
||||
"""Save again the instance if creating and at least one hook ran."""
|
||||
|
@ -81,3 +89,4 @@ class NewRevisionDocEventFactory(DocEventFactory):
|
|||
@factory.lazy_attribute
|
||||
def desc(self):
|
||||
return 'New version available %s-%s'%(self.doc.name,self.rev)
|
||||
|
||||
|
|
|
@ -1102,7 +1102,7 @@ class DocumentMeetingTests(TestCase):
|
|||
|
||||
|
||||
class ChartTests(ResourceTestCaseMixin, TestCase):
|
||||
def test_stats(self):
|
||||
def test_search_charts(self):
|
||||
doc = DocumentFactory.create(states=[('draft','active')])
|
||||
|
||||
data_url = urlreverse("ietf.doc.views_stats.chart_data_newrevisiondocevent")
|
||||
|
@ -1116,7 +1116,7 @@ class ChartTests(ResourceTestCaseMixin, TestCase):
|
|||
r = self.client.get(data_url + "?activedrafts=on&name=thisisnotadocumentname")
|
||||
self.assertValidJSONResponse(r)
|
||||
d = json.loads(r.content)
|
||||
self.assertEqual(d['series'][0]['data'], [])
|
||||
self.assertEqual(r.content, "{}")
|
||||
|
||||
r = self.client.get(data_url + "?activedrafts=on&name=%s"%doc.name[6:12])
|
||||
self.assertValidJSONResponse(r)
|
||||
|
@ -1130,4 +1130,19 @@ class ChartTests(ResourceTestCaseMixin, TestCase):
|
|||
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):
|
||||
person = PersonFactory.create()
|
||||
DocumentFactory.create(
|
||||
states=[('draft','active')],
|
||||
authors=[person.email(), ],
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -55,6 +55,7 @@ urlpatterns = patterns('',
|
|||
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'^all/$', views_search.index_all_drafts, name="index_all_drafts"),
|
||||
url(r'^active/$', views_search.index_active_drafts, name="index_active_drafts"),
|
||||
|
|
|
@ -42,25 +42,35 @@ def get_doctypes(queryargs, pluralize=False):
|
|||
|
||||
def make_title(queryargs):
|
||||
title = 'New '
|
||||
title += get_doctypes(queryargs)
|
||||
title += ' Revisions'
|
||||
title += get_doctypes(queryargs).lower()
|
||||
title += ' revisions'
|
||||
# radio choices
|
||||
by = queryargs.get('by')
|
||||
if by == "author":
|
||||
title += ' with author "%s"' % queryargs['author'].title()
|
||||
elif by == "group":
|
||||
title += ' for %s' % queryargs['group'].capitalize()
|
||||
group = queryargs['group']
|
||||
if group:
|
||||
title += ' for %s' % group.capitalize()
|
||||
elif by == "area":
|
||||
title += ' in %s Area' % queryargs['area'].upper()
|
||||
area = queryargs['area']
|
||||
if area:
|
||||
title += ' in %s Area' % area.upper()
|
||||
elif by == "ad":
|
||||
title += ' with AD %s' % Person.objects.get(id=queryargs['ad'])
|
||||
ad_id = queryargs['ad']
|
||||
if ad_id:
|
||||
title += ' with AD %s' % Person.objects.get(id=ad_id)
|
||||
elif by == "state":
|
||||
title += ' in state %s::%s' % (queryargs['state'], queryargs['substate'])
|
||||
state = queryargs['state']
|
||||
if state:
|
||||
title += ' in state %s::%s' % (state, queryargs['substate'])
|
||||
elif by == "stream":
|
||||
title += ' in stream %s' % queryargs['stream']
|
||||
stream = queryargs['stream']
|
||||
if stream:
|
||||
title += ' in stream %s' % stream
|
||||
name = queryargs.get('name')
|
||||
if name:
|
||||
title += ' matching "%s"' % name
|
||||
title += ' with name matching "%s"' % name
|
||||
return title
|
||||
|
||||
def chart_newrevisiondocevent(request):
|
||||
|
@ -76,12 +86,79 @@ def dt(s):
|
|||
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)
|
||||
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')
|
||||
|
@ -98,73 +175,28 @@ def chart_data_newrevisiondocevent(request):
|
|||
if results.exists():
|
||||
cache.set(key, results)
|
||||
# debug.lap('cached search result')
|
||||
|
||||
if results.exists():
|
||||
events = ( DocEvent.objects.filter(doc__in=results, type='new_revision')
|
||||
.order_by('date')
|
||||
.extra(select={'date': 'date(doc_docevent.time)'})
|
||||
.values('date')
|
||||
.annotate(count=Count('id')))
|
||||
if events.exists():
|
||||
# debug.lap('got event query')
|
||||
event_list = list(events)
|
||||
# debug.lap('got event list')
|
||||
# This is needed for sqlite, when we're running tests:
|
||||
if type(event_list[0]['date']) != datetime.date:
|
||||
# debug.say('converting string dates to datetime.date')
|
||||
event_list = [ {'date': dt(e['date']), 'count': e['count']} for e in event_list ]
|
||||
points = [ ((e['date'].toordinal()-epochday)*1000*60*60*24, e['count']) for e in event_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 = 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(),
|
||||
else:
|
||||
data = []
|
||||
info = {
|
||||
|
||||
"chart": {
|
||||
"type": 'column'
|
||||
},
|
||||
|
||||
"rangeSelector" : {
|
||||
"selected": 4,
|
||||
"allButtonsEnabled": True,
|
||||
},
|
||||
"title" : {
|
||||
"text" : make_title(queryargs)
|
||||
},
|
||||
"credits": {
|
||||
"enabled": False,
|
||||
},
|
||||
"series" : [{
|
||||
"name" : "Submitted %s"%get_doctypes(queryargs, pluralize=True),
|
||||
"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,
|
||||
|
||||
}]
|
||||
|
||||
}
|
||||
info = {}
|
||||
# debug.clock('set up info dict')
|
||||
else:
|
||||
info = {}
|
||||
return JsonResponse(info)
|
||||
|
||||
|
||||
@cache_page(60*15)
|
||||
def chart_data_person_drafts(request, id):
|
||||
# debug.mark()
|
||||
person = Person.objects.filter(id=id).first()
|
||||
if not person:
|
||||
info = {}
|
||||
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)
|
||||
|
||||
|
||||
|
|
|
@ -63,6 +63,8 @@ class PersonInfo(models.Model):
|
|||
return initials(self.ascii or self.name)
|
||||
def last_name(self):
|
||||
return name_parts(self.name)[3]
|
||||
def first_name(self):
|
||||
return name_parts(self.name)[1]
|
||||
def role_email(self, role_name, group=None):
|
||||
"""Lookup email for role for person, optionally on group which
|
||||
may be an object or the group acronym."""
|
||||
|
@ -108,6 +110,19 @@ class PersonInfo(models.Model):
|
|||
_, first, _, last, _ = name_parts(self.ascii)
|
||||
return u'%s-%s%s' % ( slugify(u"%s %s" % (first, last)), hasher.encode(self.id), '-th' if thumb else '' )
|
||||
|
||||
def has_drafts(self):
|
||||
from ietf.doc.models import Document
|
||||
return Document.objects.filter(authors__person=self, type='draft').exists()
|
||||
def rfcs(self):
|
||||
from ietf.doc.models import Document
|
||||
return Document.objects.filter(authors__person=self, type='draft', states__slug='rfc').order_by('-time')
|
||||
def active_drafts(self):
|
||||
from ietf.doc.models import Document
|
||||
return Document.objects.filter(authors__person=self, type='draft', states__slug='active').order_by('-time')
|
||||
def expired_drafts(self):
|
||||
from ietf.doc.models import Document
|
||||
return Document.objects.filter(authors__person=self, type='draft', states__slug__in=['repl', 'expired', 'auth-rm', 'ietf-rm']).order_by('-time')
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import datetime
|
||||
|
||||
from django.db.models import Q
|
||||
from django.http import HttpResponse, Http404
|
||||
|
@ -58,7 +59,7 @@ def profile(request, email_or_name):
|
|||
persons = [ get_object_or_404(Email, address=email_or_name).person, ]
|
||||
else:
|
||||
aliases = Alias.objects.filter(name=email_or_name)
|
||||
persons = set([ a.person for a in aliases ])
|
||||
persons = list(set([ a.person for a in aliases ]))
|
||||
if not persons:
|
||||
raise Http404
|
||||
return render(request, 'person/profile.html', {'persons': persons})
|
||||
return render(request, 'person/profile.html', {'persons': persons, 'today':datetime.date.today()})
|
||||
|
|
|
@ -24,6 +24,6 @@
|
|||
{% block content %}
|
||||
{% origin %}
|
||||
|
||||
<div id="chart" style="width:100%; height:400px; border: solid gray 1px;"></div>
|
||||
<div id="chart" style="width:100%; height:400px; " class="panel panel-default panel-body"></div>
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
@ -2,8 +2,9 @@
|
|||
{# Copyright The IETF Trust 2015, All Rights Reserved #}
|
||||
{% load origin %}
|
||||
{% load markup_tags %}
|
||||
{% load staticfiles %}
|
||||
|
||||
{% block title %}Profile for {{ person }}{% endblock %}
|
||||
{% block title %}Profile for {{ persons.0 }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% origin %}
|
||||
|
@ -30,5 +31,62 @@
|
|||
{{ person.biography | apply_markup:"restructuredtext" }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<h2>RFCs</h2>
|
||||
{% if person.rfcs.exists %}
|
||||
<list>
|
||||
{% for doc in person.rfcs %}
|
||||
<li> <a href="{{ doc.get_absolute_url }}">{{ doc.canonical_name }}</a></li>
|
||||
{% endfor %}
|
||||
</list>
|
||||
{% else %}
|
||||
{{ person.first_name }} has no RFCs as of {{ today }}.
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h2>Active Drafts</h2>
|
||||
{% if person.active_drafts.exists %}
|
||||
<list>
|
||||
{% for doc in person.active_drafts %}
|
||||
<li> <a href="{{ doc.get_absolute_url }}">{{ doc.canonical_name }}</a></li>
|
||||
{% endfor %}
|
||||
</list>
|
||||
{% else %}
|
||||
{{ person.first_name }} has no active drafts as of {{ today }}.
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h2>Expired Drafts</h2>
|
||||
{% if person.expired_drafts.exists %}
|
||||
<list>
|
||||
{% for doc in person.expired_drafts %}
|
||||
<li> <a href="{{ doc.get_absolute_url }}">{{ doc.canonical_name }}</a></li>
|
||||
{% endfor %}
|
||||
</list>
|
||||
{% else %}
|
||||
{{ person.first_name }} has no expired drafts as of {{ today }}.
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if person.has_drafts %}
|
||||
<div id="chart" style="width:100%; height:400px; margin-top:1em;" class="panel panel-default panel-body col-md-12"></div>
|
||||
{% endif %}
|
||||
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
{% if persons|length == 1 %}
|
||||
<script src="{% static 'highcharts/highstock.js' %}"></script>
|
||||
<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) {
|
||||
// Create the chart
|
||||
$('#chart').highcharts('StockChart', info);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
|
Loading…
Reference in a new issue