Skip to content

Commit 03d5b07

Browse files
committed
Added document information and submission timeline to profile pages. Refactored the chart data generation.
- Legacy-Id: 11928
1 parent 74f7c57 commit 03d5b07

9 files changed

Lines changed: 231 additions & 78 deletions

File tree

changelog

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,25 @@
1+
ietfdb (6.32.0) ietf; urgency=low
2+
3+
**Initial charting support**
4+
5+
This release brings in some basic charting support, and a set of initial
6+
charts showing new-revision timelines for document search results. There
7+
are also a few bugfixes:
8+
9+
* Fixed a blowup which could happen if an rfc doesn't have its standard
10+
level set.
11+
12+
* Fixed a bug in the rfceditor index sync introduced by the event saving
13+
refactoring.
14+
15+
* Fixed document methods .get_file_path() and .href() for historic
16+
meeting documents, to make urls like /doc/minutes-96-detnet/1/ work.
17+
18+
* Fixed a bug in bin/mkrelease.
19+
20+
-- Henrik Levkowetz <henrik@levkowetz.com> 06 Sep 2016 06:20:52 -0700
21+
22+
123
ietfdb (6.31.1) ietf; urgency=low
224

325
This release adds more proceedings generation functionality, adds

ietf/doc/factories.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import factory
22

3-
from ietf.doc.models import Document, DocEvent, NewRevisionDocEvent, DocAlias, State
3+
from ietf.doc.models import Document, DocEvent, NewRevisionDocEvent, DocAlias, State, DocumentAuthor
44

55
def draft_name_generator(type_id,group,n):
66
return '%s-%s-%s-%s%d'%(
@@ -42,6 +42,14 @@ def states(self, create, extracted, **kwargs):
4242
for (state_type_id,state_slug) in extracted:
4343
self.set_state(State.objects.get(type_id=state_type_id,slug=state_slug))
4444

45+
@factory.post_generation
46+
def authors(self, create, extracted, **kwargs):
47+
if create and extracted:
48+
order = 0
49+
for email in extracted:
50+
DocumentAuthor.objects.create(document=self, author=email, order=order)
51+
order += 1
52+
4553
@classmethod
4654
def _after_postgeneration(cls, obj, create, results=None):
4755
"""Save again the instance if creating and at least one hook ran."""
@@ -81,3 +89,4 @@ class Meta:
8189
@factory.lazy_attribute
8290
def desc(self):
8391
return 'New version available %s-%s'%(self.doc.name,self.rev)
92+

ietf/doc/tests.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1102,7 +1102,7 @@ def test_add_document_session(self):
11021102

11031103

11041104
class ChartTests(ResourceTestCaseMixin, TestCase):
1105-
def test_stats(self):
1105+
def test_search_charts(self):
11061106
doc = DocumentFactory.create(states=[('draft','active')])
11071107

11081108
data_url = urlreverse("ietf.doc.views_stats.chart_data_newrevisiondocevent")
@@ -1116,7 +1116,7 @@ def test_stats(self):
11161116
r = self.client.get(data_url + "?activedrafts=on&name=thisisnotadocumentname")
11171117
self.assertValidJSONResponse(r)
11181118
d = json.loads(r.content)
1119-
self.assertEqual(d['series'][0]['data'], [])
1119+
self.assertEqual(r.content, "{}")
11201120

11211121
r = self.client.get(data_url + "?activedrafts=on&name=%s"%doc.name[6:12])
11221122
self.assertValidJSONResponse(r)
@@ -1130,4 +1130,19 @@ def test_stats(self):
11301130
r = self.client.get(chart_url + "?activedrafts=on&name=%s"%doc.name[6:12])
11311131
self.assertEqual(r.status_code, 200)
11321132

1133+
def test_personal_chart(self):
1134+
person = PersonFactory.create()
1135+
DocumentFactory.create(
1136+
states=[('draft','active')],
1137+
authors=[person.email(), ],
1138+
)
1139+
1140+
data_url = urlreverse("ietf.doc.views_stats.chart_data_person_drafts", kwargs=dict(id=person.id))
1141+
1142+
r = self.client.get(data_url)
1143+
self.assertValidJSONResponse(r)
1144+
d = json.loads(r.content)
1145+
self.assertEqual(len(d['series'][0]['data']), 1)
1146+
11331147

1148+

ietf/doc/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
url(r'^email-aliases/$', views_doc.email_aliases),
5656
url(r'^stats/newrevisiondocevent/?$', views_stats.chart_newrevisiondocevent),
5757
url(r'^stats/data/newrevisiondocevent/?$', views_stats.chart_data_newrevisiondocevent),
58+
url(r'^stats/data/person/(?P<id>[0-9]+)/drafts/?$', views_stats.chart_data_person_drafts),
5859

5960
url(r'^all/$', views_search.index_all_drafts, name="index_all_drafts"),
6061
url(r'^active/$', views_search.index_active_drafts, name="index_active_drafts"),

ietf/doc/views_stats.py

Lines changed: 103 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -42,25 +42,35 @@ def get_doctypes(queryargs, pluralize=False):
4242

4343
def make_title(queryargs):
4444
title = 'New '
45-
title += get_doctypes(queryargs)
46-
title += ' Revisions'
45+
title += get_doctypes(queryargs).lower()
46+
title += ' revisions'
4747
# radio choices
4848
by = queryargs.get('by')
4949
if by == "author":
5050
title += ' with author "%s"' % queryargs['author'].title()
5151
elif by == "group":
52-
title += ' for %s' % queryargs['group'].capitalize()
52+
group = queryargs['group']
53+
if group:
54+
title += ' for %s' % group.capitalize()
5355
elif by == "area":
54-
title += ' in %s Area' % queryargs['area'].upper()
56+
area = queryargs['area']
57+
if area:
58+
title += ' in %s Area' % area.upper()
5559
elif by == "ad":
56-
title += ' with AD %s' % Person.objects.get(id=queryargs['ad'])
60+
ad_id = queryargs['ad']
61+
if ad_id:
62+
title += ' with AD %s' % Person.objects.get(id=ad_id)
5763
elif by == "state":
58-
title += ' in state %s::%s' % (queryargs['state'], queryargs['substate'])
64+
state = queryargs['state']
65+
if state:
66+
title += ' in state %s::%s' % (state, queryargs['substate'])
5967
elif by == "stream":
60-
title += ' in stream %s' % queryargs['stream']
68+
stream = queryargs['stream']
69+
if stream:
70+
title += ' in stream %s' % stream
6171
name = queryargs.get('name')
6272
if name:
63-
title += ' matching "%s"' % name
73+
title += ' with name matching "%s"' % name
6474
return title
6575

6676
def chart_newrevisiondocevent(request):
@@ -76,12 +86,79 @@ def dt(s):
7686
ys, ms, ds = s.split('-')
7787
return datetime.date(int(ys), int(ms), int(ds))
7888

89+
def model_to_timeline(model, **kwargs):
90+
"""Takes a Django model and a set of queryset filter arguments, and
91+
returns a dictionary with highchart settings and data, suitable as
92+
a JsonResponse() argument. The model must have a time field."""
93+
#debug.pprint('model._meta.fields')
94+
assert 'time' in model._meta.get_all_field_names()
95+
96+
objects = ( model.objects.filter(**kwargs)
97+
.order_by('date')
98+
.extra(select={'date': 'date(doc_docevent.time)'})
99+
.values('date')
100+
.annotate(count=Count('id')))
101+
if objects.exists():
102+
# debug.lap('got event query')
103+
obj_list = list(objects)
104+
# debug.lap('got event list')
105+
# This is needed for sqlite, when we're running tests:
106+
if type(obj_list[0]['date']) != datetime.date:
107+
# debug.say('converting string dates to datetime.date')
108+
obj_list = [ {'date': dt(e['date']), 'count': e['count']} for e in obj_list ]
109+
points = [ ((e['date'].toordinal()-epochday)*1000*60*60*24, e['count']) for e in obj_list ]
110+
# debug.lap('got event points')
111+
counts = dict(points)
112+
# debug.lap('got points dictionary')
113+
day_ms = 1000*60*60*24
114+
days = range(points[0][0], points[-1][0]+day_ms, day_ms)
115+
# debug.lap('got days array')
116+
data = [ (d, counts[d] if d in counts else 0) for d in days ]
117+
# debug.lap('merged points into days')
118+
else:
119+
data = []
120+
121+
info = {
122+
"chart": {
123+
"type": 'column'
124+
},
125+
"rangeSelector" : {
126+
"selected": 4,
127+
"allButtonsEnabled": True,
128+
},
129+
"title" : {
130+
"text" : "%s items over time" % model._meta.model_name
131+
},
132+
"credits": {
133+
"enabled": False,
134+
},
135+
"series" : [{
136+
"name" : "Items",
137+
"type" : "column",
138+
"data" : data,
139+
"dataGrouping": {
140+
"units": [[
141+
'week', # unit name
142+
[1,], # allowed multiples
143+
], [
144+
'month',
145+
[1, 4,],
146+
]]
147+
},
148+
"turboThreshold": 1, # Only check format of first data point. All others are the same
149+
"pointInterval": 24*60*60*1000,
150+
"pointPadding": 0.05,
151+
}]
152+
}
153+
return info
154+
155+
156+
79157
@cache_page(60*15)
80158
def chart_data_newrevisiondocevent(request):
81159
# debug.mark()
82160
queryargs = request.GET
83161
if queryargs:
84-
85162
# debug.lap('got queryargs')
86163
key = get_search_cache_key(queryargs)
87164
# debug.lap('got cache key')
@@ -98,73 +175,28 @@ def chart_data_newrevisiondocevent(request):
98175
if results.exists():
99176
cache.set(key, results)
100177
# debug.lap('cached search result')
101-
102178
if results.exists():
103-
events = ( DocEvent.objects.filter(doc__in=results, type='new_revision')
104-
.order_by('date')
105-
.extra(select={'date': 'date(doc_docevent.time)'})
106-
.values('date')
107-
.annotate(count=Count('id')))
108-
if events.exists():
109-
# debug.lap('got event query')
110-
event_list = list(events)
111-
# debug.lap('got event list')
112-
# This is needed for sqlite, when we're running tests:
113-
if type(event_list[0]['date']) != datetime.date:
114-
# debug.say('converting string dates to datetime.date')
115-
event_list = [ {'date': dt(e['date']), 'count': e['count']} for e in event_list ]
116-
points = [ ((e['date'].toordinal()-epochday)*1000*60*60*24, e['count']) for e in event_list ]
117-
# debug.lap('got event points')
118-
counts = dict(points)
119-
# debug.lap('got points dictionary')
120-
day_ms = 1000*60*60*24
121-
days = range(points[0][0], points[-1][0]+day_ms, day_ms)
122-
# debug.lap('got days array')
123-
data = [ (d, counts[d] if d in counts else 0) for d in days ]
124-
# debug.lap('merged points into days')
125-
else:
126-
data = []
179+
info = model_to_timeline(DocEvent, doc__in=results, type='new_revision')
180+
info['title']['text'] = make_title(queryargs)
181+
info['series'][0]['name'] = "Submitted %s" % get_doctypes(queryargs, pluralize=True).lower(),
127182
else:
128-
data = []
129-
info = {
130-
131-
"chart": {
132-
"type": 'column'
133-
},
134-
135-
"rangeSelector" : {
136-
"selected": 4,
137-
"allButtonsEnabled": True,
138-
},
139-
"title" : {
140-
"text" : make_title(queryargs)
141-
},
142-
"credits": {
143-
"enabled": False,
144-
},
145-
"series" : [{
146-
"name" : "Submitted %s"%get_doctypes(queryargs, pluralize=True),
147-
"type" : "column",
148-
"data" : data,
149-
"dataGrouping": {
150-
"units": [[
151-
'week', # unit name
152-
[1,], # allowed multiples
153-
], [
154-
'month',
155-
[1, 4,],
156-
]]
157-
},
158-
"turboThreshold": 1, # Only check format of first data point. All others are the same
159-
"pointInterval": 24*60*60*1000,
160-
"pointPadding": 0.05,
161-
162-
}]
163-
164-
}
183+
info = {}
165184
# debug.clock('set up info dict')
166185
else:
167186
info = {}
168187
return JsonResponse(info)
169188

170189

190+
@cache_page(60*15)
191+
def chart_data_person_drafts(request, id):
192+
# debug.mark()
193+
person = Person.objects.filter(id=id).first()
194+
if not person:
195+
info = {}
196+
else:
197+
info = model_to_timeline(DocEvent, doc__authors__person=person, type='new_revision')
198+
info['title']['text'] = "New draft revisions over time for %s" % person.name
199+
info['series'][0]['name'] = "Submitted drafts"
200+
return JsonResponse(info)
201+
202+

ietf/person/models.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ def initials(self):
6363
return initials(self.ascii or self.name)
6464
def last_name(self):
6565
return name_parts(self.name)[3]
66+
def first_name(self):
67+
return name_parts(self.name)[1]
6668
def role_email(self, role_name, group=None):
6769
"""Lookup email for role for person, optionally on group which
6870
may be an object or the group acronym."""
@@ -108,6 +110,19 @@ def photo_name(self,thumb=False):
108110
_, first, _, last, _ = name_parts(self.ascii)
109111
return u'%s-%s%s' % ( slugify(u"%s %s" % (first, last)), hasher.encode(self.id), '-th' if thumb else '' )
110112

113+
def has_drafts(self):
114+
from ietf.doc.models import Document
115+
return Document.objects.filter(authors__person=self, type='draft').exists()
116+
def rfcs(self):
117+
from ietf.doc.models import Document
118+
return Document.objects.filter(authors__person=self, type='draft', states__slug='rfc').order_by('-time')
119+
def active_drafts(self):
120+
from ietf.doc.models import Document
121+
return Document.objects.filter(authors__person=self, type='draft', states__slug='active').order_by('-time')
122+
def expired_drafts(self):
123+
from ietf.doc.models import Document
124+
return Document.objects.filter(authors__person=self, type='draft', states__slug__in=['repl', 'expired', 'auth-rm', 'ietf-rm']).order_by('-time')
125+
111126
class Meta:
112127
abstract = True
113128

ietf/person/views.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import datetime
12

23
from django.db.models import Q
34
from django.http import HttpResponse, Http404
@@ -58,7 +59,7 @@ def profile(request, email_or_name):
5859
persons = [ get_object_or_404(Email, address=email_or_name).person, ]
5960
else:
6061
aliases = Alias.objects.filter(name=email_or_name)
61-
persons = set([ a.person for a in aliases ])
62+
persons = list(set([ a.person for a in aliases ]))
6263
if not persons:
6364
raise Http404
64-
return render(request, 'person/profile.html', {'persons': persons})
65+
return render(request, 'person/profile.html', {'persons': persons, 'today':datetime.date.today()})

ietf/templates/doc/stats/highstock.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,6 @@
2424
{% block content %}
2525
{% origin %}
2626

27-
<div id="chart" style="width:100%; height:400px; border: solid gray 1px;"></div>
27+
<div id="chart" style="width:100%; height:400px; " class="panel panel-default panel-body"></div>
2828

2929
{% endblock %}

0 commit comments

Comments
 (0)