datatracker/ietf/idrfc/views_doc.py
2012-07-28 20:19:36 +00:00

621 lines
26 KiB
Python

# Copyright (C) 2009-2010 Nokia Corporation and/or its subsidiary(-ies).
# All rights reserved. Contact: Pasi Eronen <pasi.eronen@nokia.com>
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
#
# * Neither the name of the Nokia Corporation and/or its
# subsidiary(-ies) nor the names of its contributors may be used
# to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import re, os, datetime
from django.http import HttpResponse, Http404
from django.shortcuts import render_to_response, get_object_or_404, redirect
from django.template import RequestContext
from django.template.loader import render_to_string
from django.template.defaultfilters import truncatewords_html
from django.utils import simplejson as json
from django.utils.decorators import decorator_from_middleware
from django.middleware.gzip import GZipMiddleware
from django.core.urlresolvers import reverse as urlreverse, NoReverseMatch
from django.conf import settings
from ietf.idtracker.models import InternetDraft, IDInternal, BallotInfo, DocumentComment
from ietf.idtracker.templatetags.ietf_filters import format_textarea, fill
from ietf.idrfc import markup_txt
from ietf.idrfc.models import RfcIndex, DraftVersions
from ietf.idrfc.idrfc_wrapper import BallotWrapper, IdWrapper, RfcWrapper
from ietf.ietfworkflows.utils import get_full_info_for_draft
from ietf.doc.models import *
from ietf.doc.utils import *
from ietf.utils.history import find_history_active_at
from ietf.ietfauth.decorators import has_role
def render_document_top(request, doc, tab, name):
tabs = []
tabs.append(("Document", "document", urlreverse("ietf.idrfc.views_doc.document_main", kwargs=dict(name=name)), True))
ballot = doc.latest_event(BallotDocEvent, type="created_ballot")
if doc.type_id in ("draft","conflrev"):
# if doc.in_ietf_process and doc.ietf_process.has_iesg_ballot:
tabs.append(("IESG Evaluation Record", "ballot", urlreverse("ietf.idrfc.views_doc.document_ballot", kwargs=dict(name=name)), ballot))
elif doc.type_id == "charter":
tabs.append(("IESG Review", "ballot", urlreverse("ietf.idrfc.views_doc.document_ballot", kwargs=dict(name=name)), ballot))
# FIXME: if doc.in_ietf_process and doc.ietf_process.has_iesg_ballot:
if doc.type_id != "conflrev":
tabs.append(("IESG Writeups", "writeup", urlreverse("ietf.idrfc.views_doc.document_writeup", kwargs=dict(name=doc.name)), True))
tabs.append(("History", "history", urlreverse("ietf.idrfc.views_doc.document_history", kwargs=dict(name=doc.name)), True))
name = doc.canonical_name()
if name.startswith("rfc"):
name = "RFC %s" % name[3:]
else:
name += "-" + doc.rev
return render_to_string("idrfc/document_top.html",
dict(doc=doc,
tabs=tabs,
selected=tab,
name=name))
def document_main(request, name, rev=None):
if name.lower().startswith("draft") or name.lower().startswith("rfc"):
if rev != None: # no support for old revisions at the moment
raise Http404()
return document_main_idrfc(request, name, tab="document")
doc = get_object_or_404(Document, docalias__name=name)
group = doc.group
if doc.type_id == 'conflrev':
conflictdoc = doc.relateddocument_set.get(relationship__slug='conflrev').target.document
revisions = []
for h in doc.history_set.order_by("time", "id"):
if h.rev and not h.rev in revisions:
revisions.append(h.rev)
if not doc.rev in revisions:
revisions.append(doc.rev)
snapshot = False
if rev != None:
if rev == doc.rev:
return redirect('doc_view', name=name)
# find the entry in the history
for h in doc.history_set.order_by("-time"):
if rev == h.rev:
snapshot = True
doc = h
break
if not snapshot:
return redirect('doc_view', name=name)
if doc.type_id == "charter":
# find old group, too
gh = find_history_active_at(doc.group, doc.time)
if gh:
group = gh
top = render_document_top(request, doc, "document", name)
telechat = doc.latest_event(TelechatDocEvent, type="scheduled_for_telechat")
if telechat and not telechat.telechat_date:
telechat = None
if telechat and telechat.telechat_date < datetime.date.today():
telechat = None
if doc.type_id == "charter":
filename = "%s-%s.txt" % (doc.canonical_name(), doc.rev)
content = _get_html(filename, os.path.join(settings.CHARTER_PATH, filename), split=False)
ballot_summary = None
if doc.get_state_slug() in ("intrev", "iesgrev"):
ballot_summary = needed_ballot_positions(doc, active_ballot_positions(doc).values())
return render_to_response("idrfc/document_charter.html",
dict(doc=doc,
top=top,
chartering=get_chartering_type(doc),
content=content,
txt_url=settings.CHARTER_TXT_URL + filename,
revisions=revisions,
snapshot=snapshot,
telechat=telechat,
ballot_summary=ballot_summary,
group=group,
),
context_instance=RequestContext(request))
if doc.type_id == "conflrev":
filename = "%s-%s.txt" % (doc.canonical_name(), doc.rev)
pathname = os.path.join(settings.CONFLICT_REVIEW_PATH,filename)
if doc.rev == "00" and not os.path.isfile(pathname):
# This could move to a template
content = "A conflict review response has not yet been proposed."
else:
content = _get_html(filename, pathname, split=False)
ballot_summary = None
if doc.get_state_slug() in ("iesgeval"):
ballot_summary = needed_ballot_positions(doc, active_ballot_positions(doc).values())
return render_to_response("idrfc/document_conflict_review.html",
dict(doc=doc,
top=top,
content=content,
revisions=revisions,
snapshot=snapshot,
telechat=telechat,
conflictdoc=conflictdoc,
ballot_summary=ballot_summary,
approved_states=('appr-reqnopub-pend','appr-reqnopub-sent','appr-noprob-pend','appr-noprob-sent')
),
context_instance=RequestContext(request))
raise Http404()
def document_history(request, name):
# todo: remove need for specific handling of drafts by porting the
# two event text hacks
if name.lower().startswith("draft") or name.lower().startswith("rfc"):
return document_main_idrfc(request, name, "history")
doc = get_object_or_404(Document, docalias__name=name)
top = render_document_top(request, doc, "history", name)
diff_documents = [ doc ]
diff_documents.extend(Document.objects.filter(docalias__relateddocument__source=doc, docalias__relateddocument__relationship="replaces"))
# pick up revisions from events
diff_revisions = []
seen = set()
diffable = name.startswith("draft") or name.startswith("charter")
if diffable:
for e in NewRevisionDocEvent.objects.filter(type="new_revision", doc__in=diff_documents).select_related('doc').order_by("-time", "-id"):
if not (e.doc.name, e.rev) in seen:
seen.add((e.doc.name, e.rev))
url = ""
if name.startswith("charter"):
h = find_history_active_at(e.doc, e.time)
url = settings.CHARTER_TXT_URL + ("%s-%s.txt" % ((h or doc).canonical_name(), e.rev))
elif name.startswith("draft"):
# rfcdiff tool has special support for IDs
url = e.doc.name + "-" + e.rev
diff_revisions.append((e.doc.name, e.rev, e.time, url))
# grab event history
events = doc.docevent_set.all().order_by("-time", "-id").select_related("by")
augment_events_with_revision(doc, events)
return render_to_response("idrfc/document_history.html",
dict(doc=doc,
top=top,
diff_revisions=diff_revisions,
events=events,
),
context_instance=RequestContext(request))
def document_writeup(request, name):
if name.lower().startswith("draft") or name.lower().startswith("rfc"):
# todo: migrate idrfc to pattern below
return document_main_idrfc(request, name, "writeup")
doc = get_object_or_404(Document, docalias__name=name)
top = render_document_top(request, doc, "writeup", name)
writeups = []
if doc.type_id == "charter":
e = doc.latest_event(WriteupDocEvent, type="changed_review_announcement")
writeups.append(("WG Review Announcement",
e.text if e else "",
urlreverse("ietf.wgcharter.views.announcement_text", kwargs=dict(name=doc.name, ann="review"))))
e = doc.latest_event(WriteupDocEvent, type="changed_action_announcement")
writeups.append(("WG Action Announcement",
e.text if e else "",
urlreverse("ietf.wgcharter.views.announcement_text", kwargs=dict(name=doc.name, ann="action"))))
if doc.latest_event(BallotDocEvent, type="created_ballot"):
e = doc.latest_event(WriteupDocEvent, type="changed_ballot_writeup_text")
writeups.append(("Ballot Announcement",
e.text if e else "",
urlreverse("ietf.wgcharter.views.ballot_writeupnotes", kwargs=dict(name=doc.name))))
if not writeups:
raise Http404()
return render_to_response("idrfc/document_writeup.html",
dict(doc=doc,
top=top,
writeups=writeups,
can_edit=has_role(request.user, ("Area Director", "Secretariat")),
),
context_instance=RequestContext(request))
def document_ballot_content(request, doc, ballot_id, editable=True):
"""Render HTML string with content of ballot page."""
all_ballots = list(BallotDocEvent.objects.filter(doc=doc, type="created_ballot").order_by("time"))
augment_events_with_revision(doc, all_ballots)
ballot = None
if ballot_id != None:
ballot_id = int(ballot_id)
for b in all_ballots:
if b.id == ballot_id:
ballot = b
break
elif all_ballots:
ballot = all_ballots[-1]
if not ballot:
raise Http404
deferred = doc.active_defer_event()
# collect positions
active_ads = list(Person.objects.filter(role__name="ad", role__group__state="active").distinct())
positions = []
seen = {}
for e in BallotPositionDocEvent.objects.filter(doc=doc, type="changed_ballot_position", ballot=ballot).select_related('ad', 'pos').order_by("-time", '-id'):
if e.ad not in seen:
e.old_ad = e.ad not in active_ads
e.old_positions = []
positions.append(e)
seen[e.ad] = e
else:
latest = seen[e.ad]
if latest.old_positions:
prev = latest.old_positions[-1]
else:
prev = latest.pos.name
if e.pos.name != prev:
latest.old_positions.append(e.pos.name)
# add any missing ADs through fake No Record events
norecord = BallotPositionName.objects.get(slug="norecord")
for ad in active_ads:
if ad not in seen:
e = BallotPositionDocEvent(type="changed_ballot_position", doc=doc, ad=ad)
e.pos = norecord
e.old_ad = False
e.old_positions = []
positions.append(e)
# put into position groups
position_groups = []
for n in BallotPositionName.objects.filter(slug__in=[p.pos_id for p in positions]).order_by('order'):
g = (n, [p for p in positions if p.pos_id == n.slug])
g[1].sort(key=lambda p: (p.old_ad, p.ad.plain_name()))
if n.blocking:
position_groups.insert(0, g)
else:
position_groups.append(g)
summary = needed_ballot_positions(doc, [p for p in positions if not p.old_ad])
text_positions = [p for p in positions if p.discuss or p.comment]
text_positions.sort(key=lambda p: (p.old_ad, p.ad.plain_name()))
ballot_open = not BallotDocEvent.objects.filter(doc=doc,
type__in=("closed_ballot", "created_ballot"),
time__gt=ballot.time,
ballot_type=ballot.ballot_type)
if not ballot_open:
editable = False
return render_to_string("idrfc/document_ballot_content.html",
dict(doc=doc,
ballot=ballot,
position_groups=position_groups,
text_positions=text_positions,
editable=editable,
ballot_open=ballot_open,
deferred=deferred,
summary=summary,
all_ballots=all_ballots,
),
context_instance=RequestContext(request))
def document_ballot(request, name, ballot_id=None):
doc = get_object_or_404(Document, docalias__name=name)
top = render_document_top(request, doc, "ballot", name)
c = document_ballot_content(request, doc, ballot_id, editable=True)
return render_to_response("idrfc/document_ballot.html",
dict(doc=doc,
top=top,
ballot_content=c,
),
context_instance=RequestContext(request))
def document_debug(request, name):
r = re.compile("^rfc([1-9][0-9]*)$")
m = r.match(name)
if m:
rfc_number = int(m.group(1))
rfci = get_object_or_404(RfcIndex, rfc_number=rfc_number)
doc = RfcWrapper(rfci)
else:
id = get_object_or_404(InternetDraft, filename=name)
doc = IdWrapper(draft=id)
return HttpResponse(doc.to_json(), mimetype='text/plain')
def _get_html(key, filename, split=True):
return get_document_content(key, filename, split=split, markup=True)
def include_text(request):
include_text = request.GET.get( 'include_text' )
if "full_draft" in request.COOKIES:
if request.COOKIES["full_draft"] == "on":
include_text = 1
return include_text
def document_main_rfc(request, rfc_number, tab):
rfci = get_object_or_404(RfcIndex, rfc_number=rfc_number, states__type="draft", states__slug="rfc")
rfci.viewing_as_rfc = True
doc = RfcWrapper(rfci)
info = {}
info['is_rfc'] = True
info['has_pdf'] = (".pdf" in doc.file_types())
info['has_txt'] = (".txt" in doc.file_types())
info['has_ps'] = (".ps" in doc.file_types())
if info['has_txt']:
(content1, content2) = _get_html(
"rfc"+str(rfc_number)+",html",
os.path.join(settings.RFC_PATH, "rfc"+str(rfc_number)+".txt"))
else:
content1 = ""
content2 = ""
history = _get_history(doc, None)
template = "idrfc/doc_tab_%s" % tab
if tab == "document":
template += "_rfc"
return render_to_response(template + ".html",
{'content1':content1, 'content2':content2,
'doc':doc, 'info':info, 'tab':tab,
'include_text':include_text(request),
'history':history},
context_instance=RequestContext(request));
@decorator_from_middleware(GZipMiddleware)
def document_main_idrfc(request, name, tab):
r = re.compile("^rfc([1-9][0-9]*)$")
m = r.match(name)
if m:
return document_main_rfc(request, int(m.group(1)), tab)
id = get_object_or_404(InternetDraft, filename=name)
doc = IdWrapper(id)
info = {}
info['has_pdf'] = (".pdf" in doc.file_types())
info['is_rfc'] = False
info['conflict_reviews'] = [ rel.source for alias in id.docalias_set.all() for rel in alias.relateddocument_set.filter(relationship='conflrev') ]
(content1, content2) = _get_html(
str(name)+","+str(id.revision)+",html",
os.path.join(settings.INTERNET_DRAFT_PATH, name+"-"+id.revision+".txt"))
versions = _get_versions(id)
history = _get_history(doc, versions)
template = "idrfc/doc_tab_%s" % tab
if tab == "document":
template += "_id"
return render_to_response(template + ".html",
{'content1':content1, 'content2':content2,
'doc':doc, 'info':info, 'tab':tab,
'include_text':include_text(request),
'stream_info': get_full_info_for_draft(id),
'versions':versions, 'history':history},
context_instance=RequestContext(request));
# doc is either IdWrapper or RfcWrapper
def _get_history(doc, versions):
results = []
if settings.USE_DB_REDESIGN_PROXY_CLASSES:
versions = [] # clear versions
event_holder = doc._draft if hasattr(doc, "_draft") else doc._rfcindex
for e in event_holder.docevent_set.all().select_related('by').order_by('-time', 'id'):
info = {}
if e.type == "new_revision":
filename = u"%s-%s" % (e.doc.name, e.newrevisiondocevent.rev)
e.desc = 'New version available: <a href="http://tools.ietf.org/id/%s.txt">%s</a>' % (filename, filename)
if int(e.newrevisiondocevent.rev) != 0:
e.desc += ' (<a href="http:{{rfcdiff_prefix}}?url2=%s">diff from -%02d</a>)' % (filename, int(e.newrevisiondocevent.rev) - 1)
info["dontmolest"] = True
multiset_ballot_text = "This was part of a ballot set with: "
if e.desc.startswith(multiset_ballot_text):
names = [ n.strip() for n in e.desc[len(multiset_ballot_text):].split(",") ]
e.desc = multiset_ballot_text + ", ".join(u'<a href="%s">%s</a>' % (urlreverse("doc_view", kwargs={'name': n }), n) for n in names)
info["dontmolest"] = True
info['text'] = e.desc
info['by'] = e.by.plain_name()
info['textSnippet'] = truncatewords_html(format_textarea(fill(info['text'], 80)), 25)
info['snipped'] = info['textSnippet'][-3:] == "..." and e.type != "new_revision"
results.append({'comment':e, 'info':info, 'date':e.time, 'is_com':True})
prev_rev = "00"
# actually, we're already sorted and this ruins the sort from
# the ids which is sometimes needed, so the function should be
# rewritten to not rely on a resort
results.sort(key=lambda x: x['date'])
for o in results:
e = o["comment"]
if e.type == "new_revision":
e.version = e.newrevisiondocevent.rev
else:
e.version = prev_rev
prev_rev = e.version
else:
if doc.is_id_wrapper:
comments = DocumentComment.objects.filter(document=doc.tracker_id).exclude(rfc_flag=1)
else:
comments = DocumentComment.objects.filter(document=doc.rfc_number,rfc_flag=1)
if len(comments) > 0:
# also include rfc_flag=NULL, but only if at least one
# comment with rfc_flag=1 exists (usually NULL means same as 0)
comments = DocumentComment.objects.filter(document=doc.rfc_number).exclude(rfc_flag=0)
for comment in comments.order_by('-date','-time','-id').filter(public_flag=1).select_related('created_by'):
info = {}
info['text'] = comment.comment_text
info['by'] = comment.get_fullname()
info['textSnippet'] = truncatewords_html(format_textarea(fill(info['text'], 80)), 25)
info['snipped'] = info['textSnippet'][-3:] == "..."
results.append({'comment':comment, 'info':info, 'date':comment.datetime(), 'is_com':True})
if doc.is_id_wrapper and versions:
for v in versions:
if v['draft_name'] == doc.draft_name:
v = dict(v) # copy it, since we're modifying datetimes later
v['is_rev'] = True
results.insert(0, v)
if not settings.USE_DB_REDESIGN_PROXY_CLASSES and doc.is_id_wrapper and doc.draft_status == "Expired" and doc._draft.expiration_date:
results.append({'is_text':True, 'date':doc._draft.expiration_date, 'text':'Draft expired'})
if not settings.USE_DB_REDESIGN_PROXY_CLASSES and doc.is_rfc_wrapper:
text = 'RFC Published'
if doc.draft_name:
try:
text = 'RFC Published (see <a href="%s">%s</a> for earlier history)' % (urlreverse('doc_view', args=[doc.draft_name]),doc.draft_name)
except NoReverseMatch:
pass
results.append({'is_text':True, 'date':doc.publication_date, 'text':text})
# convert plain dates to datetimes (required for sorting)
for x in results:
if not isinstance(x['date'], datetime.datetime):
if x['date']:
x['date'] = datetime.datetime.combine(x['date'], datetime.time(0,0,0))
else:
x['date'] = datetime.datetime(1970,1,1)
results.sort(key=lambda x: x['date'])
results.reverse()
return results
# takes InternetDraft instance
def _get_versions(draft, include_replaced=True):
ov = []
ov.append({"draft_name":draft.filename, "revision":draft.revision_display(), "date":draft.revision_date})
if include_replaced:
draft_list = [draft]+list(draft.replaces_set.all())
else:
draft_list = [draft]
if settings.USE_DB_REDESIGN_PROXY_CLASSES:
from ietf.doc.models import NewRevisionDocEvent
for e in NewRevisionDocEvent.objects.filter(type="new_revision", doc__in=draft_list).select_related('doc').order_by("-time", "-id"):
if not (e.doc.name == draft.name and e.rev == draft.rev):
ov.append(dict(draft_name=e.doc.name, revision=e.rev, date=e.time.date()))
return ov
for d in draft_list:
for v in DraftVersions.objects.filter(filename=d.filename).order_by('-revision'):
if (d.filename == draft.filename) and (draft.revision_display() == v.revision):
continue
ov.append({"draft_name":d.filename, "revision":v.revision, "date":v.revision_date})
return ov
def get_ballot(name):
from ietf.doc.models import DocAlias
alias = get_object_or_404(DocAlias, name=name)
d = alias.document
id = None
bw = None
dw = None
if (d.type_id=='draft'):
id = get_object_or_404(InternetDraft, name=d.name)
try:
if not id.ballot.ballot_issued:
raise Http404
except BallotInfo.DoesNotExist:
raise Http404
bw = BallotWrapper(id) # XXX Fixme: Eliminate this as we go forward
# Python caches ~100 regex'es -- explicitly compiling it inside a method
# (where you then throw away the compiled version!) doesn't make sense at
# all.
if re.search("^rfc([1-9][0-9]*)$", name):
id.viewing_as_rfc = True
dw = RfcWrapper(id)
else:
dw = IdWrapper(id)
# XXX Fixme: Eliminate 'dw' as we go forward
try:
b = d.latest_event(BallotDocEvent, type="created_ballot")
except BallotDocEvent.DoesNotExist:
raise Http404
return (bw, dw, b, d)
def ballot_for_popup(request, name):
doc = get_object_or_404(Document, docalias__name=name)
return HttpResponse(document_ballot_content(request, doc, ballot_id=None, editable=False))
def ballot_html(request, name):
bw, dw, ballot, doc = get_ballot(name)
content = document_ballot_content(request, doc, ballot.pk, editable=True)
return HttpResponse(content)
def ballot_tsv(request, name):
ballot, doc, b, d = get_ballot(name)
return HttpResponse(render_to_string('idrfc/ballot.tsv', {'ballot':ballot}, RequestContext(request)), content_type="text/plain")
def ballot_json(request, name):
ballot, doc, b, d = get_ballot(name)
response = HttpResponse(mimetype='text/plain')
response.write(json.dumps(ballot.dict(), indent=2))
return response