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:
parent
e1ff3a5782
commit
d4d09a2f75
|
@ -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'])
|
||||||
|
|
|
@ -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)
|
||||||
|
|
126
ietf/static/ietf/js/document_timeline.js
Normal file
126
ietf/static/ietf/js/document_timeline.js
Normal 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();
|
||||||
|
}
|
||||||
|
});
|
|
@ -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 %}
|
||||||
|
|
|
@ -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>
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue