456 lines
18 KiB
Python
456 lines
18 KiB
Python
# Copyright The IETF Trust 2008, All Rights Reserved
|
|
|
|
# Portion Copyright (C) 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 os
|
|
import itertools
|
|
from tempfile import mkstemp
|
|
|
|
from django.shortcuts import get_object_or_404, render_to_response
|
|
from django.template.loader import render_to_string
|
|
from django.template import RequestContext
|
|
from django.http import HttpResponse
|
|
from django.conf import settings
|
|
from django.core.urlresolvers import reverse as urlreverse
|
|
from django.views.decorators.cache import cache_page
|
|
from django.db.models import Q
|
|
|
|
from ietf.doc.views_search import SearchForm, retrieve_search_results
|
|
from ietf.group.models import Group, Role
|
|
from ietf.doc.models import State, DocAlias, RelatedDocument
|
|
from ietf.doc.utils import get_chartering_type
|
|
from ietf.group.utils import get_charter_text
|
|
from ietf.doc.templatetags.ietf_filters import clean_whitespace
|
|
from ietf.ietfauth.utils import has_role
|
|
from ietf.utils.pipe import pipe
|
|
|
|
def roles(group, role_name):
|
|
return Role.objects.filter(group=group, name=role_name).select_related("email", "person")
|
|
|
|
def fill_in_charter_info(group, include_drafts=False):
|
|
group.areadirector = group.ad.role_email("ad", group.parent) if group.ad else None
|
|
group.chairs = roles(group, "chair")
|
|
group.techadvisors = roles(group, "techadv")
|
|
group.editors = roles(group, "editor")
|
|
group.secretaries = roles(group, "secr")
|
|
milestone_state = "charter" if group.state_id == "proposed" else "active"
|
|
group.milestones = group.groupmilestone_set.filter(state=milestone_state).order_by('due')
|
|
|
|
if group.charter:
|
|
group.charter_text = get_charter_text(group)
|
|
else:
|
|
group.charter_text = u"Not chartered yet."
|
|
|
|
if include_drafts:
|
|
aliases = DocAlias.objects.filter(document__type="draft", document__group=group).select_related('document').order_by("name")
|
|
group.drafts = []
|
|
group.rfcs = []
|
|
for a in aliases:
|
|
if a.name.startswith("draft"):
|
|
group.drafts.append(a)
|
|
else:
|
|
group.rfcs.append(a)
|
|
a.rel = RelatedDocument.objects.filter(source=a.document).distinct()
|
|
a.invrel = RelatedDocument.objects.filter(target=a).distinct()
|
|
|
|
def extract_last_name(role):
|
|
return role.person.name_parts()[3]
|
|
|
|
def wg_summary_area(request):
|
|
areas = Group.objects.filter(type="area", state="active").order_by("name")
|
|
for area in areas:
|
|
area.ads = sorted(roles(area, "ad"), key=extract_last_name)
|
|
area.groups = Group.objects.filter(parent=area, type="wg", state="active").order_by("acronym")
|
|
for group in area.groups:
|
|
group.chairs = sorted(roles(group, "chair"), key=extract_last_name)
|
|
|
|
areas = [a for a in areas if a.groups]
|
|
|
|
return render_to_response('wginfo/1wg-summary.txt',
|
|
{ 'areas': areas },
|
|
content_type='text/plain; charset=UTF-8')
|
|
|
|
def wg_summary_acronym(request):
|
|
areas = Group.objects.filter(type="area", state="active").order_by("name")
|
|
groups = Group.objects.filter(type="wg", state="active").order_by("acronym").select_related("parent")
|
|
for group in groups:
|
|
group.chairs = sorted(roles(group, "chair"), key=extract_last_name)
|
|
return render_to_response('wginfo/1wg-summary-by-acronym.txt',
|
|
{ 'areas': areas,
|
|
'groups': groups },
|
|
content_type='text/plain; charset=UTF-8')
|
|
|
|
def wg_charters(request):
|
|
areas = Group.objects.filter(type="area", state="active").order_by("name")
|
|
for area in areas:
|
|
area.ads = sorted(roles(area, "ad"), key=extract_last_name)
|
|
area.groups = Group.objects.filter(parent=area, type="wg", state="active").order_by("name")
|
|
for group in area.groups:
|
|
fill_in_charter_info(group, include_drafts=True)
|
|
group.area = area
|
|
return render_to_response('wginfo/1wg-charters.txt',
|
|
{ 'areas': areas },
|
|
content_type='text/plain; charset=UTF-8')
|
|
|
|
def wg_charters_by_acronym(request):
|
|
areas = dict((a.id, a) for a in Group.objects.filter(type="area", state="active").order_by("name"))
|
|
|
|
for area in areas.itervalues():
|
|
area.ads = sorted(roles(area, "ad"), key=extract_last_name)
|
|
|
|
groups = Group.objects.filter(type="wg", state="active").exclude(parent=None).order_by("acronym")
|
|
for group in groups:
|
|
fill_in_charter_info(group, include_drafts=True)
|
|
group.area = areas.get(group.parent_id)
|
|
return render_to_response('wginfo/1wg-charters-by-acronym.txt',
|
|
{ 'groups': groups },
|
|
content_type='text/plain; charset=UTF-8')
|
|
|
|
def active_wgs(request):
|
|
areas = Group.objects.filter(type="area", state="active").order_by("name")
|
|
for area in areas:
|
|
# dig out information for template
|
|
area.ads = (list(sorted(roles(area, "ad"), key=extract_last_name))
|
|
+ list(sorted(roles(area, "pre-ad"), key=extract_last_name)))
|
|
|
|
area.groups = Group.objects.filter(parent=area, type="wg", state="active").order_by("acronym")
|
|
area.urls = area.groupurl_set.all().order_by("name")
|
|
for group in area.groups:
|
|
group.chairs = sorted(roles(group, "chair"), key=extract_last_name)
|
|
|
|
return render_to_response('wginfo/active_wgs.html', {'areas':areas}, RequestContext(request))
|
|
|
|
def bofs(request):
|
|
groups = Group.objects.filter(type="wg", state="bof")
|
|
return render_to_response('wginfo/bofs.html',dict(groups=groups), RequestContext(request))
|
|
|
|
def chartering_wgs(request):
|
|
charter_states = State.objects.filter(used=True, type="charter").exclude(slug__in=("approved", "notrev"))
|
|
groups = Group.objects.filter(type="wg", charter__states__in=charter_states).select_related("state", "charter")
|
|
|
|
for g in groups:
|
|
g.chartering_type = get_chartering_type(g.charter)
|
|
|
|
return render_to_response('wginfo/chartering_wgs.html',
|
|
dict(charter_states=charter_states,
|
|
groups=groups),
|
|
RequestContext(request))
|
|
|
|
|
|
def construct_group_menu_context(request, group, selected, others):
|
|
"""Return context with info for the group menu filled in."""
|
|
actions = []
|
|
|
|
is_chair = group.has_role(request.user, "chair")
|
|
is_ad_or_secretariat = has_role(request.user, ("Area Director", "Secretariat"))
|
|
|
|
if group.state_id != "proposed" and (is_chair or is_ad_or_secretariat):
|
|
actions.append((u"Add or edit milestones", urlreverse("wg_edit_milestones", kwargs=dict(acronym=group.acronym))))
|
|
|
|
if group.state_id != "conclude" and is_ad_or_secretariat:
|
|
actions.append((u"Edit group", urlreverse("group_edit", kwargs=dict(acronym=group.acronym))))
|
|
|
|
if is_chair or is_ad_or_secretariat:
|
|
actions.append((u"Customize workflow", urlreverse("ietf.wginfo.edit.customize_workflow", kwargs=dict(acronym=group.acronym))))
|
|
|
|
if group.state_id in ("active", "dormant") and is_ad_or_secretariat:
|
|
actions.append((u"Request closing group", urlreverse("wg_conclude", kwargs=dict(acronym=group.acronym))))
|
|
|
|
d = {
|
|
"group": group,
|
|
"selected": selected,
|
|
"menu_actions": actions,
|
|
}
|
|
|
|
d.update(others)
|
|
|
|
return d
|
|
|
|
def search_for_group_documents(group):
|
|
form = SearchForm({ 'by':'group', 'group': group.acronym or "", 'rfcs':'on', 'activedrafts': 'on' })
|
|
docs, meta = retrieve_search_results(form)
|
|
|
|
# get the related docs
|
|
form_related = SearchForm({ 'by':'group', 'name': u'-%s-' % group.acronym, 'activedrafts': 'on' })
|
|
raw_docs_related, meta_related = retrieve_search_results(form_related)
|
|
|
|
docs_related = []
|
|
for d in raw_docs_related:
|
|
parts = d.name.split("-", 2);
|
|
# canonical form draft-<name|ietf>-wg-etc
|
|
if len(parts) >= 3 and parts[1] != "ietf" and parts[2].startswith(group.acronym + "-"):
|
|
d.search_heading = "Related Internet-Draft"
|
|
docs_related.append(d)
|
|
|
|
# move call for WG adoption to related
|
|
cleaned_docs = []
|
|
docs_related_names = set(d.name for d in docs_related)
|
|
for d in docs:
|
|
if d.stream_id and d.get_state_slug("draft-stream-%s" % d.stream_id) in ("c-adopt", "wg-cand"):
|
|
if d.name not in docs_related_names:
|
|
d.search_heading = "Related Internet-Draft"
|
|
docs_related.append(d)
|
|
else:
|
|
cleaned_docs.append(d)
|
|
|
|
docs = cleaned_docs
|
|
|
|
docs_related.sort(key=lambda d: d.name)
|
|
|
|
return docs, meta, docs_related, meta_related
|
|
|
|
def group_documents(request, acronym):
|
|
group = get_object_or_404(Group, type="wg", acronym=acronym)
|
|
|
|
docs, meta, docs_related, meta_related = search_for_group_documents(group)
|
|
|
|
return render_to_response('wginfo/group_documents.html',
|
|
construct_group_menu_context(request, group, "documents", {
|
|
'docs': docs,
|
|
'meta': meta,
|
|
'docs_related': docs_related,
|
|
'meta_related': meta_related
|
|
}), RequestContext(request))
|
|
|
|
def group_documents_txt(request, acronym):
|
|
"""Return tabulator-separated rows with documents for group."""
|
|
group = get_object_or_404(Group, type="wg", acronym=acronym)
|
|
|
|
docs, meta, docs_related, meta_related = search_for_group_documents(group)
|
|
|
|
for d in docs:
|
|
d.prefix = d.get_state().name
|
|
|
|
for d in docs_related:
|
|
d.prefix = u"Related %s" % d.get_state().name
|
|
|
|
rows = []
|
|
for d in itertools.chain(docs, docs_related):
|
|
rfc_number = d.rfc_number()
|
|
if rfc_number != None:
|
|
name = rfc_number
|
|
else:
|
|
name = "%s-%s" % (d.name, d.rev)
|
|
|
|
rows.append(u"\t".join((d.prefix, name, clean_whitespace(d.title))))
|
|
|
|
return HttpResponse(u"\n".join(rows), content_type='text/plain; charset=UTF-8')
|
|
|
|
|
|
def group_charter(request, acronym):
|
|
group = get_object_or_404(Group, type="wg", acronym=acronym)
|
|
|
|
fill_in_charter_info(group, include_drafts=False)
|
|
group.delegates = roles(group, "delegate")
|
|
|
|
e = group.latest_event(type__in=("changed_state", "requested_close",))
|
|
requested_close = group.state_id != "conclude" and e and e.type == "requested_close"
|
|
|
|
return render_to_response('wginfo/group_charter.html',
|
|
construct_group_menu_context(request, group, "charter", {
|
|
"milestones_in_review": group.groupmilestone_set.filter(state="review"),
|
|
"requested_close": requested_close,
|
|
}), RequestContext(request))
|
|
|
|
|
|
def history(request, acronym):
|
|
group = get_object_or_404(Group, acronym=acronym)
|
|
|
|
events = group.groupevent_set.all().select_related('by').order_by('-time', '-id')
|
|
|
|
return render_to_response('wginfo/history.html',
|
|
construct_group_menu_context(request, group, "history", {
|
|
"events": events,
|
|
}), RequestContext(request))
|
|
|
|
|
|
def nodename(name):
|
|
return name.replace('-','_')
|
|
|
|
class Edge(object):
|
|
def __init__(self,relateddocument):
|
|
self.relateddocument=relateddocument
|
|
|
|
def __hash__(self):
|
|
return hash("|".join([str(hash(nodename(self.relateddocument.source.name))),
|
|
str(hash(nodename(self.relateddocument.target.document.name))),
|
|
self.relateddocument.relationship.slug]))
|
|
|
|
def __eq__(self,other):
|
|
return self.__hash__() == other.__hash__()
|
|
|
|
def sourcename(self):
|
|
return nodename(self.relateddocument.source.name)
|
|
|
|
def targetname(self):
|
|
return nodename(self.relateddocument.target.document.name)
|
|
|
|
def styles(self):
|
|
|
|
# Note that the old style=dotted, color=red styling is never used
|
|
|
|
if self.relateddocument.is_downref():
|
|
return { 'color':'red','arrowhead':'normalnormal' }
|
|
else:
|
|
styles = { 'refnorm' : { 'color':'blue' },
|
|
'refinfo' : { 'color':'green' },
|
|
'refold' : { 'color':'orange' },
|
|
'refunk' : { 'style':'dashed' },
|
|
'replaces': { 'color':'pink', 'style':'dashed', 'arrowhead':'diamond' },
|
|
}
|
|
return styles[self.relateddocument.relationship.slug]
|
|
|
|
def get_node_styles(node,group):
|
|
|
|
styles=dict()
|
|
|
|
# Shape and style (note that old diamond shape is never used
|
|
|
|
styles['style'] = 'filled'
|
|
|
|
if node.get_state('draft').slug == 'rfc':
|
|
styles['shape'] = 'box'
|
|
elif node.get_state('draft-iesg') and not node.get_state('draft-iesg').slug in ['watching','dead']:
|
|
styles['shape'] = 'parallelogram'
|
|
elif node.get_state('draft').slug == 'expired':
|
|
styles['shape'] = 'house'
|
|
styles['style'] ='solid'
|
|
styles['peripheries'] = 3
|
|
elif node.get_state('draft').slug == 'repl':
|
|
styles['shape'] = 'ellipse'
|
|
styles['style'] ='solid'
|
|
styles['peripheries'] = 3
|
|
else:
|
|
pass # quieter form of styles['shape'] = 'ellipse'
|
|
|
|
# Color (note that the old 'Flat out red' is never used
|
|
if node.group.acronym == 'none':
|
|
styles['color'] = '"#FF800D"' # orangeish
|
|
elif node.group == group:
|
|
styles['color'] = '"#0AFE47"' # greenish
|
|
else:
|
|
styles['color'] = '"#9999FF"' # blueish
|
|
|
|
# Label
|
|
label = node.name
|
|
if label.startswith('draft-'):
|
|
if label.startswith('draft-ietf-'):
|
|
label=label[11:]
|
|
else:
|
|
label=label[6:]
|
|
try:
|
|
t=label.index('-')
|
|
label="%s\\n%s" % (label[:t],label[t+1:])
|
|
except:
|
|
pass
|
|
if node.group.acronym != 'none' and node.group != group:
|
|
label = "(%s) %s"%(node.group.acronym,label)
|
|
if node.get_state('draft').slug == 'rfc':
|
|
label = "%s\\n(%s)"%(label,node.canonical_name())
|
|
styles['label'] = '"%s"'%label
|
|
|
|
return styles
|
|
|
|
def make_dot(group):
|
|
|
|
references = Q(source__group=group,source__type='draft',relationship__slug__startswith='ref')
|
|
both_rfcs = Q(source__states__slug='rfc',target__document__states__slug='rfc')
|
|
inactive = Q(source__states__slug__in=['expired','repl'])
|
|
attractor = Q(target__name__in=['rfc5000','rfc5741'])
|
|
removed = Q(source__states__slug__in=['auth-rm','ietf-rm'])
|
|
relations = RelatedDocument.objects.filter(references).exclude(both_rfcs).exclude(inactive).exclude(attractor).exclude(removed)
|
|
|
|
edges = set()
|
|
for x in relations:
|
|
target_state = x.target.document.get_state_slug('draft')
|
|
if target_state!='rfc' or x.is_downref():
|
|
edges.add(Edge(x))
|
|
|
|
replacements = RelatedDocument.objects.filter(relationship__slug='replaces',target__document__in=[x.relateddocument.target.document for x in edges])
|
|
|
|
for x in replacements:
|
|
edges.add(Edge(x))
|
|
|
|
nodes = set([x.relateddocument.source for x in edges]).union([x.relateddocument.target.document for x in edges])
|
|
|
|
for node in nodes:
|
|
node.nodename=nodename(node.name)
|
|
node.styles = get_node_styles(node,group)
|
|
|
|
return render_to_string('wginfo/dot.txt',
|
|
dict( nodes=nodes, edges=edges )
|
|
)
|
|
|
|
def dependencies_dot(request, acronym):
|
|
|
|
group = get_object_or_404(Group, acronym=acronym)
|
|
|
|
return HttpResponse(make_dot(group),
|
|
content_type='text/plain; charset=UTF-8'
|
|
)
|
|
|
|
@cache_page ( 60 * 60 )
|
|
def dependencies_pdf(request, acronym):
|
|
|
|
group = get_object_or_404(Group, acronym=acronym)
|
|
|
|
dothandle,dotname = mkstemp()
|
|
os.close(dothandle)
|
|
dotfile = open(dotname,"w")
|
|
dotfile.write(make_dot(group))
|
|
dotfile.close()
|
|
|
|
unflathandle,unflatname = mkstemp()
|
|
os.close(unflathandle)
|
|
|
|
pshandle,psname = mkstemp()
|
|
os.close(pshandle)
|
|
|
|
pdfhandle,pdfname = mkstemp()
|
|
os.close(pdfhandle)
|
|
|
|
pipe("%s -f -l 10 -o %s %s" % (settings.UNFLATTEN_BINARY,unflatname,dotname))
|
|
pipe("%s -Tps -Gsize=10.5,8.0 -Gmargin=0.25 -Gratio=auto -Grotate=90 -o %s %s" % (settings.DOT_BINARY,psname,unflatname))
|
|
pipe("%s %s %s" % (settings.PS2PDF_BINARY,psname,pdfname))
|
|
|
|
pdfhandle = open(pdfname,"r")
|
|
pdf = pdfhandle.read()
|
|
pdfhandle.close()
|
|
|
|
os.unlink(pdfname)
|
|
os.unlink(psname)
|
|
os.unlink(unflatname)
|
|
os.unlink(dotname)
|
|
|
|
return HttpResponse(pdf, content_type='application/pdf')
|