From d4d09a2f75da259061137a193847ba2546678a6e Mon Sep 17 00:00:00 2001 From: Lars Eggert Date: Tue, 8 Dec 2015 14:29:35 +0000 Subject: [PATCH] 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 --- ietf/doc/utils.py | 17 ++- ietf/doc/views_doc.py | 15 +-- ietf/static/ietf/js/document_timeline.js | 126 +++++++++++++++++++++++ ietf/templates/doc/document_draft.html | 54 +++++++--- ietf/templates/doc/timeline.html | 110 -------------------- 5 files changed, 181 insertions(+), 141 deletions(-) create mode 100644 ietf/static/ietf/js/document_timeline.js delete mode 100644 ietf/templates/doc/timeline.html diff --git a/ietf/doc/utils.py b/ietf/doc/utils.py index 5f38fd90a..fbcf075f9 100644 --- a/ietf/doc/utils.py +++ b/ietf/doc/utils.py @@ -3,7 +3,6 @@ import re import urllib import math import datetime -import operator from django.conf import settings from django.db.models.query import EmptyQuerySet @@ -542,7 +541,7 @@ def uppercase_std_abbreviated_name(name): return name 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): retval = [] if hasattr(doc, 'relateddocument_set'): @@ -559,12 +558,20 @@ def crawl_history(doc): for d in history: for e in d.docevent_set.filter(type='new_revision'): 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": e = doc.latest_event(type='published_rfc') else: e = doc.latest_event(type='iesg_approved') if e: - retval.append((doc.name, e.doc.canonical_name, e.time.isoformat())) - return sorted(retval, key=operator.itemgetter(2)) + retval.append({ + 'name': e.doc.canonical_name(), + 'rev': e.doc.canonical_name(), + 'published': e.time.isoformat() + }) + return sorted(retval, key=lambda x: x['published']) diff --git a/ietf/doc/views_doc.py b/ietf/doc/views_doc.py index cbe1ebd7a..3564a3e57 100644 --- a/ietf/doc/views_doc.py +++ b/ietf/doc/views_doc.py @@ -417,7 +417,6 @@ def document_main(request, name, rev=None): search_archive=search_archive, actions=actions, tracking_document=tracking_document, - rev_history=crawl_history(latest_revision.doc if latest_revision else doc), ), 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) - latest_revision = doc.latest_event(NewRevisionDocEvent, type="new_revision") - return render_to_response("doc/document_charter.html", dict(doc=doc, top=top, @@ -459,7 +456,6 @@ def document_main(request, name, rev=None): group=group, milestones=milestones, can_manage=can_manage, - rev_history=crawl_history(latest_revision.doc if latest_revision else doc), ), 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(): 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", dict(doc=doc, top=top, @@ -490,7 +484,6 @@ def document_main(request, name, rev=None): conflictdoc=conflictdoc, ballot_summary=ballot_summary, 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)) @@ -515,8 +508,6 @@ def document_main(request, name, rev=None): else: sorted_relations=None - latest_revision = doc.latest_event(NewRevisionDocEvent, type="new_revision") - return render_to_response("doc/document_status_change.html", dict(doc=doc, top=top, @@ -528,7 +519,6 @@ def document_main(request, name, rev=None): ballot_summary=ballot_summary, approved_states=('appr-pend','appr-sent'), sorted_relations=sorted_relations, - rev_history=crawl_history(latest_revision.doc if latest_revision else doc), ), 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["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": data["iesg_state"] = extract_name(doc.get_state("draft-iesg")) 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["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): comment = forms.CharField(required=True, widget=forms.Textarea) diff --git a/ietf/static/ietf/js/document_timeline.js b/ietf/static/ietf/js/document_timeline.js new file mode 100644 index 000000000..a84944746 --- /dev/null +++ b/ietf/static/ietf/js/document_timeline.js @@ -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(); + } +}); diff --git a/ietf/templates/doc/document_draft.html b/ietf/templates/doc/document_draft.html index d68b4a63f..6d1eadc9b 100644 --- a/ietf/templates/doc/document_draft.html +++ b/ietf/templates/doc/document_draft.html @@ -10,22 +10,40 @@ {% endblock %} {% block morecss %} - .inline { display: inline; } -{% endblock %} +.inline { display: inline; } -{% block js %} - -{% endblock %} +#timeline .axis text { font-size: small; } +#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 %} {% if doc.get_state_slug == "rfc" %} @@ -40,7 +58,7 @@ $(window).on({ {{ top|safe }} {% include "doc/revisions_list.html" %} - {% include "doc/timeline.html" %} +
@@ -509,3 +527,9 @@ $(window).on({ {% endif %} {% endblock %} + + +{% block js %} + + +{% endblock %} diff --git a/ietf/templates/doc/timeline.html b/ietf/templates/doc/timeline.html deleted file mode 100644 index 8638f37ea..000000000 --- a/ietf/templates/doc/timeline.html +++ /dev/null @@ -1,110 +0,0 @@ -{% load staticfiles %} - - - - - - - - -