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&amp;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 &raquo;</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')