From 74e4ff72e9723148aec90a80b35fd122800e1c85 Mon Sep 17 00:00:00 2001 From: Robert Sparks <rjsparks@nostrum.com> Date: Thu, 6 Mar 2014 16:29:35 +0000 Subject: [PATCH] Adds dependency graphs for drafts belonging to a group. Removes links to Bill's dependency tools. Fixes bug #536. Commit ready to merge. - Legacy-Id: 7445 --- ietf/doc/models.py | 5 +- ietf/settings.py | 4 + ietf/templates/doc/document_draft.html | 1 - ietf/templates/wginfo/dot.txt | 88 +++++++++++++ ietf/templates/wginfo/group_base.html | 1 + ietf/wginfo/urls.py | 2 + ietf/wginfo/views.py | 166 +++++++++++++++++++++++++ 7 files changed, 265 insertions(+), 2 deletions(-) create mode 100644 ietf/templates/wginfo/dot.txt diff --git a/ietf/doc/models.py b/ietf/doc/models.py index b03b21a05..05e89d09c 100644 --- a/ietf/doc/models.py +++ b/ietf/doc/models.py @@ -177,10 +177,13 @@ class RelatedDocument(models.Model): if self.source.get_state().slug == 'rfc': source_lvl = self.source.std_level.slug elif self.source.intended_std_level: - source_lvl = self.source.intended_std_level.slug + source_lvl = self.source.intended_std_level and self.source.intended_std_level.slug else: source_lvl = None + if not source_lvl: + return None + if source_lvl not in ['bcp','ps','ds','std']: return None diff --git a/ietf/settings.py b/ietf/settings.py index 8928fc9d8..843facca6 100644 --- a/ietf/settings.py +++ b/ietf/settings.py @@ -366,6 +366,10 @@ IDSUBMIT_MAX_DAILY_SAME_GROUP_SIZE = 450 # in MB IDSUBMIT_MAX_DAILY_SUBMISSIONS = 1000 IDSUBMIT_MAX_DAILY_SUBMISSIONS_SIZE = 2000 # in MB +DOT_BINARY = '/usr/bin/dot' +UNFLATTEN_BINARY= '/usr/bin/unflatten' +PS2PDF_BINARY = '/usr/bin/ps2pdf' + # Account settings DAYS_TO_EXPIRE_REGISTRATION_LINK = 3 HTPASSWD_COMMAND = "/usr/bin/htpasswd2" diff --git a/ietf/templates/doc/document_draft.html b/ietf/templates/doc/document_draft.html index 014c3535b..08ab459ce 100644 --- a/ietf/templates/doc/document_draft.html +++ b/ietf/templates/doc/document_draft.html @@ -232,7 +232,6 @@ <div class="links"> <a href="mailto:{{ doc.name }}@tools.ietf.org?subject=Mail%20regarding%20{{ doc.name }}" rel="nofollow">Email Authors</a> | <a href="{% url "ipr_search" %}?option=document_search&id={{ doc.name }}" rel="nofollow">IPR Disclosures{% if doc.related_ipr %} ({{doc.related_ipr|length}}){% endif %}</a> - | <a href="http://www.fenron.net/~fenner/ietf/deps/index.cgi?dep={{ name }}" rel="nofollow">Dependencies to this document</a> | <a href="{% url 'doc_references' doc.canonical_name %}" rel="nofollow">References</a> | <a href="{% url 'doc_referenced_by' doc.canonical_name %}" rel="nofollow">Referenced By</a> | <a href="http://www.ietf.org/tools/idnits?url=http://www.ietf.org/archive/id/{{ doc.filename_with_rev }}" rel="nofollow" target="_blank">Check nits</a> diff --git a/ietf/templates/wginfo/dot.txt b/ietf/templates/wginfo/dot.txt new file mode 100644 index 000000000..b31943961 --- /dev/null +++ b/ietf/templates/wginfo/dot.txt @@ -0,0 +1,88 @@ +{% load mail_filters %}{% autoescape off %} +digraph draftdeps { + graph [fontame=Helvetica]; + node [fontname=Helvetica]; + edge [fontname=Helvetica]; + subgraph cluster_key { + graph [label=Key, + rankdir=LR, + margin=.5, + fontname=Helvetica + ]; + subgraph key_a { + graph [rank=same]; + key_colors [color=white, + fontcolor=black, + label="Colors in\nthis row"]; + key_wgdoc [color="#0AFE47", + label="Product of\nthis WG", + style=filled, + wg=this]; + key_otherwgdoc [color="#9999FF", + label="Product of\nother WG", + style=filled, + wg=blort]; + key_individual [color="#FF800D", + label="Individual\nsubmission", + style=filled, + wg=individual]; + } + subgraph key_b { + graph [rank=same]; + key_shapes [color=white, + fontcolor=black, + label="Shapes in\nthis row"]; + key_active [color="#9999FF", + label="Active\ndocument", + style=filled]; + key_iesg [color="#9999FF", + label="IESG or\nRFC Queue", + shape=parallelogram, + style=filled]; + key_rfc [color="#9999FF", + label="RFC\nPublished", + shape=box, + style=filled]; + key_expired [color="#9999FF", + label="Expired!", + peripheries=3, + shape=house, + style=solid]; + key_replaced [color="#9999FF", + label="Replaced", + peripheries=3, + shape=ellipse]; + } + key_colors -> key_shapes [color=white, + fontcolor=black, + label="Line\ncolor\nreflects\nReference\ntype"]; + key_wgdoc -> key_active [color=orange, + label="Orange link:\nUnsplit"]; + key_otherwgdoc -> key_active [color=green, + label="Green link:\nInformative"]; + key_individual -> key_iesg [color=blue, + label="Blue link:\nNormative"]; + key_otherwgdoc -> key_expired [label="Black link:\nUnknown", + style=dashed]; + key_wgdoc -> key_rfc [color=red, + label="Red link:\nDownref!", + arrowhead=normalnormal]; + key_individual -> key_replaced [color=pink, + label="Pink link:\nReplaces", + style=dashed, + arrowhead=diamond]; + } + +{% for node in nodes %} + {{ node.nodename }} [ status="{{ node.get_state.slug }}", + wg="{{ node.group.acronym }}",{% for key,value in node.styles.items %} + {{ key }}={{ value }},{% endfor %} + ]; +{% endfor %} + +{% for edge in edges%} + {{ edge.sourcename }} -> {{ edge.targetname }} {% if edge.styles %}[ {% for key,value in edge.styles.items %}{{ key }}={{ value }}{% if not forloop.last %}, {% endif %}{% endfor %} ] {% endif %};{% endfor %} + + +} +{% endautoescape %} diff --git a/ietf/templates/wginfo/group_base.html b/ietf/templates/wginfo/group_base.html index 91d271606..d6dfea8bc 100644 --- a/ietf/templates/wginfo/group_base.html +++ b/ietf/templates/wginfo/group_base.html @@ -69,6 +69,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. <a {% if selected == "documents" %}class="selected"{% else %}href="{% url "ietf.wginfo.views.group_documents" acronym=group.acronym %}"{% endif %}>Documents</a> | <a {% if selected == "charter" %}class="selected"{% else %}href="{% url "ietf.wginfo.views.group_charter" acronym=group.acronym %}"{% endif %}>Charter</a> | <a {% if selected == "history" %}class="selected"{% else %}href="{% url "ietf.wginfo.views.history" acronym=group.acronym %}"{% endif %}>History</a> + | <a href="{% url 'ietf.wginfo.views.dependencies_pdf' acronym=group.acronym %}">Dependency Graph</a> {% if group.list_archive|startswith:"http:" or group.list_archive|startswith:"https:" or group.list_archive|startswith:"ftp:" %} | <a href="{{ group.list_archive }}">List Archive »</a> {% endif %} diff --git a/ietf/wginfo/urls.py b/ietf/wginfo/urls.py index 6e323ce0e..2249e9c90 100644 --- a/ietf/wginfo/urls.py +++ b/ietf/wginfo/urls.py @@ -23,6 +23,8 @@ urlpatterns = patterns('', (r'^(?P<acronym>[a-zA-Z0-9-]+)/charter/$', views.group_charter, None, 'group_charter'), (r'^(?P<acronym>[a-zA-Z0-9-]+)/init-charter/', edit.submit_initial_charter, None, "wg_init_charter"), (r'^(?P<acronym>[a-zA-Z0-9-]+)/history/$', views.history), + (r'^(?P<acronym>[a-zA-Z0-9-]+)/deps/dot/$', views.dependencies_dot), + (r'^(?P<acronym>[a-zA-Z0-9-]+)/deps/pdf/$', views.dependencies_pdf), (r'^(?P<acronym>[a-zA-Z0-9-]+)/edit/$', edit.edit, {'action': "edit"}, "group_edit"), (r'^(?P<acronym>[a-zA-Z0-9-]+)/conclude/$', edit.conclude, None, "wg_conclude"), (r'^(?P<acronym>[a-zA-Z0-9-]+)/milestones/$', milestones.edit_milestones, {'milestone_set': "current"}, "wg_edit_milestones"), diff --git a/ietf/wginfo/views.py b/ietf/wginfo/views.py index ef594fa7c..758a22f75 100644 --- a/ietf/wginfo/views.py +++ b/ietf/wginfo/views.py @@ -35,10 +35,12 @@ import itertools 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.db.models import Q from ietf.doc.views_search import SearchForm, retrieve_search_results from ietf.group.models import Group, GroupURL, Role @@ -48,6 +50,10 @@ 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 +from tempfile import mkstemp +import os + def roles(group, role_name): return Role.objects.filter(group=group, name=role_name).select_related("email", "person") @@ -286,3 +292,163 @@ def history(request, acronym): 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' + ) + +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')