Refactor graphical timeline, by making the data available via doc.json. Also

refactor associated js/css/html. Finally, a lot of display tweaks.
 - Legacy-Id: 10550
This commit is contained in:
Lars Eggert 2015-12-08 14:29:35 +00:00
parent e1ff3a5782
commit d4d09a2f75
5 changed files with 181 additions and 141 deletions

View file

@ -3,7 +3,6 @@ import re
import urllib import urllib
import math import math
import datetime import datetime
import operator
from django.conf import settings from django.conf import settings
from django.db.models.query import EmptyQuerySet from django.db.models.query import EmptyQuerySet
@ -542,7 +541,7 @@ def uppercase_std_abbreviated_name(name):
return name return name
def crawl_history(doc): def crawl_history(doc):
# return document history data for use in ietf/templates/doc/timeline.html # return document history data for inclusion in doc.json (used by timeline)
def ancestors(doc): def ancestors(doc):
retval = [] retval = []
if hasattr(doc, 'relateddocument_set'): if hasattr(doc, 'relateddocument_set'):
@ -559,12 +558,20 @@ def crawl_history(doc):
for d in history: for d in history:
for e in d.docevent_set.filter(type='new_revision'): for e in d.docevent_set.filter(type='new_revision'):
if hasattr(e, 'newrevisiondocevent'): if hasattr(e, 'newrevisiondocevent'):
retval.append((d.name, e.newrevisiondocevent.rev, e.time.isoformat())) retval.append({
'name': d.name,
'rev': e.newrevisiondocevent.rev,
'published': e.time.isoformat()
})
if doc.type_id == "draft": if doc.type_id == "draft":
e = doc.latest_event(type='published_rfc') e = doc.latest_event(type='published_rfc')
else: else:
e = doc.latest_event(type='iesg_approved') e = doc.latest_event(type='iesg_approved')
if e: if e:
retval.append((doc.name, e.doc.canonical_name, e.time.isoformat())) retval.append({
return sorted(retval, key=operator.itemgetter(2)) 'name': e.doc.canonical_name(),
'rev': e.doc.canonical_name(),
'published': e.time.isoformat()
})
return sorted(retval, key=lambda x: x['published'])

View file

@ -417,7 +417,6 @@ def document_main(request, name, rev=None):
search_archive=search_archive, search_archive=search_archive,
actions=actions, actions=actions,
tracking_document=tracking_document, tracking_document=tracking_document,
rev_history=crawl_history(latest_revision.doc if latest_revision else doc),
), ),
context_instance=RequestContext(request)) context_instance=RequestContext(request))
@ -443,8 +442,6 @@ def document_main(request, name, rev=None):
can_manage = can_manage_group_type(request.user, doc.group.type_id) can_manage = can_manage_group_type(request.user, doc.group.type_id)
latest_revision = doc.latest_event(NewRevisionDocEvent, type="new_revision")
return render_to_response("doc/document_charter.html", return render_to_response("doc/document_charter.html",
dict(doc=doc, dict(doc=doc,
top=top, top=top,
@ -459,7 +456,6 @@ def document_main(request, name, rev=None):
group=group, group=group,
milestones=milestones, milestones=milestones,
can_manage=can_manage, can_manage=can_manage,
rev_history=crawl_history(latest_revision.doc if latest_revision else doc),
), ),
context_instance=RequestContext(request)) context_instance=RequestContext(request))
@ -477,8 +473,6 @@ def document_main(request, name, rev=None):
if doc.get_state_slug() in ("iesgeval") and doc.active_ballot(): if doc.get_state_slug() in ("iesgeval") and doc.active_ballot():
ballot_summary = needed_ballot_positions(doc, doc.active_ballot().active_ad_positions().values()) ballot_summary = needed_ballot_positions(doc, doc.active_ballot().active_ad_positions().values())
latest_revision = doc.latest_event(NewRevisionDocEvent, type="new_revision")
return render_to_response("doc/document_conflict_review.html", return render_to_response("doc/document_conflict_review.html",
dict(doc=doc, dict(doc=doc,
top=top, top=top,
@ -490,7 +484,6 @@ def document_main(request, name, rev=None):
conflictdoc=conflictdoc, conflictdoc=conflictdoc,
ballot_summary=ballot_summary, ballot_summary=ballot_summary,
approved_states=('appr-reqnopub-pend','appr-reqnopub-sent','appr-noprob-pend','appr-noprob-sent'), approved_states=('appr-reqnopub-pend','appr-reqnopub-sent','appr-noprob-pend','appr-noprob-sent'),
rev_history=crawl_history(latest_revision.doc if latest_revision else doc),
), ),
context_instance=RequestContext(request)) context_instance=RequestContext(request))
@ -515,8 +508,6 @@ def document_main(request, name, rev=None):
else: else:
sorted_relations=None sorted_relations=None
latest_revision = doc.latest_event(NewRevisionDocEvent, type="new_revision")
return render_to_response("doc/document_status_change.html", return render_to_response("doc/document_status_change.html",
dict(doc=doc, dict(doc=doc,
top=top, top=top,
@ -528,7 +519,6 @@ def document_main(request, name, rev=None):
ballot_summary=ballot_summary, ballot_summary=ballot_summary,
approved_states=('appr-pend','appr-sent'), approved_states=('appr-pend','appr-sent'),
sorted_relations=sorted_relations, sorted_relations=sorted_relations,
rev_history=crawl_history(latest_revision.doc if latest_revision else doc),
), ),
context_instance=RequestContext(request)) context_instance=RequestContext(request))
@ -921,6 +911,9 @@ def document_json(request, name):
data["shepherd"] = doc.shepherd.formatted_email() if doc.shepherd else None data["shepherd"] = doc.shepherd.formatted_email() if doc.shepherd else None
data["ad"] = doc.ad.role_email("ad").formatted_email() if doc.ad else None data["ad"] = doc.ad.role_email("ad").formatted_email() if doc.ad else None
latest_revision = doc.latest_event(NewRevisionDocEvent, type="new_revision")
data["rev_history"] = crawl_history(latest_revision.doc if latest_revision else doc)
if doc.type_id == "draft": if doc.type_id == "draft":
data["iesg_state"] = extract_name(doc.get_state("draft-iesg")) data["iesg_state"] = extract_name(doc.get_state("draft-iesg"))
data["rfceditor_state"] = extract_name(doc.get_state("draft-rfceditor")) data["rfceditor_state"] = extract_name(doc.get_state("draft-rfceditor"))
@ -932,7 +925,7 @@ def document_json(request, name):
data["consensus"] = e.consensus if e else None data["consensus"] = e.consensus if e else None
data["stream"] = extract_name(doc.stream) data["stream"] = extract_name(doc.stream)
return HttpResponse(json.dumps(data, indent=2), content_type='text/plain') return HttpResponse(json.dumps(data, indent=2), content_type='application/json')
class AddCommentForm(forms.Form): class AddCommentForm(forms.Form):
comment = forms.CharField(required=True, widget=forms.Textarea) comment = forms.CharField(required=True, widget=forms.Textarea)

View file

@ -0,0 +1,126 @@
var data;
d3.json("doc.json", function(error, json) {
if (error) return console.warn(error);
data = json["rev_history"];
// make js dates out of publication dates
data.forEach(function(el) { el.published = new Date(el.published); });
// add pseudo entry for beginning of year of first publication
var year = data[0].published.getFullYear();
data.unshift({ name:'', rev: '', published: new Date(year, 0, 0)});
// add pseudo entry at end of year of last revision
year = data[data.length - 1].published.getFullYear();
data.push({ name:'', rev: '', published: new Date(year + 1, 0, 0)});
draw_timeline();
});
var xscale;
var y;
var bar_height;
function offset(d, i) {
if (i > 1 && data[i - 1].name !== d.name || d.rev.match("rfc"))
y += bar_height;
return "translate(" + xscale(d.published) + ", " + y + ")";
}
function bar_width(d, i) {
if (i > 0 && i < data.length - 1)
return xscale(data[i + 1].published) - xscale(d.published);
}
function draw_timeline() {
var w = $("#timeline").width();
// bar_height = parseFloat($("body").css('line-height'));
bar_height = 30;
xscale = d3.time.scale().domain([
d3.min(data, function(d) { return d.published; }),
d3.max(data, function(d) { return d.published; })
]).range([0, w]);
y = 0;
var chart = d3.select("#timeline svg").attr("width", w);
var bar = chart.selectAll("g").data(data);
// update
bar
.attr("transform", offset)
.select("rect")
.attr("width", bar_width);
// enter
var g = bar.enter()
.append("g")
.attr({
class: "bar",
transform: offset
});
g.append("rect")
.attr({
height: bar_height,
width: bar_width
});
g.append("text")
.attr({
x: 3,
y: bar_height/2
})
.text(function(d) { return d.rev; });
// exit
bar.exit().remove();
var xaxis = d3.svg.axis()
.scale(xscale)
.tickValues(data.slice(1, -1).map(function(d) { return d.published; }))
.tickFormat(d3.time.format("%b %Y"))
.orient("bottom");
var ids = data
.map(function(elem) { return elem.name; })
.filter(function(val, i, self) { return self.indexOf(val) === i; });
ids.shift(); // first one is pseudo entry (last one, too, but filtered above)
console.log(ids);
var yaxis = d3.svg.axis()
.scale(d3.scale.ordinal().domain(ids).rangePoints([0, y - bar_height]))
.tickValues(ids)
.orient("left");
chart.append("g")
.attr({
class: "x axis",
transform: "translate(0, " + y + ")"
})
.call(xaxis)
.selectAll("text")
.style("text-anchor", "end")
.attr("transform", "translate(-18, 8) rotate(-90)");
chart.append("g")
.attr({
class: "y axis",
transform: "translate(10, " + bar_height/2 + ")"
})
.call(yaxis)
.selectAll("text")
.style("text-anchor", "start");
chart.attr('height', y);
}
$(window).on({
resize: function (event) {
draw_timeline();
}
});

View file

@ -10,22 +10,40 @@
{% endblock %} {% endblock %}
{% block morecss %} {% block morecss %}
.inline { display: inline; } .inline { display: inline; }
{% endblock %}
{% block js %} #timeline .axis text { font-size: small; }
<script>
$(window).on({
resize: function (event) {
draw_timeline();
},
load: function (event) {
draw_timeline();
}
});
</script>
{% endblock %}
#timeline .axis path, #timeline .axis line {
fill: none;
stroke: black;
}
#timeline .axis.y path, #timeline .axis.y line { stroke: none; }
#timeline .axis.x text {
dominant-baseline: central;
fill: darkgrey;
}
#timeline .bar text {
fill: white;
dominant-baseline: central;
}
#timeline .bar:nth-child(odd) rect {
fill: #3abf03;
stroke: #32a602;
stroke-width: 1;
}
#timeline .bar:nth-child(even) rect {
fill: #6b5bad;
stroke: #5f4f9f;
stroke-width: 1;
}
{% endblock %}
{% block title %} {% block title %}
{% if doc.get_state_slug == "rfc" %} {% if doc.get_state_slug == "rfc" %}
@ -40,7 +58,7 @@ $(window).on({
{{ top|safe }} {{ top|safe }}
{% include "doc/revisions_list.html" %} {% include "doc/revisions_list.html" %}
{% include "doc/timeline.html" %} <div id="timeline"><svg></svg></div>
<table class="table table-condensed"> <table class="table table-condensed">
<thead id="message-row"> <thead id="message-row">
@ -509,3 +527,9 @@ $(window).on({
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block js %}
<script src="{% static 'd3/d3.min.js' %}"></script>
<script src="{% static 'ietf/js/document_timeline.js' %}"></script>
{% endblock %}

View file

@ -1,110 +0,0 @@
{% load staticfiles %}
<script src="{% static 'd3/d3.min.js' %}"></script>
<style>
#timeline { width: 100%; }
#timeline text { fill: white; }
#timeline > :nth-child(odd) { fill: steelblue; }
#timeline > :nth-child(even) { fill: red; }
.axis path,
.axis line {
fill: none;
stroke: black;
}
#timeline .axis text {
fill: black;
font-size: small;
}
</style>
<svg id="timeline"></svg>
<script>
var data = [
{% for r in rev_history %}
{% if forloop.first %}
{ name: '{{ r.2 }}'.substring(0, 4), rev: '', time: new Date('{{ r.2 }}'.substring(0, 4))},
{% endif %}
{ name: '{{r.0}}', rev: '{{r.1}}', time: new Date('{{ r.2 }}')},
{% if forloop.last %}
{ name: (parseInt('{{ r.2 }}'.substring(0, 4)) + 1).toString(), rev: '', time: new Date((parseInt('{{ r.2 }}'.substring(0, 4)) + 1).toString())},
{% endif %}
{% endfor %}
];
var x;
var y;
var bar_height;
function offset(d, i) {
if (i > 1 && data[i - 1].name !== d.name || d.rev.match("rfc"))
y += bar_height;
return "translate(" + x(d.time) + ", " + y + ")";
}
function bar_width(d, i) {
if (i > 0 && i < data.length - 1)
return x(data[i + 1].time) - x(d.time);
}
function draw_timeline() {
var w = $("#timeline").width();
bar_height = parseFloat($("body").css('line-height'));
x = d3.time.scale().domain([
d3.min(data, function(d) { return d.time; }),
d3.max(data, function(d) { return d.time; })
]).range([0, w]);
y = 0;
var chart = d3.select("#timeline");
var bar = chart.selectAll("g").data(data);
// update
bar
.attr("transform", offset)
.select("rect")
.attr("width", bar_width);
// enter
var g = bar.enter()
.append("g")
.attr("transform", offset);
g.append("rect")
.attr({
height: bar_height,
width: bar_width
});
g.append("text")
.attr("y", bar_height/2)
.text(function (d) { return d.rev; });
// exit
bar.exit().remove();
chart.attr("height", y + 3*bar_height);
var axis = d3.svg.axis()
.scale(x)
.tickValues(data.slice(1, -1).map(function(d) { return d.time; }))
.tickFormat(d3.time.format("%b %Y"))
.orient("bottom");
chart.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0, " + y + ")")
.call(axis)
.selectAll("text")
.style("text-anchor", "end")
.attr("transform", "translate(-13, 10) rotate(-90)");
}
</script>