Merged in /personal/lars/6.11.1.dev0@10590 from lars@netapp.com, which brings in a timeline view at the top of document pages.
- Legacy-Id: 10597
This commit is contained in:
commit
213ae4921b
19
.eslintrc.js
Normal file
19
.eslintrc.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
module.exports = {
|
||||
rules: {
|
||||
indent: [2, 4],
|
||||
camelcase: 0,
|
||||
"require-jsdoc": 0,
|
||||
quotes: [2, "double"],
|
||||
"no-multiple-empty-lines": [2, {max: 2}],
|
||||
"quote-props": [2, "as-needed"],
|
||||
"brace-style": [2, "1tbs", {allowSingleLine: true}]
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
jquery: true
|
||||
},
|
||||
globals: {
|
||||
d3: true
|
||||
},
|
||||
extends: "google"
|
||||
};
|
|
@ -5,6 +5,7 @@
|
|||
"main": [],
|
||||
"dependencies": {
|
||||
"bootstrap-datepicker": "1.5.0",
|
||||
"d3": "3.5.10",
|
||||
"font-awesome": "4.5.0",
|
||||
"html5shiv": "3.7.3",
|
||||
"jquery": "1.11.3",
|
||||
|
|
|
@ -65,7 +65,7 @@ urlpatterns = patterns('',
|
|||
url(r'^(?P<name>[A-Za-z0-9._+-]+)/ballot/(?P<ballot_id>[0-9]+)/emailposition/$', views_ballot.send_ballot_comment, name='doc_send_ballot_comment'),
|
||||
url(r'^(?P<name>[A-Za-z0-9._+-]+)/ballot/(?P<ballot_id>[0-9]+)/$', views_doc.document_ballot, name="doc_ballot"),
|
||||
url(r'^(?P<name>[A-Za-z0-9._+-]+)/ballot/$', views_doc.document_ballot, name="doc_ballot"),
|
||||
(r'^(?P<name>[A-Za-z0-9._+-]+)/doc.json$', views_doc.document_json),
|
||||
(r'^(?P<name>[A-Za-z0-9._+-]+)/(?:(?P<rev>[0-9-]+)/)?doc.json$', views_doc.document_json),
|
||||
(r'^(?P<name>[A-Za-z0-9._+-]+)/ballotpopup/(?P<ballot_id>[0-9]+)/$', views_doc.ballot_popup),
|
||||
|
||||
url(r'^(?P<name>[A-Za-z0-9._+-]+)/email-aliases/$', RedirectView.as_view(pattern_name='doc_email', permanent=False),name='doc_specific_email_aliases'),
|
||||
|
|
|
@ -8,6 +8,7 @@ from django.conf import settings
|
|||
from django.db.models.query import EmptyQuerySet
|
||||
from django.forms import ValidationError
|
||||
from django.utils.html import strip_tags, escape
|
||||
from django.core.urlresolvers import reverse as urlreverse
|
||||
|
||||
from ietf.doc.models import Document, DocHistory, State
|
||||
from ietf.doc.models import DocAlias, RelatedDocument, BallotType, DocReminder
|
||||
|
@ -27,7 +28,7 @@ def email_update_telechat(request, doc, text):
|
|||
|
||||
if not to:
|
||||
return
|
||||
|
||||
|
||||
text = strip_tags(text)
|
||||
send_mail(request, to, None,
|
||||
"Telechat update notice: %s" % doc.file_tag(),
|
||||
|
@ -41,7 +42,7 @@ def get_state_types(doc):
|
|||
|
||||
if not doc:
|
||||
return res
|
||||
|
||||
|
||||
res.append(doc.type_id)
|
||||
|
||||
if doc.type_id == "draft":
|
||||
|
@ -52,7 +53,7 @@ def get_state_types(doc):
|
|||
res.append("draft-iana-review")
|
||||
res.append("draft-iana-action")
|
||||
res.append("draft-rfceditor")
|
||||
|
||||
|
||||
return res
|
||||
|
||||
def get_tags_for_stream_id(stream_id):
|
||||
|
@ -144,7 +145,7 @@ def needed_ballot_positions(doc, active_positions):
|
|||
answer.append("Has enough positions to pass.")
|
||||
|
||||
return " ".join(answer)
|
||||
|
||||
|
||||
def create_ballot_if_not_open(doc, by, ballot_slug, time=None):
|
||||
if not doc.ballot_open(ballot_slug):
|
||||
if time:
|
||||
|
@ -359,7 +360,7 @@ def make_notify_changed_event(request, doc, by, new_notify, time=None):
|
|||
|
||||
def update_telechat(request, doc, by, new_telechat_date, new_returning_item=None):
|
||||
from ietf.doc.models import TelechatDocEvent
|
||||
|
||||
|
||||
on_agenda = bool(new_telechat_date)
|
||||
|
||||
prev = doc.latest_event(TelechatDocEvent, type="scheduled_for_telechat")
|
||||
|
@ -378,7 +379,7 @@ def update_telechat(request, doc, by, new_telechat_date, new_returning_item=None
|
|||
|
||||
# auto-set returning item _ONLY_ if the caller did not provide a value
|
||||
if ( new_returning_item != None
|
||||
and on_agenda
|
||||
and on_agenda
|
||||
and prev_agenda
|
||||
and new_telechat_date != prev_telechat
|
||||
and prev_telechat < datetime.date.today()
|
||||
|
@ -392,7 +393,7 @@ def update_telechat(request, doc, by, new_telechat_date, new_returning_item=None
|
|||
e.doc = doc
|
||||
e.returning_item = returning
|
||||
e.telechat_date = new_telechat_date
|
||||
|
||||
|
||||
if on_agenda != prev_agenda:
|
||||
if on_agenda:
|
||||
e.desc = "Placed on agenda for telechat - %s" % (new_telechat_date)
|
||||
|
@ -426,7 +427,7 @@ def rebuild_reference_relations(doc,filename=None):
|
|||
refs = draft.Draft(draft._gettext(filename), filename).get_refs()
|
||||
except IOError as e:
|
||||
return { 'errors': ["%s :%s" % (e.strerror, filename)] }
|
||||
|
||||
|
||||
doc.relateddocument_set.filter(relationship__slug__in=['refnorm','refinfo','refold','refunk']).delete()
|
||||
|
||||
warnings = []
|
||||
|
@ -449,11 +450,11 @@ def rebuild_reference_relations(doc,filename=None):
|
|||
|
||||
ret = {}
|
||||
if errors:
|
||||
ret['errors']=errors
|
||||
ret['errors']=errors
|
||||
if warnings:
|
||||
ret['warnings']=warnings
|
||||
ret['warnings']=warnings
|
||||
if unfound:
|
||||
ret['unfound']=list(unfound)
|
||||
ret['unfound']=list(unfound)
|
||||
|
||||
return ret
|
||||
|
||||
|
@ -539,3 +540,41 @@ def uppercase_std_abbreviated_name(name):
|
|||
return name.upper()
|
||||
else:
|
||||
return name
|
||||
|
||||
def crawl_history(doc):
|
||||
# return document history data for inclusion in doc.json (used by timeline)
|
||||
def ancestors(doc):
|
||||
retval = []
|
||||
if hasattr(doc, 'relateddocument_set'):
|
||||
for rel in doc.relateddocument_set.filter(relationship__slug='replaces'):
|
||||
if rel.target.document not in retval:
|
||||
retval.append(rel.target.document)
|
||||
retval.extend(ancestors(rel.target.document))
|
||||
return retval
|
||||
|
||||
retval = []
|
||||
history = ancestors(doc)
|
||||
if history is not None:
|
||||
history.append(doc)
|
||||
for d in history:
|
||||
for e in d.docevent_set.filter(type='new_revision'):
|
||||
if hasattr(e, 'newrevisiondocevent'):
|
||||
retval.append({
|
||||
'name': d.name,
|
||||
'rev': e.newrevisiondocevent.rev,
|
||||
'published': e.time.isoformat(),
|
||||
'url': urlreverse("doc_view", kwargs=dict(name=d)) + e.newrevisiondocevent.rev + "/"
|
||||
})
|
||||
|
||||
if doc.type_id == "draft":
|
||||
e = doc.latest_event(type='published_rfc')
|
||||
else:
|
||||
e = doc.latest_event(type='iesg_approved')
|
||||
if e:
|
||||
retval.append({
|
||||
'name': e.doc.canonical_name(),
|
||||
'rev': e.doc.canonical_name(),
|
||||
'published': e.time.isoformat(),
|
||||
'url': urlreverse("doc_view", kwargs=dict(name=e.doc))
|
||||
})
|
||||
return sorted(retval, key=lambda x: x['published'])
|
||||
|
|
|
@ -49,7 +49,7 @@ from ietf.doc.models import ( Document, DocAlias, DocHistory, DocEvent, BallotDo
|
|||
from ietf.doc.utils import ( add_links_in_new_revision_events, augment_events_with_revision,
|
||||
can_adopt_draft, get_chartering_type, get_document_content, get_tags_for_stream_id,
|
||||
needed_ballot_positions, nice_consensus, prettify_std_name, update_telechat, has_same_ballot,
|
||||
get_initial_notify, make_notify_changed_event )
|
||||
get_initial_notify, make_notify_changed_event, crawl_history)
|
||||
from ietf.community.models import CommunityList
|
||||
from ietf.group.models import Role
|
||||
from ietf.group.utils import can_manage_group_type, can_manage_materials
|
||||
|
@ -878,7 +878,7 @@ def ballot_popup(request, name, ballot_id):
|
|||
context_instance=RequestContext(request))
|
||||
|
||||
|
||||
def document_json(request, name):
|
||||
def document_json(request, name, rev=None):
|
||||
doc = get_object_or_404(Document, docalias__name=name)
|
||||
|
||||
def extract_name(s):
|
||||
|
@ -911,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"))
|
||||
|
@ -922,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)
|
||||
|
|
5
ietf/externals/static/d3/d3.min.js
vendored
Normal file
5
ietf/externals/static/d3/d3.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -431,3 +431,32 @@ h1 small .pull-right { margin-top: 10.5px; }
|
|||
*/
|
||||
form.navbar-form input.form-control.input-sm { width: 141px; }
|
||||
|
||||
|
||||
|
||||
/* Styles for d3.js graphical SVG timelines */
|
||||
#timeline { 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; }
|
||||
|
||||
#timeline .bar text {
|
||||
fill: white;
|
||||
dominant-baseline: central;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* like label-success */
|
||||
#timeline .bar:nth-child(odd) rect { fill: #5CB85C; }
|
||||
|
||||
/* like label-primary */
|
||||
#timeline .bar:nth-child(even) rect { fill: #337AB7; }
|
||||
|
||||
/* like label-warning */
|
||||
#timeline .gradient.left { stop-color: #F0AD4E; }
|
||||
#timeline .gradient.right { stop-color: white; }
|
||||
|
|
228
ietf/static/ietf/js/document_timeline.js
Normal file
228
ietf/static/ietf/js/document_timeline.js
Normal file
|
@ -0,0 +1,228 @@
|
|||
"use strict";
|
||||
|
||||
var data;
|
||||
var x_scale;
|
||||
var bar_y;
|
||||
var bar_height;
|
||||
var y_label_width;
|
||||
var x_axis;
|
||||
var width;
|
||||
|
||||
|
||||
function expiration_date(d) {
|
||||
return new Date(d.published.getTime() + 1000 * 60 * 60 * 24 * 185);
|
||||
}
|
||||
|
||||
|
||||
function max(arr) {
|
||||
return Math.max.apply(null, Object.keys(arr).map(function(e) {
|
||||
return arr[e];
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
function offset(d) {
|
||||
if (bar_y[d.name] === undefined) {
|
||||
var m = Object.keys(bar_y).length === 0 ? -bar_height : max(bar_y);
|
||||
bar_y[d.name] = m + bar_height;
|
||||
}
|
||||
return "translate(" + x_scale(d.published) + ", " + bar_y[d.name] + ")";
|
||||
}
|
||||
|
||||
|
||||
function bar_width(d, i) {
|
||||
// check for next rev of this name
|
||||
for (i++; i < data.length; i++) {
|
||||
if (data[i].name === d.name) { break; }
|
||||
}
|
||||
|
||||
var w = i === data.length ? expiration_date(d) : data[i].published;
|
||||
return x_scale(w) - x_scale(d.published);
|
||||
}
|
||||
|
||||
|
||||
function scale_x() {
|
||||
width = $("#timeline").width();
|
||||
|
||||
// scale data to width of container minus y label width
|
||||
x_scale = d3.time.scale().domain([
|
||||
d3.min(data, function(d) { return d.published; }),
|
||||
d3.max(data, function(d) { return d.published; })
|
||||
]).range([y_label_width, width]);
|
||||
|
||||
// resort data by publication time to suppress some ticks if they are closer
|
||||
// than 12px, and don't add a tick for the final pseudo entry
|
||||
var tv = data
|
||||
.slice(0, -1)
|
||||
.sort(function(a, b) { return a.published - b.published; })
|
||||
.map(function(d, i, arr) {
|
||||
if (i === 0 ||
|
||||
x_scale(d.published) > x_scale(arr[i - 1].published) + 12) {
|
||||
return d.published;
|
||||
}
|
||||
}).filter(function(d) { return d !== undefined; });
|
||||
|
||||
x_axis = d3.svg.axis()
|
||||
.scale(x_scale)
|
||||
.tickValues(tv)
|
||||
.tickFormat(d3.time.format("%b %Y"))
|
||||
.orient("bottom");
|
||||
}
|
||||
|
||||
|
||||
function update_x_axis() {
|
||||
d3.select("#timeline svg .x.axis").call(x_axis)
|
||||
.selectAll("text")
|
||||
.style("text-anchor", "end")
|
||||
.attr("transform", "translate(-14, 2) rotate(-60)");
|
||||
}
|
||||
|
||||
|
||||
function update_timeline() {
|
||||
bar_y = {};
|
||||
scale_x();
|
||||
var chart = d3.select("#timeline svg").attr("width", width);
|
||||
// enter data (skip the last pseudo entry)
|
||||
var bar = chart.selectAll("g").data(data.slice(0, -1));
|
||||
bar.attr("transform", offset).select("rect").attr("width", bar_width);
|
||||
update_x_axis();
|
||||
}
|
||||
|
||||
|
||||
function draw_timeline() {
|
||||
bar_height = parseFloat($("body").css("line-height"));
|
||||
|
||||
var div = $("#timeline");
|
||||
if (div.is(":empty")) {
|
||||
div.append("<svg></svg>");
|
||||
}
|
||||
var chart = d3.select("#timeline svg").attr("width", width);
|
||||
|
||||
var gradient = chart.append("defs")
|
||||
.append("linearGradient")
|
||||
.attr("id", "gradient");
|
||||
gradient.append("stop")
|
||||
.attr({
|
||||
class: "gradient left",
|
||||
offset: 0
|
||||
});
|
||||
gradient.append("stop")
|
||||
.attr({
|
||||
class: "gradient right",
|
||||
offset: 1
|
||||
});
|
||||
|
||||
var y_labels = data
|
||||
.map(function(d) { return d.name; })
|
||||
.filter(function(val, i, self) { return self.indexOf(val) === i; });
|
||||
|
||||
// calculate the width of the widest y axis label by drawing them off-screen
|
||||
// and measuring the bounding boxes
|
||||
y_label_width = 10 + d3.max(y_labels, function(l) {
|
||||
var lw;
|
||||
chart.append("text")
|
||||
.attr({
|
||||
class: "y axis",
|
||||
transform: "translate(0, " + -bar_height + ")"
|
||||
})
|
||||
.text(l)
|
||||
.each(function() {
|
||||
lw = this.getBBox().width;
|
||||
})
|
||||
.remove().remove();
|
||||
return lw;
|
||||
});
|
||||
|
||||
// update
|
||||
update_timeline();
|
||||
|
||||
// re-order data by document name, for CSS background color alternation
|
||||
var ndata = [];
|
||||
y_labels.forEach(function(l) {
|
||||
ndata = ndata.concat(data.filter(function(d) { return d.name === l; }));
|
||||
});
|
||||
data = ndata;
|
||||
|
||||
// enter data (skip the last pseudo entry)
|
||||
var bar = chart.selectAll("g").data(data.slice(0, -1));
|
||||
var g = bar.enter()
|
||||
.append("g")
|
||||
.attr({
|
||||
class: "bar",
|
||||
transform: offset
|
||||
});
|
||||
g.append("a")
|
||||
.attr("xlink:href", function(d) { return d.url; })
|
||||
.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; });
|
||||
|
||||
// since the gradient is defined inside the SVG, we need to set the CSS
|
||||
// style here, so the relative URL works
|
||||
$("#timeline .bar:last-child rect").css("fill", "url(#gradient)");
|
||||
|
||||
var y_scale = d3.scale.ordinal()
|
||||
.domain(y_labels)
|
||||
.rangePoints([0, max(bar_y) + bar_height]);
|
||||
|
||||
var y_axis = d3.svg.axis()
|
||||
.scale(y_scale)
|
||||
.tickValues(y_labels)
|
||||
.orient("left");
|
||||
|
||||
chart.append("g").attr({
|
||||
class: "x axis",
|
||||
transform: "translate(0, " + (max(bar_y) + bar_height) + ")"
|
||||
});
|
||||
update_x_axis();
|
||||
|
||||
chart.append("g")
|
||||
.attr({
|
||||
class: "y axis",
|
||||
transform: "translate(10, " + bar_height / 2 + ")"
|
||||
})
|
||||
.call(y_axis)
|
||||
.selectAll("text")
|
||||
.style("text-anchor", "start");
|
||||
|
||||
// set height of timeline
|
||||
var x_label_height;
|
||||
d3.select(".x.axis").each(function() {
|
||||
x_label_height = this.getBBox().height;
|
||||
});
|
||||
chart.attr("height", max(bar_y) + bar_height + x_label_height);
|
||||
}
|
||||
|
||||
|
||||
d3.json("doc.json", function(error, json) {
|
||||
if (error) { return; }
|
||||
data = json.rev_history;
|
||||
|
||||
if (data.length) {
|
||||
// make js dates out of publication dates
|
||||
data.forEach(function(d) { d.published = new Date(d.published); });
|
||||
|
||||
// add pseudo entry when the ID will expire
|
||||
data.push({
|
||||
name: "",
|
||||
rev: "",
|
||||
published: expiration_date(data[data.length - 1])
|
||||
});
|
||||
draw_timeline();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
$(window).on({
|
||||
resize: function() {
|
||||
update_timeline();
|
||||
}
|
||||
});
|
|
@ -15,6 +15,7 @@
|
|||
{{ top|safe }}
|
||||
|
||||
{% include "doc/revisions_list.html" %}
|
||||
<div id="timeline"></div>
|
||||
|
||||
<table class="table table-condensed">
|
||||
<thead id="message-row">
|
||||
|
@ -210,3 +211,7 @@
|
|||
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script src="{% static 'd3/d3.min.js' %}"></script>
|
||||
<script src="{% static 'ietf/js/document_timeline.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
{{ top|safe }}
|
||||
|
||||
{% include "doc/revisions_list.html" %}
|
||||
<div id="timeline"></div>
|
||||
|
||||
<table class="table table-condensed">
|
||||
<thead id="message-row">
|
||||
|
@ -137,6 +138,10 @@
|
|||
<p></p>
|
||||
{{ content|fill:"80"|safe|linebreaksbr|keep_spacing|sanitize_html|safe }}
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block js %}
|
||||
<script src="{% static 'd3/d3.min.js' %}"></script>
|
||||
<script src="{% static 'ietf/js/document_timeline.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block morecss %}
|
||||
.inline { display: inline; }
|
||||
.inline { display: inline; }
|
||||
{% endblock %}
|
||||
|
||||
{% block title %}
|
||||
|
@ -26,6 +26,7 @@
|
|||
{{ top|safe }}
|
||||
|
||||
{% include "doc/revisions_list.html" %}
|
||||
<div id="timeline"></div>
|
||||
|
||||
<table class="table table-condensed">
|
||||
<thead id="message-row">
|
||||
|
@ -34,7 +35,7 @@
|
|||
<th colspan="4" class="alert-warning">The information below is for an old version of the document</th>
|
||||
{% else %}
|
||||
<th colspan="4"></th>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
|
@ -492,5 +493,9 @@
|
|||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script src="{% static 'd3/d3.min.js' %}"></script>
|
||||
<script src="{% static 'ietf/js/document_timeline.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{% extends "base.html" %}
|
||||
{# Copyright The IETF Trust 2015, All Rights Reserved #}
|
||||
{% load origin %}
|
||||
|
||||
{% load staticfiles %}
|
||||
{% load ietf_filters %}
|
||||
|
||||
{% block title %}{{ doc.title }}{% endblock %}
|
||||
|
@ -11,6 +11,7 @@
|
|||
{{ top|safe }}
|
||||
|
||||
{% include "doc/revisions_list.html" %}
|
||||
<div id="timeline"></div>
|
||||
|
||||
<table class="table table-condensed">
|
||||
<thead id="message-row">
|
||||
|
@ -125,5 +126,10 @@
|
|||
<p class="download-instead"><a href="{{ other_types.0.1 }}">Download as {{ other_types.0.0.upper }}</a></p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block js %}
|
||||
<script src="{% static 'd3/d3.min.js' %}"></script>
|
||||
<script src="{% static 'ietf/js/document_timeline.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{% extends "base.html" %}
|
||||
{# Copyright The IETF Trust 2015, All Rights Reserved #}
|
||||
{% load origin %}
|
||||
|
||||
{% load staticfiles %}
|
||||
{% load ietf_filters %}
|
||||
|
||||
{% block title %}{{ doc.title }}{% endblock %}
|
||||
|
@ -11,6 +11,7 @@
|
|||
{{ top|safe }}
|
||||
|
||||
{% include "doc/revisions_list.html" %}
|
||||
<div id="timeline"></div>
|
||||
|
||||
<table class="table table-condensed">
|
||||
<thead id="message-row">
|
||||
|
@ -153,3 +154,8 @@
|
|||
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block js %}
|
||||
<script src="{% static 'd3/d3.min.js' %}"></script>
|
||||
<script src="{% static 'ietf/js/document_timeline.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
|
Loading…
Reference in a new issue